Compare commits

...

30 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
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
a1e808345e v3.65.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:32:10 +00:00
efea2d62d9 feat(dees-table): add schema-based in-cell editing with keyboard navigation and cell edit events 2026-04-07 15:32:10 +00:00
2f95979cc6 v3.64.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 14:34:19 +00:00
b3f098b41e feat(dees-table): add file-manager style row selection and JSON copy support 2026-04-07 14:34:19 +00:00
a0d5462ff1 v3.63.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-07 13:55:43 +00:00
f1c204f790 feat(dees-table): add floating header support with fixed-height table mode 2026-04-07 13:55:43 +00:00
e806c9bce6 v3.62.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 12:19:59 +00:00
fa56c7cce8 feat(dees-table): add multi-column sorting with header menu controls and priority indicators 2026-04-07 12:19:59 +00:00
22 changed files with 3674 additions and 555 deletions

View File

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

View File

@@ -1,5 +1,100 @@
# 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
- 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
- replace editableFields with per-column editor configuration for text, number, checkbox, dropdown, date, and tags inputs
- add focused/editing cell state with arrow key navigation plus Enter, Tab, Shift+Tab, F2, and Escape editing controls
- dispatch cellEdit and cellEditError events with typed payloads and support column-level format, parse, validate, and editorOptions hooks
- update table styles and demos to reflect editable cell behavior and rename sticky header usage to fixedHeight
## 2026-04-07 - 3.64.0 - feat(dees-table)
add file-manager style row selection and JSON copy support
- adds optional selection checkbox rendering via the show-selection-checkbox property
- supports plain, ctrl/cmd, and shift-click row selection with range selection behavior
- adds Ctrl/Cmd+C and context menu actions to copy selected rows as formatted JSON
- updates row selection styling to prevent native text selection during range selection
## 2026-04-07 - 3.63.0 - feat(dees-table)
add floating header support with fixed-height table mode
- replace the sticky-header option with a fixed-height mode for internal scrolling
- add a JS-managed floating header so column headers remain visible when tables scroll inside ancestor containers
- sync floating header column widths and filter rows with the rendered table
## 2026-04-07 - 3.62.0 - feat(dees-table)
add multi-column sorting with header menu controls and priority indicators
- replace single-column sort state with ordered sort descriptors for cascading client-side sorting
- add Shift+click header sorting, context menu actions, and confirmation before replacing an active sort cascade
- show multi-sort direction and priority badges in table headers and add a demo showcasing the new behavior
## 2026-04-06 - 3.61.2 - fix(dees-input-list,dees-icon)
preserve input focus after list updates and make icons ignore pointer events

View File

