Compare commits

...

6 Commits

Author SHA1 Message Date
c0375508f0 v3.78.3
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-14 13:58:00 +00:00
3e86ba034b fix(dees-table): stabilize live updates by reusing row DOM and avoiding redundant layout recalculations 2026-04-14 13:58:00 +00:00
05e74cbe2e v3.78.2
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-12 23:34:56 +00:00
8fbdbf9f64 fix(deps): bump @design.estate/dees-wcctools to ^3.9.0 2026-04-12 23:34:56 +00:00
bfbc0f108e v3.78.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-12 23:33:52 +00:00
bab7528f0b fix(dees-simple-login): use dees-tile for the login credentials container 2026-04-12 23:33:52 +00:00
7 changed files with 326 additions and 117 deletions

View File

@@ -1,5 +1,26 @@
# Changelog # Changelog
## 2026-04-14 - 3.78.3 - fix(dees-table)
stabilize live updates by reusing row DOM and avoiding redundant layout recalculations
- reuse keyed table rows across live-sorted updates so existing row elements persist while cells reorder
- limit flash animation restarts to changed cells by tracking per-cell flash tokens
- avoid repeated column width measurements unless table layout inputs actually change
- replace async header and footer action rendering with direct mapped output to prevent comment node growth during updates
- add Chromium live update tests covering width measurement stability, comment growth, and row DOM reuse
## 2026-04-12 - 3.78.2 - fix(deps)
bump @design.estate/dees-wcctools to ^3.9.0
- Updates the @design.estate/dees-wcctools dependency from ^3.8.4 to ^3.9.0 in package.json.
## 2026-04-12 - 3.78.1 - fix(dees-simple-login)
use dees-tile for the login credentials container
- replace the custom login card wrapper with a dees-tile component
- update styles to target dees-tile and its content part while preserving form layout
- add a credentials heading to the login tile
## 2026-04-12 - 3.78.0 - feat(dees-settings) ## 2026-04-12 - 3.78.0 - feat(dees-settings)
add dees-settings layout component for displaying read-only settings with footer actions add dees-settings layout component for displaying read-only settings with footer actions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@design.estate/dees-catalog", "name": "@design.estate/dees-catalog",
"version": "3.78.0", "version": "3.78.3",
"private": false, "private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.", "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"main": "dist_ts_web/index.js", "main": "dist_ts_web/index.js",
@@ -18,7 +18,7 @@
"dependencies": { "dependencies": {
"@design.estate/dees-domtools": "^2.5.4", "@design.estate/dees-domtools": "^2.5.4",
"@design.estate/dees-element": "^2.2.4", "@design.estate/dees-element": "^2.2.4",
"@design.estate/dees-wcctools": "^3.8.4", "@design.estate/dees-wcctools": "^3.9.0",
"@fortawesome/fontawesome-svg-core": "^7.2.0", "@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-brands-svg-icons": "^7.2.0", "@fortawesome/free-brands-svg-icons": "^7.2.0",
"@fortawesome/free-regular-svg-icons": "^7.2.0", "@fortawesome/free-regular-svg-icons": "^7.2.0",

33
pnpm-lock.yaml generated
View File

@@ -15,8 +15,8 @@ importers:
specifier: ^2.2.4 specifier: ^2.2.4
version: 2.2.4 version: 2.2.4
'@design.estate/dees-wcctools': '@design.estate/dees-wcctools':
specifier: ^3.8.4 specifier: ^3.9.0
version: 3.8.4 version: 3.9.0
'@fortawesome/fontawesome-svg-core': '@fortawesome/fontawesome-svg-core':
specifier: ^7.2.0 specifier: ^7.2.0
version: 7.2.0 version: 7.2.0
@@ -323,8 +323,8 @@ packages:
'@design.estate/dees-element@2.2.4': '@design.estate/dees-element@2.2.4':
resolution: {integrity: sha512-O9cA6flBMMd+pBwMQrZXwAWel9yVxgokolb+Em6gvkXxPJ0P/B5UDn4Vc2d4ts3ta55PTBm+l2dPeDVGx/bl7Q==} resolution: {integrity: sha512-O9cA6flBMMd+pBwMQrZXwAWel9yVxgokolb+Em6gvkXxPJ0P/B5UDn4Vc2d4ts3ta55PTBm+l2dPeDVGx/bl7Q==}
'@design.estate/dees-wcctools@3.8.4': '@design.estate/dees-wcctools@3.9.0':
resolution: {integrity: sha512-KpFK/azK+a/Xpq33pXKcho+tdFKVHhKZM5ArvHqo9QMwTczgp5DZZgowTDUuqAofjZwnuVfCPHK/Pw9e64N46A==} resolution: {integrity: sha512-0vZBaGBEGIbl8hx+8BezIIea3U5T7iSHHF9VqlJZGf+nOFIW4zBAxcCljH8YzZ1Yayp6BEjxp/pQXjHN2YB3Jg==}
'@emnapi/core@1.8.1': '@emnapi/core@1.8.1':
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
@@ -2070,6 +2070,12 @@ packages:
'@types/debug@4.1.12': '@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
'@types/dom-mediacapture-transform@0.1.11':
resolution: {integrity: sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==}
'@types/dom-webcodecs@0.1.13':
resolution: {integrity: sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==}
'@types/fs-extra@11.0.4': '@types/fs-extra@11.0.4':
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
@@ -3136,6 +3142,9 @@ packages:
mdurl@2.0.0: mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
mediabunny@1.40.1:
resolution: {integrity: sha512-HU/stGzAkdWaJIly6ypbUVgAUvT9kt39DIg0IaErR7/1fwtTmgUYs4i8uEPYcgcjPjbB9gtBmUXOLnXi6J2LDw==}
memory-pager@1.5.0: memory-pager@1.5.0:
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
@@ -4711,7 +4720,7 @@ snapshots:
dependencies: dependencies:
'@design.estate/dees-domtools': 2.5.4 '@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4 '@design.estate/dees-element': 2.2.4
'@design.estate/dees-wcctools': 3.8.4 '@design.estate/dees-wcctools': 3.9.0
'@fortawesome/fontawesome-svg-core': 7.2.0 '@fortawesome/fontawesome-svg-core': 7.2.0
'@fortawesome/free-brands-svg-icons': 7.2.0 '@fortawesome/free-brands-svg-icons': 7.2.0
'@fortawesome/free-regular-svg-icons': 7.2.0 '@fortawesome/free-regular-svg-icons': 7.2.0
@@ -4787,12 +4796,13 @@ snapshots:
- supports-color - supports-color
- vue - vue
'@design.estate/dees-wcctools@3.8.4': '@design.estate/dees-wcctools@3.9.0':
dependencies: dependencies:
'@design.estate/dees-domtools': 2.5.4 '@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4 '@design.estate/dees-element': 2.2.4
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
lit: 3.3.2 lit: 3.3.2
mediabunny: 1.40.1
transitivePeerDependencies: transitivePeerDependencies:
- '@nuxt/kit' - '@nuxt/kit'
- react - react
@@ -7245,6 +7255,12 @@ snapshots:
dependencies: dependencies:
'@types/ms': 2.1.0 '@types/ms': 2.1.0
'@types/dom-mediacapture-transform@0.1.11':
dependencies:
'@types/dom-webcodecs': 0.1.13
'@types/dom-webcodecs@0.1.13': {}
'@types/fs-extra@11.0.4': '@types/fs-extra@11.0.4':
dependencies: dependencies:
'@types/jsonfile': 6.1.4 '@types/jsonfile': 6.1.4
@@ -8468,6 +8484,11 @@ snapshots:
mdurl@2.0.0: {} mdurl@2.0.0: {}
mediabunny@1.40.1:
dependencies:
'@types/dom-mediacapture-transform': 0.1.11
'@types/dom-webcodecs': 0.1.13
memory-pager@1.5.0: {} memory-pager@1.5.0: {}
micromark-core-commonmark@2.0.3: micromark-core-commonmark@2.0.3:

View File

@@ -0,0 +1,167 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as deesCatalog from '../ts_web/index.js';
import type {
Column,
ISortDescriptor,
} from '../ts_web/elements/00group-dataview/dees-table/index.js';
interface ITestRow {
id: string;
score: number;
label: string;
}
const testColumns: Column<ITestRow>[] = [
{ key: 'id', header: 'ID' },
{ key: 'score', header: 'Score' },
{ key: 'label', header: 'Label' },
];
const scoreSort: ISortDescriptor[] = [{ key: 'score', dir: 'desc' }];
const waitForNextFrame = async () => {
await new Promise<void>((resolve) => {
requestAnimationFrame(() => resolve());
});
};
const waitForMacrotask = async () => {
await new Promise<void>((resolve) => {
window.setTimeout(() => resolve(), 0);
});
};
const settleTable = async (table: deesCatalog.DeesTable<ITestRow>) => {
await table.updateComplete;
await waitForNextFrame();
await waitForMacrotask();
await table.updateComplete;
};
const createRows = (iteration: number): ITestRow[] => {
const cycle = iteration % 3;
if (cycle === 0) {
return [
{ id: 'alpha', score: 60, label: `Alpha ${iteration}` },
{ id: 'beta', score: 20, label: `Beta ${iteration}` },
{ id: 'gamma', score: 40, label: `Gamma ${iteration}` },
];
}
if (cycle === 1) {
return [
{ id: 'alpha', score: 30, label: `Alpha ${iteration}` },
{ id: 'beta', score: 70, label: `Beta ${iteration}` },
{ id: 'gamma', score: 50, label: `Gamma ${iteration}` },
];
}
return [
{ id: 'alpha', score: 55, label: `Alpha ${iteration}` },
{ id: 'beta', score: 35, label: `Beta ${iteration}` },
{ id: 'gamma', score: 75, label: `Gamma ${iteration}` },
];
};
const createTable = (
rows: ITestRow[],
highlightUpdates: 'none' | 'flash'
): deesCatalog.DeesTable<ITestRow> => {
const table = new deesCatalog.DeesTable<ITestRow>();
table.searchable = false;
table.columns = testColumns;
table.rowKey = 'id';
table.sortBy = scoreSort;
table.highlightUpdates = highlightUpdates;
table.data = rows;
document.body.appendChild(table);
return table;
};
const countComments = (root: Node): number => {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
let count = 0;
while (walker.nextNode()) count++;
return count;
};
const getBodyRows = (table: deesCatalog.DeesTable<ITestRow>): HTMLTableRowElement[] =>
Array.from(
table.shadowRoot?.querySelectorAll('tbody tr[data-row-idx]') ?? []
) as HTMLTableRowElement[];
const getRenderedRowIds = (table: deesCatalog.DeesTable<ITestRow>): string[] =>
getBodyRows(table).map((row) => row.cells[0]?.textContent?.trim() ?? '');
const getRenderedRowMap = (
table: deesCatalog.DeesTable<ITestRow>
): Map<string, HTMLTableRowElement> => {
const rowMap = new Map<string, HTMLTableRowElement>();
for (const row of getBodyRows(table)) {
const rowId = row.cells[0]?.textContent?.trim() ?? '';
if (rowId) rowMap.set(rowId, row);
}
return rowMap;
};
tap.test('dees-table avoids repeated width measurement and comment growth on live updates', async () => {
const table = new deesCatalog.DeesTable<ITestRow>();
let widthMeasureCalls = 0;
const originalDetermineColumnWidths = table.determineColumnWidths.bind(table);
table.determineColumnWidths = (async () => {
widthMeasureCalls++;
await originalDetermineColumnWidths();
}) as typeof table.determineColumnWidths;
table.searchable = false;
table.columns = testColumns;
table.rowKey = 'id';
table.sortBy = scoreSort;
table.highlightUpdates = 'none';
table.data = createRows(0);
document.body.appendChild(table);
try {
await settleTable(table);
const initialWidthMeasureCalls = widthMeasureCalls;
const initialCommentCount = countComments(table.shadowRoot!);
expect(initialWidthMeasureCalls).toBeGreaterThan(0);
for (let iteration = 1; iteration <= 10; iteration++) {
table.data = createRows(iteration);
await settleTable(table);
}
expect(widthMeasureCalls).toEqual(initialWidthMeasureCalls);
expect(countComments(table.shadowRoot!)).toEqual(initialCommentCount);
} finally {
table.remove();
}
});
tap.test('dees-table reuses row DOM while flashing live-sorted updates', async () => {
const table = createTable(createRows(0), 'flash');
try {
await settleTable(table);
const initialRowMap = getRenderedRowMap(table);
table.data = createRows(1);
await settleTable(table);
const updatedRowMap = getRenderedRowMap(table);
expect(getRenderedRowIds(table)).toEqual(['beta', 'gamma', 'alpha']);
expect(updatedRowMap.get('alpha')).toEqual(initialRowMap.get('alpha'));
expect(updatedRowMap.get('beta')).toEqual(initialRowMap.get('beta'));
expect(updatedRowMap.get('gamma')).toEqual(initialRowMap.get('gamma'));
} finally {
table.remove();
}
});
export default tap.start();

View File

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

View File

@@ -293,9 +293,9 @@ export class DeesTable<T> extends DeesElement {
private accessor __floatingActive: boolean = false; private accessor __floatingActive: boolean = false;
// ─── Flash-on-update state (only populated when highlightUpdates === 'flash') ── // ─── Flash-on-update state (only populated when highlightUpdates === 'flash') ──
/** rowId → set of colKey strings currently flashing. */ /** rowId → (colKey → flash token) for cells currently flashing. */
@state() @state()
private accessor __flashingCells: Map<string, Set<string>> = new Map(); private accessor __flashingCells: Map<string, Map<string, number>> = new Map();
/** rowId → (colKey → last-seen resolved cell value). Populated per diff pass. */ /** rowId → (colKey → last-seen resolved cell value). Populated per diff pass. */
private __prevSnapshot?: Map<string, Map<string, unknown>>; private __prevSnapshot?: Map<string, Map<string, unknown>>;
@@ -303,7 +303,7 @@ export class DeesTable<T> extends DeesElement {
/** Single shared timer that clears __flashingCells after highlightDuration ms. */ /** Single shared timer that clears __flashingCells after highlightDuration ms. */
private __flashClearTimer?: ReturnType<typeof setTimeout>; private __flashClearTimer?: ReturnType<typeof setTimeout>;
/** Monotonic counter bumped each flash batch so directives.keyed recreates the cell node and restarts the animation. */ /** Monotonic counter bumped per flash batch so only changed cells restart their animation. */
private __flashTick: number = 0; private __flashTick: number = 0;
/** One-shot console.warn gate for missing rowKey in flash mode. */ /** One-shot console.warn gate for missing rowKey in flash mode. */
@@ -317,7 +317,7 @@ export class DeesTable<T> extends DeesElement {
columns: any; columns: any;
augment: boolean; augment: boolean;
displayFunction: any; displayFunction: any;
data: any; displayShapeKey: string;
out: Column<T>[]; out: Column<T>[];
}; };
private __memoViewData?: { private __memoViewData?: {
@@ -329,8 +329,13 @@ export class DeesTable<T> extends DeesElement {
effectiveColumns: Column<T>[]; effectiveColumns: Column<T>[];
out: T[]; out: T[];
}; };
/** Tracks the (data, columns) pair that `determineColumnWidths()` last sized for. */ /** Tracks the layout inputs that `determineColumnWidths()` last sized for. */
private __columnsSizedFor?: { data: any; columns: any }; private __columnsSizedFor?: {
effectiveColumns: Column<T>[];
showSelectionCheckbox: boolean;
inRowActionCount: number;
table: HTMLTableElement;
};
// ─── Virtualization state ──────────────────────────────────────────── // ─── Virtualization state ────────────────────────────────────────────
/** Estimated row height (px). Measured once from the first rendered row. */ /** Estimated row height (px). Measured once from the first rendered row. */
@@ -409,15 +414,7 @@ export class DeesTable<T> extends DeesElement {
const view: T[] = (this as any)._lastViewData ?? []; const view: T[] = (this as any)._lastViewData ?? [];
const item = view.find((r) => this.getRowId(r) === this.__focusedCell!.rowId); const item = view.find((r) => this.getRowId(r) === this.__focusedCell!.rowId);
if (!item) return; if (!item) return;
const allCols: Column<T>[] = const allCols = this.__getEffectiveColumns();
Array.isArray(this.columns) && this.columns.length > 0
? computeEffectiveColumnsFn(
this.columns,
this.augmentFromDisplayFunction,
this.displayFunction,
this.data
)
: computeColumnsFromDisplayFunctionFn(this.displayFunction, this.data);
const col = allCols.find((c) => String(c.key) === this.__focusedCell!.colKey); const col = allCols.find((c) => String(c.key) === this.__focusedCell!.colKey);
if (!col || !this.__isColumnEditable(col)) return; if (!col || !this.__isColumnEditable(col)) return;
eventArg.preventDefault(); eventArg.preventDefault();
@@ -469,15 +466,24 @@ export class DeesTable<T> extends DeesElement {
* that affect it. Avoids re-running `computeEffectiveColumnsFn` / * that affect it. Avoids re-running `computeEffectiveColumnsFn` /
* `computeColumnsFromDisplayFunctionFn` on every Lit update. * `computeColumnsFromDisplayFunctionFn` on every Lit update.
*/ */
private __getDisplayFunctionShapeKey(): string {
if (!this.data || this.data.length === 0) return '';
const firstTransformedItem = this.displayFunction(this.data[0]) ?? {};
return Object.keys(firstTransformedItem).join('\u0000');
}
private __getEffectiveColumns(): Column<T>[] { private __getEffectiveColumns(): Column<T>[] {
const usingColumns = Array.isArray(this.columns) && this.columns.length > 0; const usingColumns = Array.isArray(this.columns) && this.columns.length > 0;
const displayShapeKey = !usingColumns || this.augmentFromDisplayFunction
? this.__getDisplayFunctionShapeKey()
: '';
const cache = this.__memoEffectiveCols; const cache = this.__memoEffectiveCols;
if ( if (
cache && cache &&
cache.columns === this.columns && cache.columns === this.columns &&
cache.augment === this.augmentFromDisplayFunction && cache.augment === this.augmentFromDisplayFunction &&
cache.displayFunction === this.displayFunction && cache.displayFunction === this.displayFunction &&
cache.data === this.data cache.displayShapeKey === displayShapeKey
) { ) {
return cache.out; return cache.out;
} }
@@ -493,7 +499,7 @@ export class DeesTable<T> extends DeesElement {
columns: this.columns, columns: this.columns,
augment: this.augmentFromDisplayFunction, augment: this.augmentFromDisplayFunction,
displayFunction: this.displayFunction, displayFunction: this.displayFunction,
data: this.data, displayShapeKey,
out, out,
}; };
return out; return out;
@@ -543,6 +549,9 @@ export class DeesTable<T> extends DeesElement {
public render(): TemplateResult { public render(): TemplateResult {
const effectiveColumns = this.__getEffectiveColumns(); const effectiveColumns = this.__getEffectiveColumns();
const viewData = this.__getViewData(effectiveColumns); const viewData = this.__getViewData(effectiveColumns);
const headerActions = this.getActionsForType('header');
const footerActions = this.getActionsForType('footer');
const inRowActions = this.getActionsForType('inRow');
(this as any)._lastViewData = viewData; (this as any)._lastViewData = viewData;
// Virtualization slice — only the rows in `__virtualRange` actually // Virtualization slice — only the rows in `__virtualRange` actually
@@ -572,29 +581,22 @@ export class DeesTable<T> extends DeesElement {
<div class="heading heading2">${this.heading2}</div> <div class="heading heading2">${this.heading2}</div>
</div> </div>
<div class="headerActions"> <div class="headerActions">
${directives.resolveExec(async () => { ${headerActions.map(
const resultArray: TemplateResult[] = []; (action) => html`<div
for (const action of this.dataActions) { class="headerAction"
if (!action.type?.includes('header')) continue; @click=${() => {
resultArray.push( action.actionFunc({
html`<div item: this.selectedDataRow,
class="headerAction" table: this,
@click=${() => { });
action.actionFunc({ }}
item: this.selectedDataRow, >
table: this, ${action.iconName
}); ? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon>
}} ${action.name}`
> : action.name}
${action.iconName </div>`
? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon> )}
${action.name}`
: action.name}
</div>`
);
}
return resultArray;
})}
</div> </div>
</div> </div>
<div class="headingSeparation"></div> <div class="headingSeparation"></div>
@@ -658,11 +660,11 @@ export class DeesTable<T> extends DeesElement {
: html``} : html``}
${directives.repeat( ${directives.repeat(
renderRows, renderRows,
(itemArg, sliceIdx) => `${this.getRowId(itemArg)}::${renderStart + sliceIdx}`, (itemArg) => this.getRowId(itemArg),
(itemArg, sliceIdx) => { (itemArg, sliceIdx) => {
const rowIndex = renderStart + sliceIdx; const rowIndex = renderStart + sliceIdx;
const rowId = this.getRowId(itemArg); const rowId = this.getRowId(itemArg);
const flashSet = this.__flashingCells.get(rowId); const flashTokens = this.__flashingCells.get(rowId);
return html` return html`
<tr <tr
data-row-idx=${rowIndex} data-row-idx=${rowIndex}
@@ -694,7 +696,8 @@ export class DeesTable<T> extends DeesElement {
const isEditing = const isEditing =
this.__editingCell?.rowId === rowId && this.__editingCell?.rowId === rowId &&
this.__editingCell?.colKey === editKey; this.__editingCell?.colKey === editKey;
const isFlashing = !!flashSet?.has(editKey); const flashToken = flashTokens?.get(editKey);
const isFlashing = flashToken !== undefined;
const useFlashBorder = isFlashing && !!col.flashBorder; const useFlashBorder = isFlashing && !!col.flashBorder;
const cellClasses = [ const cellClasses = [
isEditable ? 'editable' : '', isEditable ? 'editable' : '',
@@ -720,7 +723,7 @@ export class DeesTable<T> extends DeesElement {
> >
${isFlashing ${isFlashing
? directives.keyed( ? directives.keyed(
`${rowId}:${editKey}:${this.__flashTick}`, `${rowId}:${editKey}:${flashToken}`,
innerHtml innerHtml
) )
: innerHtml} : innerHtml}
@@ -728,11 +731,11 @@ export class DeesTable<T> extends DeesElement {
`; `;
})} })}
${(() => { ${(() => {
if (this.dataActions && this.dataActions.length > 0) { if (inRowActions.length > 0) {
return html` return html`
<td class="actionsCol"> <td class="actionsCol">
<div class="actionsContainer"> <div class="actionsContainer">
${this.getActionsForType('inRow').map( ${inRowActions.map(
(actionArg) => html` (actionArg) => html`
<div <div
class="action" class="action"
@@ -780,29 +783,22 @@ export class DeesTable<T> extends DeesElement {
selected selected
</div> </div>
<div class="footerActions"> <div class="footerActions">
${directives.resolveExec(async () => { ${footerActions.map(
const resultArray: TemplateResult[] = []; (action) => html`<div
for (const action of this.dataActions) { class="footerAction"
if (!action.type?.includes('footer')) continue; @click=${() => {
resultArray.push( action.actionFunc({
html`<div item: this.selectedDataRow,
class="footerAction" table: this,
@click=${() => { });
action.actionFunc({ }}
item: this.selectedDataRow, >
table: this, ${action.iconName
}); ? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon>
}} ${action.name}`
> : action.name}
${action.iconName </div>`
? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon> )}
${action.name}`
: action.name}
</div>`
);
}
return resultArray;
})}
</div> </div>
</div> </div>
</dees-tile> </dees-tile>
@@ -1160,7 +1156,7 @@ export class DeesTable<T> extends DeesElement {
/** /**
* Measures the height of the first rendered body row and stores it for * Measures the height of the first rendered body row and stores it for
* subsequent virtualization math. Idempotent — only measures once per * subsequent virtualization math. Idempotent — only measures once per
* `data`/`columns` pair (cleared in `updated()` when those change). * rendered table layout (cleared in `updated()` when that layout changes).
*/ */
private __measureRowHeight() { private __measureRowHeight() {
if (!this.virtualized || this.__rowHeightMeasured) return; if (!this.virtualized || this.__rowHeightMeasured) return;
@@ -1426,20 +1422,16 @@ export class DeesTable<T> extends DeesElement {
if (newlyFlashing.size === 0) return; if (newlyFlashing.size === 0) return;
// Merge with any in-flight flashes from a rapid second update so a cell // 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 // that changes twice before its animation ends gets a clean restart,
// restart (via __flashTick / directives.keyed) instead of stacking. // while unrelated cells keep their existing DOM subtree.
const flashToken = ++this.__flashTick;
const nextFlashingCells = new Map(this.__flashingCells);
for (const [rowId, cols] of newlyFlashing) { for (const [rowId, cols] of newlyFlashing) {
const existing = this.__flashingCells.get(rowId); const existing = new Map(nextFlashingCells.get(rowId) ?? []);
if (existing) { for (const colKey of cols) existing.set(colKey, flashToken);
for (const c of cols) existing.add(c); nextFlashingCells.set(rowId, existing);
} else {
this.__flashingCells.set(rowId, cols);
}
} }
this.__flashTick++; this.__flashingCells = nextFlashingCells;
// 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); if (this.__flashClearTimer) clearTimeout(this.__flashClearTimer);
this.__flashClearTimer = setTimeout(() => { this.__flashClearTimer = setTimeout(() => {
this.__flashingCells = new Map(); this.__flashingCells = new Map();
@@ -1449,6 +1441,9 @@ export class DeesTable<T> extends DeesElement {
public async updated(changedProperties: Map<string | number | symbol, unknown>): Promise<void> { public async updated(changedProperties: Map<string | number | symbol, unknown>): Promise<void> {
super.updated(changedProperties); super.updated(changedProperties);
const effectiveColumns = this.__getEffectiveColumns();
const currentTable = this.shadowRoot?.querySelector('table') ?? null;
const inRowActionCount = this.getActionsForType('inRow').length;
// Feed highlightDuration into the CSS variable so JS and CSS stay in // Feed highlightDuration into the CSS variable so JS and CSS stay in
// sync via a single source of truth. // sync via a single source of truth.
@@ -1456,15 +1451,23 @@ export class DeesTable<T> extends DeesElement {
this.style.setProperty('--dees-table-flash-duration', `${this.highlightDuration}ms`); this.style.setProperty('--dees-table-flash-duration', `${this.highlightDuration}ms`);
} }
// Only re-measure column widths when the data or schema actually changed // Only re-measure column widths when layout-affecting inputs changed or
// (or on first paint). `determineColumnWidths` is the single biggest // when a new <table> element was rendered after previously having none.
// first-paint cost — it forces multiple layout flushes per row. const columnLayoutChanged =
const dataOrColsChanged = !!currentTable && (
!this.__columnsSizedFor || !this.__columnsSizedFor ||
this.__columnsSizedFor.data !== this.data || this.__columnsSizedFor.effectiveColumns !== effectiveColumns ||
this.__columnsSizedFor.columns !== this.columns; this.__columnsSizedFor.showSelectionCheckbox !== this.showSelectionCheckbox ||
if (dataOrColsChanged) { this.__columnsSizedFor.inRowActionCount !== inRowActionCount ||
this.__columnsSizedFor = { data: this.data, columns: this.columns }; this.__columnsSizedFor.table !== currentTable
);
if (currentTable && columnLayoutChanged) {
this.__columnsSizedFor = {
effectiveColumns,
showSelectionCheckbox: this.showSelectionCheckbox,
inRowActionCount,
table: currentTable,
};
this.determineColumnWidths(); this.determineColumnWidths();
// Force re-measure of row height; structure may have changed. // Force re-measure of row height; structure may have changed.
this.__rowHeightMeasured = false; this.__rowHeightMeasured = false;
@@ -1502,7 +1505,7 @@ export class DeesTable<T> extends DeesElement {
if ( if (
!this.fixedHeight && !this.fixedHeight &&
this.data.length > 0 && this.data.length > 0 &&
(this.__floatingActive || dataOrColsChanged) (this.__floatingActive || columnLayoutChanged)
) { ) {
this.__syncFloatingHeader(); this.__syncFloatingHeader();
} }
@@ -1804,10 +1807,7 @@ export class DeesTable<T> extends DeesElement {
* Used by the modal helper to render human-friendly labels. * Used by the modal helper to render human-friendly labels.
*/ */
private _lookupColumnByKey(key: string): Column<T> | undefined { private _lookupColumnByKey(key: string): Column<T> | undefined {
const usingColumns = Array.isArray(this.columns) && this.columns.length > 0; const effective = this.__getEffectiveColumns();
const effective = usingColumns
? computeEffectiveColumnsFn(this.columns, this.augmentFromDisplayFunction, this.displayFunction, this.data)
: computeColumnsFromDisplayFunctionFn(this.displayFunction, this.data);
return effective.find((c) => String(c.key) === key); return effective.find((c) => String(c.key) === key);
} }
@@ -2543,9 +2543,7 @@ export class DeesTable<T> extends DeesElement {
const view: T[] = (this as any)._lastViewData ?? []; const view: T[] = (this as any)._lastViewData ?? [];
if (view.length === 0) return; if (view.length === 0) return;
// Recompute editable columns from the latest effective set. // Recompute editable columns from the latest effective set.
const allCols: Column<T>[] = Array.isArray(this.columns) && this.columns.length > 0 const allCols = this.__getEffectiveColumns();
? computeEffectiveColumnsFn(this.columns, this.augmentFromDisplayFunction, this.displayFunction, this.data)
: computeColumnsFromDisplayFunctionFn(this.displayFunction, this.data);
const editableCols = this.__editableColumns(allCols); const editableCols = this.__editableColumns(allCols);
if (editableCols.length === 0) return; if (editableCols.length === 0) return;

View File

@@ -10,6 +10,7 @@ import {
css, css,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { themeDefaultStyles } from '../../00theme.js'; import { themeDefaultStyles } from '../../00theme.js';
import '../../00group-layout/dees-tile/dees-tile.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -90,24 +91,25 @@ export class DeesSimpleLogin extends DeesElement {
color: var(--dees-color-text-muted); color: var(--dees-color-text-muted);
} }
.login-card { dees-tile {
background: var(--dees-color-bg-primary); width: 100%;
border: 1px solid var(--dees-color-border-default); }
border-radius: 8px;
dees-tile::part(content) {
padding: 24px; padding: 24px;
} }
.login-card dees-form { dees-tile dees-form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
} }
.login-card dees-input-text { dees-tile dees-input-text {
width: 100%; width: 100%;
} }
.login-card dees-form-submit { dees-tile dees-form-submit {
margin-top: 8px; margin-top: 8px;
width: 100%; width: 100%;
} }
@@ -122,13 +124,13 @@ export class DeesSimpleLogin extends DeesElement {
<div class="header">Sign in</div> <div class="header">Sign in</div>
<div class="subheader">Enter your credentials to access ${this.name}</div> <div class="subheader">Enter your credentials to access ${this.name}</div>
</div> </div>
<div class="login-card"> <dees-tile .heading=${'Credentials'}>
<dees-form> <dees-form>
<dees-input-text key="username" label="Username" required></dees-input-text> <dees-input-text key="username" label="Username" required></dees-input-text>
<dees-input-text key="password" label="Password" isPasswordBool required></dees-input-text> <dees-input-text key="password" label="Password" isPasswordBool required></dees-input-text>
<dees-form-submit>Sign in</dees-form-submit> <dees-form-submit>Sign in</dees-form-submit>
</dees-form> </dees-form>
</div> </dees-tile>
</div> </div>
</div> </div>
<div class="slotContainer"> <div class="slotContainer">