feat(dees-stepper): refactor to use dees-tile and add overlay functionality

- Updated IStep interface to include optional footerContent for step-specific actions.
- Implemented createAndShow static method for displaying stepper as an overlay with DeesWindowLayer.
- Refactored render method to wrap each step in a dees-tile, separating header, body, and footer content.
- Enhanced CSS for dual-mode operation, adjusting styles for overlay and inline modes.
- Added z-index management for overlay stepper to ensure proper stacking with window layer.
- Updated demo to include a button for launching the stepper as an overlay.
This commit is contained in:
2026-04-11 09:28:11 +00:00
parent d5c9bc69b3
commit 39a4bf0dd3
3 changed files with 663 additions and 195 deletions

352
readme.plan.md Normal file
View File

@@ -0,0 +1,352 @@
# Plan: dees-stepper — adopt dees-tile + optional overlay window layer
> First line (per CLAUDE.md): Please reread `/home/philkunz/.claude/CLAUDE.md` before continuing.
## Context
Today `dees-stepper` is an inline-only layout component: it hard-codes each step as a custom `.step` `<div>` with its own border / background / box-shadow / border-radius, and its `:host` is `position: absolute; width: 100%; height: 100%;` so it can only live inside a bounded parent container.
The user wants it to behave more like `dees-modal`:
1. Each step should be wrapped in a `<dees-tile>` — the unified "rounded on rounded" frame used by modals and panels — rather than a bespoke `.step` div.
2. A `DeesWindowLayer` should be added behind the stepper, the same way `DeesModal.createAndShow` does, so the stepper can appear as an overlay on top of the page.
User has confirmed (via AskUserQuestion in this session):
- **API**: keep the current inline usage working AND add a static `createAndShow()` like dees-modal.
- **Layout**: keep the current vertical stack + SweetScroll behavior inside the overlay (don't switch to single-tile swap).
- **Nav placement**: split header/footer — goBack + step counter go into the `dees-tile` header slot; the title stays in the content area; the tile footer is used for optional next/submit buttons supplied per-step.
No external consumers of `dees-stepper` were found inside this package (`grep dees-stepper|DeesStepper` only matches its own source, demo, index, changelog, readme). External consumers in dependent projects may exist — the refactor is kept backward-compatible for the inline path.
## Current state (reference)
**File:** `ts_web/elements/00group-layout/dees-stepper/dees-stepper.ts` (lines 20299)
- `IStep` interface — `title`, `content: TemplateResult`, `validationFunc`, `onReturnToStepFunc`, internal flags (lines 2027).
- `:host { position: absolute; width: 100%; height: 100%; }` (lines 5963).
- `.stepperContainer` — absolute, 100% w/h, `overflow: hidden`, holds SweetScroll (lines 6469).
- `.step` — max-width 500, min-height 300, `border-radius: 12px`, theme background, theme border, `box-shadow: 0 8px 32px rgba(0,0,0,0.4)`, `filter: opacity(0.55) saturate(0.85)`, transform transition (lines 7197). **These frame styles overlap with what `dees-tile` already provides.**
- `.step.selected``filter: opacity(1) saturate(1)` (lines 8993). **Scroll-through visual cue, keep.**
- `.step.hiddenStep``filter: opacity(0)` (line 95). **Keep.**
- `.step.entrance` — faster transition variant for first-render (lines 99105). **Keep.**
- `.step .stepCounter``position: absolute; top: 12px; right: 12px;` pill (lines 111121). **Move into header slot as a flex child.**
- `.step .goBack``position: absolute; top: 12px; left: 12px;` pill + icon + hover (lines 123161). **Move into header slot as a flex child.**
- `.step .title` — centered, 24px, 64px top padding (lines 163171). **Keep inside the tile's content slot; remove the 64px top padding since goBack/counter no longer overlap it.**
- `.step .content` — 32px padding (lines 173175). **Keep.**
- `render()` (lines 179204) — maps `steps` to `.step` divs.
- `setScrollStatus()` (lines 226263) — SweetScroll container setup + step validation kick-off. **Keep mostly as-is; selectors still target `.step`/`.selected` so rename cautiously.**
- `firstUpdated` (lines 210218), `updated` (lines 220222), `goBack` (lines 265282), `goNext` (lines 284298) — untouched in behavior, only DOM selectors may need adjusting.
**Reference files (read, do not modify):**
- `ts_web/elements/00group-overlay/dees-modal/dees-modal.ts` — canonical `createAndShow` + `destroy` + window-layer coordination + z-index registry usage.
- `ts_web/elements/00group-layout/dees-tile/dees-tile.ts` — slot API: `slot="header"`, default slot, `slot="footer"`. Auto-hides footer when slotted nodes are empty. Uses `part="outer"`, `part="header"`, `part="content"`, `part="footer"` for external shadow-part styling.
- `ts_web/elements/00group-overlay/dees-windowlayer/dees-windowlayer.ts``createAndShow({ blur })`, `destroy()`, dispatches `clicked` event on backdrop click, uses `zIndexRegistry`.
- `ts_web/elements/00group-layout/dees-stepper/dees-stepper.demo.ts` — existing inline demo.
## Target state
### 1. IStep interface — add one optional field
```ts
export interface IStep {
title: string;
content: TemplateResult;
footerContent?: TemplateResult; // NEW: optional, rendered in dees-tile footer slot
validationFunc?: (stepper: DeesStepper, htmlElement: HTMLElement, signal?: AbortSignal) => Promise<any>;
onReturnToStepFunc?: (stepper: DeesStepper, htmlElement: HTMLElement) => Promise<any>;
validationFuncCalled?: boolean;
abortController?: AbortController;
}
```
Form-based steps don't need `footerContent` — their `dees-form-submit` stays inside the form in the content slot (as today). `footerContent` is for non-form steps that need an explicit primary action, or for any step that wants buttons in the conventional tile footer location.
### 2. New overlay-mode state + API on DeesStepper
```ts
@state() accessor overlay: boolean = false;
@state() accessor stepperZIndex: number = 1000;
private windowLayer?: DeesWindowLayer;
public static async createAndShow(optionsArg: {
steps: IStep[];
}): Promise<DeesStepper> {
const body = document.body;
const stepper = new DeesStepper();
stepper.steps = optionsArg.steps;
stepper.overlay = true;
stepper.windowLayer = await DeesWindowLayer.createAndShow({ blur: true });
stepper.windowLayer.addEventListener('click', async () => {
await stepper.destroy();
});
body.append(stepper.windowLayer); // (already appended inside createAndShow, but mirror dees-modal's pattern; see note)
body.append(stepper);
stepper.stepperZIndex = zIndexRegistry.getNextZIndex();
zIndexRegistry.register(stepper, stepper.stepperZIndex);
return stepper;
}
public async destroy() {
const domtools = await this.domtoolsPromise;
const container = this.shadowRoot!.querySelector('.stepperContainer');
container?.classList.add('predestroy');
await domtools.convenience.smartdelay.delayFor(200);
if (this.parentElement) this.parentElement.removeChild(this);
if (this.windowLayer) await this.windowLayer.destroy();
zIndexRegistry.unregister(this);
}
```
**Note on `body.append(windowLayer)`:** `DeesWindowLayer.createAndShow` already appends the window layer to `document.body` (line 27 of `dees-windowlayer.ts`). `dees-modal.ts:71` still calls `body.append(modal.windowLayer)` — that's either a no-op (already-attached nodes) or a re-parent to keep ordering. I will match dees-modal's exact sequence verbatim to avoid introducing subtle differences; if it's a bug in dees-modal it is out of scope for this task.
**Minimum new scope for createAndShow:** just `steps` for now. No `onComplete`, no `showCloseButton`, no width options. Future-proofing via additional options is an explicit follow-up — this plan keeps scope razor-sharp (per CLAUDE.md). The caller can already wire completion via the last step's `validationFunc` calling back into their own code.
### 3. Render template — wrap each step in `<dees-tile>`
```ts
public render() {
return html`
<div class="stepperContainer ${this.overlay ? 'overlay' : ''}" style="${this.overlay ? `z-index: ${this.stepperZIndex}` : ''}">
${this.steps.map((stepArg, i) => {
const isSelected = stepArg === this.selectedStep;
const isHidden = this.getIndexOfStep(stepArg) > this.getIndexOfStep(this.selectedStep);
const isFirst = i === 0;
const stepNumber = i + 1;
return html`
<dees-tile
class="step ${isSelected ? 'selected' : ''} ${isHidden ? 'hiddenStep' : ''} ${isFirst ? 'entrance' : ''}"
>
<div slot="header" class="step-header">
${!isFirst
? html`<div class="goBack" @click=${this.goBack}>
<span>&larr;</span> go to previous step
</div>`
: html`<div class="goBack-spacer"></div>`}
<div class="stepCounter">Step ${stepNumber} of ${this.steps.length}</div>
</div>
<div class="step-body">
<div class="title">${stepArg.title}</div>
<div class="content">${stepArg.content}</div>
</div>
${stepArg.footerContent
? html`<div slot="footer" class="step-footer">${stepArg.footerContent}</div>`
: ''}
</dees-tile>
`;
})}
</div>
`;
}
```
**Key detail:** on the first step, render a `.goBack-spacer` (empty div) in the header instead of nothing — so the `stepCounter` stays right-aligned via `justify-content: space-between`. Without a spacer, flex would left-align the counter on step 1.
### 4. CSS changes
**Remove from `.step`:**
- `border-radius: 12px;`
- `background: ${cssManager.bdTheme(...)};`
- `border: 1px solid ${cssManager.bdTheme(...)};`
- `color: ${cssManager.bdTheme(...)};`
- `box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);`
- `overflow: hidden;`
**Why:** `dees-tile` owns all of these now. The `.step` selector still exists (since `dees-tile` has `class="step ..."` on it), but it only controls the outer animation wrapper: `max-width`, `min-height`, `margin`, `filter`, `transform`, `transition`, `user-select`, `pointer-events`.
**Keep on `.step`:**
- `position: relative;`
- `pointer-events: none;` + `.step.selected { pointer-events: all; }`
- `max-width: 500px;` / `min-height: 300px;`
- `margin: auto; margin-bottom: 20px;`
- `filter: opacity(0.55) saturate(0.85);` + `.selected { filter: opacity(1) saturate(1); }`
- `.hiddenStep { filter: opacity(0); }`
- All the cubic-bezier transitions (transform/filter/box-shadow — but box-shadow is now a no-op since dees-tile provides the shadow; leave the transition spec in so we don't have to re-check browser parsing; or just drop `box-shadow` from the transition list — I'll drop it for cleanliness).
- `.step.entrance` + `.step.entrance.hiddenStep { transform: translateY(16px); }`
- `.step:last-child { margin-bottom: 100vh; }`
**Add for dees-tile shadow enhancement:** use `::part(outer)` to apply the modal-style elevated shadow only when in overlay mode (optional polish — inline mode stays flat):
```css
.stepperContainer.overlay dees-tile.step::part(outer) {
box-shadow:
0 0 0 1px ${cssManager.bdTheme('hsl(0 0% 0% / 0.03)', 'hsl(0 0% 100% / 0.03)')},
0 8px 40px ${cssManager.bdTheme('hsl(0 0% 0% / 0.12)', 'hsl(0 0% 0% / 0.5)')},
0 2px 8px ${cssManager.bdTheme('hsl(0 0% 0% / 0.06)', 'hsl(0 0% 0% / 0.25)')};
}
```
This exactly mirrors the dees-modal::part(outer) shadow stack (dees-modal.ts:157161) so the overlay stepper reads as "same visual language as modal."
**Restyle `.step-header` (NEW — the `<div slot="header">`):**
```css
.step-header {
height: 48px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
gap: 12px;
}
```
**Restyle `.step .stepCounter` → `.step-header .stepCounter` (move from absolute to flex child):**
- Drop `position: absolute; top: 12px; right: 12px;`
- Keep everything else (padding, font-size, border-radius, background, border).
**Restyle `.step .goBack` → `.step-header .goBack` (move from absolute to flex child):**
- Drop `position: absolute; top: 12px; left: 12px;`
- Keep everything else (padding, font-size, border-radius, background, border, hover/active states).
**Add `.goBack-spacer`:**
```css
.goBack-spacer { width: 1px; } /* placeholder so flex space-between works on step 1 */
```
**Restyle `.step .title`:**
- Drop `padding-top: 64px;` — no longer overlaps anything since header is in its own slot.
- Keep `text-align: center; font-family: 'Geist Sans', sans-serif; font-size: 24px; font-weight: 600; letter-spacing: -0.01em; color: inherit;`
- Add `padding-top: 32px;` (or similar) so there's consistent breathing room above the title inside the tile content.
**Add `.step-footer` (new container for `stepArg.footerContent`):**
```css
.step-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 12px 16px;
}
```
**Add overlay-mode positioning:**
```css
.stepperContainer {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
}
.stepperContainer.overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
}
.stepperContainer.predestroy {
opacity: 0;
transition: opacity 0.2s ease-in;
}
```
**Adjust `:host` for dual-mode:**
```css
:host {
position: absolute; /* inline default */
width: 100%;
height: 100%;
font-family: ${cssGeistFontFamily};
color: var(--dees-color-text-primary);
}
:host([overlay]) {
position: fixed; /* overlay mode */
top: 0;
left: 0;
width: 100vw;
height: 100vh;
}
```
The `overlay` @state needs to reflect to an attribute for the `:host([overlay])` selector to work. Since `@state` doesn't reflect attributes, use `@property({ type: Boolean, reflect: true })` instead — change the decorator accordingly.
### 5. Imports to add in `dees-stepper.ts`
```ts
import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
import { zIndexRegistry } from '../../00zindex.js';
import { cssGeistFontFamily } from '../../00fonts.js';
import '../../00group-layout/dees-tile/dees-tile.js';
```
`dees-tile` side-effect import registers the custom element. `cssGeistFontFamily` is only needed if I add it to `:host` (which I want, to match modal).
### 6. SweetScroll selector stability
`setScrollStatus()` selectors target `.step` and `.selected` (lines 228229). These continue to match since I'm keeping those class names on the `<dees-tile>` elements. **No selector changes needed.**
One subtlety: `offsetTop` / `offsetHeight` on `<dees-tile>` should still work — the tile's `:host` is `display: flex; flex-direction: column;` which participates in layout. I'll verify visually in the demo.
### 7. Demo update
**File:** `ts_web/elements/00group-layout/dees-stepper/dees-stepper.demo.ts`
Current demo renders one inline stepper directly. I'll keep that and add an **overlay launcher button** above it:
```ts
export const stepperDemo = () => html`
<div style="padding: 16px;">
<dees-button @click=${async () => {
const stepper = await DeesStepper.createAndShow({
steps: [/* same steps as inline demo */],
});
}}>Open stepper as overlay</dees-button>
</div>
<dees-stepper .steps=${[/* ... existing inline demo steps ... */]}></dees-stepper>
`;
```
Extract the step definitions into a `const demoSteps = [...]` above the template so both the inline and overlay paths reuse them (DRY). Import `DeesStepper` at the top of the demo file.
## Files to modify
1. **`ts_web/elements/00group-layout/dees-stepper/dees-stepper.ts`** — main refactor (IStep, imports, render, styles, createAndShow, destroy, overlay state).
2. **`ts_web/elements/00group-layout/dees-stepper/dees-stepper.demo.ts`** — add overlay launcher button, extract shared `demoSteps` const, import `DeesStepper`.
**Files explicitly NOT modified:**
- `dees-tile.ts` — used as-is via its slot API.
- `dees-windowlayer.ts` — used as-is via `createAndShow` / `destroy` / `click` event.
- `dees-modal.ts` — reference only.
- `00zindex.ts` — reference only.
## Verification
1. **Build**: `pnpm run build` — must pass with no TS errors. Pure refactor, no new dependencies, no lib-check regressions expected.
2. **Inline demo (backward compat)**:
- Start the demo server (port 8080 is already running) and navigate to the dees-stepper demo page.
- Confirm the stepper renders inline exactly like before: first step centered, subsequent steps dimmed below, scroll-through animation on goNext / goBack.
- Fill out the first form, submit → stepper scrolls to step 2. Click goBack → scrolls back.
- Confirm the `dees-tile` frame is visible on each step (rounded, bordered, themed) and that the title + form are inside the tile's content area.
- Confirm goBack button + step counter sit in the tile's header row, space-between, left/right respectively.
3. **Overlay demo (new path)**:
- Click the "Open stepper as overlay" button.
- Confirm a `dees-windowlayer` with blur appears behind the stepper.
- Confirm the stepper fills the viewport (fixed, 100vw×100vh).
- Confirm z-index stacking: stepper above window layer above page content.
- Click the window layer (outside the tile) → stepper animates out, then destroys along with the window layer.
- Re-open and step through forward & back — behavior identical to inline mode.
4. **Playwright visual check** (per CLAUDE.md: screenshots MUST go in `.playwright-mcp/`):
- `.playwright-mcp/dees-stepper-inline.png` — inline mode, step 1 with form.
- `.playwright-mcp/dees-stepper-overlay.png` — overlay mode, same step.
- `.playwright-mcp/dees-stepper-overlay-step3.png` — overlay mode mid-flow, to verify scroll-stack visual.
- Both light and dark themes if the demo has a theme toggle.
5. **Grep sanity**:
- Confirm `dees-stepper` has no new unexpected match locations: `grep dees-stepper ts_web/` should still only match stepper's own files.
- Confirm no `.step` class collisions elsewhere (unlikely — `.step` is a plain class name; all usages should be shadow-scoped to `dees-stepper`).
## Open assumptions & deferred scope
These are explicit defaults in this plan. If the user wants different behavior for any of them, they should flag it on review — each is a simple follow-up but not in scope right now (CLAUDE.md: stay focused, no "while we're at it"):
- **No close button on overlay stepper.** Clicking the window layer backdrop is the only way to dismiss. Matches how dees-modal with `showCloseButton: false` behaves. Can add a close button in a follow-up.
- **No `onComplete` callback in `createAndShow`.** The last step doesn't auto-destroy the overlay — the app controls it via the step's `validationFunc`. Can add a callback option in a follow-up.
- **No width/size options in `createAndShow`.** The step tile continues to use the stepper's existing `max-width: 500px`. Can parameterize in a follow-up.
- **Box-shadow in the `.step` transition list** is dropped from the transition for cleanliness — the box-shadow is now on `dees-tile::part(outer)` and doesn't change between selected/hiddenStep, so transitioning it was already a no-op.
- **`pnpm start` / dev server path**: I'll reuse the existing server on port 8080 that was already listening when this session began; if that server doesn't serve the stepper demo, I'll start wcctools manually.
## Risk
- **Low-medium.** The change is localized to one component and its demo. No API removal, only an additive `createAndShow` + an optional `footerContent` field. External consumers of the inline API continue to work if they only set `steps` + `selectedStep`.
- **Biggest risk:** SweetScroll's `offsetTop` / `offsetHeight` measurements on `<dees-tile>` may compute differently than on the former `<div class="step">` because `dees-tile` has an internal `display: flex; flex-direction: column;` host and a `.tile-outer { flex: 1; min-height: 0; }` inner frame. If the scroll math drifts, the mitigation is to keep the `.step` wrapper as an outer `<div>` that **contains** a `<dees-tile>`, rather than putting the class directly on `<dees-tile>`. That preserves the exact box model SweetScroll was measuring. I'll try the direct-class approach first (simpler) and fall back to the wrapper approach if the scroll target looks off in the demo.
- **Second risk:** The `:host([overlay])` attribute selector requires `overlay` to be a reflected `@property`, not `@state`. I've already accounted for this in the plan (decorator change).

View File

@@ -1,8 +1,7 @@
import { html } from '@design.estate/dees-element';
import { DeesStepper, type IStep } from './dees-stepper.js';
export const stepperDemo = () => html`
<dees-stepper
.steps=${[
const demoSteps: IStep[] = [
{
title: 'Account Setup',
content: html`
@@ -14,7 +13,7 @@ export const stepperDemo = () => html`
`,
validationFunc: async (stepperArg, elementArg) => {
const deesForm = elementArg.querySelector('dees-form');
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
deesForm!.addEventListener('formData', () => stepperArg.goNext(), { once: true });
},
},
{
@@ -28,7 +27,7 @@ export const stepperDemo = () => html`
`,
validationFunc: async (stepperArg, elementArg) => {
const deesForm = elementArg.querySelector('dees-form');
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
deesForm!.addEventListener('formData', () => stepperArg.goNext(), { once: true });
},
},
{
@@ -42,7 +41,7 @@ export const stepperDemo = () => html`
`,
validationFunc: async (stepperArg, elementArg) => {
const deesForm = elementArg.querySelector('dees-form');
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
deesForm!.addEventListener('formData', () => stepperArg.goNext(), { once: true });
},
},
{
@@ -65,7 +64,7 @@ export const stepperDemo = () => html`
`,
validationFunc: async (stepperArg, elementArg) => {
const deesForm = elementArg.querySelector('dees-form');
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
deesForm!.addEventListener('formData', () => stepperArg.goNext(), { once: true });
},
},
{
@@ -87,7 +86,7 @@ export const stepperDemo = () => html`
`,
validationFunc: async (stepperArg, elementArg) => {
const deesForm = elementArg.querySelector('dees-form');
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
deesForm!.addEventListener('formData', () => stepperArg.goNext(), { once: true });
},
},
{
@@ -101,7 +100,7 @@ export const stepperDemo = () => html`
`,
validationFunc: async (stepperArg, elementArg) => {
const deesForm = elementArg.querySelector('dees-form');
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
deesForm!.addEventListener('formData', () => stepperArg.goNext(), { once: true });
},
},
{
@@ -118,7 +117,7 @@ export const stepperDemo = () => html`
`,
validationFunc: async (stepperArg, elementArg) => {
const deesForm = elementArg.querySelector('dees-form');
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
deesForm!.addEventListener('formData', () => stepperArg.goNext(), { once: true });
},
},
{
@@ -129,6 +128,21 @@ export const stepperDemo = () => html`
</dees-panel>
`,
},
] as const}
></dees-stepper>
];
const cloneSteps = (): IStep[] => demoSteps.map((step) => ({ ...step }));
export const stepperDemo = () => html`
<div style="position: absolute; inset: 0;">
<div
style="position: absolute; top: 16px; left: 50%; transform: translateX(-50%); z-index: 10;"
>
<dees-button
@click=${async () => {
await DeesStepper.createAndShow({ steps: cloneSteps() });
}}
>Open stepper as overlay</dees-button>
</div>
<dees-stepper .steps=${cloneSteps()}></dees-stepper>
</div>
`;

