Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f4c47f0d2 | |||
| 2be1ce6908 | |||
| c0375508f0 | |||
| 3e86ba034b | |||
| 05e74cbe2e | |||
| 8fbdbf9f64 |
22
changelog.md
22
changelog.md
@@ -1,5 +1,27 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-16 - 3.79.0 - feat(dees-progressbar)
|
||||||
|
add status panels, terminal output, and legacy progress input support
|
||||||
|
|
||||||
|
- Extend dees-progressbar with label, statusText, terminalLines, statusRows, indeterminate, and showPercentage properties.
|
||||||
|
- Support legacy value input and normalized progress values while clamping and formatting percentages consistently.
|
||||||
|
- Add fixed-height status and terminal-style output with spinner animation and auto-scroll behavior for live activity updates.
|
||||||
|
- Refresh the progressbar demo and readme examples to showcase determinate, indeterminate, terminal, and compatibility usage patterns.
|
||||||
|
|
||||||
|
## 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)
|
## 2026-04-12 - 3.78.1 - fix(dees-simple-login)
|
||||||
use dees-tile for the login credentials container
|
use dees-tile for the login credentials container
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@design.estate/dees-catalog",
|
"name": "@design.estate/dees-catalog",
|
||||||
"version": "3.78.1",
|
"version": "3.79.0",
|
||||||
"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
33
pnpm-lock.yaml
generated
@@ -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:
|
||||||
|
|||||||
20
readme.md
20
readme.md
@@ -1524,15 +1524,25 @@ Multi-step navigation component for guided user flows.
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### `DeesProgressbar`
|
#### `DeesProgressbar`
|
||||||
Progress indicator component for tracking completion status.
|
Progress indicator component for tracking completion status, with optional fixed-height status text or terminal-style recent activity output.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
<dees-progressbar
|
<dees-progressbar
|
||||||
value={75}
|
.percentage=${75}
|
||||||
label="Uploading"
|
label="Uploading"
|
||||||
showPercentage
|
statusText="Uploading thumbnails to edge cache..."
|
||||||
type="determinate" // Options: determinate, indeterminate
|
.statusRows=${2}
|
||||||
status="normal" // Options: normal, success, warning, error
|
></dees-progressbar>
|
||||||
|
|
||||||
|
<dees-progressbar
|
||||||
|
label="Installing dependencies"
|
||||||
|
.indeterminate=${true}
|
||||||
|
.statusRows=${4}
|
||||||
|
.terminalLines=${[
|
||||||
|
'Resolving workspace packages',
|
||||||
|
'Downloading tarballs',
|
||||||
|
'Linking local binaries'
|
||||||
|
]}
|
||||||
></dees-progressbar>
|
></dees-progressbar>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
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 = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-catalog',
|
name: '@design.estate/dees-catalog',
|
||||||
version: '3.78.1',
|
version: '3.79.0',
|
||||||
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.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,245 @@
|
|||||||
import { html } from '@design.estate/dees-element';
|
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||||
|
import '@design.estate/dees-wcctools/demotools';
|
||||||
import { DeesProgressbar } from '../dees-progressbar/dees-progressbar.js';
|
import type { DeesProgressbar } from './dees-progressbar.js';
|
||||||
|
|
||||||
export const demoFunc = () => {
|
export const demoFunc = () => {
|
||||||
|
const terminalSnapshots = [
|
||||||
|
['Resolving workspace packages'],
|
||||||
|
['Resolving workspace packages', 'Downloading ui-assets.tar.gz'],
|
||||||
|
['Resolving workspace packages', 'Downloading ui-assets.tar.gz', 'Verifying checksum'],
|
||||||
|
['Resolving workspace packages', 'Downloading ui-assets.tar.gz', 'Verifying checksum', 'Extracting release bundle'],
|
||||||
|
['Resolving workspace packages', 'Downloading ui-assets.tar.gz', 'Verifying checksum', 'Extracting release bundle', 'Restarting application'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const getUploadStatus = (percentage: number): string => {
|
||||||
|
if (percentage >= 100) {
|
||||||
|
return 'Upload complete. Finalizing package manifest...';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (percentage >= 82) {
|
||||||
|
return 'Verifying checksums before handoff...';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (percentage >= 55) {
|
||||||
|
return 'Uploading thumbnails to edge cache...';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (percentage >= 25) {
|
||||||
|
return 'Streaming source files to the remote worker...';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Preparing archive and dependency graph...';
|
||||||
|
};
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<dees-progressbar
|
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||||
.percentage=${50}
|
const liveProgressbar = elementArg.querySelector('#live-progress') as DeesProgressbar | null;
|
||||||
></dees-progressbar>
|
const terminalProgressbar = elementArg.querySelector('#terminal-progress') as DeesProgressbar | null;
|
||||||
|
const demoElement = elementArg as HTMLElement & {
|
||||||
|
__progressbarDemoIntervalId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!liveProgressbar || !terminalProgressbar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (demoElement.__progressbarDemoIntervalId) {
|
||||||
|
window.clearInterval(demoElement.__progressbarDemoIntervalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let livePercentage = 12;
|
||||||
|
let terminalSnapshotIndex = 0;
|
||||||
|
|
||||||
|
const updateDemo = () => {
|
||||||
|
liveProgressbar.percentage = livePercentage;
|
||||||
|
liveProgressbar.statusText = getUploadStatus(livePercentage);
|
||||||
|
|
||||||
|
terminalProgressbar.terminalLines = [...terminalSnapshots[terminalSnapshotIndex]];
|
||||||
|
terminalProgressbar.percentage = Math.min(100, (terminalSnapshotIndex + 1) * 20);
|
||||||
|
terminalProgressbar.indeterminate = terminalSnapshotIndex < terminalSnapshots.length - 1;
|
||||||
|
|
||||||
|
livePercentage = livePercentage >= 100 ? 12 : Math.min(100, livePercentage + 11);
|
||||||
|
terminalSnapshotIndex = terminalSnapshotIndex >= terminalSnapshots.length - 1 ? 0 : terminalSnapshotIndex + 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateDemo();
|
||||||
|
demoElement.__progressbarDemoIntervalId = window.setInterval(updateDemo, 1400);
|
||||||
|
}}>
|
||||||
|
<style>
|
||||||
|
${css`
|
||||||
|
.demoBox {
|
||||||
|
position: relative;
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')};
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoIntro {
|
||||||
|
max-width: 720px;
|
||||||
|
color: ${cssManager.bdTheme('hsl(215 20% 30%)', 'hsl(215 18% 76%)')};
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcaseGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcaseCard {
|
||||||
|
background: ${cssManager.bdTheme('rgba(255,255,255,0.78)', 'rgba(255,255,255,0.04)')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('hsl(210 22% 86%)', 'hsl(210 10% 18%)')};
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcaseCard.wide {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcaseCard h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcaseCard p {
|
||||||
|
margin: 0;
|
||||||
|
color: ${cssManager.bdTheme('hsl(215 14% 40%)', 'hsl(215 10% 66%)')};
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressStack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeLabel {
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: ${cssManager.bdTheme('hsl(215 14% 44%)', 'hsl(215 10% 70%)')};
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.demoBox {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcaseCard.wide {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
<div class="demoBox">
|
||||||
|
<div class="demoIntro">
|
||||||
|
<code>dees-progressbar</code> can now pair a classic progress bar with a fixed-height status area. Use simple status text for clear user-facing updates or switch to terminal-like lines when you want recent steps to stay visible without causing layout jumps.
|
||||||
|
</div>
|
||||||
|
<div class="showcaseGrid">
|
||||||
|
<section class="showcaseCard">
|
||||||
|
<div class="codeLabel">Determinate</div>
|
||||||
|
<h3>Percentage plus current task</h3>
|
||||||
|
<p>Use a label, a percentage, and one short status line when the work is measurable.</p>
|
||||||
|
<div class="progressStack">
|
||||||
|
<dees-progressbar
|
||||||
|
label="Media upload"
|
||||||
|
.percentage=${68}
|
||||||
|
statusText="Uploading thumbnails to edge cache..."
|
||||||
|
.statusRows=${2}
|
||||||
|
></dees-progressbar>
|
||||||
|
<dees-progressbar
|
||||||
|
label="Asset sync"
|
||||||
|
.percentage=${100}
|
||||||
|
statusText="All files are synced and available."
|
||||||
|
.statusRows=${2}
|
||||||
|
></dees-progressbar>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="showcaseCard">
|
||||||
|
<div class="codeLabel">Indeterminate</div>
|
||||||
|
<h3>Spinner-style text indicator</h3>
|
||||||
|
<p>When there is no trustworthy percentage yet, keep the bar moving and let the text explain what is happening.</p>
|
||||||
|
<div class="progressStack">
|
||||||
|
<dees-progressbar
|
||||||
|
label="Dependency install"
|
||||||
|
.indeterminate=${true}
|
||||||
|
statusText="Downloading package metadata..."
|
||||||
|
.statusRows=${2}
|
||||||
|
></dees-progressbar>
|
||||||
|
<dees-progressbar
|
||||||
|
label="Queued job"
|
||||||
|
.percentage=${32}
|
||||||
|
.showPercentage=${false}
|
||||||
|
statusText="Waiting for a worker slot to become available..."
|
||||||
|
.statusRows=${2}
|
||||||
|
></dees-progressbar>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="showcaseCard wide">
|
||||||
|
<div class="codeLabel">Terminal Lines</div>
|
||||||
|
<h3>Fixed-height terminal-style status output</h3>
|
||||||
|
<p>The panel stays the same height while the latest step stays visible. This is useful for update flows, downloads, and staged background work.</p>
|
||||||
|
<dees-progressbar
|
||||||
|
id="terminal-progress"
|
||||||
|
label="Release bundle"
|
||||||
|
.percentage=${20}
|
||||||
|
.indeterminate=${true}
|
||||||
|
.statusRows=${4}
|
||||||
|
.terminalLines=${terminalSnapshots[0]}
|
||||||
|
></dees-progressbar>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="showcaseCard">
|
||||||
|
<div class="codeLabel">Live Demo</div>
|
||||||
|
<h3>Updating percentage and text together</h3>
|
||||||
|
<p>A single component can express both how far the job is and which phase is currently active.</p>
|
||||||
|
<dees-progressbar
|
||||||
|
id="live-progress"
|
||||||
|
label="Customer export"
|
||||||
|
.percentage=${12}
|
||||||
|
statusText="Preparing archive and dependency graph..."
|
||||||
|
.statusRows=${2}
|
||||||
|
></dees-progressbar>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="showcaseCard">
|
||||||
|
<div class="codeLabel">Compatibility</div>
|
||||||
|
<h3>Legacy <code>value</code> and <code>progress</code> inputs</h3>
|
||||||
|
<p>Existing usages can keep passing percentages directly or normalized progress values from 0 to 1.</p>
|
||||||
|
<div class="progressStack">
|
||||||
|
<dees-progressbar
|
||||||
|
label="From value"
|
||||||
|
.value=${75}
|
||||||
|
statusText="Migrating existing readme-style usage..."
|
||||||
|
.statusRows=${2}
|
||||||
|
></dees-progressbar>
|
||||||
|
<dees-progressbar
|
||||||
|
label="From progress"
|
||||||
|
.progress=${0.42}
|
||||||
|
.showPercentage=${false}
|
||||||
|
statusText="Rendering normalized progress input..."
|
||||||
|
.statusRows=${2}
|
||||||
|
></dees-progressbar>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dees-demowrapper>
|
||||||
`;
|
`;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as plugins from '../../00plugins.js';
|
|
||||||
import * as colors from '../../00colors.js';
|
import * as colors from '../../00colors.js';
|
||||||
import { demoFunc } from './dees-progressbar.demo.js';
|
import { demoFunc } from './dees-progressbar.demo.js';
|
||||||
import {
|
import {
|
||||||
@@ -6,94 +5,342 @@ import {
|
|||||||
html,
|
html,
|
||||||
DeesElement,
|
DeesElement,
|
||||||
property,
|
property,
|
||||||
type TemplateResult,
|
|
||||||
cssManager,
|
cssManager,
|
||||||
css,
|
css,
|
||||||
type CSSResult,
|
|
||||||
unsafeCSS,
|
|
||||||
unsafeHTML,
|
|
||||||
state,
|
state,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
|
||||||
import { themeDefaultStyles } from '../../00theme.js';
|
import { themeDefaultStyles } from '../../00theme.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'dees-progressbar': DeesProgressbar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@customElement('dees-progressbar')
|
@customElement('dees-progressbar')
|
||||||
export class DeesProgressbar extends DeesElement {
|
export class DeesProgressbar extends DeesElement {
|
||||||
// STATIC
|
|
||||||
public static demo = demoFunc;
|
public static demo = demoFunc;
|
||||||
public static demoGroups = ['Feedback'];
|
public static demoGroups = ['Feedback'];
|
||||||
|
|
||||||
// INSTANCE
|
|
||||||
@property({
|
@property({
|
||||||
type: Number,
|
type: Number,
|
||||||
})
|
})
|
||||||
accessor percentage = 0;
|
accessor percentage = 0;
|
||||||
|
|
||||||
|
// `value` and `progress` keep existing readme/internal usages working.
|
||||||
|
@property({
|
||||||
|
type: Number,
|
||||||
|
})
|
||||||
|
accessor value: number | null = null;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Number,
|
||||||
|
})
|
||||||
|
accessor progress: number | null = null;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
accessor label = '';
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
accessor statusText = '';
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Array,
|
||||||
|
})
|
||||||
|
accessor terminalLines: string[] = [];
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Number,
|
||||||
|
})
|
||||||
|
accessor statusRows = 3;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Boolean,
|
||||||
|
})
|
||||||
|
accessor indeterminate = false;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Boolean,
|
||||||
|
})
|
||||||
|
accessor showPercentage = true;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor activeSpinnerFrame = 0;
|
||||||
|
|
||||||
|
private spinnerIntervalId: number | null = null;
|
||||||
|
private readonly spinnerFrames = ['|', '/', '-', '\\'];
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
themeDefaultStyles,
|
themeDefaultStyles,
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
css`
|
css`
|
||||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
|
||||||
:host {
|
:host {
|
||||||
|
display: block;
|
||||||
color: ${cssManager.bdTheme(colors.bright.text, colors.dark.text)};
|
color: ${cssManager.bdTheme(colors.bright.text, colors.dark.text)};
|
||||||
}
|
}
|
||||||
|
|
||||||
.progressBarContainer {
|
.progressBarContainer {
|
||||||
padding: 8px;
|
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressLabel {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressValue {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: ${cssManager.bdTheme('hsl(215 15% 40%)', 'hsl(215 15% 70%)')};
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progressBar {
|
.progressBar {
|
||||||
background: ${cssManager.bdTheme('#eeeeeb', '#444')};
|
position: relative;
|
||||||
height: 8px;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 4px;
|
height: 8px;
|
||||||
border-top: 0.5px solid ${cssManager.bdTheme('none', '#555')};
|
border-radius: 999px;
|
||||||
|
background: ${cssManager.bdTheme('#eeeeeb', '#444')};
|
||||||
|
border-top: 0.5px solid ${cssManager.bdTheme('transparent', '#555')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.progressBarFill {
|
.progressBarFill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
background: ${cssManager.bdTheme(colors.dark.blueActive, colors.bright.blueActive)};
|
background: ${cssManager.bdTheme(colors.dark.blueActive, colors.bright.blueActive)};
|
||||||
height: 8px;
|
transition: width 0.2s ease;
|
||||||
margin-top: -0.5px;
|
|
||||||
transition: 0.2s width;
|
|
||||||
border-radius: 4px;
|
|
||||||
width: 0px;
|
|
||||||
border-top: 0.5 solid ${cssManager.bdTheme('none', '#398fff')};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progressText {
|
.progressBarFill.indeterminate {
|
||||||
padding: 8px;
|
width: 34%;
|
||||||
|
transition: none;
|
||||||
|
animation: indeterminateSlide 1.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusPanel {
|
||||||
|
margin-top: 10px;
|
||||||
|
height: calc(var(--status-rows, 3) * 1.35em + 16px);
|
||||||
|
min-height: calc(var(--status-rows, 3) * 1.35em + 16px);
|
||||||
|
padding: 8px 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('hsl(210 20% 86%)', 'hsl(210 10% 26%)')};
|
||||||
|
background: ${cssManager.bdTheme('hsl(210 33% 98%)', 'hsl(220 20% 10%)')};
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusTextRow,
|
||||||
|
.terminalLine {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 1.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalScroller {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalScroller::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalScroller::-webkit-scrollbar-thumb {
|
||||||
|
background: ${cssManager.bdTheme('hsl(215 18% 78%)', 'hsl(215 10% 34%)')};
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalScroller::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linePrefix {
|
||||||
|
width: 1ch;
|
||||||
|
flex: 0 0 1ch;
|
||||||
|
color: ${cssManager.bdTheme(colors.dark.blueActive, colors.bright.blueActive)};
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
`
|
|
||||||
|
.lineText {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
color: ${cssManager.bdTheme('hsl(220 15% 25%)', 'hsl(210 15% 86%)')};
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalLine:not(.current) .lineText {
|
||||||
|
color: ${cssManager.bdTheme('hsl(215 12% 42%)', 'hsl(215 12% 63%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes indeterminateSlide {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-120%);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateX(320%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public async connectedCallback(): Promise<void> {
|
||||||
|
await super.connectedCallback();
|
||||||
|
this.syncSpinnerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disconnectedCallback(): Promise<void> {
|
||||||
|
this.stopSpinner();
|
||||||
|
await super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
public updated(changedProperties: Map<string | number | symbol, unknown>): void {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
this.syncSpinnerState();
|
||||||
|
|
||||||
|
if (changedProperties.has('terminalLines') && this.terminalLines.length > 0) {
|
||||||
|
this.scrollTerminalToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
const effectivePercentage = this.getEffectivePercentage();
|
||||||
|
const showHeader = Boolean(this.label) || (this.showPercentage && !this.indeterminate);
|
||||||
|
const hasTerminalLines = this.terminalLines.length > 0;
|
||||||
|
const hasStatusContent = hasTerminalLines || this.statusText.trim().length > 0;
|
||||||
|
const renderedRows = this.getRenderedStatusRows();
|
||||||
|
const spinnerFrame = this.spinnerFrames[this.activeSpinnerFrame] ?? this.spinnerFrames[0];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="progressBarContainer">
|
<div class="progressBarContainer">
|
||||||
|
${showHeader ? html`
|
||||||
|
<div class="progressHeader">
|
||||||
|
<div class="progressLabel">${this.label}</div>
|
||||||
|
${this.showPercentage && !this.indeterminate ? html`
|
||||||
|
<div class="progressValue">${this.formatPercentage(effectivePercentage)}%</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
<div class="progressBar">
|
<div class="progressBar">
|
||||||
<div class="progressBarFill"></div>
|
<div
|
||||||
<div class="progressText">
|
class="progressBarFill ${this.indeterminate ? 'indeterminate' : ''}"
|
||||||
${this.percentage}%
|
style="${this.indeterminate ? '' : `width: ${effectivePercentage}%;`}"
|
||||||
<div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
${hasStatusContent ? html`
|
||||||
|
<div
|
||||||
|
class="statusPanel"
|
||||||
|
style="--status-rows: ${renderedRows};"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-atomic="true"
|
||||||
|
>
|
||||||
|
${hasTerminalLines ? html`
|
||||||
|
<div class="terminalScroller">
|
||||||
|
${this.terminalLines.map((line, index) => {
|
||||||
|
const isCurrentLine = index === this.terminalLines.length - 1;
|
||||||
|
const prefix = this.indeterminate && isCurrentLine ? spinnerFrame : '>';
|
||||||
|
return html`
|
||||||
|
<div class="terminalLine ${isCurrentLine ? 'current' : ''}">
|
||||||
|
<span class="linePrefix">${prefix}</span>
|
||||||
|
<span class="lineText">${line}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
` : html`
|
||||||
|
<div class="statusTextRow">
|
||||||
|
<span class="linePrefix">${this.indeterminate ? spinnerFrame : '>'}</span>
|
||||||
|
<span class="lineText">${this.statusText}</span>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
`
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated (_changedProperties: Map<string | number | symbol, unknown>): void {
|
private getEffectivePercentage(): number {
|
||||||
super.firstUpdated(_changedProperties);
|
if (typeof this.value === 'number' && Number.isFinite(this.value)) {
|
||||||
this.updateComplete.then(() => {
|
return this.clampPercentage(this.value);
|
||||||
this.updatePercentage();
|
}
|
||||||
|
|
||||||
|
if (typeof this.progress === 'number' && Number.isFinite(this.progress)) {
|
||||||
|
const normalizedProgress = this.progress >= 0 && this.progress <= 1
|
||||||
|
? this.progress * 100
|
||||||
|
: this.progress;
|
||||||
|
return this.clampPercentage(normalizedProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.clampPercentage(this.percentage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRenderedStatusRows(): number {
|
||||||
|
const rows = Number.isFinite(this.statusRows) ? Math.floor(this.statusRows) : 3;
|
||||||
|
return Math.max(1, rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clampPercentage(input: number): number {
|
||||||
|
return Math.max(0, Math.min(100, input));
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatPercentage(input: number): string {
|
||||||
|
return Number.isInteger(input) ? `${input}` : input.toFixed(1).replace(/\.0$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncSpinnerState(): void {
|
||||||
|
const shouldAnimate = this.indeterminate && (this.statusText.trim().length > 0 || this.terminalLines.length > 0);
|
||||||
|
|
||||||
|
if (shouldAnimate && this.spinnerIntervalId === null) {
|
||||||
|
this.spinnerIntervalId = window.setInterval(() => {
|
||||||
|
this.activeSpinnerFrame = (this.activeSpinnerFrame + 1) % this.spinnerFrames.length;
|
||||||
|
}, 120);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldAnimate) {
|
||||||
|
this.stopSpinner();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopSpinner(): void {
|
||||||
|
if (this.spinnerIntervalId !== null) {
|
||||||
|
window.clearInterval(this.spinnerIntervalId);
|
||||||
|
this.spinnerIntervalId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeSpinnerFrame = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollTerminalToBottom(): void {
|
||||||
|
const terminalScroller = this.shadowRoot?.querySelector('.terminalScroller') as HTMLElement | null;
|
||||||
|
|
||||||
|
if (!terminalScroller) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
terminalScroller.scrollTop = terminalScroller.scrollHeight;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public async updatePercentage() {
|
|
||||||
const progressBarFill = this.shadowRoot!.querySelector('.progressBarFill') as HTMLElement;
|
|
||||||
progressBarFill.style.width = `${this.percentage}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
updated(){
|
|
||||||
this.updatePercentage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user