Compare commits

...

7 Commits

Author SHA1 Message Date
c34037265e v3.71.1
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-11 16:20:34 +00:00
8c230fe3af fix(dees-modal): move modal content scrolling into dees-tile so long content stays scrollable with pinned header and actions 2026-04-11 16:20:34 +00:00
a695d60770 v3.71.0
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-11 10:46:56 +00:00
ea30cbd381 feat(dees-stepper): add footer menu actions with form-aware step validation 2026-04-11 10:46:56 +00:00
39a4bf0dd3 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.
2026-04-11 09:28:11 +00:00
d5c9bc69b3 v3.70.1
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-11 08:47:25 +00:00
27f8ab752d fix(dees-modal): use icon font sizing for modal header buttons 2026-04-11 08:47:25 +00:00
10 changed files with 941 additions and 209 deletions

View File

@@ -0,0 +1 @@
{"sessionId":"4b0f0a7f-f187-40a3-a38b-cb9a7e877011","pid":3110692,"acquiredAt":1775914414249}

View File

@@ -1,5 +1,25 @@
# Changelog
## 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)
add opt-in flash highlighting for updated table cells

View File

@@ -1,6 +1,6 @@
{
"name": "@design.estate/dees-catalog",
"version": "3.70.0",
"version": "3.71.1",
"private": false,
"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",

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

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@design.estate/dees-catalog',
version: '3.70.0',
version: '3.71.1',
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
}

View File

@@ -1,134 +1,137 @@
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`
<dees-stepper
.steps=${[
{
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-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: '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>
<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,16 @@ 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 type { DeesForm } from '../../00group-form/dees-form/dees-form.js';
import '../dees-tile/dees-tile.js';
export interface IStep {
title: string;
content: TemplateResult;
menuOptions?: plugins.tsclass.website.IMenuItem<DeesStepper>[];
validationFunc?: (stepper: DeesStepper, htmlElement: HTMLElement, signal?: AbortSignal) => Promise<any>;
onReturnToStepFunc?: (stepper: DeesStepper, htmlElement: HTMLElement) => Promise<any>;
validationFuncCalled?: boolean;
@@ -34,9 +39,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 +75,25 @@ 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;
@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() {
super();
}
@@ -60,7 +107,24 @@ export class DeesStepper extends DeesElement {
position: absolute;
width: 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 {
position: absolute;
width: 100%;
@@ -68,101 +132,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 +253,142 @@ export class DeesStepper extends DeesElement {
color: inherit;
}
.step .content {
.step-body .content {
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;
border-left: 1px solid var(--dees-color-border-subtle);
color: var(--dees-color-text-muted);
white-space: nowrap;
display: flex;
align-items: center;
}
.bottomButtons .bottomButton:first-child {
border-left: none;
}
.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() {
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' : ''}"
>
${this.getIndexOfStep(stepArg) > 0
? html`<div class="goBack" @click=${this.goBack}><span style="font-family: Inter"><-</span> go to previous step</div>`
: ``}
<div
class="stepperContainer ${this.overlay ? 'overlay' : ''}"
style=${this.overlay ? `z-index: ${this.stepperZIndex};` : ''}
>
${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.menuOptions && stepArg.menuOptions.length > 0
? html`<div slot="footer" class="bottomButtons">
${isSelected && this.activeForm !== null && !this.activeFormValid
? html`<div class="stepHint">Complete form to continue</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>
`;
}
@@ -230,6 +420,7 @@ export class DeesStepper extends DeesElement {
if (!selectedStepElement) {
return;
}
this.scanActiveForm(selectedStepElement);
if (!stepperContainer.style.paddingTop) {
stepperContainer.style.paddingTop = `${
stepperContainer.offsetHeight / 2 - selectedStepElement.offsetHeight / 2
@@ -296,4 +487,93 @@ export class DeesStepper extends DeesElement {
nextStep.validationFuncCalled = false;
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);
}
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);
}
}

View File

@@ -87,14 +87,24 @@ export class DeesTile extends DeesElement {
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 {
flex: 1;
position: relative;
border-radius: 8px;
border-top: 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 {

View File

@@ -352,5 +352,80 @@ export const demoFunc = () => html`
});
}}>Test Responsive</dees-button>
</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>
`

View File

@@ -268,18 +268,9 @@ export class DeesModal extends DeesElement {
}
.heading .header-button dees-icon {
width: 14px;
height: 14px;
display: block;
font-size: 14px;
}
.content {
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
}
.bottomButtons {
display: flex;
flex-direction: row;