View File

@@ -1,5 +1,4 @@
import * as plugins from '../../00plugins.js';
import * as colors from '../../00colors.js';
import {
DeesElement,
@@ -16,10 +15,15 @@ import {
import * as domtools from '@design.estate/dees-domtools';
import { stepperDemo } from './dees-stepper.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
import { cssGeistFontFamily } from '../../00fonts.js';
import { zIndexRegistry } from '../../00zindex.js';
import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
import '../dees-tile/dees-tile.js';
export interface IStep {
title: string;
content: TemplateResult;
footerContent?: TemplateResult;
validationFunc?: (stepper: DeesStepper, htmlElement: HTMLElement, signal?: AbortSignal) => Promise<any>;
onReturnToStepFunc?: (stepper: DeesStepper, htmlElement: HTMLElement) => Promise<any>;
validationFuncCalled?: boolean;
@@ -34,9 +38,32 @@ declare global {
@customElement('dees-stepper')
export class DeesStepper extends DeesElement {
// STATIC
public static demo = stepperDemo;
public static demoGroups = ['Layout', 'Form'];
public static async createAndShow(optionsArg: {
steps: IStep[];
}): Promise<DeesStepper> {
const body = document.body;
const stepper = new DeesStepper();
stepper.steps = optionsArg.steps;
stepper.overlay = true;
stepper.windowLayer = await DeesWindowLayer.createAndShow({ blur: true });
stepper.windowLayer.addEventListener('click', async () => {
await stepper.destroy();
});
body.append(stepper.windowLayer);
body.append(stepper);
// Get z-index for stepper (should be above window layer)
stepper.stepperZIndex = zIndexRegistry.getNextZIndex();
zIndexRegistry.register(stepper, stepper.stepperZIndex);
return stepper;
}
// INSTANCE
@property({
type: Array,
})
@@ -47,6 +74,17 @@ export class DeesStepper extends DeesElement {
})
accessor selectedStep!: IStep;
@property({
type: Boolean,
reflect: true,
})
accessor overlay: boolean = false;
@property({ type: Number, attribute: false })
accessor stepperZIndex: number = 1000;
private windowLayer?: DeesWindowLayer;
constructor() {
super();
}
@@ -60,7 +98,18 @@ export class DeesStepper extends DeesElement {
position: absolute;
width: 100%;
height: 100%;
font-family: ${cssGeistFontFamily};
color: var(--dees-color-text-primary);
}
:host([overlay]) {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
}
.stepperContainer {
position: absolute;
width: 100%;
@@ -68,101 +117,120 @@ export class DeesStepper extends DeesElement {
overflow: hidden;
}
.step {
.stepperContainer.overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
}
.stepperContainer.predestroy {
opacity: 0;
transition: opacity 0.2s ease-in;
}
dees-tile.step {
position: relative;
pointer-events: none;
overflow: hidden;
transition: transform 0.7s cubic-bezier(0.87, 0, 0.13, 1), box-shadow 0.7s cubic-bezier(0.87, 0, 0.13, 1), filter 0.7s cubic-bezier(0.87, 0, 0.13, 1), border 0.7s cubic-bezier(0.87, 0, 0.13, 1);
max-width: 500px;
min-height: 300px;
border-radius: 12px;
background: ${cssManager.bdTheme('#ffffff', '#0f0f11')};
border: 1px solid ${cssManager.bdTheme('#e2e8f0', '#272729')};
color: ${cssManager.bdTheme('#0f172a', '#f5f5f5')};
margin: auto;
margin-bottom: 20px;
filter: opacity(0.55) saturate(0.85);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
transition: transform 0.7s cubic-bezier(0.87, 0, 0.13, 1), filter 0.7s cubic-bezier(0.87, 0, 0.13, 1);
user-select: none;
}
.step.selected {
dees-tile.step.selected {
pointer-events: all;
filter: opacity(1) saturate(1);
user-select: auto;
}
.step.hiddenStep {
dees-tile.step.hiddenStep {
filter: opacity(0);
}
.step.entrance {
transition: transform 0.35s ease, box-shadow 0.35s ease, filter 0.35s ease, border 0.35s ease;
dees-tile.step.entrance {
transition: transform 0.35s ease, filter 0.35s ease;
}
.step.entrance.hiddenStep {
dees-tile.step.entrance.hiddenStep {
transform: translateY(16px);
}
.step:last-child {
dees-tile.step:last-child {
margin-bottom: 100vh;
}
.step .stepCounter {
color: ${cssManager.bdTheme('#64748b', '#a1a1aa')};
position: absolute;
top: 12px;
right: 12px;
padding: 6px 14px;
font-size: 12px;
border-radius: 999px;
background: ${cssManager.bdTheme('rgba(226, 232, 240, 0.5)', 'rgba(63, 63, 70, 0.45)')};
border: 1px solid ${cssManager.bdTheme('rgba(226, 232, 240, 0.7)', 'rgba(63, 63, 70, 0.6)')};
.stepperContainer.overlay dees-tile.step::part(outer) {
box-shadow:
0 0 0 1px ${cssManager.bdTheme('hsl(0 0% 0% / 0.03)', 'hsl(0 0% 100% / 0.03)')},
0 8px 40px ${cssManager.bdTheme('hsl(0 0% 0% / 0.12)', 'hsl(0 0% 0% / 0.5)')},
0 2px 8px ${cssManager.bdTheme('hsl(0 0% 0% / 0.06)', 'hsl(0 0% 0% / 0.25)')};
}
.step .goBack {
position: absolute;
top: 12px;
left: 12px;
.step-header {
height: 36px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px 0 12px;
gap: 12px;
}
.goBack-spacer {
width: 1px;
}
.step-header .goBack {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
height: 24px;
padding: 0 8px;
font-size: 12px;
font-weight: 500;
border-radius: 999px;
border: 1px solid ${cssManager.bdTheme('rgba(226, 232, 240, 0.9)', 'rgba(63, 63, 70, 0.85)')};
background: ${cssManager.bdTheme('rgba(255, 255, 255, 0.9)', 'rgba(39, 39, 42, 0.85)')};
color: ${cssManager.bdTheme('#475569', '#d4d4d8')};
line-height: 1;
border: none;
background: transparent;
color: var(--dees-color-text-muted);
border-radius: 4px;
cursor: pointer;
transition: border 0.2s ease, color 0.2s ease, background 0.2s ease, transform 0.2s ease;
transition: background 0.15s ease, color 0.15s ease, transform 0.2s ease;
}
.step .goBack:hover {
color: ${cssManager.bdTheme('#0f172a', '#fafafa')};
border-color: ${cssManager.bdTheme(colors.dark.blue, colors.dark.blue)};
background: ${cssManager.bdTheme('rgba(226, 232, 240, 0.95)', 'rgba(63, 63, 70, 0.7)')};
.step-header .goBack:hover {
background: var(--dees-color-hover);
color: var(--dees-color-text-secondary);
transform: translateX(-2px);
}
.step .goBack:active {
color: ${cssManager.bdTheme('#0f172a', '#fafafa')};
border-color: ${cssManager.bdTheme(colors.dark.blueActive, colors.dark.blueActive)};
background: ${cssManager.bdTheme('rgba(226, 232, 240, 0.85)', 'rgba(63, 63, 70, 0.6)')};
.step-header .goBack:active {
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 15%)')};
}
.step .goBack span {
.step-header .goBack span {
transition: transform 0.2s ease;
display: inline-block;
}
.step .goBack:hover span {
.step-header .goBack:hover span {
transform: translateX(-2px);
}
.step .title {
.step-header .stepCounter {
color: var(--dees-color-text-muted);
font-size: 12px;
font-weight: 500;
letter-spacing: -0.01em;
padding: 0 8px;
}
.step-body .title {
text-align: center;
padding-top: 64px;
padding-top: 32px;
font-family: 'Geist Sans', sans-serif;
font-size: 24px;
font-weight: 600;
@@ -170,35 +238,53 @@ export class DeesStepper extends DeesElement {
color: inherit;
}
.step .content {
.step-body .content {
padding: 32px;
}
.step-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 12px 16px;
}
`,
];
public render() {
return html`
<div class="stepperContainer">
${this.steps.map(
(stepArg) =>
html`<div
class="step ${stepArg === this.selectedStep
? 'selected'
: null} ${this.getIndexOfStep(stepArg) > this.getIndexOfStep(this.selectedStep)
? 'hiddenStep'
: ''} ${this.getIndexOfStep(stepArg) === 0 ? 'entrance' : ''}"
<div
class="stepperContainer ${this.overlay ? 'overlay' : ''}"
style=${this.overlay ? `z-index: ${this.stepperZIndex};` : ''}
>
${this.getIndexOfStep(stepArg) > 0
? html`<div class="goBack" @click=${this.goBack}><span style="font-family: Inter"><-</span> go to previous step</div>`
: ``}
${this.steps.map((stepArg, stepIndex) => {
const isSelected = stepArg === this.selectedStep;
const isHidden =
this.getIndexOfStep(stepArg) > this.getIndexOfStep(this.selectedStep);
const isFirst = stepIndex === 0;
return html`<dees-tile
class="step ${isSelected ? 'selected' : ''} ${isHidden ? 'hiddenStep' : ''} ${isFirst ? 'entrance' : ''}"
>
<div slot="header" class="step-header">
${!isFirst
? html`<div class="goBack" @click=${this.goBack}>
<span style="font-family: Inter"><-</span> go to previous step
</div>`
: html`<div class="goBack-spacer"></div>`}
<div class="stepCounter">
Step ${this.steps.findIndex((elementArg) => elementArg === stepArg) + 1} of
${this.steps.length}
Step ${stepIndex + 1} of ${this.steps.length}
</div>
</div>
<div class="step-body">
<div class="title">${stepArg.title}</div>
<div class="content">${stepArg.content}</div>
</div> `
)}
</div>
${stepArg.footerContent
? html`<div slot="footer" class="step-footer">${stepArg.footerContent}</div>`
: ''}
</dees-tile>`;
})}
</div>
`;
}
@@ -296,4 +382,20 @@ export class DeesStepper extends DeesElement {
nextStep.validationFuncCalled = false;
this.selectedStep = nextStep;
}
public async destroy() {
const domtools = await this.domtoolsPromise;
const container = this.shadowRoot!.querySelector('.stepperContainer');
container?.classList.add('predestroy');
await domtools.convenience.smartdelay.delayFor(200);
if (this.parentElement) {
this.parentElement.removeChild(this);
}
if (this.windowLayer) {
await this.windowLayer.destroy();
}
// Unregister from z-index registry
zIndexRegistry.unregister(this);
}
}