2026-01-27 10:57:42 +00:00
|
|
|
import * as colors from '../../00colors.js';
|
2024-01-15 19:42:15 +01:00
|
|
|
import { demoFunc } from './dees-progressbar.demo.js';
|
|
|
|
|
import {
|
|
|
|
|
customElement,
|
|
|
|
|
html,
|
|
|
|
|
DeesElement,
|
|
|
|
|
property,
|
|
|
|
|
cssManager,
|
|
|
|
|
css,
|
|
|
|
|
state,
|
|
|
|
|
} from '@design.estate/dees-element';
|
2026-01-27 10:57:42 +00:00
|
|
|
import { themeDefaultStyles } from '../../00theme.js';
|
2024-01-15 19:42:15 +01:00
|
|
|
|
2026-04-16 10:14:46 +00:00
|
|
|
declare global {
|
|
|
|
|
interface HTMLElementTagNameMap {
|
|
|
|
|
'dees-progressbar': DeesProgressbar;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-15 19:42:15 +01:00
|
|
|
@customElement('dees-progressbar')
|
|
|
|
|
export class DeesProgressbar extends DeesElement {
|
|
|
|
|
public static demo = demoFunc;
|
2026-01-27 10:57:42 +00:00
|
|
|
public static demoGroups = ['Feedback'];
|
2024-01-15 19:42:15 +01:00
|
|
|
|
|
|
|
|
@property({
|
|
|
|
|
type: Number,
|
|
|
|
|
})
|
2025-11-17 13:27:11 +00:00
|
|
|
accessor percentage = 0;
|
2024-01-15 19:42:15 +01:00
|
|
|
|
2026-04-16 10:14:46 +00:00
|
|
|
// `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 = ['|', '/', '-', '\\'];
|
|
|
|
|
|
2024-01-15 19:42:15 +01:00
|
|
|
public static styles = [
|
2025-12-29 01:20:24 +00:00
|
|
|
themeDefaultStyles,
|
2024-01-15 19:42:15 +01:00
|
|
|
cssManager.defaultStyles,
|
|
|
|
|
css`
|
|
|
|
|
:host {
|
2026-04-16 10:14:46 +00:00
|
|
|
display: block;
|
2024-01-15 19:42:15 +01:00
|
|
|
color: ${cssManager.bdTheme(colors.bright.text, colors.dark.text)};
|
|
|
|
|
}
|
2026-04-16 10:14:46 +00:00
|
|
|
|
2024-01-15 19:42:15 +01:00
|
|
|
.progressBarContainer {
|
|
|
|
|
min-width: 200px;
|
2026-04-16 10:14:46 +00:00
|
|
|
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;
|
2024-01-15 19:42:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.progressBar {
|
2026-04-16 10:14:46 +00:00
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
2024-01-15 19:42:15 +01:00
|
|
|
width: 100%;
|
2026-04-16 10:14:46 +00:00
|
|
|
height: 8px;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: ${cssManager.bdTheme('#eeeeeb', '#444')};
|
|
|
|
|
border-top: 0.5px solid ${cssManager.bdTheme('transparent', '#555')};
|
2024-01-15 19:42:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.progressBarFill {
|
2026-04-16 10:14:46 +00:00
|
|
|
height: 100%;
|
|
|
|
|
border-radius: inherit;
|
2024-01-15 19:42:15 +01:00
|
|
|
background: ${cssManager.bdTheme(colors.dark.blueActive, colors.bright.blueActive)};
|
2026-04-16 10:14:46 +00:00
|
|
|
transition: width 0.2s ease;
|
2024-01-15 19:42:15 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-16 10:14:46 +00:00
|
|
|
.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)};
|
2024-01-15 19:42:15 +01:00
|
|
|
text-align: center;
|
|
|
|
|
}
|
2026-04-16 10:14:46 +00:00
|
|
|
|
|
|
|
|
.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%);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`,
|
2024-01-15 19:42:15 +01:00
|
|
|
];
|
|
|
|
|
|
2026-04-16 10:14:46 +00:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-15 19:42:15 +01:00
|
|
|
public render() {
|
2026-04-16 10:14:46 +00:00
|
|
|
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];
|
|
|
|
|
|
2024-01-15 19:42:15 +01:00
|
|
|
return html`
|
|
|
|
|
<div class="progressBarContainer">
|
2026-04-16 10:14:46 +00:00
|
|
|
${showHeader ? html`
|
|
|
|
|
<div class="progressHeader">
|
|
|
|
|
<div class="progressLabel">${this.label}</div>
|
|
|
|
|
${this.showPercentage && !this.indeterminate ? html`
|
|
|
|
|
<div class="progressValue">${this.formatPercentage(effectivePercentage)}%</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
2024-01-15 19:42:15 +01:00
|
|
|
<div class="progressBar">
|
2026-04-16 10:14:46 +00:00
|
|
|
<div
|
|
|
|
|
class="progressBarFill ${this.indeterminate ? 'indeterminate' : ''}"
|
|
|
|
|
style="${this.indeterminate ? '' : `width: ${effectivePercentage}%;`}"
|
|
|
|
|
></div>
|
2024-01-15 19:42:15 +01:00
|
|
|
</div>
|
2026-04-16 10:14:46 +00:00
|
|
|
${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>
|
|
|
|
|
` : ''}
|
2024-01-15 19:42:15 +01:00
|
|
|
</div>
|
2026-04-16 10:14:46 +00:00
|
|
|
`;
|
2024-01-15 19:42:15 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-16 10:14:46 +00:00
|
|
|
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);
|
2024-01-15 19:42:15 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-16 10:14:46 +00:00
|
|
|
private getRenderedStatusRows(): number {
|
|
|
|
|
const rows = Number.isFinite(this.statusRows) ? Math.floor(this.statusRows) : 3;
|
|
|
|
|
return Math.max(1, rows);
|
2024-01-15 19:42:15 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-16 10:14:46 +00:00
|
|
|
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;
|
|
|
|
|
});
|
2024-01-15 19:42:15 +01:00
|
|
|
}
|
2026-04-16 10:14:46 +00:00
|
|
|
}
|