Compare commits

..

24 Commits

Author SHA1 Message Date
8a1be59a51 v3.72.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 20:04:37 +00:00
a3b2ace88d feat(dees-stepper): add configurable cancellation flow with confirmation modal 2026-04-11 20:04:37 +00:00
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
5948fc83ea v3.70.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-08 14:51:49 +00:00
72e3d6a09e feat(dees-table): add opt-in flash highlighting for updated table cells 2026-04-08 14:51:49 +00:00
de6f4a3ac5 v3.69.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-08 09:57:57 +00:00
eecdc51557 fix(ui): refine heading emphasis and animate app dashboard subview expansion 2026-04-08 09:57:57 +00:00
c841c49e1e v3.69.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-08 08:58:05 +00:00
2595d822d0 feat(dees-heading): add numeric aliases for horizontal rule heading levels and refine heading spacing styles 2026-04-08 08:58:05 +00:00
3ae0541065 v3.68.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-08 08:05:53 +00:00
4b735b768a feat(dees-simple-appdash): add nested sidebar subviews and preserve submit labels from slotted text 2026-04-08 08:05:53 +00:00
9422edbfa1 v3.67.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-07 21:35:21 +00:00
37c5e92d6d fix(repo): no changes to commit 2026-04-07 21:35:21 +00:00
c7503de11e update 2026-04-07 21:31:43 +00:00
408362f3be v3.67.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-07 21:04:52 +00:00
b3f5ab3d31 feat(dees-table): improve inline cell editors with integrated input styling and auto-open dropdowns 2026-04-07 21:04:52 +00:00
8d954b17ad v3.66.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-07 15:56:55 +00:00
ac9cc8cfed feat(dees-table): add virtualized row rendering for large tables and optimize table rendering performance 2026-04-07 15:56:55 +00:00
21 changed files with 2417 additions and 423 deletions

View File

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

View File

@@ -1,5 +1,77 @@
# 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)
add opt-in flash highlighting for updated table cells
- introduces highlight-updates and highlight-duration properties for diff-based cell update highlighting
- adds a warning banner when flash highlighting is enabled without rowKey
- keeps selection stable across data refreshes and avoids flashing user-edited cells
- includes a live demo showcasing flashing updates and reduced-motion support
## 2026-04-08 - 3.69.1 - fix(ui)
refine heading emphasis and animate app dashboard subview expansion
- Adjust heading color hierarchy so h1-h2 use primary text while h3-h6 use secondary text, and reduce h1 font weight for better visual balance
- Replace app dashboard subview conditional rendering with animated expand/collapse behavior using grid transitions and inert state handling
## 2026-04-08 - 3.69.0 - feat(dees-heading)
add numeric aliases for horizontal rule heading levels and refine heading spacing styles
- Support level="7" as an alias for "hr" and level="8" as an alias for "hr-small".
- Update heading and hr variant styles to use design tokens for spacing and colors, with per-level margin tuning.
- Extend the demo to show both named and numeric hr heading level variants.
## 2026-04-08 - 3.68.0 - feat(dees-simple-appdash)
add nested sidebar subviews and preserve submit labels from slotted text
- support grouped navigation items with expandable subviews and parent-to-first-subview fallback in the app dashboard
- allow dees-form-submit to derive its button text from light DOM content when no explicit text property is set
## 2026-04-07 - 3.67.1 - fix(repo)
no changes to commit
## 2026-04-07 - 3.67.0 - feat(dees-table)
improve inline cell editors with integrated input styling and auto-open dropdowns
- add a visually integrated mode to dees-input-text and dees-input-dropdown for table cell editing
- auto-open dropdown editors when a table cell enters edit mode
- refine table editing cell outline and dropdown value matching for inline editors
## 2026-04-07 - 3.66.0 - feat(dees-table)
add virtualized row rendering for large tables and optimize table rendering performance
- add a virtualized mode with configurable overscan to render only visible rows while preserving scroll height
- improve table render performance with memoized column and view-data computation plus deferred floating header rendering
- update the dees-table demo to showcase virtualized scrolling in the fixed-height example
## 2026-04-07 - 3.65.0 - feat(dees-table)
add schema-based in-cell editing with keyboard navigation and cell edit events

View File

@@ -1,6 +1,6 @@
{
"name": "@design.estate/dees-catalog",
"version": "3.65.0",
"version": "3.72.0",
"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.65.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.'
}

View File

