diff --git a/changelog.md b/changelog.md index ffcc12f..dc07d61 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # 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 diff --git a/readme.md b/readme.md index 8b6518e..a948bad 100644 --- a/readme.md +++ b/readme.md @@ -1524,15 +1524,25 @@ Multi-step navigation component for guided user flows. ``` #### `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 + + ``` diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 15624b7..b0cac0d 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@design.estate/dees-catalog', - version: '3.78.3', + version: '3.79.0', description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' } diff --git a/ts_web/elements/00group-feedback/dees-progressbar/dees-progressbar.demo.ts b/ts_web/elements/00group-feedback/dees-progressbar/dees-progressbar.demo.ts index d4f6997..295ca04 100644 --- a/ts_web/elements/00group-feedback/dees-progressbar/dees-progressbar.demo.ts +++ b/ts_web/elements/00group-feedback/dees-progressbar/dees-progressbar.demo.ts @@ -1,11 +1,245 @@ -import { html } from '@design.estate/dees-element'; - -import { DeesProgressbar } from '../dees-progressbar/dees-progressbar.js'; +import { html, css, cssManager } from '@design.estate/dees-element'; +import '@design.estate/dees-wcctools/demotools'; +import type { DeesProgressbar } from './dees-progressbar.js'; 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` - + { + const liveProgressbar = elementArg.querySelector('#live-progress') as DeesProgressbar | null; + 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); + }}> + +
+
+ dees-progressbar 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. +
+
+
+
Determinate
+

Percentage plus current task

+

Use a label, a percentage, and one short status line when the work is measurable.

+
+ + +
+
+ +
+
Indeterminate
+

Spinner-style text indicator

+

When there is no trustworthy percentage yet, keep the bar moving and let the text explain what is happening.

+
+ + +
+
+ +
+
Terminal Lines
+

Fixed-height terminal-style status output

+

The panel stays the same height while the latest step stays visible. This is useful for update flows, downloads, and staged background work.

+ +
+ +
+
Live Demo
+

Updating percentage and text together

+

A single component can express both how far the job is and which phase is currently active.

+ +
+ +
+
Compatibility
+

Legacy value and progress inputs

+

Existing usages can keep passing percentages directly or normalized progress values from 0 to 1.

+
+ + +
+
+
+
+
`; -} \ No newline at end of file +}; diff --git a/ts_web/elements/00group-feedback/dees-progressbar/dees-progressbar.ts b/ts_web/elements/00group-feedback/dees-progressbar/dees-progressbar.ts index b0480ac..1504492 100644 --- a/ts_web/elements/00group-feedback/dees-progressbar/dees-progressbar.ts +++ b/ts_web/elements/00group-feedback/dees-progressbar/dees-progressbar.ts @@ -1,4 +1,3 @@ -import * as plugins from '../../00plugins.js'; import * as colors from '../../00colors.js'; import { demoFunc } from './dees-progressbar.demo.js'; import { @@ -6,94 +5,342 @@ import { html, DeesElement, property, - type TemplateResult, cssManager, css, - type CSSResult, - unsafeCSS, - unsafeHTML, state, } from '@design.estate/dees-element'; - -import * as domtools from '@design.estate/dees-domtools'; import { themeDefaultStyles } from '../../00theme.js'; +declare global { + interface HTMLElementTagNameMap { + 'dees-progressbar': DeesProgressbar; + } +} + @customElement('dees-progressbar') export class DeesProgressbar extends DeesElement { - // STATIC public static demo = demoFunc; public static demoGroups = ['Feedback']; - // INSTANCE @property({ type: Number, }) 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 = [ themeDefaultStyles, cssManager.defaultStyles, css` - /* TODO: Migrate hardcoded values to --dees-* CSS variables */ :host { + display: block; color: ${cssManager.bdTheme(colors.bright.text, colors.dark.text)}; } + .progressBarContainer { - padding: 8px; 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 { - background: ${cssManager.bdTheme('#eeeeeb', '#444')}; - height: 8px; + position: relative; + overflow: hidden; width: 100%; - border-radius: 4px; - border-top: 0.5px solid ${cssManager.bdTheme('none', '#555')}; + height: 8px; + border-radius: 999px; + background: ${cssManager.bdTheme('#eeeeeb', '#444')}; + border-top: 0.5px solid ${cssManager.bdTheme('transparent', '#555')}; } .progressBarFill { + height: 100%; + border-radius: inherit; background: ${cssManager.bdTheme(colors.dark.blueActive, colors.bright.blueActive)}; - height: 8px; - margin-top: -0.5px; - transition: 0.2s width; - border-radius: 4px; - width: 0px; - border-top: 0.5 solid ${cssManager.bdTheme('none', '#398fff')}; + transition: width 0.2s ease; } - .progressText { - padding: 8px; + .progressBarFill.indeterminate { + 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; } - ` + + .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 { + await super.connectedCallback(); + this.syncSpinnerState(); + } + + public async disconnectedCallback(): Promise { + this.stopSpinner(); + await super.disconnectedCallback(); + } + + public updated(changedProperties: Map): void { + super.updated(changedProperties); + this.syncSpinnerState(); + + if (changedProperties.has('terminalLines') && this.terminalLines.length > 0) { + this.scrollTerminalToBottom(); + } + } + 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`
+ ${showHeader ? html` +
+
${this.label}
+ ${this.showPercentage && !this.indeterminate ? html` +
${this.formatPercentage(effectivePercentage)}%
+ ` : ''} +
+ ` : ''}
-
-
- ${this.percentage}% -
+
+ ${hasStatusContent ? html` +
+ ${hasTerminalLines ? html` +
+ ${this.terminalLines.map((line, index) => { + const isCurrentLine = index === this.terminalLines.length - 1; + const prefix = this.indeterminate && isCurrentLine ? spinnerFrame : '>'; + return html` +
+ ${prefix} + ${line} +
+ `; + })} +
+ ` : html` +
+ ${this.indeterminate ? spinnerFrame : '>'} + ${this.statusText} +
+ `} +
+ ` : ''}
- ` + `; } - firstUpdated (_changedProperties: Map): void { - super.firstUpdated(_changedProperties); - this.updateComplete.then(() => { - this.updatePercentage(); + private getEffectivePercentage(): number { + if (typeof this.value === 'number' && Number.isFinite(this.value)) { + return this.clampPercentage(this.value); + } + + 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(); - } -} \ No newline at end of file +}