${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`
${stepArg.title}
${stepArg.content}
${stepArg.footerContent
? html``
: ''}
`;
})}
`;
}
```
**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:157–161) so the overlay stepper reads as "same visual language as modal."
**Restyle `.step-header` (NEW — the ``):**
```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 228–229). These continue to match since I'm keeping those class names on the `
` elements. **No selector changes needed.**
One subtlety: `offsetTop` / `offsetHeight` on `` 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`
{
const stepper = await DeesStepper.createAndShow({
steps: [/* same steps as inline demo */],
});
}}>Open stepper as overlay
`;
```
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 `` may compute differently than on the former `` 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 `
` that **contains** a ``, rather than putting the class directly on ``. 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).