@@ -1,6 +1,7 @@
import { type ITableAction } from './dees-table.js';
import * as plugins from '../../00plugins.js';
import { html, css, cssManager } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
interface ITableDemoData {
date: string;
@@ -700,7 +701,8 @@ export const demoFunc = () => html`
<dees-table
id="scrollSmallHeight"
.fixedHeight=${true}
heading1="People Directory (Scrollable)"
.virtualized=${true}
heading1="People Directory (Scrollable, Virtualized)"
heading2="Forced scrolling with many items"
.columns=${[
{ key: 'id', header: 'ID', sortable: true },
@@ -741,6 +743,71 @@ export const demoFunc = () => html`
] as ITableAction[]}
></dees-table>
</div>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
const tableEl = elementArg.querySelector('#demoLiveFlash') as any;
if (!tableEl) return;
// Guard against double-start if runAfterRender fires more than once
// (e.g. across hot-reload cycles).
if (tableEl.__liveFlashTimerId) {
window.clearInterval(tableEl.__liveFlashTimerId);
}
const tick = () => {
if (!Array.isArray(tableEl.data) || tableEl.data.length === 0) return;
const next = tableEl.data.map((r: any) => ({ ...r }));
const count = 1 + Math.floor(Math.random() * 3);
for (let i = 0; i < count; i++) {
const idx = Math.floor(Math.random() * next.length);
const delta = +((Math.random() * 2 - 1) * 3).toFixed(2);
const newPrice = Math.max(1, +(next[idx].price + delta).toFixed(2));
next[idx] = {
...next[idx],
price: newPrice,
change: delta,
updatedAt: new Date().toLocaleTimeString(),
};
}
tableEl.data = next;
};
tableEl.__liveFlashTimerId = window.setInterval(tick, 1500);
}}>
<div class="demo-section">
<h2 class="demo-title">Live Updates with Flash Highlighting</h2>
<p class="demo-description">
Opt-in cell-flash via <code>highlight-updates="flash"</code>. The ticker below mutates
random rows every 1.5s and reassigns <code>.data</code>. Updated cells briefly flash
amber and fade out. Requires <code>rowKey</code> (here <code>"symbol"</code>). Honors
<code>prefers-reduced-motion</code>. Row selection persists across updates click a
row, then watch it stay selected as the data churns.
</p>
<dees-table
id="demoLiveFlash"
.rowKey=${'symbol'}
highlight-updates="flash"
.selectionMode=${'multi'}
heading1="Live Market Feed"
heading2="Flashing cells indicate updated values"
.columns=${[
{ key: 'symbol', header: 'Symbol', sortable: true },
{ key: 'price', header: 'Price', sortable: true },
{ key: 'change', header: 'Δ', sortable: true },
{ key: 'updatedAt', header: 'Updated' },
]}
.data=${[
{ symbol: 'AAPL', price: 182.52, change: 0, updatedAt: '—' },
{ symbol: 'MSFT', price: 414.18, change: 0, updatedAt: '—' },
{ symbol: 'GOOG', price: 168.74, change: 0, updatedAt: '—' },
{ symbol: 'AMZN', price: 186.13, change: 0, updatedAt: '—' },
{ symbol: 'TSLA', price: 248.50, change: 0, updatedAt: '—' },
{ symbol: 'NVDA', price: 877.35, change: 0, updatedAt: '—' },
{ symbol: 'META', price: 492.96, change: 0, updatedAt: '—' },
{ symbol: 'NFLX', price: 605.88, change: 0, updatedAt: '—' },
{ symbol: 'AMD', price: 165.24, change: 0, updatedAt: '—' },
{ symbol: 'INTC', price: 42.15, change: 0, updatedAt: '—' },
]}
></dees-table>
</div>
</dees-demowrapper>
</div>
</div>
`;

File diff suppressed because it is too large Load Diff

View File

@@ -373,6 +373,72 @@ export const tableStyles: CSSResult[] = [
line-height: 24px;
}
/* ---- Cell flash highlighting (opt-in via highlight-updates="flash") ----
Bloomberg/TradingView-style: the text itself briefly takes an accent
color then fades back to the default. No background tint, no layout
shift, no weight change. Readable, modern, subtle.
Consumers can override per instance:
dees-table#myTable { --dees-table-flash-color: hsl(142 76% 40%); }
*/
:host {
--dees-table-flash-color: ${cssManager.bdTheme(
'hsl(32 95% 44%)',
'hsl(45 93% 62%)'
)};
--dees-table-flash-easing: cubic-bezier(0.22, 0.61, 0.36, 1);
}
.innerCellContainer.flashing {
animation: dees-table-cell-flash
var(--dees-table-flash-duration, 900ms)
var(--dees-table-flash-easing);
}
/* Hold the accent color briefly, then fade back to the theme's default
text color. Inherits to child text and to SVG icons that use
currentColor. Cells with explicit color overrides in renderers are
intentionally unaffected. */
@keyframes dees-table-cell-flash {
0%,
35% { color: var(--dees-table-flash-color); }
100% { color: var(--dees-color-text-primary); }
}
@media (prefers-reduced-motion: reduce) {
.innerCellContainer.flashing {
animation: none;
color: var(--dees-table-flash-color);
}
}
/* Dev-time warning banner shown when highlight-updates="flash" but
rowKey is missing. Consumers should never ship this to production. */
.flashConfigWarning {
display: flex;
align-items: center;
gap: 8px;
margin: 8px 16px 0;
padding: 8px 12px;
border-left: 3px solid ${cssManager.bdTheme('hsl(38 92% 50%)', 'hsl(48 96% 63%)')};
background: ${cssManager.bdTheme('hsl(48 96% 89% / 0.6)', 'hsl(48 96% 30% / 0.15)')};
color: ${cssManager.bdTheme('hsl(32 81% 29%)', 'hsl(48 96% 80%)')};
font-size: 12px;
line-height: 1.4;
border-radius: 4px;
}
.flashConfigWarning dees-icon {
width: 14px;
height: 14px;
flex: 0 0 auto;
}
.flashConfigWarning code {
padding: 1px 4px;
border-radius: 3px;
background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.6)', 'hsl(0 0% 0% / 0.3)')};
font-family: ${cssGeistFontFamily};
font-size: 11px;
}
/* Editable cell affordances */
td.editable {
cursor: text;
@@ -386,6 +452,11 @@ export const tableStyles: CSSResult[] = [
}
td.editingCell {
padding: 0;
outline: 2px solid ${cssManager.bdTheme(
'hsl(222.2 47.4% 51.2% / 0.6)',
'hsl(217.2 91.2% 59.8% / 0.6)'
)};
outline-offset: -2px;
}
td.editingCell .innerCellContainer {
padding: 0;

View File

@@ -48,6 +48,7 @@ export interface Column<T = any> {
header?: string | TemplateResult;
value?: (row: T) => any;
renderer?: (value: any, row: T, ctx: { rowIndex: number; colIndex: number; column: Column<T> }) => TemplateResult | string;
/** Whether this column can be sorted by clicking its header. Defaults to `true`; set to `false` to disable. */
sortable?: boolean;
/** whether this column participates in per-column quick filtering (default: true) */
filterable?: boolean;

View File

@@ -75,12 +75,23 @@ export class DeesFormSubmit extends DeesElement {
.text=${this.text}
?disabled=${this.disabled}
@clicked=${this.submit}
>
<slot></slot>
</dees-button>
></dees-button>
`;
}
public async firstUpdated() {
// Capture light DOM text content as the button label. dees-button wipes
// its own light DOM during extractLightDom(), so we cannot simply forward
// a <slot> into it — we have to hoist the text onto the .text property
// ourselves before handing it to dees-button.
if (!this.text) {
const slotText = this.textContent?.trim();
if (slotText) {
this.text = slotText;
}
}
}
public async submit() {
if (this.disabled) {
return;

View File

@@ -46,6 +46,12 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
})
accessor enableSearch: boolean = true;
@property({
type: Boolean,
reflect: true,
})
accessor vintegrated: boolean = false;
@state()
accessor isOpened = false;
@@ -126,6 +132,36 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
.selectedBox.open::after {
transform: translateY(-50%) rotate(180deg);
}
/* Visually integrated mode: shed chrome to blend into a host component
(e.g. a dees-table cell in edit mode). */
:host([vintegrated]) dees-label {
display: none;
}
:host([vintegrated]) .maincontainer {
height: 40px;
}
:host([vintegrated]) .selectedBox {
height: 40px;
line-height: 40px;
padding: 0 32px 0 16px;
font-size: 13px;
border: none;
border-radius: 0;
background: transparent;
box-shadow: none;
transition: none;
}
:host([vintegrated]) .selectedBox:hover:not(.disabled),
:host([vintegrated]) .selectedBox:focus-visible {
border: none;
box-shadow: none;
background: transparent;
}
:host([vintegrated]) .selectedBox::after {
right: 12px;
opacity: 0.6;
}
`,
];

View File

@@ -57,6 +57,12 @@ export class DeesInputText extends DeesInputBase {
@property({})
accessor validationFunction!: (value: string) => boolean;
@property({
type: Boolean,
reflect: true,
})
accessor vintegrated: boolean = false;
public static styles = [
themeDefaultStyles,
...DeesInputBase.baseStyles,
@@ -194,6 +200,36 @@ export class DeesInputText extends DeesInputBase {
border-color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.05)', 'hsl(142.1 70.6% 45.3% / 0.05)')};
}
/* Visually integrated mode: shed chrome to blend into a host component
(e.g. a dees-table cell in edit mode). */
:host([vintegrated]) dees-label,
:host([vintegrated]) .validationContainer {
display: none;
}
:host([vintegrated]) .maincontainer {
height: 40px;
}
:host([vintegrated]) input {
height: 40px;
line-height: 24px;
padding: 0 16px;
font-size: 13px;
border: none;
border-radius: 0;
background: transparent;
box-shadow: none;
transition: none;
}
:host([vintegrated]) input:hover:not(:disabled):not(:focus),
:host([vintegrated]) input:focus {
border: none;
box-shadow: none;
background: transparent;
}
:host([vintegrated]) .showPassword {
display: none;
}
`,
];

