Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a1be59a51 | |||
| a3b2ace88d | |||
| c34037265e | |||
| 8c230fe3af | |||
| a695d60770 | |||
| ea30cbd381 | |||
| 39a4bf0dd3 | |||
| d5c9bc69b3 | |||
| 27f8ab752d |
1
.claude/scheduled_tasks.lock
Normal file
1
.claude/scheduled_tasks.lock
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"sessionId":"4b0f0a7f-f187-40a3-a38b-cb9a7e877011","pid":3110692,"acquiredAt":1775914414249}
|
||||||
27
changelog.md
27
changelog.md
@@ -1,5 +1,32 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-11 - 3.72.0 - feat(dees-stepper)
|
||||||
|
add configurable cancellation flow with confirmation modal
|
||||||
|
|
||||||
|
- adds a cancelable option to control whether steppers can be dismissed
|
||||||
|
- shows a confirmation modal when canceling via the new Cancel button or overlay backdrop
|
||||||
|
- updates footer button rendering and separators to support the new cancel action consistently
|
||||||
|
|
||||||
|
## 2026-04-11 - 3.71.1 - fix(dees-modal)
|
||||||
|
move modal content scrolling into dees-tile so long content stays scrollable with pinned header and actions
|
||||||
|
|
||||||
|
- Update dees-tile content area to use vertical scrolling when constrained by a max-height while keeping horizontal overflow clipped.
|
||||||
|
- Remove duplicate scrolling styles from dees-modal and rely on the shared tile container behavior.
|
||||||
|
- Add modal demo cases for long article, list, and form content to verify internal scrolling.
|
||||||
|
|
||||||
|
## 2026-04-11 - 3.71.0 - feat(dees-stepper)
|
||||||
|
add footer menu actions with form-aware step validation
|
||||||
|
|
||||||
|
- replace step footer submit handling with configurable menuOptions actions
|
||||||
|
- disable the primary footer action until required form fields are completed and show a completion hint
|
||||||
|
- dispatch form data before running primary step actions and clean up form subscriptions on destroy
|
||||||
|
- adjust overlay host positioning so the stepper container controls viewport layering correctly
|
||||||
|
|
||||||
|
## 2026-04-11 - 3.70.1 - fix(dees-modal)
|
||||||
|
use icon font sizing for modal header buttons
|
||||||
|
|
||||||
|
- replace fixed width and height on header button icons with font-size to align dees-icon rendering
|
||||||
|
|
||||||
## 2026-04-08 - 3.70.0 - feat(dees-table)
|
## 2026-04-08 - 3.70.0 - feat(dees-table)
|
||||||
add opt-in flash highlighting for updated table cells
|
add opt-in flash highlighting for updated table cells
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@design.estate/dees-catalog",
|
"name": "@design.estate/dees-catalog",
|
||||||
"version": "3.70.0",
|
"version": "3.72.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
||||||
"main": "dist_ts_web/index.js",
|
"main": "dist_ts_web/index.js",
|
||||||
|
|||||||
352
readme.plan.md
Normal file
352
readme.plan.md
Normal 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 20–299)
|
||||||
|
|
||||||
|
- `IStep` interface — `title`, `content: TemplateResult`, `validationFunc`, `onReturnToStepFunc`, internal flags (lines 20–27).
|
||||||
|
- `:host { position: absolute; width: 100%; height: 100%; }` (lines 59–63).
|
||||||
|
- `.stepperContainer` — absolute, 100% w/h, `overflow: hidden`, holds SweetScroll (lines 64–69).
|
||||||
|
- `.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 71–97). **These frame styles overlap with what `dees-tile` already provides.**
|
||||||
|
- `.step.selected` — `filter: opacity(1) saturate(1)` (lines 89–93). **Scroll-through visual cue, keep.**
|
||||||
|
- `.step.hiddenStep` — `filter: opacity(0)` (line 95). **Keep.**
|
||||||
|
- `.step.entrance` — faster transition variant for first-render (lines 99–105). **Keep.**
|
||||||
|
- `.step .stepCounter` — `position: absolute; top: 12px; right: 12px;` pill (lines 111–121). **Move into header slot as a flex child.**
|
||||||
|
- `.step .goBack` — `position: absolute; top: 12px; left: 12px;` pill + icon + hover (lines 123–161). **Move into header slot as a flex child.**
|
||||||
|
- `.step .title` — centered, 24px, 64px top padding (lines 163–171). **Keep inside the tile's content slot; remove the 64px top padding since goBack/counter no longer overlap it.**
|
||||||
|
- `.step .content` — 32px padding (lines 173–175). **Keep.**
|
||||||
|
- `render()` (lines 179–204) — maps `steps` to `.step` divs.
|
||||||
|
- `setScrollStatus()` (lines 226–263) — SweetScroll container setup + step validation kick-off. **Keep mostly as-is; selectors still target `.step`/`.selected` so rename cautiously.**
|
||||||
|
- `firstUpdated` (lines 210–218), `updated` (lines 220–222), `goBack` (lines 265–282), `goNext` (lines 284–298) — 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>←</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:157–161) 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 228–229). 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).
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-catalog',
|
name: '@design.estate/dees-catalog',
|
||||||
version: '3.70.0',
|
version: '3.72.0',
|
||||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,134 +1,137 @@
|
|||||||
import { html } from '@design.estate/dees-element';
|
import { html } from '@design.estate/dees-element';
|
||||||
|
import { DeesStepper, type IStep } from './dees-stepper.js';
|
||||||
|
|
||||||
|
const demoSteps: IStep[] = [
|
||||||
|
{
|
||||||
|
title: 'Account Setup',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text key="email" label="Work Email" required></dees-input-text>
|
||||||
|
<dees-input-text key="password" label="Create Password" type="password" required></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Profile Details',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text key="firstName" label="First Name" required></dees-input-text>
|
||||||
|
<dees-input-text key="lastName" label="Last Name" required></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Contact Information',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-phone key="phone" label="Mobile Number" required></dees-input-phone>
|
||||||
|
<dees-input-text key="company" label="Company"></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Team Size',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-dropdown
|
||||||
|
key="teamSize"
|
||||||
|
label="How big is your team?"
|
||||||
|
.options=${[
|
||||||
|
{ label: '1-5', value: '1-5' },
|
||||||
|
{ label: '6-20', value: '6-20' },
|
||||||
|
{ label: '21-50', value: '21-50' },
|
||||||
|
{ label: '51+', value: '51+' },
|
||||||
|
]}
|
||||||
|
required
|
||||||
|
></dees-input-dropdown>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Goals',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-multitoggle
|
||||||
|
key="goal"
|
||||||
|
label="Main objective"
|
||||||
|
.options=${[
|
||||||
|
{ label: 'Onboarding', value: 'onboarding' },
|
||||||
|
{ label: 'Analytics', value: 'analytics' },
|
||||||
|
{ label: 'Automation', value: 'automation' },
|
||||||
|
]}
|
||||||
|
required
|
||||||
|
></dees-input-multitoggle>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Brand Preferences',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text key="brandColor" label="Primary brand color"></dees-input-text>
|
||||||
|
<dees-input-text key="tone" label="Preferred tone (e.g. friendly, formal)"></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Integrations',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-list
|
||||||
|
key="integrations"
|
||||||
|
label="Integrations in use"
|
||||||
|
placeholder="Add integration"
|
||||||
|
></dees-input-list>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Review & Launch',
|
||||||
|
content: html`
|
||||||
|
<dees-panel>
|
||||||
|
<p>Almost there! Review your selections and launch whenever you're ready.</p>
|
||||||
|
</dees-panel>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Launch', action: async (stepper) => stepper!.goNext() },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const cloneSteps = (): IStep[] => demoSteps.map((step) => ({ ...step }));
|
||||||
|
|
||||||
export const stepperDemo = () => html`
|
export const stepperDemo = () => html`
|
||||||
<dees-stepper
|
<div style="position: absolute; inset: 0;">
|
||||||
.steps=${[
|
<div
|
||||||
{
|
style="position: absolute; top: 16px; left: 50%; transform: translateX(-50%); z-index: 10;"
|
||||||
title: 'Account Setup',
|
>
|
||||||
content: html`
|
<dees-button
|
||||||
<dees-form>
|
@click=${async () => {
|
||||||
<dees-input-text key="email" label="Work Email" required></dees-input-text>
|
await DeesStepper.createAndShow({ steps: cloneSteps() });
|
||||||
<dees-input-text key="password" label="Create Password" type="password" required></dees-input-text>
|
}}
|
||||||
<dees-form-submit>Continue</dees-form-submit>
|
>Open stepper as overlay</dees-button>
|
||||||
</dees-form>
|
</div>
|
||||||
`,
|
<dees-stepper .steps=${cloneSteps()}></dees-stepper>
|
||||||
validationFunc: async (stepperArg, elementArg) => {
|
</div>
|
||||||
const deesForm = elementArg.querySelector('dees-form');
|
|
||||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Profile Details',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text key="firstName" label="First Name" required></dees-input-text>
|
|
||||||
<dees-input-text key="lastName" label="Last Name" required></dees-input-text>
|
|
||||||
<dees-form-submit>Continue</dees-form-submit>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
validationFunc: async (stepperArg, elementArg) => {
|
|
||||||
const deesForm = elementArg.querySelector('dees-form');
|
|
||||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Contact Information',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-phone key="phone" label="Mobile Number" required></dees-input-phone>
|
|
||||||
<dees-input-text key="company" label="Company"></dees-input-text>
|
|
||||||
<dees-form-submit>Continue</dees-form-submit>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
validationFunc: async (stepperArg, elementArg) => {
|
|
||||||
const deesForm = elementArg.querySelector('dees-form');
|
|
||||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Team Size',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-dropdown
|
|
||||||
key="teamSize"
|
|
||||||
label="How big is your team?"
|
|
||||||
.options=${[
|
|
||||||
{ label: '1-5', value: '1-5' },
|
|
||||||
{ label: '6-20', value: '6-20' },
|
|
||||||
{ label: '21-50', value: '21-50' },
|
|
||||||
{ label: '51+', value: '51+' },
|
|
||||||
]}
|
|
||||||
required
|
|
||||||
></dees-input-dropdown>
|
|
||||||
<dees-form-submit>Continue</dees-form-submit>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
validationFunc: async (stepperArg, elementArg) => {
|
|
||||||
const deesForm = elementArg.querySelector('dees-form');
|
|
||||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Goals',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-multitoggle
|
|
||||||
key="goal"
|
|
||||||
label="Main objective"
|
|
||||||
.options=${[
|
|
||||||
{ label: 'Onboarding', value: 'onboarding' },
|
|
||||||
{ label: 'Analytics', value: 'analytics' },
|
|
||||||
{ label: 'Automation', value: 'automation' },
|
|
||||||
]}
|
|
||||||
required
|
|
||||||
></dees-input-multitoggle>
|
|
||||||
<dees-form-submit>Continue</dees-form-submit>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
validationFunc: async (stepperArg, elementArg) => {
|
|
||||||
const deesForm = elementArg.querySelector('dees-form');
|
|
||||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Brand Preferences',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text key="brandColor" label="Primary brand color"></dees-input-text>
|
|
||||||
<dees-input-text key="tone" label="Preferred tone (e.g. friendly, formal)"></dees-input-text>
|
|
||||||
<dees-form-submit>Continue</dees-form-submit>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
validationFunc: async (stepperArg, elementArg) => {
|
|
||||||
const deesForm = elementArg.querySelector('dees-form');
|
|
||||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Integrations',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-list
|
|
||||||
key="integrations"
|
|
||||||
label="Integrations in use"
|
|
||||||
placeholder="Add integration"
|
|
||||||
></dees-input-list>
|
|
||||||
<dees-form-submit>Continue</dees-form-submit>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
validationFunc: async (stepperArg, elementArg) => {
|
|
||||||
const deesForm = elementArg.querySelector('dees-form');
|
|
||||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Review & Launch',
|
|
||||||
content: html`
|
|
||||||
<dees-panel>
|
|
||||||
<p>Almost there! Review your selections and launch whenever you're ready.</p>
|
|
||||||
</dees-panel>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
] as const}
|
|
||||||
></dees-stepper>
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as plugins from '../../00plugins.js';
|
import * as plugins from '../../00plugins.js';
|
||||||
import * as colors from '../../00colors.js';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeesElement,
|
DeesElement,
|
||||||
@@ -16,10 +15,17 @@ import {
|
|||||||
import * as domtools from '@design.estate/dees-domtools';
|
import * as domtools from '@design.estate/dees-domtools';
|
||||||
import { stepperDemo } from './dees-stepper.demo.js';
|
import { stepperDemo } from './dees-stepper.demo.js';
|
||||||
import { themeDefaultStyles } from '../../00theme.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 { DeesModal } from '../../00group-overlay/dees-modal/dees-modal.js';
|
||||||
|
import type { DeesForm } from '../../00group-form/dees-form/dees-form.js';
|
||||||
|
import '../dees-tile/dees-tile.js';
|
||||||
|
|
||||||
export interface IStep {
|
export interface IStep {
|
||||||
title: string;
|
title: string;
|
||||||
content: TemplateResult;
|
content: TemplateResult;
|
||||||
|
menuOptions?: plugins.tsclass.website.IMenuItem<DeesStepper>[];
|
||||||
validationFunc?: (stepper: DeesStepper, htmlElement: HTMLElement, signal?: AbortSignal) => Promise<any>;
|
validationFunc?: (stepper: DeesStepper, htmlElement: HTMLElement, signal?: AbortSignal) => Promise<any>;
|
||||||
onReturnToStepFunc?: (stepper: DeesStepper, htmlElement: HTMLElement) => Promise<any>;
|
onReturnToStepFunc?: (stepper: DeesStepper, htmlElement: HTMLElement) => Promise<any>;
|
||||||
validationFuncCalled?: boolean;
|
validationFuncCalled?: boolean;
|
||||||
@@ -34,9 +40,33 @@ declare global {
|
|||||||
|
|
||||||
@customElement('dees-stepper')
|
@customElement('dees-stepper')
|
||||||
export class DeesStepper extends DeesElement {
|
export class DeesStepper extends DeesElement {
|
||||||
|
// STATIC
|
||||||
public static demo = stepperDemo;
|
public static demo = stepperDemo;
|
||||||
public static demoGroups = ['Layout', 'Form'];
|
public static demoGroups = ['Layout', 'Form'];
|
||||||
|
|
||||||
|
public static async createAndShow(optionsArg: {
|
||||||
|
steps: IStep[];
|
||||||
|
cancelable?: boolean;
|
||||||
|
}): Promise<DeesStepper> {
|
||||||
|
const body = document.body;
|
||||||
|
const stepper = new DeesStepper();
|
||||||
|
stepper.steps = optionsArg.steps;
|
||||||
|
stepper.overlay = true;
|
||||||
|
if (optionsArg.cancelable !== undefined) {
|
||||||
|
stepper.cancelable = optionsArg.cancelable;
|
||||||
|
}
|
||||||
|
stepper.windowLayer = await DeesWindowLayer.createAndShow({ blur: true });
|
||||||
|
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({
|
@property({
|
||||||
type: Array,
|
type: Array,
|
||||||
})
|
})
|
||||||
@@ -47,6 +77,37 @@ export class DeesStepper extends DeesElement {
|
|||||||
})
|
})
|
||||||
accessor selectedStep!: IStep;
|
accessor selectedStep!: IStep;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
})
|
||||||
|
accessor overlay: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When true (default), the stepper renders a Cancel button in every step's
|
||||||
|
* footer, and clicking the backdrop (overlay mode) triggers the same cancel
|
||||||
|
* confirmation flow. Set to false for forced flows where the user must
|
||||||
|
* complete the stepper — no Cancel button, no backdrop dismissal.
|
||||||
|
*/
|
||||||
|
@property({
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
})
|
||||||
|
accessor cancelable: boolean = true;
|
||||||
|
|
||||||
|
@property({ type: Number, attribute: false })
|
||||||
|
accessor stepperZIndex: number = 1000;
|
||||||
|
|
||||||
|
@property({ type: Object, attribute: false })
|
||||||
|
accessor activeForm: DeesForm | null = null;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: false })
|
||||||
|
accessor activeFormValid: boolean = true;
|
||||||
|
|
||||||
|
private activeFormSubscription?: { unsubscribe: () => void };
|
||||||
|
|
||||||
|
private windowLayer?: DeesWindowLayer;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -60,7 +121,24 @@ export class DeesStepper extends DeesElement {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
font-family: ${cssGeistFontFamily};
|
||||||
|
color: var(--dees-color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* In overlay mode the host is "transparent" to layout — the inner
|
||||||
|
* .stepperContainer.overlay is what pins to the viewport and carries the
|
||||||
|
* z-index. Keeping :host unpositioned avoids nesting the stacking context
|
||||||
|
* under an auto-z-index parent (which was trapping .stepperContainer
|
||||||
|
* below DeesWindowLayer's sibling layers). This mirrors how dees-modal
|
||||||
|
* keeps its own :host unpositioned and lets .modalContainer drive layout.
|
||||||
|
*/
|
||||||
|
:host([overlay]) {
|
||||||
|
position: static;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.stepperContainer {
|
.stepperContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -68,101 +146,120 @@ export class DeesStepper extends DeesElement {
|
|||||||
overflow: hidden;
|
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;
|
position: relative;
|
||||||
pointer-events: none;
|
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;
|
max-width: 500px;
|
||||||
min-height: 300px;
|
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: auto;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
filter: opacity(0.55) saturate(0.85);
|
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;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step.selected {
|
dees-tile.step.selected {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
filter: opacity(1) saturate(1);
|
filter: opacity(1) saturate(1);
|
||||||
user-select: auto;
|
user-select: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step.hiddenStep {
|
dees-tile.step.hiddenStep {
|
||||||
filter: opacity(0);
|
filter: opacity(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.step.entrance {
|
dees-tile.step.entrance {
|
||||||
transition: transform 0.35s ease, box-shadow 0.35s ease, filter 0.35s ease, border 0.35s ease;
|
transition: transform 0.35s ease, filter 0.35s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step.entrance.hiddenStep {
|
dees-tile.step.entrance.hiddenStep {
|
||||||
transform: translateY(16px);
|
transform: translateY(16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.step:last-child {
|
dees-tile.step:last-child {
|
||||||
margin-bottom: 100vh;
|
margin-bottom: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step .stepCounter {
|
.stepperContainer.overlay dees-tile.step::part(outer) {
|
||||||
color: ${cssManager.bdTheme('#64748b', '#a1a1aa')};
|
box-shadow:
|
||||||
position: absolute;
|
0 0 0 1px ${cssManager.bdTheme('hsl(0 0% 0% / 0.03)', 'hsl(0 0% 100% / 0.03)')},
|
||||||
top: 12px;
|
0 8px 40px ${cssManager.bdTheme('hsl(0 0% 0% / 0.12)', 'hsl(0 0% 0% / 0.5)')},
|
||||||
right: 12px;
|
0 2px 8px ${cssManager.bdTheme('hsl(0 0% 0% / 0.06)', 'hsl(0 0% 0% / 0.25)')};
|
||||||
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)')};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.step .goBack {
|
.step-header {
|
||||||
position: absolute;
|
height: 36px;
|
||||||
top: 12px;
|
display: flex;
|
||||||
left: 12px;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 8px 0 12px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goBack-spacer {
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-header .goBack {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 6px 12px;
|
height: 24px;
|
||||||
|
padding: 0 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border-radius: 999px;
|
line-height: 1;
|
||||||
border: 1px solid ${cssManager.bdTheme('rgba(226, 232, 240, 0.9)', 'rgba(63, 63, 70, 0.85)')};
|
border: none;
|
||||||
background: ${cssManager.bdTheme('rgba(255, 255, 255, 0.9)', 'rgba(39, 39, 42, 0.85)')};
|
background: transparent;
|
||||||
color: ${cssManager.bdTheme('#475569', '#d4d4d8')};
|
color: var(--dees-color-text-muted);
|
||||||
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
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 {
|
.step-header .goBack:hover {
|
||||||
color: ${cssManager.bdTheme('#0f172a', '#fafafa')};
|
background: var(--dees-color-hover);
|
||||||
border-color: ${cssManager.bdTheme(colors.dark.blue, colors.dark.blue)};
|
color: var(--dees-color-text-secondary);
|
||||||
background: ${cssManager.bdTheme('rgba(226, 232, 240, 0.95)', 'rgba(63, 63, 70, 0.7)')};
|
|
||||||
transform: translateX(-2px);
|
transform: translateX(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.step .goBack:active {
|
.step-header .goBack:active {
|
||||||
color: ${cssManager.bdTheme('#0f172a', '#fafafa')};
|
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 15%)')};
|
||||||
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 .goBack span {
|
.step-header .goBack span {
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step .goBack:hover span {
|
.step-header .goBack:hover span {
|
||||||
transform: translateX(-2px);
|
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;
|
text-align: center;
|
||||||
padding-top: 64px;
|
padding-top: 32px;
|
||||||
font-family: 'Geist Sans', sans-serif;
|
font-family: 'Geist Sans', sans-serif;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -170,35 +267,149 @@ export class DeesStepper extends DeesElement {
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step .content {
|
.step-body .content {
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Footer: modal-style bottom buttons --- */
|
||||||
|
.bottomButtons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
height: 36px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomButtons .bottomButton {
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--dees-color-text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Border-left separator on every button EXCEPT the first one.
|
||||||
|
Uses general sibling so the stepHint (if rendered on the left) does
|
||||||
|
not shift which button counts as "first" and create a phantom border. */
|
||||||
|
.bottomButtons .bottomButton ~ .bottomButton {
|
||||||
|
border-left: 1px solid var(--dees-color-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomButtons .bottomButton:hover {
|
||||||
|
background: var(--dees-color-hover);
|
||||||
|
color: var(--dees-color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomButtons .bottomButton:active {
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(0 0% 13%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomButtons .bottomButton.primary {
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomButtons .bottomButton.primary:hover {
|
||||||
|
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomButtons .bottomButton.primary:active {
|
||||||
|
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.12)', 'hsl(213.1 93.9% 67.8% / 0.12)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomButtons .bottomButton.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomButtons .bottomButton.disabled:hover {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--dees-color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hint shown on the left of the footer when the active step's form has
|
||||||
|
unfilled required fields. Uses margin-right: auto to push right-aligned
|
||||||
|
buttons to the right while keeping the hint flush-left. */
|
||||||
|
.bottomButtons .stepHint {
|
||||||
|
margin-right: auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--dees-color-text-muted);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return html`
|
return html`
|
||||||
<div class="stepperContainer">
|
<div
|
||||||
${this.steps.map(
|
class="stepperContainer ${this.overlay ? 'overlay' : ''}"
|
||||||
(stepArg) =>
|
style=${this.overlay ? `z-index: ${this.stepperZIndex};` : ''}
|
||||||
html`<div
|
@click=${this.handleOutsideClick}
|
||||||
class="step ${stepArg === this.selectedStep
|
>
|
||||||
? 'selected'
|
${this.steps.map((stepArg, stepIndex) => {
|
||||||
: null} ${this.getIndexOfStep(stepArg) > this.getIndexOfStep(this.selectedStep)
|
const isSelected = stepArg === this.selectedStep;
|
||||||
? 'hiddenStep'
|
const isHidden =
|
||||||
: ''} ${this.getIndexOfStep(stepArg) === 0 ? 'entrance' : ''}"
|
this.getIndexOfStep(stepArg) > this.getIndexOfStep(this.selectedStep);
|
||||||
>
|
const isFirst = stepIndex === 0;
|
||||||
${this.getIndexOfStep(stepArg) > 0
|
return html`<dees-tile
|
||||||
? html`<div class="goBack" @click=${this.goBack}><span style="font-family: Inter"><-</span> go to previous step</div>`
|
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">
|
<div class="stepCounter">
|
||||||
Step ${this.steps.findIndex((elementArg) => elementArg === stepArg) + 1} of
|
Step ${stepIndex + 1} of ${this.steps.length}
|
||||||
${this.steps.length}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step-body">
|
||||||
<div class="title">${stepArg.title}</div>
|
<div class="title">${stepArg.title}</div>
|
||||||
<div class="content">${stepArg.content}</div>
|
<div class="content">${stepArg.content}</div>
|
||||||
</div> `
|
</div>
|
||||||
)}
|
<div slot="footer" class="bottomButtons">
|
||||||
|
${isSelected && this.activeForm !== null && !this.activeFormValid
|
||||||
|
? html`<div class="stepHint">Complete form to continue</div>`
|
||||||
|
: ''}
|
||||||
|
${this.cancelable
|
||||||
|
? html`<div
|
||||||
|
class="bottomButton"
|
||||||
|
@click=${() => this.handleCancelRequest()}
|
||||||
|
>Cancel</div>`
|
||||||
|
: ''}
|
||||||
|
${stepArg.menuOptions?.map((actionArg, actionIndex) => {
|
||||||
|
const isPrimary = actionIndex === stepArg.menuOptions!.length - 1;
|
||||||
|
const isDisabled = isPrimary && this.activeForm !== null && !this.activeFormValid;
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="bottomButton ${isPrimary ? 'primary' : ''} ${isDisabled ? 'disabled' : ''}"
|
||||||
|
@click=${() => this.handleMenuOptionClick(actionArg, isPrimary)}
|
||||||
|
>${actionArg.name}</div>
|
||||||
|
`;
|
||||||
|
}) ?? ''}
|
||||||
|
</div>
|
||||||
|
</dees-tile>`;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -230,6 +441,7 @@ export class DeesStepper extends DeesElement {
|
|||||||
if (!selectedStepElement) {
|
if (!selectedStepElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.scanActiveForm(selectedStepElement);
|
||||||
if (!stepperContainer.style.paddingTop) {
|
if (!stepperContainer.style.paddingTop) {
|
||||||
stepperContainer.style.paddingTop = `${
|
stepperContainer.style.paddingTop = `${
|
||||||
stepperContainer.offsetHeight / 2 - selectedStepElement.offsetHeight / 2
|
stepperContainer.offsetHeight / 2 - selectedStepElement.offsetHeight / 2
|
||||||
@@ -296,4 +508,163 @@ export class DeesStepper extends DeesElement {
|
|||||||
nextStep.validationFuncCalled = false;
|
nextStep.validationFuncCalled = false;
|
||||||
this.selectedStep = nextStep;
|
this.selectedStep = nextStep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans the currently selected step for a <dees-form> in its content. When
|
||||||
|
* found, subscribes to the form's RxJS changeSubject so the primary
|
||||||
|
* menuOption button can auto-enable/disable as required fields are filled.
|
||||||
|
*
|
||||||
|
* If the form reference is the same as the previous activation (e.g. on a
|
||||||
|
* same-step re-render), we just recompute validity without re-subscribing.
|
||||||
|
*/
|
||||||
|
private scanActiveForm(selectedStepElement: HTMLElement) {
|
||||||
|
const form = selectedStepElement.querySelector('dees-form') as DeesForm | null;
|
||||||
|
if (form === this.activeForm) {
|
||||||
|
this.recomputeFormValid();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.activeFormSubscription?.unsubscribe();
|
||||||
|
this.activeFormSubscription = undefined;
|
||||||
|
this.activeForm = form;
|
||||||
|
if (!form) {
|
||||||
|
this.activeFormValid = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Initial check before subscribing, in case the form's firstUpdated fires
|
||||||
|
// synchronously between scan and subscribe.
|
||||||
|
this.recomputeFormValid();
|
||||||
|
this.activeFormSubscription = form.changeSubject.subscribe(() => {
|
||||||
|
this.recomputeFormValid();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recomputes activeFormValid by checking every required field in the active
|
||||||
|
* form for a non-empty value. Mirrors dees-form.updateRequiredStatus's logic
|
||||||
|
* but stores the result on the stepper instead of mutating a submit button.
|
||||||
|
*/
|
||||||
|
private recomputeFormValid() {
|
||||||
|
const form = this.activeForm;
|
||||||
|
if (!form) {
|
||||||
|
this.activeFormValid = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fields = form.getFormElements();
|
||||||
|
this.activeFormValid = fields.every(
|
||||||
|
(field) => !field.required || (field.value !== null && field.value !== undefined && field.value !== ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click handler for menuOption buttons in the footer. For the primary (last)
|
||||||
|
* button, if an active form is present, gates on required-field validity and
|
||||||
|
* triggers the form's gatherAndDispatch() before running the action. The
|
||||||
|
* action is awaited so any async work (e.g. goNext → scroll animation)
|
||||||
|
* completes before the click handler returns.
|
||||||
|
*/
|
||||||
|
private async handleMenuOptionClick(
|
||||||
|
optionArg: plugins.tsclass.website.IMenuItem<DeesStepper>,
|
||||||
|
isPrimary: boolean,
|
||||||
|
) {
|
||||||
|
const form = this.activeForm;
|
||||||
|
if (isPrimary && form) {
|
||||||
|
if (!this.activeFormValid) return;
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
form.addEventListener('formData', () => resolve(), { once: true });
|
||||||
|
form.gatherAndDispatch();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await optionArg.action(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currently-open confirmation modal (if any). Prevents double-stacking when
|
||||||
|
* the user clicks the backdrop or the Cancel button while a confirm modal
|
||||||
|
* is already visible. The reference may become stale (point to a destroyed
|
||||||
|
* modal) if the user dismisses the confirm modal by clicking its own
|
||||||
|
* backdrop — so handleCancelRequest() uses isConnected to detect that.
|
||||||
|
*/
|
||||||
|
private cancelConfirmModal?: DeesModal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click handler on .stepperContainer. Mirrors dees-modal.handleOutsideClick:
|
||||||
|
* when the user clicks the empty backdrop area (target === stepperContainer,
|
||||||
|
* not any descendant tile), trigger the cancel confirmation flow. Clicks
|
||||||
|
* that originate inside a step tile have a different event.target and are
|
||||||
|
* ignored here.
|
||||||
|
*/
|
||||||
|
private handleOutsideClick(eventArg: MouseEvent) {
|
||||||
|
if (!this.overlay) return;
|
||||||
|
if (!this.cancelable) return;
|
||||||
|
eventArg.stopPropagation();
|
||||||
|
const stepperContainer = this.shadowRoot!.querySelector('.stepperContainer');
|
||||||
|
if (eventArg.target === stepperContainer) {
|
||||||
|
this.handleCancelRequest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shown by both the backdrop click and the Cancel button in the footer.
|
||||||
|
* Presents a dees-modal asking the user to confirm cancellation. If they
|
||||||
|
* confirm, the stepper and window layer are destroyed; otherwise the
|
||||||
|
* confirm modal is dismissed and the stepper stays open.
|
||||||
|
*
|
||||||
|
* The isConnected check on the cached reference handles the case where the
|
||||||
|
* user dismissed the previous confirm modal by clicking ITS OWN backdrop —
|
||||||
|
* dees-modal.handleOutsideClick calls destroy() directly, bypassing our
|
||||||
|
* action callbacks, so our cached reference would be stale without this
|
||||||
|
* fallback check.
|
||||||
|
*/
|
||||||
|
public async handleCancelRequest() {
|
||||||
|
if (!this.cancelable) return;
|
||||||
|
if (this.cancelConfirmModal && this.cancelConfirmModal.isConnected) return;
|
||||||
|
this.cancelConfirmModal = undefined;
|
||||||
|
this.cancelConfirmModal = await DeesModal.createAndShow({
|
||||||
|
heading: 'Cancel setup?',
|
||||||
|
width: 'small',
|
||||||
|
content: html`
|
||||||
|
<p style="margin: 0;">
|
||||||
|
Are you sure you want to cancel? Any progress on the current step will be lost.
|
||||||
|
</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Continue setup',
|
||||||
|
action: async (modal) => {
|
||||||
|
this.cancelConfirmModal = undefined;
|
||||||
|
await modal!.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Yes, cancel',
|
||||||
|
action: async (modal) => {
|
||||||
|
this.cancelConfirmModal = undefined;
|
||||||
|
await modal!.destroy();
|
||||||
|
await this.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tear down form subscription to avoid leaks when the overlay closes.
|
||||||
|
this.activeFormSubscription?.unsubscribe();
|
||||||
|
this.activeFormSubscription = undefined;
|
||||||
|
this.activeForm = null;
|
||||||
|
|
||||||
|
// Unregister from z-index registry
|
||||||
|
zIndexRegistry.unregister(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,14 +87,24 @@ export class DeesTile extends DeesElement {
|
|||||||
color: var(--dees-color-text-secondary);
|
color: var(--dees-color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Content: the rounded inset --- */
|
/* --- Content: the rounded inset ---
|
||||||
|
Uses overflow-y: auto so that when a consumer (e.g. dees-modal) caps
|
||||||
|
the tile with max-height, long content scrolls inside the tile
|
||||||
|
instead of being clipped. For consumers without max-height
|
||||||
|
(e.g. dees-stepper), the tile grows with content and the scroll
|
||||||
|
never activates. Horizontal overflow stays clipped to preserve the
|
||||||
|
rounded corners. */
|
||||||
.tile-content {
|
.tile-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border-top: 1px solid var(--dees-color-border-subtle);
|
border-top: 1px solid var(--dees-color-border-subtle);
|
||||||
border-bottom: 1px solid var(--dees-color-border-subtle);
|
border-bottom: 1px solid var(--dees-color-border-subtle);
|
||||||
overflow: hidden;
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-content.no-footer {
|
.tile-content.no-footer {
|
||||||
|
|||||||
@@ -352,5 +352,80 @@ export const demoFunc = () => html`
|
|||||||
});
|
});
|
||||||
}}>Test Responsive</dees-button>
|
}}>Test Responsive</dees-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>Scrollable Content</h3>
|
||||||
|
<p>When content exceeds the modal's max-height (<code>calc(100vh - 80px)</code>), the tile caps at that height and the content area scrolls inside. The heading and bottom buttons stay pinned.</p>
|
||||||
|
<div class="button-grid">
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'Long Article',
|
||||||
|
width: 'medium',
|
||||||
|
content: html`
|
||||||
|
<h4 style="margin-top: 0;">Lorem ipsum dolor sit amet</h4>
|
||||||
|
${Array.from({ length: 40 }, (_, i) => html`
|
||||||
|
<p>
|
||||||
|
<strong>§ ${i + 1}.</strong>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
|
||||||
|
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
|
||||||
|
enim ad minim veniam, quis nostrud exercitation ullamco laboris
|
||||||
|
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
|
||||||
|
in reprehenderit in voluptate velit esse cillum dolore eu fugiat
|
||||||
|
nulla pariatur. Excepteur sint occaecat cupidatat non proident,
|
||||||
|
sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||||
|
</p>
|
||||||
|
`)}
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modal) => modal!.destroy()
|
||||||
|
}, {
|
||||||
|
name: 'Accept',
|
||||||
|
action: async (modal) => modal!.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Long Article</dees-button>
|
||||||
|
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'Long List',
|
||||||
|
width: 'small',
|
||||||
|
content: html`
|
||||||
|
<p>Selected items:</p>
|
||||||
|
<ul style="padding-left: 20px; margin: 0;">
|
||||||
|
${Array.from({ length: 80 }, (_, i) => html`
|
||||||
|
<li style="padding: 4px 0;">Item ${i + 1} — option label</li>
|
||||||
|
`)}
|
||||||
|
</ul>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Done',
|
||||||
|
action: async (modal) => modal!.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Long List</dees-button>
|
||||||
|
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'Tall Form',
|
||||||
|
width: 'medium',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
${Array.from({ length: 25 }, (_, i) => html`
|
||||||
|
<dees-input-text .label=${`Field ${i + 1}`}></dees-input-text>
|
||||||
|
`)}
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modal) => modal!.destroy()
|
||||||
|
}, {
|
||||||
|
name: 'Submit',
|
||||||
|
action: async (modal) => modal!.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Tall Form</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@@ -268,18 +268,9 @@ export class DeesModal extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.heading .header-button dees-icon {
|
.heading .header-button dees-icon {
|
||||||
width: 14px;
|
font-size: 14px;
|
||||||
height: 14px;
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
|
|
||||||
}
|
|
||||||
.bottomButtons {
|
.bottomButtons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
Reference in New Issue
Block a user