Compare commits

...

4 Commits

Author SHA1 Message Date
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
8 changed files with 538 additions and 36 deletions

View File

@@ -1,5 +1,19 @@
# Changelog
## 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

View File

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

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@design.estate/dees-catalog',
version: '3.69.0',
version: '3.70.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;
@@ -742,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>
`;

View File

@@ -214,6 +214,30 @@ export class DeesTable<T> extends DeesElement {
@property({ type: Number, attribute: 'virtual-overscan' })
accessor virtualOverscan: number = 8;
/**
* Opt-in visual indication of cell-value changes across data updates.
*
* - `'none'` (default): no diffing, zero overhead.
* - `'flash'`: when `data` is reassigned to a new array reference, diff the
* new rows against the previous snapshot and briefly flash any cells
* whose resolved value changed. Equality is strict `===`; object-valued
* cells are compared by reference. The currently-edited cell is never
* flashed. User-initiated cell edits do not flash.
*
* Requires `rowKey` to be set — without it, the feature silently no-ops
* and renders a visible dev warning banner. Honors `prefers-reduced-motion`
* (fades are replaced with a static background hint of the same duration).
*/
@property({ type: String, attribute: 'highlight-updates' })
accessor highlightUpdates: 'none' | 'flash' = 'none';
/**
* Duration of the flash animation in milliseconds. Fed into the
* `--dees-table-flash-duration` CSS variable on the host.
*/
@property({ type: Number, attribute: 'highlight-duration' })
accessor highlightDuration: number = 900;
/**
* When set, the table renders inside a fixed-height scroll container
* (`max-height: var(--table-max-height, 360px)`) and the header sticks
@@ -268,6 +292,23 @@ export class DeesTable<T> extends DeesElement {
@state()
private accessor __floatingActive: boolean = false;
// ─── Flash-on-update state (only populated when highlightUpdates === 'flash') ──
/** rowId → set of colKey strings currently flashing. */
@state()
private accessor __flashingCells: Map<string, Set<string>> = new Map();
/** rowId → (colKey → last-seen resolved cell value). Populated per diff pass. */
private __prevSnapshot?: Map<string, Map<string, unknown>>;
/** Single shared timer that clears __flashingCells after highlightDuration ms. */
private __flashClearTimer?: ReturnType<typeof setTimeout>;
/** Monotonic counter bumped each flash batch so directives.keyed recreates the cell node and restarts the animation. */
private __flashTick: number = 0;
/** One-shot console.warn gate for missing rowKey in flash mode. */
private __flashWarnedNoRowKey: boolean = false;
// ─── Render memoization ──────────────────────────────────────────────
// These caches let render() short-circuit when the relevant inputs
// (by reference) haven't changed. They are NOT @state — mutating them
@@ -557,6 +598,15 @@ export class DeesTable<T> extends DeesElement {
</div>
</div>
<div class="headingSeparation"></div>
${this.highlightUpdates === 'flash' && !this.rowKey
? html`<div class="flashConfigWarning" role="alert">
<dees-icon .icon=${'lucide:triangleAlert'}></dees-icon>
<span>
<code>highlight-updates="flash"</code> requires
<code>rowKey</code> to be set. Flash is disabled.
</span>
</div>`
: html``}
<div class="searchGrid hidden">
<dees-input-text
.label=${'lucene syntax search'}
@@ -606,9 +656,13 @@ export class DeesTable<T> extends DeesElement {
${useVirtual && topSpacerHeight > 0
? html`<tr aria-hidden="true" style="height:${topSpacerHeight}px"><td></td></tr>`
: html``}
${renderRows.map((itemArg, sliceIdx) => {
${directives.repeat(
renderRows,
(itemArg, sliceIdx) => `${this.getRowId(itemArg)}::${renderStart + sliceIdx}`,
(itemArg, sliceIdx) => {
const rowIndex = renderStart + sliceIdx;
const rowId = this.getRowId(itemArg);
const flashSet = this.__flashingCells.get(rowId);
return html`
<tr
data-row-idx=${rowIndex}
@@ -640,6 +694,7 @@ export class DeesTable<T> extends DeesElement {
const isEditing =
this.__editingCell?.rowId === rowId &&
this.__editingCell?.colKey === editKey;
const isFlashing = !!flashSet?.has(editKey);
const cellClasses = [
isEditable ? 'editable' : '',
isFocused && !isEditing ? 'focused' : '',
@@ -647,14 +702,22 @@ export class DeesTable<T> extends DeesElement {
]
.filter(Boolean)
.join(' ');
const innerHtml = html`<div
class=${isFlashing ? 'innerCellContainer flashing' : 'innerCellContainer'}
>
${isEditing ? this.renderCellEditor(itemArg, col) : content}
</div>`;
return html`
<td
class=${cellClasses}
data-col-key=${editKey}
>
<div class="innerCellContainer">
${isEditing ? this.renderCellEditor(itemArg, col) : content}
</div>
${isFlashing
? directives.keyed(
`${rowId}:${editKey}:${this.__flashTick}`,
innerHtml
)
: innerHtml}
</td>
`;
})}
@@ -685,7 +748,8 @@ export class DeesTable<T> extends DeesElement {
}
})()}
</tr>`;
})}
}
)}
${useVirtual && bottomSpacerHeight > 0
? html`<tr aria-hidden="true" style="height:${bottomSpacerHeight}px"><td></td></tr>`
: html``}
@@ -801,7 +865,7 @@ export class DeesTable<T> extends DeesElement {
const key = String(col.key);
if (col.filterable === false) return html`<th></th>`;
return html`<th>
<input type="text" placeholder="Filter..." .value=${this.columnFilters[key] || ''}
<input type="text" placeholder="Filter..." data-col-key=${key} .value=${this.columnFilters[key] || ''}
@input=${(e: Event) => this.setColumnFilter(key, (e.target as HTMLInputElement).value)} />
</th>`;
})}
@@ -957,6 +1021,84 @@ export class DeesTable<T> extends DeesElement {
if (fh) fh.classList.remove('active');
}
/**
* If a filter `<input>` inside the floating-header clone currently has
* focus, copy its value, caret, and selection range onto the matching
* input in the real header, then focus that real input. This lets the
* user keep typing uninterrupted when filter input causes the table to
* shrink below the viewport stick line and the floating header has to
* unmount.
*
* Safe to call at any time — it is a no-op unless an input inside the
* floating header is focused and has a `data-col-key` attribute that
* matches a real-header input.
*/
private __transferFocusToRealHeader(): void {
const fh = this.__floatingHeaderEl;
if (!fh) return;
const active = this.shadowRoot?.activeElement as HTMLElement | null;
if (!active || !fh.contains(active)) return;
const colKey = active.getAttribute('data-col-key');
if (!colKey) return;
const fromInput = active as HTMLInputElement;
const real = this.shadowRoot?.querySelector(
`.tableScroll > table > thead input[data-col-key="${CSS.escape(colKey)}"]`
) as HTMLInputElement | null;
if (!real || real === fromInput) return;
const selStart = fromInput.selectionStart;
const selEnd = fromInput.selectionEnd;
const selDir = fromInput.selectionDirection as any;
real.focus({ preventScroll: true });
try {
if (selStart != null && selEnd != null) {
real.setSelectionRange(selStart, selEnd, selDir || undefined);
}
} catch {
/* setSelectionRange throws on unsupported input types — ignore */
}
}
/**
* Symmetric counterpart to `__transferFocusToRealHeader`. When the
* floating header has just activated and a real-header filter input
* was focused (and is now scrolled off-screen behind the floating
* clone), move focus to the clone's matching input so the user keeps
* typing in the visible one.
*
* Called from `__syncFloatingHeader` inside the post-activation
* `updateComplete` callback — by then the clone subtree exists in the
* DOM and can receive focus.
*/
private __transferFocusToFloatingHeader(): void {
const fh = this.__floatingHeaderEl;
if (!fh || !this.__floatingActive) return;
const active = this.shadowRoot?.activeElement as HTMLElement | null;
if (!active) return;
// Only handle focus that lives in the real header (not already in the clone).
const realThead = this.shadowRoot?.querySelector(
'.tableScroll > table > thead'
) as HTMLElement | null;
if (!realThead || !realThead.contains(active)) return;
const colKey = active.getAttribute('data-col-key');
if (!colKey) return;
const fromInput = active as HTMLInputElement;
const clone = fh.querySelector(
`input[data-col-key="${CSS.escape(colKey)}"]`
) as HTMLInputElement | null;
if (!clone || clone === fromInput) return;
const selStart = fromInput.selectionStart;
const selEnd = fromInput.selectionEnd;
const selDir = fromInput.selectionDirection as any;
clone.focus({ preventScroll: true });
try {
if (selStart != null && selEnd != null) {
clone.setSelectionRange(selStart, selEnd, selDir || undefined);
}
} catch {
/* ignore */
}
}
// ─── Virtualization ─────────────────────────────────────────────────
/**
@@ -1062,6 +1204,15 @@ export class DeesTable<T> extends DeesElement {
const shouldBeActive = tableRect.top < stick.top && distance > 0;
if (shouldBeActive !== this.__floatingActive) {
if (!shouldBeActive) {
// Before we flag the clone for unmount, hand off any focused
// filter input to its counterpart in the real header. This is the
// "user is typing in a sticky filter input, filter shrinks the
// table so the floating header hides" case — without this
// handoff the user's focus (and caret position) would be lost
// when the clone unmounts.
this.__transferFocusToRealHeader();
}
this.__floatingActive = shouldBeActive;
fh.classList.toggle('active', shouldBeActive);
if (!shouldBeActive) {
@@ -1072,8 +1223,14 @@ export class DeesTable<T> extends DeesElement {
}
if (shouldBeActive) {
// Clone subtree doesn't exist yet — wait for the next render to
// materialize it, then complete geometry sync.
this.updateComplete.then(() => this.__syncFloatingHeader());
// materialize it, then complete geometry sync. Additionally, if a
// real-header filter input was focused when we activated, hand
// off to the clone once it exists so the user keeps typing in
// the visible (floating) input.
this.updateComplete.then(() => {
this.__syncFloatingHeader();
this.__transferFocusToFloatingHeader();
});
return;
}
}
@@ -1127,6 +1284,10 @@ export class DeesTable<T> extends DeesElement {
public async disconnectedCallback() {
super.disconnectedCallback();
this.teardownFloatingHeader();
if (this.__flashClearTimer) {
clearTimeout(this.__flashClearTimer);
this.__flashClearTimer = undefined;
}
}
public async firstUpdated() {
@@ -1134,9 +1295,141 @@ export class DeesTable<T> extends DeesElement {
// table markup actually exists (it only renders when data.length > 0).
}
/**
* Runs before each render. Drives two independent concerns:
*
* 1. **Selection rebind** — when `data` is reassigned to a fresh array
* (typical live-data pattern), `selectedDataRow` still points at the
* stale row object from the old array. We re-resolve it by rowKey so
* consumers of `selectedDataRow` (footer indicator, header/footer
* actions, copy fallback) see the live reference. `selectedIds`,
* `__focusedCell`, `__editingCell`, `__selectionAnchorId` are all
* keyed by string rowId and persist automatically — no change needed.
* This runs regardless of `highlightUpdates` — it is a baseline
* correctness fix for live data.
*
* 2. **Flash diff** — when `highlightUpdates === 'flash'`, diff the new
* data against `__prevSnapshot` and populate `__flashingCells` with
* the (rowId, colKey) pairs whose resolved cell value changed. A
* single shared timer clears `__flashingCells` after
* `highlightDuration` ms. Skipped if `rowKey` is missing (with a
* one-shot console.warn; the render surface also shows a warning
* banner).
*/
public willUpdate(changedProperties: Map<string | number | symbol, unknown>): void {
// --- Phase 1: selection rebind (always runs) ---
if (changedProperties.has('data') && this.selectedDataRow && this.rowKey) {
const prevId = this.getRowId(this.selectedDataRow);
let found: T | undefined;
for (const row of this.data) {
if (this.getRowId(row) === prevId) {
found = row;
break;
}
}
if (found) {
if (found !== this.selectedDataRow) this.selectedDataRow = found;
} else {
this.selectedDataRow = undefined as unknown as T;
}
}
// --- Phase 2: flash diff ---
if (this.highlightUpdates !== 'flash') {
// Mode was toggled off (or never on) — drop any lingering state so
// re-enabling later starts with a clean slate.
if (this.__prevSnapshot || this.__flashingCells.size > 0) {
this.__prevSnapshot = undefined;
if (this.__flashingCells.size > 0) this.__flashingCells = new Map();
if (this.__flashClearTimer) {
clearTimeout(this.__flashClearTimer);
this.__flashClearTimer = undefined;
}
}
return;
}
if (!this.rowKey) {
if (!this.__flashWarnedNoRowKey) {
this.__flashWarnedNoRowKey = true;
console.warn(
'[dees-table] highlightUpdates="flash" requires `rowKey` to be set. Flash is disabled. ' +
'Set the rowKey property/attribute to a stable identifier on your row data (e.g. `rowKey="id"`).'
);
}
return;
}
if (!changedProperties.has('data')) return;
const effectiveColumns = this.__getEffectiveColumns();
const visibleCols = effectiveColumns.filter((c) => !c.hidden);
const nextSnapshot = new Map<string, Map<string, unknown>>();
const newlyFlashing = new Map<string, Set<string>>();
for (const row of this.data) {
const rowId = this.getRowId(row);
const cellMap = new Map<string, unknown>();
for (const col of visibleCols) {
cellMap.set(String(col.key), getCellValueFn(row, col, this.displayFunction));
}
nextSnapshot.set(rowId, cellMap);
const prevCells = this.__prevSnapshot?.get(rowId);
if (!prevCells) continue; // new row — not an "update"
for (const [colKey, nextVal] of cellMap) {
if (prevCells.get(colKey) !== nextVal) {
// Don't flash the cell the user is actively editing.
if (
this.__editingCell &&
this.__editingCell.rowId === rowId &&
this.__editingCell.colKey === colKey
) continue;
let set = newlyFlashing.get(rowId);
if (!set) {
set = new Set();
newlyFlashing.set(rowId, set);
}
set.add(colKey);
}
}
}
const hadPrev = !!this.__prevSnapshot;
this.__prevSnapshot = nextSnapshot;
if (!hadPrev) return; // first time seeing data — no flashes
if (newlyFlashing.size === 0) return;
// Merge with any in-flight flashes from a rapid second update so a cell
// that changes twice before its animation ends gets a single clean
// restart (via __flashTick / directives.keyed) instead of stacking.
for (const [rowId, cols] of newlyFlashing) {
const existing = this.__flashingCells.get(rowId);
if (existing) {
for (const c of cols) existing.add(c);
} else {
this.__flashingCells.set(rowId, cols);
}
}
this.__flashTick++;
// Reactivity nudge: we've mutated the Map in place, so give Lit a fresh
// reference so the @state change fires for render.
this.__flashingCells = new Map(this.__flashingCells);
if (this.__flashClearTimer) clearTimeout(this.__flashClearTimer);
this.__flashClearTimer = setTimeout(() => {
this.__flashingCells = new Map();
this.__flashClearTimer = undefined;
}, Math.max(0, this.highlightDuration));
}
public async updated(changedProperties: Map<string | number | symbol, unknown>): Promise<void> {
super.updated(changedProperties);
// Feed highlightDuration into the CSS variable so JS and CSS stay in
// sync via a single source of truth.
if (changedProperties.has('highlightDuration')) {
this.style.setProperty('--dees-table-flash-duration', `${this.highlightDuration}ms`);
}
// Only re-measure column widths when the data or schema actually changed
// (or on first paint). `determineColumnWidths` is the single biggest
// first-paint cost — it forces multiple layout flushes per row.
@@ -2090,6 +2383,10 @@ export class DeesTable<T> extends DeesElement {
}
if (parsed !== oldValue) {
(item as any)[col.key] = parsed;
// Keep the flash-diff snapshot in sync so the next external update
// does not see this user edit as an external change (which would
// otherwise flash the cell the user just typed into).
this.__recordCellInSnapshot(item, col);
this.dispatchEvent(
new CustomEvent('cellEdit', {
detail: { row: item, key, oldValue, newValue: parsed },
@@ -2103,6 +2400,24 @@ export class DeesTable<T> extends DeesElement {
this.requestUpdate();
}
/**
* Updates the flash diff snapshot for a single cell to match its current
* resolved value. Called from `commitCellEdit` so a user-initiated edit
* does not register as an external change on the next diff pass.
* No-op when flash mode is off or no snapshot exists yet.
*/
private __recordCellInSnapshot(item: T, col: Column<T>): void {
if (this.highlightUpdates !== 'flash' || !this.__prevSnapshot) return;
if (!this.rowKey) return;
const rowId = this.getRowId(item);
let cellMap = this.__prevSnapshot.get(rowId);
if (!cellMap) {
cellMap = new Map();
this.__prevSnapshot.set(rowId, cellMap);
}
cellMap.set(String(col.key), getCellValueFn(item, col, this.displayFunction));
}
/** Renders the appropriate dees-input-* component for this column. */
private renderCellEditor(item: T, col: Column<T>): TemplateResult {
const raw = (item as any)[col.key];

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;

View File

@@ -44,17 +44,30 @@ export class DeesHeading extends DeesElement {
display: block;
}
/* Heading styles */
/* 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;

View File

@@ -269,12 +269,20 @@ export class DeesSimpleAppDash extends DeesElement {
}
.subViews {
display: flex;
flex-direction: column;
gap: 2px;
margin: 2px 0 4px 12px;
padding-left: 12px;
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 {
@@ -285,6 +293,21 @@ export class DeesSimpleAppDash extends DeesElement {
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 {
@@ -631,26 +654,31 @@ export class DeesSimpleAppDash extends DeesElement {
<dees-icon class="chevron" .icon="${'lucide:chevronDown'}"></dees-icon>
` : ''}
</div>
${hasSubs && groupActive ? html`
<div class="subViews">
${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>
`
)}
${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>
` : ''}
`;