View File

@@ -8,7 +8,9 @@ export function demoFunc() {
<dees-heading level="4">This is a H4 heading</dees-heading>
<dees-heading level="5">This is a H5 heading</dees-heading>
<dees-heading level="6">This is a H6 heading</dees-heading>
<dees-heading level="hr">This is an hr heading</dees-heading>
<dees-heading level="hr-small">This is an hr small heading</dees-heading>
<dees-heading level="hr">This is an hr heading (level="hr")</dees-heading>
<dees-heading level="7">This is an hr heading (level="7")</dees-heading>
<dees-heading level="hr-small">This is an hr-small heading (level="hr-small")</dees-heading>
<dees-heading level="8">This is an hr-small heading (level="8")</dees-heading>
`;
}

View File

@@ -27,68 +27,104 @@ export class DeesHeading extends DeesElement {
// properties
/**
* Heading level: 1-6 for h1-h6, or 'hr' for horizontal rule style
* Heading level:
* '1'-'6' → <h1>..<h6>
* '7'|'hr' → horizontal-rule style heading
* '8'|'hr-small' → small horizontal-rule style heading
*/
@property({ type: String, reflect: true })
accessor level: '1' | '2' | '3' | '4' | '5' | '6' | 'hr' | 'hr-small' = '1';
accessor level: '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | 'hr' | 'hr-small' = '1';
// STATIC STYLES
public static styles: CSSResult[] = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
/* Heading styles */
h1, h2, h3, h4, h5, h6 {
margin: 16px 0 8px;
font-weight: 600;
color: ${cssManager.bdTheme('#000', '#fff')};
:host {
display: block;
}
h1 { font-size: 32px; font-family: ${cssCalSansFontFamily}; letter-spacing: 0.025em;}
h2 { font-size: 28px; }
h3 { font-size: 24px; }
h4 { font-size: 20px; }
h5 { font-size: 16px; }
h6 { font-size: 14px; }
/* Heading styles.
* Color hierarchy: h1-h2 stay prominent with text-primary; h3-h6 step
* down to text-secondary so they read as subheadings instead of
* mini-h1s. Keeps the visual loudness out of smaller headings. */
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
}
h1, h2 {
color: var(--dees-color-text-primary);
}
h3, h4, h5, h6 {
color: var(--dees-color-text-secondary);
}
/* Per-level typography + spacing.
* Margin scales with importance: h1 gets the most breathing room,
* h6 the least. Top margin > bottom margin so headings group with
* the content that follows them. */
h1 {
/* h1 uses weight 500, not 600: the Cal Sans display font is
* already stylized enough that bold + max contrast + 32px stacks
* too much emphasis. 500 keeps the typographic impact without
* shouting. */
font-weight: 500;
font-size: 32px;
font-family: ${cssCalSansFontFamily};
letter-spacing: 0.025em;
margin: var(--dees-spacing-2xl) 0 var(--dees-spacing-lg);
}
h2 {
font-size: 28px;
margin: var(--dees-spacing-xl) 0 var(--dees-spacing-md);
}
h3 {
font-size: 24px;
margin: var(--dees-spacing-xl) 0 var(--dees-spacing-md);
}
h4 {
font-size: 20px;
margin: var(--dees-spacing-lg) 0 var(--dees-spacing-sm);
}
h5 {
font-size: 16px;
margin: var(--dees-spacing-md) 0 var(--dees-spacing-sm);
}
h6 {
font-size: 14px;
margin: var(--dees-spacing-md) 0 var(--dees-spacing-xs);
}
/* Horizontal rule style heading */
.heading-hr {
display: flex;
align-items: center;
text-align: center;
margin: 16px 0;
color: ${cssManager.bdTheme('#999', '#555')};
margin: var(--dees-spacing-lg) 0;
color: var(--dees-color-text-muted);
}
/* Fade lines toward and away from text for hr style */
.heading-hr::before {
content: '';
flex: 1;
height: 1px;
/* fade in toward center */
background: ${cssManager.bdTheme(
'linear-gradient(to right, transparent, #ccc)',
'linear-gradient(to right, transparent, #333)'
)};
margin: 0 8px;
background: linear-gradient(to right, transparent, var(--dees-color-border-strong));
margin: 0 var(--dees-spacing-sm);
}
.heading-hr::after {
content: '';
flex: 1;
height: 1px;
/* fade out away from center */
background: ${cssManager.bdTheme(
'linear-gradient(to right, #ccc, transparent)',
'linear-gradient(to right, #333, transparent)'
)};
margin: 0 8px;
background: linear-gradient(to right, var(--dees-color-border-strong), transparent);
margin: 0 var(--dees-spacing-sm);
}
/* Small hr variant with reduced margins */
.heading-hr.heading-hr-small {
margin: 8px 0;
margin: var(--dees-spacing-sm) 0;
font-size: 12px;
}
.heading-hr.heading-hr-small::before,
.heading-hr.heading-hr-small::after {
margin: 0 8px;
margin: 0 var(--dees-spacing-sm);
}
`,
];
@@ -109,8 +145,10 @@ export class DeesHeading extends DeesElement {
return html`<h5><slot></slot></h5>`;
case '6':
return html`<h6><slot></slot></h6>`;
case '7':
case 'hr':
return html`<div class="heading-hr"><slot></slot></div>`;
case '8':
case 'hr-small':
return html`<div class="heading-hr heading-hr-small"><slot></slot></div>`;
default:

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,17 @@ 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 { 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 {
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 +40,33 @@ 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[];
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({
type: Array,
})
@@ -47,6 +77,37 @@ export class DeesStepper extends DeesElement {
})
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() {
super();
}
@@ -60,7 +121,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 +146,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 +267,149 @@ 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;
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() {
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};` : ''}
@click=${this.handleOutsideClick}
>
${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>
<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>
`;
}
@@ -230,6 +441,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 +508,163 @@ 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);
}
/**
* 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);
}
}

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;

View File

@@ -353,12 +353,35 @@ export const demoFunc = () => html`
name: 'Analytics',
iconName: 'lucide:lineChart',
element: DemoViewAnalytics,
subViews: [
{
name: 'Overview',
iconName: 'lucide:activity',
element: DemoViewAnalytics,
},
{
name: 'Reports',
iconName: 'lucide:fileText',
element: DemoViewDashboard,
},
],
},
{
name: 'Settings',
iconName: 'lucide:settings',
element: DemoViewSettings,
}
subViews: [
{
name: 'Profile',
iconName: 'lucide:user',
element: DemoViewSettings,
},
{
name: 'Billing',
iconName: 'lucide:creditCard',
element: DemoViewSettings,
},
],
},
] as IView[]}
@logout=${() => {
console.log('Logout event triggered');

View File

@@ -26,7 +26,8 @@ declare global {
export interface IView {
name: string;
iconName?: string;
element: DeesElement['constructor']['prototype'];
element?: DeesElement['constructor']['prototype'];
subViews?: IView[];
}
export type TGlobalMessageType = 'info' | 'success' | 'warning' | 'error';
@@ -250,6 +251,78 @@ export class DeesSimpleAppDash extends DeesElement {
white-space: nowrap;
}
.viewTab .chevron {
flex: 0 0 auto;
font-size: 14px;
opacity: 0.5;
transform: rotate(-90deg);
transition: transform 0.2s ease, opacity 0.15s ease;
}
.viewTab.hasSubs:hover .chevron {
opacity: 0.75;
}
.viewTab.hasSubs.groupActive .chevron {
transform: rotate(0deg);
opacity: 0.9;
}
.subViews {
display: grid;
grid-template-rows: 0fr;
margin-left: 12px;
position: relative;
transition:
grid-template-rows 0.25s cubic-bezier(0.4, 0, 0.2, 1),
margin-top 0.25s cubic-bezier(0.4, 0, 0.2, 1),
margin-bottom 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.subViews.expanded {
grid-template-rows: 1fr;
margin-top: 2px;
margin-bottom: 4px;
}
.subViews::before {
content: '';
position: absolute;
left: 0;
top: 4px;
bottom: 4px;
width: 1px;
background: var(--dees-color-border-default);
opacity: 0;
transition: opacity 0.2s ease;
}
.subViews.expanded::before {
opacity: 1;
}
.subViews-inner {
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 2px;
padding-left: 12px;
}
.viewTab.sub {
padding: 8px 12px;
font-size: 12px;
}
.viewTab.sub dees-icon {
font-size: 14px;
}
.viewTab.sub.selected::before {
left: -12px;
}
.appActions {
padding: 12px 8px;
border-top: 1px solid var(--dees-color-border-default);
@@ -563,10 +636,12 @@ export class DeesSimpleAppDash extends DeesElement {
<div class="viewTabs-container">
<div class="section-label">Navigation</div>
<div class="viewTabs">
${this.viewTabs.map(
(view) => html`
${this.viewTabs.map((view) => {
const hasSubs = !!view.subViews?.length;
const groupActive = hasSubs && this.isGroupActive(view);
return html`
<div
class="viewTab ${this.selectedView === view ? 'selected' : ''}"
class="viewTab ${this.selectedView === view ? 'selected' : ''} ${hasSubs ? 'hasSubs' : ''} ${groupActive ? 'groupActive' : ''}"
@click=${() => this.loadView(view)}
>
${view.iconName ? html`
@@ -575,9 +650,39 @@ export class DeesSimpleAppDash extends DeesElement {
<dees-icon .icon="${'lucide:file'}"></dees-icon>
`}
<span>${view.name}</span>
${hasSubs ? html`
<dees-icon class="chevron" .icon="${'lucide:chevronDown'}"></dees-icon>
` : ''}
</div>
`
)}
${hasSubs ? html`
<div
class="subViews ${groupActive ? 'expanded' : ''}"
?inert=${!groupActive}
>
<div class="subViews-inner">
${view.subViews!.map(
(sub) => html`
<div
class="viewTab sub ${this.selectedView === sub ? 'selected' : ''}"
@click=${(e: Event) => {
e.stopPropagation();
this.loadView(sub);
}}
>
${sub.iconName ? html`
<dees-icon .icon="${sub.iconName.includes(':') ? sub.iconName : `lucide:${sub.iconName}`}"></dees-icon>
` : html`
<dees-icon .icon="${'lucide:dot'}"></dees-icon>
`}
<span>${sub.name}</span>
</div>
`
)}
</div>
</div>
` : ''}
`;
})}
</div>
</div>
<div class="appActions">
@@ -771,8 +876,23 @@ export class DeesSimpleAppDash extends DeesElement {
}
private isGroupActive(view: IView): boolean {
if (this.selectedView === view) return true;
return view.subViews?.some((sv) => sv === this.selectedView) ?? false;
}
private currentView!: DeesElement;
public async loadView(viewArg: IView) {
// Group-only parent: resolve to first sub view with an element
if (!viewArg.element && viewArg.subViews?.length) {
const firstNavigable = viewArg.subViews.find((sv) => sv.element);
if (firstNavigable) {
return this.loadView(firstNavigable);
}
return; // nothing navigable — ignore click
}
if (!viewArg.element) return; // safety: no element and no subs → no-op
const appcontent = this.shadowRoot!.querySelector('.appcontent')!;
const view = new viewArg.element();
if (this.currentView) {
@@ -781,7 +901,7 @@ export class DeesSimpleAppDash extends DeesElement {
appcontent.appendChild(view);
this.currentView = view;
this.selectedView = viewArg;
// Emit view-select event
this.dispatchEvent(new CustomEvent('view-select', {
detail: { view: viewArg },