307 lines
10 KiB
TypeScript
307 lines
10 KiB
TypeScript
import { Progress } from "@ai-ui/ui";
|
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
|
|
function ProgressCard({
|
|
accent,
|
|
label,
|
|
note,
|
|
pattern = "linear",
|
|
segmentCount,
|
|
tone = "default",
|
|
value,
|
|
variant = "default"
|
|
}: {
|
|
accent: string;
|
|
label: string;
|
|
note: string;
|
|
pattern?: "linear" | "segmented";
|
|
segmentCount?: number;
|
|
tone?: "default" | "subtle";
|
|
value: number | null;
|
|
variant?: "default" | "success" | "warning" | "destructive";
|
|
}) {
|
|
const displayValue = value == null ? "Syncing" : `${value}%`;
|
|
|
|
return (
|
|
<article className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-5 shadow-[var(--shadow-sm)]">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="space-y-1">
|
|
<p
|
|
className="text-xs uppercase tracking-[var(--tracking-caps)]"
|
|
style={{ color: accent }}
|
|
>
|
|
{label}
|
|
</p>
|
|
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">{note}</p>
|
|
</div>
|
|
<span className="text-sm font-medium text-[var(--color-foreground)]">{displayValue}</span>
|
|
</div>
|
|
<Progress
|
|
aria-label={label}
|
|
className="mt-4"
|
|
pattern={pattern}
|
|
segmentCount={segmentCount}
|
|
tone={tone}
|
|
value={value}
|
|
variant={variant}
|
|
/>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
const meta = {
|
|
title: "Components/Progress",
|
|
component: Progress,
|
|
args: {
|
|
pattern: "linear",
|
|
segmentCount: 24,
|
|
size: "md",
|
|
value: 64,
|
|
variant: "default"
|
|
},
|
|
argTypes: {
|
|
className: {
|
|
control: false
|
|
},
|
|
size: {
|
|
control: "radio",
|
|
options: ["sm", "md", "lg"]
|
|
},
|
|
pattern: {
|
|
control: "radio",
|
|
options: ["linear", "segmented"]
|
|
},
|
|
segmentCount: {
|
|
control: {
|
|
type: "range",
|
|
min: 6,
|
|
max: 36,
|
|
step: 1
|
|
}
|
|
},
|
|
tone: {
|
|
control: "radio",
|
|
options: ["default", "subtle"]
|
|
},
|
|
value: {
|
|
control: {
|
|
type: "range",
|
|
min: 0,
|
|
max: 100,
|
|
step: 1
|
|
}
|
|
},
|
|
variant: {
|
|
control: "radio",
|
|
options: ["default", "success", "warning", "destructive"]
|
|
}
|
|
},
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
component:
|
|
"Progress is the system's inline status channel for work that is actively advancing inside the current view. Use the default linear pattern for classic completion bars, or switch to the segmented pattern when a dashboard metric needs a more discrete meter like rollout targets, cost reduction goals, and operational scorecards."
|
|
}
|
|
},
|
|
layout: "centered"
|
|
},
|
|
tags: ["autodocs"]
|
|
} satisfies Meta<typeof Progress>;
|
|
|
|
export default meta;
|
|
|
|
type Story = StoryObj<typeof meta>;
|
|
|
|
export const Playground: Story = {
|
|
render: (args) => (
|
|
<div className="w-[380px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 text-[var(--color-foreground)] shadow-[var(--shadow-sm)]">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="space-y-1">
|
|
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-primary)]">
|
|
Asset Upload
|
|
</p>
|
|
<h3 className="text-lg font-semibold tracking-[var(--tracking-tight)]">
|
|
Publishing release visuals
|
|
</h3>
|
|
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
|
|
Use the same component for classic uploads and segmented dashboard meters by swapping
|
|
only the pattern instead of reaching for a separate one-off implementation.
|
|
</p>
|
|
</div>
|
|
<span className="rounded-[var(--radius-full)] bg-[color-mix(in_oklch,var(--color-primary-container)_70%,white_30%)] px-3 py-1 text-sm font-medium text-[var(--color-on-primary-container)]">
|
|
{args.value}%
|
|
</span>
|
|
</div>
|
|
<Progress aria-label="Upload progress" className="mt-5" {...args} />
|
|
</div>
|
|
)
|
|
};
|
|
|
|
export const Patterns: Story = {
|
|
render: () => (
|
|
<div className="grid w-[720px] gap-4 md:grid-cols-2">
|
|
<ProgressCard
|
|
accent="var(--color-primary)"
|
|
label="Release Assets"
|
|
note="The marketing bundle is halfway through the upload queue."
|
|
value={46}
|
|
/>
|
|
<ProgressCard
|
|
accent="var(--color-foreground)"
|
|
label="Operational Cost Reduction"
|
|
note="Segmented meters work better for dashboard targets where discrete progress reads faster than a single slab."
|
|
pattern="segmented"
|
|
segmentCount={24}
|
|
tone="subtle"
|
|
value={42}
|
|
/>
|
|
<ProgressCard
|
|
accent="color-mix(in oklch, var(--color-success) 78%, var(--color-foreground))"
|
|
label="Audience Sync"
|
|
note="Subscriber segments are reconciling against the latest CRM export."
|
|
tone="subtle"
|
|
value={74}
|
|
variant="success"
|
|
/>
|
|
<ProgressCard
|
|
accent="color-mix(in oklch, var(--color-warning) 72%, var(--color-foreground))"
|
|
label="Compliance Review"
|
|
note="A few rule checks are still pending before the rollout can advance."
|
|
value={58}
|
|
variant="warning"
|
|
/>
|
|
<ProgressCard
|
|
accent="var(--color-destructive)"
|
|
label="Recovery Job"
|
|
note="The pipeline is retrying a failed package publish after signature drift."
|
|
tone="subtle"
|
|
value={91}
|
|
variant="destructive"
|
|
/>
|
|
</div>
|
|
)
|
|
};
|
|
|
|
export const States: Story = {
|
|
render: () => (
|
|
<div className="grid w-[720px] gap-4 md:grid-cols-3">
|
|
<ProgressCard
|
|
accent="var(--color-primary)"
|
|
label="Determinate"
|
|
note="Linear progress remains the best fit for straightforward task completion."
|
|
value={68}
|
|
/>
|
|
<ProgressCard
|
|
accent="var(--color-foreground)"
|
|
label="Segmented Goal"
|
|
note="The segmented pattern makes discrete target progress easier to scan in dense dashboards."
|
|
pattern="segmented"
|
|
segmentCount={20}
|
|
tone="subtle"
|
|
value={42}
|
|
/>
|
|
<ProgressCard
|
|
accent="color-mix(in oklch, var(--color-success) 78%, var(--color-foreground))"
|
|
label="Complete"
|
|
note="Completion stays quiet and stable instead of introducing a second celebratory treatment."
|
|
value={100}
|
|
variant="success"
|
|
/>
|
|
</div>
|
|
)
|
|
};
|
|
|
|
export const Anatomy: Story = {
|
|
render: () => (
|
|
<div className="w-[760px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 text-[var(--color-foreground)] shadow-[var(--shadow-sm)]">
|
|
<div className="space-y-4">
|
|
<div className="space-y-1">
|
|
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
|
Progress anatomy
|
|
</p>
|
|
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
|
|
The public styling contract stays intentionally small: a root track and one indicator
|
|
layer, with the data attributes carrying the meaningful state.
|
|
</p>
|
|
</div>
|
|
<div className="rounded-[var(--radius-md)] border border-dashed border-[var(--color-border-strong)] bg-[var(--color-background)] p-5">
|
|
<div className="grid gap-4">
|
|
<Progress aria-label="Progress anatomy example" size="lg" value={68} />
|
|
<Progress
|
|
aria-label="Progress anatomy segmented example"
|
|
pattern="segmented"
|
|
segmentCount={20}
|
|
size="lg"
|
|
tone="subtle"
|
|
value={42}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="grid gap-3 text-sm leading-6 text-[var(--color-muted-foreground)]">
|
|
<p>
|
|
<code className="text-[var(--color-foreground)]">data-slot="root"</code> is the tonal
|
|
track. It exposes <code className="text-[var(--color-foreground)]">data-size</code>,{" "}
|
|
<code className="text-[var(--color-foreground)]">data-pattern</code>,{" "}
|
|
<code className="text-[var(--color-foreground)]">data-tone</code>,{" "}
|
|
<code className="text-[var(--color-foreground)]">data-variant</code>, and{" "}
|
|
<code className="text-[var(--color-foreground)]">data-state</code> so consumers can
|
|
respond to contract-level state instead of incidental class names.
|
|
</p>
|
|
<p>
|
|
<code className="text-[var(--color-foreground)]">data-slot="indicator"</code> is the
|
|
filled value layer for the linear pattern. It mirrors pattern, size, variant, and
|
|
state so tests and downstream themes can target the active bar without reaching into
|
|
private markup.
|
|
</p>
|
|
<p>
|
|
In segmented mode, <code className="text-[var(--color-foreground)]">data-slot="segments"</code>{" "}
|
|
wraps the repeated meter cells and each{" "}
|
|
<code className="text-[var(--color-foreground)]">data-slot="segment"</code> exposes
|
|
active state for dashboard-style styling and assertions.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
};
|
|
|
|
export const Motion: Story = {
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story:
|
|
"Determinate values should feel calm and weighted, with the filled slab scaling into place instead of animating layout width. Indeterminate progress keeps a restrained sweep in interactive mode, then falls back to a static reading when reduced or static motion is active."
|
|
}
|
|
}
|
|
},
|
|
render: () => (
|
|
<div className="grid w-[720px] gap-4 md:grid-cols-3">
|
|
<ProgressCard
|
|
accent="var(--color-primary)"
|
|
label="Determinate"
|
|
note="A known value keeps the indicator anchored and scales the filled slab instead of animating layout width."
|
|
value={68}
|
|
/>
|
|
<ProgressCard
|
|
accent="var(--color-foreground)"
|
|
label="Segmented Meter"
|
|
note="Discrete bars stay crisp at a glance and match dashboard KPI treatments."
|
|
pattern="segmented"
|
|
segmentCount={24}
|
|
tone="subtle"
|
|
value={42}
|
|
/>
|
|
<ProgressCard
|
|
accent="color-mix(in oklch, var(--color-success) 78%, var(--color-foreground))"
|
|
label="Segmented Indeterminate"
|
|
note="Unknown duration shifts the segmented pattern into a staggered pulse instead of pretending there is a precise width."
|
|
pattern="segmented"
|
|
segmentCount={20}
|
|
tone="subtle"
|
|
value={null}
|
|
variant="success"
|
|
/>
|
|
</div>
|
|
)
|
|
};
|