feat(dees-progressbar): add status panels, terminal output, and legacy progress input support
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
20
readme.md
20
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
|
||||
<dees-progressbar
|
||||
value={75}
|
||||
.percentage=${75}
|
||||
label="Uploading"
|
||||
showPercentage
|
||||
type="determinate" // Options: determinate, indeterminate
|
||||
status="normal" // Options: normal, success, warning, error
|
||||
statusText="Uploading thumbnails to edge cache..."
|
||||
.statusRows=${2}
|
||||
></dees-progressbar>
|
||||
|
||||
<dees-progressbar
|
||||
label="Installing dependencies"
|
||||
.indeterminate=${true}
|
||||
.statusRows=${4}
|
||||
.terminalLines=${[
|
||||
'Resolving workspace packages',
|
||||
'Downloading tarballs',
|
||||
'Linking local binaries'
|
||||
]}
|
||||
></dees-progressbar>
|
||||
```
|
||||
|
||||
|
||||
@@ -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.'
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
<dees-progressbar
|
||||
.percentage=${50}
|
||||
></dees-progressbar>
|
||||
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||
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);
|
||||
}}>
|
||||
<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 { 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<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() {
|
||||
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`
|
||||
<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="progressBarFill"></div>
|
||||
<div class="progressText">
|
||||
${this.percentage}%
|
||||
<div>
|
||||
<div
|
||||
class="progressBarFill ${this.indeterminate ? 'indeterminate' : ''}"
|
||||
style="${this.indeterminate ? '' : `width: ${effectivePercentage}%;`}"
|
||||
></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>
|
||||
`
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated (_changedProperties: Map<string | number | symbol, unknown>): 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user