302 lines
11 KiB
TypeScript
302 lines
11 KiB
TypeScript
import { Button, Gauge } from "@ai-ui/ui";
|
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
import { useState } from "react";
|
|
|
|
function GaugePanel({
|
|
description,
|
|
label,
|
|
shape = "dial",
|
|
tone = "default",
|
|
value,
|
|
variant = "default"
|
|
}: {
|
|
description: string;
|
|
label: string;
|
|
shape?: "dial" | "semi";
|
|
tone?: "default" | "subtle" | "accent";
|
|
value: number | null;
|
|
variant?: "default" | "success" | "warning" | "destructive";
|
|
}) {
|
|
return (
|
|
<article className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-5 shadow-[var(--shadow-sm)]">
|
|
<Gauge
|
|
description={description}
|
|
label={label}
|
|
shape={shape}
|
|
tone={tone}
|
|
value={value}
|
|
variant={variant}
|
|
/>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
const meta = {
|
|
title: "Components/Gauge",
|
|
component: Gauge,
|
|
args: {
|
|
description: "Lead routing stays stable enough for this week's forecast handoff.",
|
|
label: "Forecast confidence",
|
|
shape: "dial",
|
|
size: "md",
|
|
tickCount: 0,
|
|
tone: "default",
|
|
value: 72,
|
|
variant: "default"
|
|
},
|
|
argTypes: {
|
|
className: {
|
|
control: false
|
|
},
|
|
description: {
|
|
control: "text"
|
|
},
|
|
label: {
|
|
control: "text"
|
|
},
|
|
shape: {
|
|
control: "radio",
|
|
options: ["dial", "semi"]
|
|
},
|
|
size: {
|
|
control: "radio",
|
|
options: ["sm", "md", "lg"]
|
|
},
|
|
tickCount: {
|
|
control: {
|
|
type: "range",
|
|
min: 0,
|
|
max: 24,
|
|
step: 1
|
|
}
|
|
},
|
|
tone: {
|
|
control: "radio",
|
|
options: ["default", "subtle", "accent"]
|
|
},
|
|
value: {
|
|
control: {
|
|
type: "range",
|
|
min: 0,
|
|
max: 100,
|
|
step: 1
|
|
}
|
|
},
|
|
valueFormatter: {
|
|
control: false
|
|
},
|
|
variant: {
|
|
control: "radio",
|
|
options: ["default", "success", "warning", "destructive"]
|
|
}
|
|
},
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
component:
|
|
"Gauge is the system's radial meter for current measurements inside a known range. Use it for capacity, forecast confidence, health scores, and KPI thresholds where the UI should communicate the current level rather than the progress of an ongoing task. The component now stages its readout with a restrained sweep and count-up on entry, while reduced or static motion snaps directly to the final state."
|
|
}
|
|
},
|
|
layout: "centered"
|
|
},
|
|
tags: ["autodocs"]
|
|
} satisfies Meta<typeof Gauge>;
|
|
|
|
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 shadow-[var(--shadow-sm)]">
|
|
<div className="space-y-2">
|
|
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-primary)]">
|
|
Capacity Watch
|
|
</p>
|
|
<h3 className="text-lg font-semibold tracking-[var(--tracking-tight)] text-[var(--color-foreground)]">
|
|
Use Gauge for the current level of a metric, not for task completion.
|
|
</h3>
|
|
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
|
|
This component is a meter. It reads like a dashboard instrument and belongs beside KPI
|
|
cards, score panels, and health indicators rather than uploads or async job states.
|
|
</p>
|
|
</div>
|
|
<div className="mt-6 flex justify-center">
|
|
<Gauge {...args} />
|
|
</div>
|
|
</div>
|
|
)
|
|
};
|
|
|
|
export const Shapes: Story = {
|
|
render: () => (
|
|
<div className="grid w-[860px] gap-4 md:grid-cols-3">
|
|
<GaugePanel
|
|
description="Sales planning is stable enough for next week's spend allocation."
|
|
label="Forecast confidence"
|
|
value={72}
|
|
/>
|
|
<GaugePanel
|
|
description="Coverage is drifting closer to the review threshold than finance would like."
|
|
label="Budget saturation"
|
|
shape="semi"
|
|
tone="subtle"
|
|
value={61}
|
|
variant="warning"
|
|
/>
|
|
<GaugePanel
|
|
description="The latest model health score is comfortably within the healthy range."
|
|
label="Model health"
|
|
tone="accent"
|
|
value={84}
|
|
variant="success"
|
|
/>
|
|
</div>
|
|
)
|
|
};
|
|
|
|
export const Anatomy: Story = {
|
|
render: () => (
|
|
<div className="w-[780px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
|
|
<div className="space-y-5">
|
|
<div className="space-y-2">
|
|
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
|
Gauge anatomy
|
|
</p>
|
|
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
|
|
Gauge keeps the public structure small: one canvas, one SVG ring system, one value
|
|
plate, and optional framing copy below the visual.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="rounded-[var(--radius-md)] border border-dashed border-[var(--color-border-strong)] bg-[var(--color-background)] p-6">
|
|
<div className="flex justify-center">
|
|
<Gauge
|
|
description="Qualified coverage is still within the board's target band."
|
|
label="Pipeline health"
|
|
shape="semi"
|
|
tone="subtle"
|
|
value={68}
|
|
variant="success"
|
|
/>
|
|
</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="canvas"</code> owns the
|
|
responsive gauge frame, while <code className="text-[var(--color-foreground)]">data-slot="svg"</code>{" "}
|
|
holds the radial drawing primitives.
|
|
</p>
|
|
<p>
|
|
<code className="text-[var(--color-foreground)]">data-slot="track"</code> is the full
|
|
meter range, and <code className="text-[var(--color-foreground)]">data-slot="indicator"</code>{" "}
|
|
is the active measured arc. If a denser dashboard wants calibration marks, opt into
|
|
them with <code className="text-[var(--color-foreground)]">tickCount</code>, which
|
|
exposes <code className="text-[var(--color-foreground)]">data-slot="tick"</code>.
|
|
</p>
|
|
<p>
|
|
<code className="text-[var(--color-foreground)]">data-slot="value"</code>,
|
|
<code className="ml-1 text-[var(--color-foreground)]">data-slot="label"</code>, and
|
|
<code className="ml-1 text-[var(--color-foreground)]">data-slot="description"</code>
|
|
keep center readout and supporting copy stable for theming, docs, and tests.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
};
|
|
|
|
function GaugeMotionShowcase() {
|
|
const [replayKey, setReplayKey] = useState(0);
|
|
|
|
return (
|
|
<div className="grid w-[760px] gap-5 rounded-[var(--radius-xl)] border border-[color-mix(in_oklch,var(--color-border)_74%,transparent)] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface)_78%,white_22%),color-mix(in_oklch,var(--color-surface-container-low)_86%,white_14%))] p-6 shadow-[var(--shadow-sm)]">
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
<div className="max-w-[34rem] space-y-2">
|
|
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-primary)]">
|
|
Motion review
|
|
</p>
|
|
<h3 className="text-lg font-semibold tracking-[var(--tracking-tight)] text-[var(--color-foreground)]">
|
|
Gauge should arrive like an instrument waking up, not a number teleporting in.
|
|
</h3>
|
|
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
|
|
The indicator sweeps in once, the center value counts up with it, and reduced/static
|
|
motion still resolves instantly.
|
|
</p>
|
|
</div>
|
|
<Button onClick={() => setReplayKey((value) => value + 1)} size="sm" variant="secondary">
|
|
Replay startup
|
|
</Button>
|
|
</div>
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div className="rounded-[var(--radius-lg)] border border-[color-mix(in_oklch,var(--color-border)_72%,transparent)] bg-[color-mix(in_oklch,var(--color-background)_82%,white_18%)] p-5 shadow-[var(--shadow-xs)]">
|
|
<Gauge
|
|
key={`dial-${replayKey}`}
|
|
description="Lead scoring remains steady enough for this week's planning handoff."
|
|
label="Forecast confidence"
|
|
tone="accent"
|
|
value={72}
|
|
/>
|
|
</div>
|
|
<div className="rounded-[var(--radius-lg)] border border-[color-mix(in_oklch,var(--color-border)_72%,transparent)] bg-[color-mix(in_oklch,var(--color-background)_82%,white_18%)] p-5 shadow-[var(--shadow-xs)]">
|
|
<Gauge
|
|
key={`semi-${replayKey}`}
|
|
description="Budget headroom is drifting closer to the review threshold."
|
|
label="Budget saturation"
|
|
shape="semi"
|
|
tickCount={7}
|
|
tone="subtle"
|
|
value={61}
|
|
variant="warning"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export const Motion: Story = {
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story:
|
|
"Gauge uses a one-time staged startup: the ring sweeps to the target, ticks wake up with the same progress, and the center value counts into place. The effect is intentionally calm and should still feel correct when motion is reduced."
|
|
}
|
|
}
|
|
},
|
|
render: () => <GaugeMotionShowcase />
|
|
};
|
|
|
|
export const Accessibility: Story = {
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story:
|
|
"Gauge uses the ARIA meter pattern, not progressbar. Use it for a current reading inside a bounded range, provide a concrete label, and keep the center number or supporting copy readable enough that color is never the only signal."
|
|
}
|
|
}
|
|
},
|
|
render: () => (
|
|
<div className="grid w-[840px] gap-4 lg:grid-cols-[minmax(0,0.94fr)_minmax(0,1.06fr)]">
|
|
<article className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
|
|
<h3 className="text-lg font-semibold tracking-[var(--tracking-tight)] text-[var(--color-foreground)]">
|
|
Review guidance
|
|
</h3>
|
|
<div className="mt-4 grid gap-3 text-sm leading-6 text-[var(--color-muted-foreground)]">
|
|
<p>Choose Gauge for current measurement. Choose Progress for ongoing completion.</p>
|
|
<p>Always give the meter a concrete label such as <code>Forecast confidence</code>.</p>
|
|
<p>Keep the numeric readout or description meaningful enough that color is supplemental.</p>
|
|
</div>
|
|
</article>
|
|
<div className="flex items-center justify-center rounded-[var(--radius-lg)] border border-dashed border-[var(--color-border-strong)] bg-[var(--color-background)] p-6">
|
|
<Gauge
|
|
description="Lead scoring remains predictable enough for finance planning."
|
|
label="Forecast confidence"
|
|
value={72}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
};
|