@@ -1,6 +1,6 @@
{
"name": "@design.estate/dees-catalog",
"version": "3.61.2",
"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.61.2',
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,4 +1,4 @@
import type { Column, TDisplayFunction } from './types.js';
import type { Column, ISortDescriptor, TDisplayFunction } from './types.js';
export function computeColumnsFromDisplayFunction<T>(
displayFunction: TDisplayFunction<T>,
@@ -36,11 +36,31 @@ export function getCellValue<T>(row: T, col: Column<T>, displayFunction?: TDispl
return col.value ? col.value(row) : (row as any)[col.key as any];
}
/**
* Compares two cell values in ascending order. Returns -1, 0, or 1.
* Null/undefined values sort before defined values. Numbers compare numerically;
* everything else compares as case-insensitive strings.
*/
export function compareCellValues(va: any, vb: any): number {
if (va == null && vb == null) return 0;
if (va == null) return -1;
if (vb == null) return 1;
if (typeof va === 'number' && typeof vb === 'number') {
if (va < vb) return -1;
if (va > vb) return 1;
return 0;
}
const sa = String(va).toLowerCase();
const sb = String(vb).toLowerCase();
if (sa < sb) return -1;
if (sa > sb) return 1;
return 0;
}
export function getViewData<T>(
data: T[],
effectiveColumns: Column<T>[],
sortKey?: string,
sortDir?: 'asc' | 'desc' | null,
sortBy: ISortDescriptor[],
filterText?: string,
columnFilters?: Record<string, string>,
filterMode: 'table' | 'data' = 'table',
@@ -94,21 +114,17 @@ export function getViewData<T>(
return true;
});
}
if (!sortKey || !sortDir) return arr;
const col = effectiveColumns.find((c) => String(c.key) === sortKey);
if (!col) return arr;
const dir = sortDir === 'asc' ? 1 : -1;
if (!sortBy || sortBy.length === 0) return arr;
// Pre-resolve descriptors -> columns once for performance.
const resolved = sortBy
.map((desc) => ({ desc, col: effectiveColumns.find((c) => String(c.key) === desc.key) }))
.filter((entry): entry is { desc: ISortDescriptor; col: Column<T> } => !!entry.col);
if (resolved.length === 0) return arr;
arr.sort((a, b) => {
const va = getCellValue(a, col);
const vb = getCellValue(b, col);
if (va == null && vb == null) return 0;
if (va == null) return -1 * dir;
if (vb == null) return 1 * dir;
if (typeof va === 'number' && typeof vb === 'number') return (va - vb) * dir;
const sa = String(va).toLowerCase();
const sb = String(vb).toLowerCase();
if (sa < sb) return -1 * dir;
if (sa > sb) return 1 * dir;
for (const { desc, col } of resolved) {
const cmp = compareCellValues(getCellValue(a, col), getCellValue(b, col));
if (cmp !== 0) return desc.dir === 'asc' ? cmp : -cmp;
}
return 0;
});
return arr;

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;
@@ -55,36 +56,66 @@ export const demoFunc = () => html`
<div class="demo-container">
<div class="demo-section">
<h2 class="demo-title">Basic Table with Actions</h2>
<p class="demo-description">A standard table with row actions, editable fields, and context menu support. Double-click on descriptions to edit. Grid lines are enabled by default.</p>
<p class="demo-description">A standard table with row actions, editable cells, and context menu support. Double-click any cell to edit. Tab moves to the next editable cell, Enter to the row below, Esc cancels.</p>
<dees-table
heading1="Current Account Statement"
heading2="Bunq - Payment Account 2 - April 2021"
.editableFields="${['description']}"
.columns=${[
{ key: 'date', header: 'Date', sortable: true, editable: true, editor: 'date' },
{ key: 'amount', header: 'Amount', editable: true, editor: 'text' },
{
key: 'category',
header: 'Category',
editable: true,
editor: 'dropdown',
editorOptions: {
options: [
{ option: 'Office Supplies', key: 'office' },
{ option: 'Hardware', key: 'hardware' },
{ option: 'Software', key: 'software' },
{ option: 'Travel', key: 'travel' },
],
},
},
{ key: 'description', header: 'Description', editable: true },
{ key: 'reconciled', header: 'OK', editable: true, editor: 'checkbox' },
]}
@cellEdit=${(e: CustomEvent) => console.log('cellEdit', e.detail)}
.data=${[
{
date: '2021-04-01',
amount: '2464.65 €',
description: 'Printing Paper (Office Supplies) - STAPLES BREMEN',
category: 'office',
description: 'Printing Paper - STAPLES BREMEN',
reconciled: true,
},
{
date: '2021-04-02',
amount: '165.65 €',
description: 'Logitech Mouse (Hardware) - logi.com OnlineShop',
category: 'hardware',
description: 'Logitech Mouse - logi.com OnlineShop',
reconciled: false,
},
{
date: '2021-04-03',
amount: '2999,00 €',
description: 'Macbook Pro 16inch (Hardware) - Apple.de OnlineShop',
category: 'hardware',
description: 'Macbook Pro 16inch - Apple.de OnlineShop',
reconciled: false,
},
{
date: '2021-04-01',
amount: '2464.65 €',
category: 'office',
description: 'Office-Supplies - STAPLES BREMEN',
reconciled: true,
},
{
date: '2021-04-01',
amount: '2464.65 €',
category: 'office',
description: 'Office-Supplies - STAPLES BREMEN',
reconciled: true,
},
]}
dataName="transactions"
@@ -510,13 +541,13 @@ export const demoFunc = () => html`
<h2 class="demo-title">Column Filters + Sticky Header (New)</h2>
<p class="demo-description">Per-column quick filters and sticky header with internal scroll. Try filtering the Name column. Uses --table-max-height var.</p>
<style>
dees-table[sticky-header] { --table-max-height: 220px; }
dees-table[fixed-height] { --table-max-height: 220px; }
</style>
<dees-table
heading1="Employees"
heading2="Quick filter per column + sticky header"
.showColumnFilters=${true}
.stickyHeader=${true}
.fixedHeight=${true}
.columns=${[
{ key: 'name', header: 'Name', sortable: true },
{ key: 'email', header: 'Email', sortable: true },
@@ -580,6 +611,44 @@ export const demoFunc = () => html`
></dees-table>
</div>
<div class="demo-section">
<h2 class="demo-title">Multi-Column Sort</h2>
<p class="demo-description">
Click any column header for a single-column sort. Hold Shift while clicking to add the
column to a multi-sort cascade (or cycle its direction). Right-click any sortable header
to open a menu where you can pin a column to a specific priority slot, remove it, or
clear the cascade.
</p>
<dees-table
heading1="People Directory"
heading2="Pre-seeded with department ▲ 1, name ▲ 2"
.sortBy=${[
{ key: 'department', dir: 'asc' },
{ key: 'name', dir: 'asc' },
]}
.columns=${[
{ key: 'department', header: 'Department', sortable: true },
{ key: 'name', header: 'Name', sortable: true },
{ key: 'role', header: 'Role', sortable: true },
{ key: 'createdAt', header: 'Created', sortable: true },
{ key: 'location', header: 'Location', sortable: true },
{ key: 'status', header: 'Status', sortable: true },
]}
.data=${[
{ department: 'R&D', name: 'Alice Johnson', role: 'Engineer', createdAt: '2023-01-12', location: 'Berlin', status: 'Active' },
{ department: 'R&D', name: 'Diana Martinez', role: 'Engineer', createdAt: '2020-06-30', location: 'Madrid', status: 'Active' },
{ department: 'R&D', name: 'Mark Lee', role: 'Engineer', createdAt: '2024-03-04', location: 'Berlin', status: 'Active' },
{ department: 'Design', name: 'Bob Smith', role: 'Designer', createdAt: '2022-11-05', location: 'Paris', status: 'Active' },
{ department: 'Design', name: 'Sara Kim', role: 'Designer', createdAt: '2021-08-19', location: 'Paris', status: 'On Leave' },
{ department: 'Ops', name: 'Charlie Davis', role: 'Manager', createdAt: '2021-04-21', location: 'London', status: 'On Leave' },
{ department: 'Ops', name: 'Helena Voss', role: 'SRE', createdAt: '2023-07-22', location: 'London', status: 'Active' },
{ department: 'QA', name: 'Fiona Clark', role: 'QA', createdAt: '2022-03-14', location: 'Vienna', status: 'Active' },
{ department: 'QA', name: 'Tomás Rivera', role: 'QA', createdAt: '2024-01-09', location: 'Madrid', status: 'Active' },
{ department: 'CS', name: 'Ethan Brown', role: 'Support', createdAt: '2019-09-18', location: 'Rome', status: 'Inactive' },
]}
></dees-table>
</div>
<div class="demo-section">
<h2 class="demo-title">Wide Properties + Many Actions</h2>
<p class="demo-description">A table with many columns and rich actions to stress test layout and sticky Actions.</p>
@@ -631,8 +700,9 @@ export const demoFunc = () => html`
</style>
<dees-table
id="scrollSmallHeight"
.stickyHeader=${true}
heading1="People Directory (Scrollable)"
.fixedHeight=${true}
.virtualized=${true}
heading1="People Directory (Scrollable, Virtualized)"
heading2="Forced scrolling with many items"
.columns=${[
{ key: 'id', header: 'ID', sortable: true },
@@ -673,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

@@ -114,29 +114,48 @@ export const tableStyles: CSSResult[] = [
border-bottom-width: 0px;
}
/* Default mode (Mode B, page sticky): horizontal scroll lives on
.tableScroll (so wide tables don't get clipped by an ancestor
overflow:hidden such as dees-tile). Vertical sticky is handled by
a JS-managed floating header (.floatingHeader, position:fixed),
which is unaffected by ancestor overflow. */
.tableScroll {
/* enable horizontal scroll only when content exceeds width */
position: relative;
overflow-x: auto;
/* prevent vertical scroll inside the table container */
overflow-y: hidden;
/* avoid reserving extra space for classic scrollbars where possible */
scrollbar-gutter: stable both-edges;
overflow-y: visible;
scrollbar-gutter: stable;
}
/* Hide horizontal scrollbar entirely when not using sticky header */
:host(:not([sticky-header])) .tableScroll {
-ms-overflow-style: none; /* IE/Edge */
scrollbar-width: none; /* Firefox (hides both axes) */
}
:host(:not([sticky-header])) .tableScroll::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
/* In sticky-header mode, hide only the horizontal scrollbar in WebKit/Blink */
:host([sticky-header]) .tableScroll::-webkit-scrollbar:horizontal {
height: 0px;
}
:host([sticky-header]) .tableScroll {
/* Mode A, internal scroll: opt-in via fixed-height attribute.
The table scrolls inside its own box and the header sticks via plain CSS sticky. */
:host([fixed-height]) .tableScroll {
max-height: var(--table-max-height, 360px);
overflow: auto;
scrollbar-gutter: stable both-edges;
}
:host([fixed-height]) .tableScroll::-webkit-scrollbar:horizontal {
height: 0px;
}
/* Floating header overlay (Mode B). Position is managed by JS so it
escapes any ancestor overflow:hidden (position:fixed is not clipped
by overflow ancestors). */
.floatingHeader {
position: fixed;
top: 0;
left: 0;
z-index: 100;
visibility: hidden;
overflow: hidden;
pointer-events: none;
}
.floatingHeader.active {
visibility: visible;
}
.floatingHeader table {
margin: 0;
}
.floatingHeader th {
pointer-events: auto;
}
table {
@@ -159,15 +178,25 @@ export const tableStyles: CSSResult[] = [
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 9%)')};
border-bottom: 1px solid var(--dees-color-border-strong);
}
:host([sticky-header]) thead th {
/* th needs its own background so sticky cells paint over scrolled rows
(browsers don't paint the <thead> box behind a sticky <th>). */
th {
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 9%)')};
}
/* Mode A — internal scroll sticky */
:host([fixed-height]) thead th {
position: sticky;
top: 0;
z-index: 2;
}
:host([fixed-height]) thead tr.filtersRow th {
top: 36px; /* matches th { height: 36px } below */
}
tbody tr {
transition: background-color 0.15s ease;
position: relative;
user-select: none;
}
/* Default horizontal lines (bottom border only) */
@@ -276,6 +305,32 @@ export const tableStyles: CSSResult[] = [
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
letter-spacing: -0.01em;
}
th[role='columnheader']:hover {
color: var(--dees-color-text-primary);
}
th .sortArrow {
display: inline-block;
margin-left: 6px;
font-size: 10px;
line-height: 1;
opacity: 0.7;
vertical-align: middle;
}
th .sortBadge {
display: inline-block;
margin-left: 3px;
padding: 1px 5px;
font-size: 10px;
font-weight: 600;
line-height: 1;
color: ${cssManager.bdTheme('hsl(222.2 47.4% 30%)', 'hsl(217.2 91.2% 75%)')};
background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.12)', 'hsl(217.2 91.2% 59.8% / 0.18)')};
border-radius: 999px;
vertical-align: middle;
}
:host([show-vertical-lines]) th {
border-right: 1px solid var(--dees-color-border-default);
@@ -317,32 +372,103 @@ export const tableStyles: CSSResult[] = [
min-height: 24px;
line-height: 24px;
}
td input {
position: absolute;
top: 4px;
bottom: 4px;
left: 20px;
right: 20px;
width: calc(100% - 40px);
height: calc(100% - 8px);
padding: 0 12px;
outline: none;
border: 1px solid var(--dees-color-border-default);
border-radius: 6px;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
color: var(--dees-color-text-primary);
font-family: inherit;
font-size: inherit;
font-weight: inherit;
transition: all 0.15s ease;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
/* ---- 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);
}
td input:focus {
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.2)', 'hsl(217.2 91.2% 59.8% / 0.2)')};
.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;
}
td.focused {
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 {
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;
line-height: normal;
}
td.editingCell dees-input-text,
td.editingCell dees-input-checkbox,
td.editingCell dees-input-dropdown,
td.editingCell dees-input-datepicker,
td.editingCell dees-input-tags {
display: block;
width: 100%;
}
/* filter row */

View File

@@ -15,15 +15,65 @@ export interface ITableAction<T = any> {
actionFunc: (actionDataArg: ITableActionDataArg<T>) => Promise<any>;
}
/**
* Available cell editor types. Each maps to a dees-input-* component.
* Use `editor` on `Column<T>` to opt a column into in-cell editing.
*/
export type TCellEditorType =
| 'text'
| 'number'
| 'checkbox'
| 'dropdown'
| 'date'
| 'tags';
/** Detail payload for the `cellEdit` CustomEvent dispatched on commit. */
export interface ICellEditDetail<T = any> {
row: T;
key: string;
oldValue: any;
newValue: any;
}
/** Detail payload for the `cellEditError` CustomEvent dispatched on validation failure. */
export interface ICellEditErrorDetail<T = any> {
row: T;
key: string;
value: any;
message: string;
}
export interface Column<T = any> {
key: keyof T | string;
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;
hidden?: boolean;
/** Marks the column as editable. Shorthand for `editor: 'text'` if no editor is specified. */
editable?: boolean;
/** Editor type — picks the dees-input-* component used for in-cell editing. */
editor?: TCellEditorType;
/** Editor-specific options forwarded to the editor (e.g. `{ options: [...] }` for dropdowns). */
editorOptions?: Record<string, any>;
/** Convert raw row value -> editor value. Defaults to identity. */
format?: (raw: any, row: T) => any;
/** Convert editor value -> raw row value. Defaults to identity. */
parse?: (editorValue: any, row: T) => any;
/** Validate the parsed value before commit. Return string for error, true/void for ok. */
validate?: (value: any, row: T) => true | string | void;
}
/**
* One entry in a multi-column sort cascade. Order in the array reflects priority:
* index 0 is the primary sort key, index 1 the secondary tiebreaker, and so on.
*/
export interface ISortDescriptor {
key: string;
dir: 'asc' | 'desc';
}
export type TDisplayFunction<T = any> = (itemArg: T) => Record<string, any>;

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,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;

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 },