263 lines
5.1 KiB
Svelte
263 lines
5.1 KiB
Svelte
<script lang="ts">
|
|
import Icon from "@iconify/svelte";
|
|
import { getButtonStyle, parseColorText, getSegmentStyle } from "./utils";
|
|
import { themeColors } from "$lib/stores/theme";
|
|
import type { ProgressLine } from "./types";
|
|
import "$lib/assets/css/tui-progress.css";
|
|
|
|
interface Props {
|
|
line: ProgressLine;
|
|
inline?: boolean;
|
|
}
|
|
|
|
let { line, inline = false }: Props = $props();
|
|
|
|
let containerWidth = $state(0);
|
|
const charWidth = 9.6; // Approximate width of a character in monospace font (adjust as needed)
|
|
|
|
const progress = $derived(Math.min(100, Math.max(0, line.progress ?? 0)));
|
|
const label = $derived(line.progressLabel || `${progress}%`);
|
|
const contentSegments = $derived(
|
|
parseColorText(line.content, $themeColors.colorMap),
|
|
);
|
|
const labelSegments = $derived(
|
|
parseColorText(label, $themeColors.colorMap),
|
|
);
|
|
|
|
// Calculate how many characters fit in the bar
|
|
const charCapacity = $derived(
|
|
Math.max(10, Math.floor((containerWidth - 30) / charWidth)),
|
|
);
|
|
</script>
|
|
|
|
<div
|
|
class="tui-progress"
|
|
class:inline
|
|
class:text-style={line.progressStyle === "text"}
|
|
style="--progress-color: {getButtonStyle(line.style)}"
|
|
bind:clientWidth={containerWidth}
|
|
>
|
|
{#if line.content}
|
|
<div class="progress-label">
|
|
{#each contentSegments as segment}
|
|
{#if segment.icon}
|
|
<Icon icon={segment.icon} width="14" class="inline-icon" />
|
|
{:else if getSegmentStyle(segment)}
|
|
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
|
{:else}
|
|
{segment.text}
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
|
|
{#if line.progressStyle === "text"}
|
|
<div class="progress-text-bar">
|
|
<span class="bracket">[</span>
|
|
<span class="bar-content">
|
|
{#each Array(charCapacity) as _, i}
|
|
<span class="char"
|
|
>{i < Math.floor((progress / 100) * charCapacity)
|
|
? "#"
|
|
: "."}</span
|
|
>
|
|
{/each}
|
|
</span>
|
|
<span class="bracket">]</span>
|
|
</div>
|
|
{:else}
|
|
<div
|
|
class="progress-bar"
|
|
class:continuous={line.progressStyle === "continuous"}
|
|
>
|
|
<div class="progress-fill" style="width: {progress}%">
|
|
<span class="progress-glow"></span>
|
|
</div>
|
|
{#if line.progressStyle !== "continuous"}
|
|
<div class="progress-blocks">
|
|
{#each Array(20) as _, i}
|
|
<span
|
|
class="block"
|
|
class:filled={i < Math.floor(progress / 5)}
|
|
></span>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="progress-value">
|
|
{#each labelSegments as segment}
|
|
{#if segment.icon}
|
|
<Icon icon={segment.icon} width="12" class="inline-icon" />
|
|
{:else if getSegmentStyle(segment)}
|
|
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
|
{:else}
|
|
{segment.text}
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.tui-progress {
|
|
margin: 0.5rem 0;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
/* Inline progress styles */
|
|
.tui-progress.inline {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin: 0;
|
|
min-width: 120px;
|
|
}
|
|
|
|
.tui-progress.inline .progress-label {
|
|
margin-bottom: 0;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.tui-progress.inline .progress-bar {
|
|
flex: 1;
|
|
min-width: 80px;
|
|
height: 0.8rem;
|
|
}
|
|
|
|
.tui-progress.inline .progress-value {
|
|
margin-top: 0;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.progress-label {
|
|
color: var(--terminal-text);
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.progress-bar {
|
|
position: relative;
|
|
height: 1.2rem;
|
|
background: var(--terminal-border);
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-fill {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
height: 100%;
|
|
background: linear-gradient(
|
|
90deg,
|
|
var(--progress-color),
|
|
color-mix(in srgb, var(--progress-color) 80%, white 20%)
|
|
);
|
|
border-radius: 3px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.progress-glow {
|
|
position: absolute;
|
|
right: 0;
|
|
top: 0;
|
|
height: 100%;
|
|
width: 20px;
|
|
background: linear-gradient(
|
|
90deg,
|
|
transparent,
|
|
rgba(255, 255, 255, 0.3)
|
|
);
|
|
animation: shimmer 1.5s infinite;
|
|
}
|
|
|
|
@keyframes shimmer {
|
|
0% {
|
|
opacity: 0;
|
|
}
|
|
50% {
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.progress-blocks {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
display: flex;
|
|
gap: 2px;
|
|
padding: 3px;
|
|
}
|
|
|
|
.block {
|
|
flex: 1;
|
|
background: rgba(0, 0, 0, 0.2);
|
|
border-radius: 1px;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.block.filled {
|
|
background: transparent;
|
|
}
|
|
|
|
.progress-value {
|
|
text-align: right;
|
|
color: var(--progress-color);
|
|
font-size: 0.75rem;
|
|
margin-top: 0.25rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
:global(.inline-icon) {
|
|
display: inline-block;
|
|
vertical-align: middle;
|
|
margin: 0 0.15em;
|
|
}
|
|
|
|
/* Mobile: inline progress becomes full-width */
|
|
@media (max-width: 768px) {
|
|
.tui-progress.inline {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
width: 100%;
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
.tui-progress.inline .progress-bar {
|
|
min-width: 100%;
|
|
}
|
|
}
|
|
|
|
/* Continuous Style */
|
|
.progress-bar.continuous .progress-blocks {
|
|
display: none;
|
|
}
|
|
|
|
/* Text Style */
|
|
.progress-text-bar {
|
|
font-family: monospace;
|
|
font-size: 1rem;
|
|
color: var(--terminal-dim);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.progress-text-bar .char {
|
|
color: var(--terminal-dim);
|
|
}
|
|
|
|
.tui-progress:not(.text-style) .progress-text-bar {
|
|
display: none;
|
|
}
|
|
|
|
.progress-text-bar .bracket {
|
|
color: var(--terminal-text);
|
|
font-weight: bold;
|
|
}
|
|
</style>
|