Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0375508f0 | |||
| 3e86ba034b | |||
| 05e74cbe2e | |||
| 8fbdbf9f64 | |||
| bfbc0f108e | |||
| bab7528f0b | |||
| 6047705e7d | |||
| ab19b561c4 | |||
| 7ef3613e91 | |||
| 940eebe29f | |||
| 8ecaffe165 |
35
changelog.md
35
changelog.md
@@ -1,5 +1,40 @@
|
||||
# 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)
|
||||
add dees-settings layout component for displaying read-only settings with footer actions
|
||||
|
||||
- introduces a new dees-settings element with heading, description, settings field grid, and footer action support
|
||||
- exports dees-settings from the 00group-layout module index
|
||||
- adds demo examples covering populated, empty, and multi-action states
|
||||
|
||||
## 2026-04-12 - 3.77.0 - feat(dees-table)
|
||||
add configurable cell flash comparison and border highlight mode
|
||||
|
||||
- adds column-level flashCompare support so update highlighting can detect meaningful changes for custom cell values
|
||||
- adds flashBorder styling for cells with badges, icons, or custom rendered content where text-color flashing is not visible
|
||||
- avoids false-positive flash animations for non-primitive cell values unless a custom comparator is provided
|
||||
|
||||
## 2026-04-12 - 3.76.1 - fix(demo-inputs)
|
||||
wrap input component demos in dees-form containers for consistent form integration
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@design.estate/dees-catalog",
|
||||
"version": "3.76.1",
|
||||
"version": "3.78.3",
|
||||
"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",
|
||||
@@ -18,7 +18,7 @@
|
||||
"dependencies": {
|
||||
"@design.estate/dees-domtools": "^2.5.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/free-brands-svg-icons": "^7.2.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.2.0",
|
||||
|
||||
33
pnpm-lock.yaml
generated
33
pnpm-lock.yaml
generated
@@ -15,8 +15,8 @@ importers:
|
||||
specifier: ^2.2.4
|
||||
version: 2.2.4
|
||||
'@design.estate/dees-wcctools':
|
||||
specifier: ^3.8.4
|
||||
version: 3.8.4
|
||||
specifier: ^3.9.0
|
||||
version: 3.9.0
|
||||
'@fortawesome/fontawesome-svg-core':
|
||||
specifier: ^7.2.0
|
||||
version: 7.2.0
|
||||
@@ -323,8 +323,8 @@ packages:
|
||||
'@design.estate/dees-element@2.2.4':
|
||||
resolution: {integrity: sha512-O9cA6flBMMd+pBwMQrZXwAWel9yVxgokolb+Em6gvkXxPJ0P/B5UDn4Vc2d4ts3ta55PTBm+l2dPeDVGx/bl7Q==}
|
||||
|
||||
'@design.estate/dees-wcctools@3.8.4':
|
||||
resolution: {integrity: sha512-KpFK/azK+a/Xpq33pXKcho+tdFKVHhKZM5ArvHqo9QMwTczgp5DZZgowTDUuqAofjZwnuVfCPHK/Pw9e64N46A==}
|
||||
'@design.estate/dees-wcctools@3.9.0':
|
||||
resolution: {integrity: sha512-0vZBaGBEGIbl8hx+8BezIIea3U5T7iSHHF9VqlJZGf+nOFIW4zBAxcCljH8YzZ1Yayp6BEjxp/pQXjHN2YB3Jg==}
|
||||
|
||||
'@emnapi/core@1.8.1':
|
||||
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
|
||||
@@ -2070,6 +2070,12 @@ packages:
|
||||
'@types/debug@4.1.12':
|
||||
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':
|
||||
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
|
||||
|
||||
@@ -3136,6 +3142,9 @@ packages:
|
||||
mdurl@2.0.0:
|
||||
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
|
||||
|
||||
mediabunny@1.40.1:
|
||||
resolution: {integrity: sha512-HU/stGzAkdWaJIly6ypbUVgAUvT9kt39DIg0IaErR7/1fwtTmgUYs4i8uEPYcgcjPjbB9gtBmUXOLnXi6J2LDw==}
|
||||
|
||||
memory-pager@1.5.0:
|
||||
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
|
||||
|
||||
@@ -4711,7 +4720,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.5.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/free-brands-svg-icons': 7.2.0
|
||||
'@fortawesome/free-regular-svg-icons': 7.2.0
|
||||
@@ -4787,12 +4796,13 @@ snapshots:
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
'@design.estate/dees-wcctools@3.8.4':
|
||||
'@design.estate/dees-wcctools@3.9.0':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.5.4
|
||||
'@design.estate/dees-element': 2.2.4
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
lit: 3.3.2
|
||||
mediabunny: 1.40.1
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- react
|
||||
@@ -7245,6 +7255,12 @@ snapshots:
|
||||
dependencies:
|
||||
'@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':
|
||||
dependencies:
|
||||
'@types/jsonfile': 6.1.4
|
||||
@@ -8468,6 +8484,11 @@ snapshots:
|
||||
|
||||
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: {}
|
||||
|
||||
micromark-core-commonmark@2.0.3:
|
||||
|
||||
31
readme.md
31
readme.md
@@ -59,8 +59,8 @@ For developers working on this library, please refer to the [UI Components Playb
|
||||
| **Forms** | [`DeesForm`](#deesform), [`DeesInputText`](#deesinputtext), [`DeesInputCheckbox`](#deesinputcheckbox), [`DeesInputDropdown`](#deesinputdropdown), [`DeesInputRadiogroup`](#deesinputradiogroup), [`DeesInputFileupload`](#deesinputfileupload), [`DeesInputIban`](#deesinputiban), [`DeesInputPhone`](#deesinputphone), [`DeesInputQuantitySelector`](#deesinputquantityselector), [`DeesInputMultitoggle`](#deesinputmultitoggle), [`DeesInputToggle`](#deesinputtoggle), [`DeesInputTags`](#deesinputtags), [`DeesInputTypelist`](#deesinputtypelist), [`DeesInputList`](#deesinputlist), [`DeesInputProfilepicture`](#deesinputprofilepicture), [`DeesInputRichtext`](#deesinputrichtext), [`DeesInputWysiwyg`](#deesinputwysiwyg), [`DeesInputDatepicker`](#deesinputdatepicker), [`DeesInputSearchselect`](#deesinputsearchselect), [`DeesInputCode`](#deesinputcode), [`DeesFormSubmit`](#deesformsubmit) |
|
||||
| **App Shell (Layout)** | [`DeesAppui`](#deesappui-️), [`DeesAppuiMainmenu`](#deesappuimainmenu), [`DeesAppuiSecondarymenu`](#deesappuisecondarymenu), [`DeesAppuiMaincontent`](#deesappuimaincontent), [`DeesAppuiAppbar`](#deesappuiappbar), [`DeesAppuiActivitylog`](#deesappuiactivitylog), [`DeesAppuiBottombar`](#deesappuibottombar), [`DeesAppuiProfiledropdown`](#deesappuiprofiledropdown), [`DeesAppuiTabs`](#deesappuitabs), [`DeesMobileNavigation`](#deesmobilenavigation), [`DeesDashboardGrid`](#deesdashboardgrid) |
|
||||
| **Data Display** | [`DeesTable`](#deestable), [`DeesDataviewCodebox`](#deesdataviewcodebox), [`DeesDataviewStatusobject`](#deesdataviewstatusobject), [`DeesPdf`](#deespdf), [`DeesStatsGrid`](#deesstatsgrid), [`DeesPagination`](#deespagination), [`DeesStorageBrowser`](#deesstorgebrowser) |
|
||||
| **Media & Tiles** | [`DeesTilePdf`](#deestilepdf), [`DeesTileImage`](#deestileimage), [`DeesTileAudio`](#deestileaudio), [`DeesTileVideo`](#deestilevideo), [`DeesTileNote`](#deestilenote), [`DeesTileFolder`](#deestilefolder), [`DeesPreview`](#deespreview), [`DeesPdfViewer`](#deespdfviewer), [`DeesPdfPreview`](#deespdfpreview), [`DeesImageViewer`](#deesimageviewer), [`DeesAudioViewer`](#deesaudioviewer), [`DeesVideoViewer`](#deesvideoviewer) |
|
||||
| **Visualization** | [`DeesChartArea`](#deeschartarea), [`DeesChartLog`](#deeschartlog) |
|
||||
| **Media & Thumbnails** | [`DeesThumbnailPdf`](#deesthumbnailpdf), [`DeesThumbnailImage`](#deesthumbnailimage), [`DeesThumbnailAudio`](#deesthumbnailaudio), [`DeesThumbnailVideo`](#deesthumbnalvideo), [`DeesThumbnailNote`](#deesthumbnailnote), [`DeesThumbnailFolder`](#deesthumbnailfolder), [`DeesPreview`](#deespreview), [`DeesPdfViewer`](#deespdfviewer), [`DeesImageViewer`](#deesimageviewer), [`DeesAudioViewer`](#deesaudioviewer), [`DeesVideoViewer`](#deesvideoviewer) |
|
||||
| **Visualization** | [`DeesChartArea`](#deeschartarea), [`DeesChartBar`](#deeschartbar), [`DeesChartDonut`](#deeschartdonut), [`DeesChartGauge`](#deeschartgauge), [`DeesChartRadar`](#deeschartradar), [`DeesChartLog`](#deeschartlog) |
|
||||
| **Dialogs & Overlays** | [`DeesModal`](#deesmodal), [`DeesContextmenu`](#deescontextmenu), [`DeesSpeechbubble`](#deesspeechbubble), [`DeesWindowlayer`](#deeswindowlayer) |
|
||||
| **Navigation** | [`DeesStepper`](#deesstepper), [`DeesProgressbar`](#deesprogressbar) |
|
||||
| **Workspace / IDE** | [`DeesWorkspace`](#deesworkspace), [`DeesWorkspaceMonaco`](#deesworkspacemonaco), [`DeesWorkspaceDiffEditor`](#deesworkspacediffeditor), [`DeesWorkspaceFiletree`](#deesworkspacefiletree), [`DeesWorkspaceTerminal`](#deesworkspaceterminal), [`DeesWorkspaceTerminalPreview`](#deesworkspaceterminalpreview), [`DeesWorkspaceMarkdown`](#deesworkspacemarkdown), [`DeesWorkspaceMarkdownoutlet`](#deesworkspacemarkdownoutlet), [`DeesWorkspaceBottombar`](#deesworkspacebottombar) |
|
||||
@@ -143,14 +143,13 @@ Display icons from FontAwesome and Lucide icon libraries with library prefixes.
|
||||
```
|
||||
|
||||
#### `DeesLabel`
|
||||
Text label component with optional icon and status indicators.
|
||||
Text label component with optional required indicator and info tooltip. Used internally by all input components.
|
||||
|
||||
```typescript
|
||||
<dees-label
|
||||
text="Status" // Label text
|
||||
icon="info-circle" // Optional: icon name
|
||||
type="info" // Options: default, info, success, warning, error
|
||||
size="medium" // Options: small, medium, large
|
||||
.label=${'Email Address'} // Label text
|
||||
.required=${true} // Optional: shows red asterisk
|
||||
.infoText=${'We will never share your email'} // Optional: shows hover info icon with tooltip
|
||||
></dees-label>
|
||||
```
|
||||
|
||||
@@ -321,7 +320,7 @@ Container component for form elements with built-in validation and data handling
|
||||
```
|
||||
|
||||
#### `DeesInputText`
|
||||
Text input field with validation and formatting options.
|
||||
Text input field with validation, info tooltips, description text, and context menu (Cut/Copy/Paste/Select All).
|
||||
|
||||
```typescript
|
||||
<dees-input-text
|
||||
@@ -330,10 +329,20 @@ Text input field with validation and formatting options.
|
||||
value="initial@value.com" // Initial value
|
||||
required // Makes the field required
|
||||
disabled // Disables the input
|
||||
placeholder="Enter your email"
|
||||
.infoText=${'Hover icon tooltip text'} // Shows ⓘ icon on label with hover tooltip
|
||||
.description=${'Permanent help text below the input'} // Small text below the input
|
||||
.validationFunction=${(value) => { // Auto-validates on every keystroke
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (emailRegex.test(value)) {
|
||||
return { valid: true, message: 'Email is valid' };
|
||||
}
|
||||
return { valid: false, message: 'Please enter a valid email' };
|
||||
}}
|
||||
></dees-input-text>
|
||||
```
|
||||
|
||||
> 💡 **All input components** share these common properties from `DeesInputBase`: `key`, `label`, `required`, `disabled`, `infoText`, `description`, `layoutMode`, `labelPosition`.
|
||||
|
||||
#### `DeesInputCheckbox`
|
||||
Checkbox input component for boolean values.
|
||||
|
||||
@@ -1780,7 +1789,7 @@ interface ITileFolderItem {
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./license) file.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
@@ -1798,5 +1807,3 @@ Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
|
||||
167
test/test.dees-table-liveupdates.chromium.ts
Normal file
167
test/test.dees-table-liveupdates.chromium.ts
Normal 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();
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-catalog',
|
||||
version: '3.76.1',
|
||||
version: '3.78.3',
|
||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||
}
|
||||
|
||||
@@ -293,9 +293,9 @@ export class DeesTable<T> extends DeesElement {
|
||||
private accessor __floatingActive: boolean = false;
|
||||
|
||||
// ─── 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()
|
||||
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. */
|
||||
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. */
|
||||
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;
|
||||
|
||||
/** One-shot console.warn gate for missing rowKey in flash mode. */
|
||||
@@ -317,7 +317,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
columns: any;
|
||||
augment: boolean;
|
||||
displayFunction: any;
|
||||
data: any;
|
||||
displayShapeKey: string;
|
||||
out: Column<T>[];
|
||||
};
|
||||
private __memoViewData?: {
|
||||
@@ -329,8 +329,13 @@ export class DeesTable<T> extends DeesElement {
|
||||
effectiveColumns: Column<T>[];
|
||||
out: T[];
|
||||
};
|
||||
/** Tracks the (data, columns) pair that `determineColumnWidths()` last sized for. */
|
||||
private __columnsSizedFor?: { data: any; columns: any };
|
||||
/** Tracks the layout inputs that `determineColumnWidths()` last sized for. */
|
||||
private __columnsSizedFor?: {
|
||||
effectiveColumns: Column<T>[];
|
||||
showSelectionCheckbox: boolean;
|
||||
inRowActionCount: number;
|
||||
table: HTMLTableElement;
|
||||
};
|
||||
|
||||
// ─── Virtualization state ────────────────────────────────────────────
|
||||
/** 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 item = view.find((r) => this.getRowId(r) === this.__focusedCell!.rowId);
|
||||
if (!item) return;
|
||||
const allCols: Column<T>[] =
|
||||
Array.isArray(this.columns) && this.columns.length > 0
|
||||
? computeEffectiveColumnsFn(
|
||||
this.columns,
|
||||
this.augmentFromDisplayFunction,
|
||||
this.displayFunction,
|
||||
this.data
|
||||
)
|
||||
: computeColumnsFromDisplayFunctionFn(this.displayFunction, this.data);
|
||||
const allCols = this.__getEffectiveColumns();
|
||||
const col = allCols.find((c) => String(c.key) === this.__focusedCell!.colKey);
|
||||
if (!col || !this.__isColumnEditable(col)) return;
|
||||
eventArg.preventDefault();
|
||||
@@ -469,15 +466,24 @@ export class DeesTable<T> extends DeesElement {
|
||||
* that affect it. Avoids re-running `computeEffectiveColumnsFn` /
|
||||
* `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>[] {
|
||||
const usingColumns = Array.isArray(this.columns) && this.columns.length > 0;
|
||||
const displayShapeKey = !usingColumns || this.augmentFromDisplayFunction
|
||||
? this.__getDisplayFunctionShapeKey()
|
||||
: '';
|
||||
const cache = this.__memoEffectiveCols;
|
||||
if (
|
||||
cache &&
|
||||
cache.columns === this.columns &&
|
||||
cache.augment === this.augmentFromDisplayFunction &&
|
||||
cache.displayFunction === this.displayFunction &&
|
||||
cache.data === this.data
|
||||
cache.displayShapeKey === displayShapeKey
|
||||
) {
|
||||
return cache.out;
|
||||
}
|
||||
@@ -493,7 +499,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
columns: this.columns,
|
||||
augment: this.augmentFromDisplayFunction,
|
||||
displayFunction: this.displayFunction,
|
||||
data: this.data,
|
||||
displayShapeKey,
|
||||
out,
|
||||
};
|
||||
return out;
|
||||
@@ -543,6 +549,9 @@ export class DeesTable<T> extends DeesElement {
|
||||
public render(): TemplateResult {
|
||||
const effectiveColumns = this.__getEffectiveColumns();
|
||||
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;
|
||||
|
||||
// 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>
|
||||
<div class="headerActions">
|
||||
${directives.resolveExec(async () => {
|
||||
const resultArray: TemplateResult[] = [];
|
||||
for (const action of this.dataActions) {
|
||||
if (!action.type?.includes('header')) continue;
|
||||
resultArray.push(
|
||||
html`<div
|
||||
class="headerAction"
|
||||
@click=${() => {
|
||||
action.actionFunc({
|
||||
item: this.selectedDataRow,
|
||||
table: this,
|
||||
});
|
||||
}}
|
||||
>
|
||||
${action.iconName
|
||||
? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon>
|
||||
${action.name}`
|
||||
: action.name}
|
||||
</div>`
|
||||
);
|
||||
}
|
||||
return resultArray;
|
||||
})}
|
||||
${headerActions.map(
|
||||
(action) => html`<div
|
||||
class="headerAction"
|
||||
@click=${() => {
|
||||
action.actionFunc({
|
||||
item: this.selectedDataRow,
|
||||
table: this,
|
||||
});
|
||||
}}
|
||||
>
|
||||
${action.iconName
|
||||
? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon>
|
||||
${action.name}`
|
||||
: action.name}
|
||||
</div>`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="headingSeparation"></div>
|
||||
@@ -658,11 +660,11 @@ export class DeesTable<T> extends DeesElement {
|
||||
: html``}
|
||||
${directives.repeat(
|
||||
renderRows,
|
||||
(itemArg, sliceIdx) => `${this.getRowId(itemArg)}::${renderStart + sliceIdx}`,
|
||||
(itemArg) => this.getRowId(itemArg),
|
||||
(itemArg, sliceIdx) => {
|
||||
const rowIndex = renderStart + sliceIdx;
|
||||
const rowId = this.getRowId(itemArg);
|
||||
const flashSet = this.__flashingCells.get(rowId);
|
||||
const flashTokens = this.__flashingCells.get(rowId);
|
||||
return html`
|
||||
<tr
|
||||
data-row-idx=${rowIndex}
|
||||
@@ -694,7 +696,9 @@ export class DeesTable<T> extends DeesElement {
|
||||
const isEditing =
|
||||
this.__editingCell?.rowId === rowId &&
|
||||
this.__editingCell?.colKey === editKey;
|
||||
const isFlashing = !!flashSet?.has(editKey);
|
||||
const flashToken = flashTokens?.get(editKey);
|
||||
const isFlashing = flashToken !== undefined;
|
||||
const useFlashBorder = isFlashing && !!col.flashBorder;
|
||||
const cellClasses = [
|
||||
isEditable ? 'editable' : '',
|
||||
isFocused && !isEditing ? 'focused' : '',
|
||||
@@ -702,8 +706,13 @@ export class DeesTable<T> extends DeesElement {
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
const flashClass = isFlashing
|
||||
? useFlashBorder
|
||||
? 'innerCellContainer flashing-border'
|
||||
: 'innerCellContainer flashing'
|
||||
: 'innerCellContainer';
|
||||
const innerHtml = html`<div
|
||||
class=${isFlashing ? 'innerCellContainer flashing' : 'innerCellContainer'}
|
||||
class=${flashClass}
|
||||
>
|
||||
${isEditing ? this.renderCellEditor(itemArg, col) : content}
|
||||
</div>`;
|
||||
@@ -714,7 +723,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
>
|
||||
${isFlashing
|
||||
? directives.keyed(
|
||||
`${rowId}:${editKey}:${this.__flashTick}`,
|
||||
`${rowId}:${editKey}:${flashToken}`,
|
||||
innerHtml
|
||||
)
|
||||
: innerHtml}
|
||||
@@ -722,11 +731,11 @@ export class DeesTable<T> extends DeesElement {
|
||||
`;
|
||||
})}
|
||||
${(() => {
|
||||
if (this.dataActions && this.dataActions.length > 0) {
|
||||
if (inRowActions.length > 0) {
|
||||
return html`
|
||||
<td class="actionsCol">
|
||||
<div class="actionsContainer">
|
||||
${this.getActionsForType('inRow').map(
|
||||
${inRowActions.map(
|
||||
(actionArg) => html`
|
||||
<div
|
||||
class="action"
|
||||
@@ -774,29 +783,22 @@ export class DeesTable<T> extends DeesElement {
|
||||
selected
|
||||
</div>
|
||||
<div class="footerActions">
|
||||
${directives.resolveExec(async () => {
|
||||
const resultArray: TemplateResult[] = [];
|
||||
for (const action of this.dataActions) {
|
||||
if (!action.type?.includes('footer')) continue;
|
||||
resultArray.push(
|
||||
html`<div
|
||||
class="footerAction"
|
||||
@click=${() => {
|
||||
action.actionFunc({
|
||||
item: this.selectedDataRow,
|
||||
table: this,
|
||||
});
|
||||
}}
|
||||
>
|
||||
${action.iconName
|
||||
? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon>
|
||||
${action.name}`
|
||||
: action.name}
|
||||
</div>`
|
||||
);
|
||||
}
|
||||
return resultArray;
|
||||
})}
|
||||
${footerActions.map(
|
||||
(action) => html`<div
|
||||
class="footerAction"
|
||||
@click=${() => {
|
||||
action.actionFunc({
|
||||
item: this.selectedDataRow,
|
||||
table: this,
|
||||
});
|
||||
}}
|
||||
>
|
||||
${action.iconName
|
||||
? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon>
|
||||
${action.name}`
|
||||
: action.name}
|
||||
</div>`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</dees-tile>
|
||||
@@ -1154,7 +1156,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
/**
|
||||
* Measures the height of the first rendered body row and stores it for
|
||||
* 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() {
|
||||
if (!this.virtualized || this.__rowHeightMeasured) return;
|
||||
@@ -1362,6 +1364,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
|
||||
const effectiveColumns = this.__getEffectiveColumns();
|
||||
const visibleCols = effectiveColumns.filter((c) => !c.hidden);
|
||||
const colByKey = new Map<string, Column<T>>(visibleCols.map((c) => [String(c.key), c]));
|
||||
const nextSnapshot = new Map<string, Map<string, unknown>>();
|
||||
const newlyFlashing = new Map<string, Set<string>>();
|
||||
|
||||
@@ -1376,7 +1379,26 @@ export class DeesTable<T> extends DeesElement {
|
||||
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) {
|
||||
const prevVal = prevCells.get(colKey);
|
||||
|
||||
// Look up the column definition for flash options.
|
||||
const colDef = colByKey.get(colKey);
|
||||
|
||||
// Determine whether the cell changed.
|
||||
let changed: boolean;
|
||||
if (colDef?.flashCompare) {
|
||||
// Explicit custom comparator — caller decides.
|
||||
changed = colDef.flashCompare(prevVal, nextVal);
|
||||
} else if (nextVal !== null && nextVal !== undefined && typeof nextVal === 'object') {
|
||||
// Non-primitive (TemplateResult, object, array, etc.) — skip by
|
||||
// default. Custom renderings don't benefit from the text-color
|
||||
// flash and reference inequality causes false positives.
|
||||
changed = false;
|
||||
} else {
|
||||
changed = prevVal !== nextVal;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
// Don't flash the cell the user is actively editing.
|
||||
if (
|
||||
this.__editingCell &&
|
||||
@@ -1400,20 +1422,16 @@ export class DeesTable<T> extends DeesElement {
|
||||
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.
|
||||
// that changes twice before its animation ends gets a clean restart,
|
||||
// while unrelated cells keep their existing DOM subtree.
|
||||
const flashToken = ++this.__flashTick;
|
||||
const nextFlashingCells = new Map(this.__flashingCells);
|
||||
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);
|
||||
}
|
||||
const existing = new Map(nextFlashingCells.get(rowId) ?? []);
|
||||
for (const colKey of cols) existing.set(colKey, flashToken);
|
||||
nextFlashingCells.set(rowId, existing);
|
||||
}
|
||||
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);
|
||||
this.__flashingCells = nextFlashingCells;
|
||||
if (this.__flashClearTimer) clearTimeout(this.__flashClearTimer);
|
||||
this.__flashClearTimer = setTimeout(() => {
|
||||
this.__flashingCells = new Map();
|
||||
@@ -1423,6 +1441,9 @@ export class DeesTable<T> extends DeesElement {
|
||||
|
||||
public async updated(changedProperties: Map<string | number | symbol, unknown>): Promise<void> {
|
||||
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
|
||||
// sync via a single source of truth.
|
||||
@@ -1430,15 +1451,23 @@ export class DeesTable<T> extends DeesElement {
|
||||
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.
|
||||
const dataOrColsChanged =
|
||||
!this.__columnsSizedFor ||
|
||||
this.__columnsSizedFor.data !== this.data ||
|
||||
this.__columnsSizedFor.columns !== this.columns;
|
||||
if (dataOrColsChanged) {
|
||||
this.__columnsSizedFor = { data: this.data, columns: this.columns };
|
||||
// Only re-measure column widths when layout-affecting inputs changed or
|
||||
// when a new <table> element was rendered after previously having none.
|
||||
const columnLayoutChanged =
|
||||
!!currentTable && (
|
||||
!this.__columnsSizedFor ||
|
||||
this.__columnsSizedFor.effectiveColumns !== effectiveColumns ||
|
||||
this.__columnsSizedFor.showSelectionCheckbox !== this.showSelectionCheckbox ||
|
||||
this.__columnsSizedFor.inRowActionCount !== inRowActionCount ||
|
||||
this.__columnsSizedFor.table !== currentTable
|
||||
);
|
||||
if (currentTable && columnLayoutChanged) {
|
||||
this.__columnsSizedFor = {
|
||||
effectiveColumns,
|
||||
showSelectionCheckbox: this.showSelectionCheckbox,
|
||||
inRowActionCount,
|
||||
table: currentTable,
|
||||
};
|
||||
this.determineColumnWidths();
|
||||
// Force re-measure of row height; structure may have changed.
|
||||
this.__rowHeightMeasured = false;
|
||||
@@ -1476,7 +1505,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
if (
|
||||
!this.fixedHeight &&
|
||||
this.data.length > 0 &&
|
||||
(this.__floatingActive || dataOrColsChanged)
|
||||
(this.__floatingActive || columnLayoutChanged)
|
||||
) {
|
||||
this.__syncFloatingHeader();
|
||||
}
|
||||
@@ -1778,10 +1807,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
* Used by the modal helper to render human-friendly labels.
|
||||
*/
|
||||
private _lookupColumnByKey(key: string): Column<T> | undefined {
|
||||
const usingColumns = Array.isArray(this.columns) && this.columns.length > 0;
|
||||
const effective = usingColumns
|
||||
? computeEffectiveColumnsFn(this.columns, this.augmentFromDisplayFunction, this.displayFunction, this.data)
|
||||
: computeColumnsFromDisplayFunctionFn(this.displayFunction, this.data);
|
||||
const effective = this.__getEffectiveColumns();
|
||||
return effective.find((c) => String(c.key) === key);
|
||||
}
|
||||
|
||||
@@ -2517,9 +2543,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
const view: T[] = (this as any)._lastViewData ?? [];
|
||||
if (view.length === 0) return;
|
||||
// Recompute editable columns from the latest effective set.
|
||||
const allCols: Column<T>[] = Array.isArray(this.columns) && this.columns.length > 0
|
||||
? computeEffectiveColumnsFn(this.columns, this.augmentFromDisplayFunction, this.displayFunction, this.data)
|
||||
: computeColumnsFromDisplayFunctionFn(this.displayFunction, this.data);
|
||||
const allCols = this.__getEffectiveColumns();
|
||||
const editableCols = this.__editableColumns(allCols);
|
||||
if (editableCols.length === 0) return;
|
||||
|
||||
|
||||
@@ -404,11 +404,44 @@ export const tableStyles: CSSResult[] = [
|
||||
100% { color: var(--dees-color-text-primary); }
|
||||
}
|
||||
|
||||
/* Border/background flash variant for cells with styled content
|
||||
(badges, icons, custom components) where a text-color animation
|
||||
would be invisible. Activated via flashBorder on Column. */
|
||||
.innerCellContainer.flashing-border {
|
||||
animation: dees-table-cell-flash-border
|
||||
var(--dees-table-flash-duration, 900ms)
|
||||
var(--dees-table-flash-easing);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@keyframes dees-table-cell-flash-border {
|
||||
0%,
|
||||
35% {
|
||||
box-shadow: inset 0 0 0 1.5px var(--dees-table-flash-color);
|
||||
background: ${cssManager.bdTheme(
|
||||
'hsl(45 93% 62% / 0.10)',
|
||||
'hsl(45 93% 62% / 0.08)'
|
||||
)};
|
||||
}
|
||||
100% {
|
||||
box-shadow: inset 0 0 0 1.5px transparent;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.innerCellContainer.flashing {
|
||||
animation: none;
|
||||
color: var(--dees-table-flash-color);
|
||||
}
|
||||
.innerCellContainer.flashing-border {
|
||||
animation: none;
|
||||
box-shadow: inset 0 0 0 1.5px var(--dees-table-flash-color);
|
||||
background: ${cssManager.bdTheme(
|
||||
'hsl(45 93% 62% / 0.10)',
|
||||
'hsl(45 93% 62% / 0.08)'
|
||||
)};
|
||||
}
|
||||
}
|
||||
|
||||
/* Dev-time warning banner shown when highlight-updates="flash" but
|
||||
|
||||
@@ -65,6 +65,25 @@ export interface Column<T = any> {
|
||||
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;
|
||||
|
||||
// ─── Flash highlight options ───
|
||||
|
||||
/**
|
||||
* Custom comparison for flash-on-update diffing.
|
||||
* Return `true` if the cell should flash (i.e. the values differ).
|
||||
* When absent, non-primitive cell values are skipped entirely
|
||||
* (only strings, numbers, booleans, null, and undefined are diffed).
|
||||
*/
|
||||
flashCompare?: (prevVal: any, nextVal: any) => boolean;
|
||||
|
||||
/**
|
||||
* When `true`, flash this cell with a border/background pulse instead of
|
||||
* the default text-color animation. Useful for cells containing styled
|
||||
* badges, icons, or custom web-component renderings where a text-color
|
||||
* change would be invisible.
|
||||
* @default false
|
||||
*/
|
||||
flashBorder?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,12 +31,6 @@ export const demoFunc = () => html`
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
|
||||
@@ -13,12 +13,6 @@ export const demoFunc = () => html`
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.payment-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -13,12 +13,6 @@ export const demoFunc = () => html`
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -14,12 +14,6 @@ export const demoFunc = () => html`
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.shopping-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
|
||||
@@ -23,12 +23,6 @@ export const demoFunc = () => html`
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
|
||||
@@ -30,12 +30,6 @@ export const demoFunc = () => html`
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.grid-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
@@ -13,12 +13,6 @@ export const demoFunc = () => html`
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import './dees-settings.js';
|
||||
import type { ISettingsField, ISettingsAction } from './dees-settings.js';
|
||||
|
||||
export const demoFunc = () => {
|
||||
const acmeFields: ISettingsField[] = [
|
||||
{ key: 'email', label: 'Account email', value: 'admin@example.com' },
|
||||
{ key: 'status', label: 'Status', value: 'enabled' },
|
||||
{ key: 'mode', label: 'Mode', value: 'production' },
|
||||
{ key: 'autoRenew', label: 'Auto-renew', value: 'on' },
|
||||
{ key: 'threshold', label: 'Renewal threshold', value: '30 days' },
|
||||
];
|
||||
|
||||
const acmeActions: ISettingsAction[] = [
|
||||
{ name: 'Edit', action: () => console.log('Edit clicked') },
|
||||
];
|
||||
|
||||
const emptyActions: ISettingsAction[] = [
|
||||
{ name: 'Configure', action: () => console.log('Configure clicked') },
|
||||
];
|
||||
|
||||
const multiActions: ISettingsAction[] = [
|
||||
{ name: 'Reset', action: () => console.log('Reset clicked') },
|
||||
{ name: 'Edit', action: () => console.log('Edit clicked') },
|
||||
];
|
||||
|
||||
return html`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
${css`
|
||||
.demoBox {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')};
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
max-width: 600px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div class="demoBox">
|
||||
<dees-settings
|
||||
.heading=${'ACME Settings'}
|
||||
.settingsFields=${acmeFields}
|
||||
.actions=${acmeActions}
|
||||
></dees-settings>
|
||||
|
||||
<dees-settings
|
||||
.heading=${'ACME Settings'}
|
||||
.description=${'No ACME configuration yet. Click Configure to set up automated TLS certificate issuance.'}
|
||||
.actions=${emptyActions}
|
||||
></dees-settings>
|
||||
|
||||
<dees-settings
|
||||
.heading=${'Server Config'}
|
||||
.settingsFields=${[
|
||||
{ key: 'host', label: 'Hostname', value: 'proxy.example.com' },
|
||||
{ key: 'port', label: 'Port', value: '443' },
|
||||
]}
|
||||
.actions=${multiActions}
|
||||
></dees-settings>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
};
|
||||
196
ts_web/elements/00group-layout/dees-settings/dees-settings.ts
Normal file
196
ts_web/elements/00group-layout/dees-settings/dees-settings.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
property,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import { demoFunc } from './dees-settings.demo.js';
|
||||
import { cssGeistFontFamily } from '../../00fonts.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import '../../00group-layout/dees-tile/dees-tile.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-settings': DeesSettings;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ISettingsField {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string | TemplateResult;
|
||||
}
|
||||
|
||||
export interface ISettingsAction {
|
||||
name: string;
|
||||
action: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* dees-settings — a read-only settings display tile with modal-style footer actions.
|
||||
*
|
||||
* Renders a dees-tile with a heading, a grid of label/value fields,
|
||||
* and a footer action bar. When an action is clicked the component
|
||||
* dispatches a `settings-action` CustomEvent with the action name.
|
||||
*/
|
||||
@customElement('dees-settings')
|
||||
export class DeesSettings extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Layout'];
|
||||
|
||||
@property({ type: String })
|
||||
accessor heading: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
accessor description: string = '';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor settingsFields: ISettingsField[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor actions: ISettingsAction[] = [];
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: ${cssGeistFontFamily};
|
||||
}
|
||||
|
||||
.settingsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px 24px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.settingsField {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.fieldLabel {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
|
||||
.fieldValue {
|
||||
font-size: 13px;
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.settingsDescription {
|
||||
padding: 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
|
||||
.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.15)', 'hsl(213.1 93.9% 67.8% / 0.15)')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
const hasFields = this.settingsFields.length > 0;
|
||||
const hasActions = this.actions.length > 0;
|
||||
|
||||
return html`
|
||||
<dees-tile .heading=${this.heading}>
|
||||
${hasFields
|
||||
? html`
|
||||
<div class="settingsGrid">
|
||||
${this.settingsFields.map(
|
||||
(field) => html`
|
||||
<div class="settingsField">
|
||||
<span class="fieldLabel">${field.label}</span>
|
||||
<span class="fieldValue">${field.value}</span>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="settingsDescription">${this.description}</div>
|
||||
`}
|
||||
${hasActions
|
||||
? html`
|
||||
<div slot="footer" class="bottomButtons">
|
||||
${this.actions.map(
|
||||
(actionArg, index) => html`
|
||||
<div
|
||||
class="bottomButton ${index === this.actions.length - 1 ? 'primary' : ''}"
|
||||
@click=${() => actionArg.action()}
|
||||
>
|
||||
${actionArg.name}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</dees-tile>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/00group-layout/dees-settings/index.ts
Normal file
1
ts_web/elements/00group-layout/dees-settings/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-settings.js';
|
||||
@@ -5,5 +5,6 @@ export * from './dees-heading/index.js';
|
||||
export * from './dees-label/index.js';
|
||||
export * from './dees-pagination/index.js';
|
||||
export * from './dees-panel/index.js';
|
||||
export * from './dees-settings/index.js';
|
||||
export * from './dees-stepper/index.js';
|
||||
export * from './dees-tile/index.js';
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
css,
|
||||
} from '@design.estate/dees-element';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import '../../00group-layout/dees-tile/dees-tile.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -90,24 +91,25 @@ export class DeesSimpleLogin extends DeesElement {
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: var(--dees-color-bg-primary);
|
||||
border: 1px solid var(--dees-color-border-default);
|
||||
border-radius: 8px;
|
||||
dees-tile {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
dees-tile::part(content) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.login-card dees-form {
|
||||
dees-tile dees-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.login-card dees-input-text {
|
||||
dees-tile dees-input-text {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-card dees-form-submit {
|
||||
dees-tile dees-form-submit {
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -122,13 +124,13 @@ export class DeesSimpleLogin extends DeesElement {
|
||||
<div class="header">Sign in</div>
|
||||
<div class="subheader">Enter your credentials to access ${this.name}</div>
|
||||
</div>
|
||||
<div class="login-card">
|
||||
<dees-tile .heading=${'Credentials'}>
|
||||
<dees-form>
|
||||
<dees-input-text key="username" label="Username" 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>
|
||||
</div>
|
||||
</dees-tile>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slotContainer">
|
||||
|
||||
Reference in New Issue
Block a user