Skip to content

Commit 459b9d0

Browse files
committed
feat(ui): dot marker on NotificationsBell, segments variant on HorizontalStepper
A queue that is either empty or not makes the count noise; marker=dot keeps 'something is waiting' without it. The segments stepper is the whisper track — hairline segments, current stretched in accent — for dense chrome where the step name is carried by the surface beneath.
1 parent 405034b commit 459b9d0

3 files changed

Lines changed: 57 additions & 7 deletions

File tree

‎src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ export * from "./ui/schedule-inspector.js";
155155
export * from "./ui/schedule-list.js";
156156
export * from "./ui/section.js";
157157
export * from "./ui/section-nav.js";
158+
export * from "./ui/segmented-control.js";
158159
export * from "./ui/select.js";
159160
export * from "./ui/selection-checkbox.js";
160161
export * from "./ui/settings-panel.js";

‎src/ui/horizontal-stepper.tsx‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ function pillClass(status: WorkflowStepStatus): string {
1818

1919
export type HorizontalStepperProps = {
2020
readonly steps: readonly WorkflowStep[];
21+
/**
22+
* `pills` is the default rail: numbered pills with connecting fills and
23+
* labels. `segments` is the whisper track — hairline segments, done in ink,
24+
* current in accent and stretched — for dense chrome like a top bar, where
25+
* the step's name is already carried by the surface beneath it.
26+
*/
27+
readonly variant?: "pills" | "segments";
2128
readonly className?: string;
2229
};
2330

@@ -28,11 +35,42 @@ export type HorizontalStepperProps = {
2835
* change, so a run with more steps than fit on screen never leaves the
2936
* active one off to the side unannounced.
3037
*/
31-
export function HorizontalStepper({ steps, className }: HorizontalStepperProps) {
38+
export function HorizontalStepper({ steps, variant = "pills", className }: HorizontalStepperProps) {
3239
const currentStep = steps.find((step) => step.status === "current");
3340
const currentRef = useScrollCurrentIntoView<HTMLLIElement>(currentStep?.number ?? -1);
3441
const compress = steps.length > LABEL_VISIBLE_STEP_THRESHOLD;
3542

43+
if (variant === "segments") {
44+
return (
45+
<ol
46+
aria-label="Workflow progress"
47+
className={cn("flex items-center gap-1", className)}
48+
>
49+
{steps.map((step) => (
50+
<li
51+
key={step.number}
52+
ref={step.status === "current" ? currentRef : undefined}
53+
aria-current={step.status === "current" ? "step" : undefined}
54+
title={step.label}
55+
className={cn(
56+
"h-0.5 rounded-full transition-[width,background-color] duration-300",
57+
step.status === "current" ? "w-6" : "w-4",
58+
step.status === "completed"
59+
? "bg-foreground"
60+
: step.status === "failed"
61+
? "bg-destructive"
62+
: step.status === "current"
63+
? "bg-primary"
64+
: "bg-border",
65+
)}
66+
>
67+
<span className="sr-only">{step.label}</span>
68+
</li>
69+
))}
70+
</ol>
71+
);
72+
}
73+
3674
return (
3775
<div className={cn("border-b border-border bg-card px-6 py-5", className)}>
3876
<ol

‎src/ui/notifications-bell.tsx‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ export type NotificationsBellProps = {
1212
readonly children: ReactNode;
1313
/** Counts above this render as "N+" so the marker cannot grow unbounded. */
1414
readonly maxCount?: number;
15+
/**
16+
* `count` shows the number; `dot` shows a bare mark — for hosts where the
17+
* count is noise (a queue that is either empty or not) but "something is
18+
* waiting" is not.
19+
*/
20+
readonly marker?: "count" | "dot";
1521
/** Controlled open state. Pair with `onOpenChange` to lift it to a parent. */
1622
readonly open?: boolean;
1723
readonly onOpenChange?: (open: boolean) => void;
@@ -36,6 +42,7 @@ export function NotificationsBell({
3642
count,
3743
children,
3844
maxCount = 99,
45+
marker = "count",
3946
open: openProp,
4047
onOpenChange,
4148
className,
@@ -68,12 +75,16 @@ export function NotificationsBell({
6875
>
6976
<Bell className="size-4" aria-hidden />
7077
{count > 0 ? (
71-
<span
72-
aria-hidden
73-
className="absolute top-1 right-1 min-w-4 rounded-full bg-primary px-1 text-center font-mono text-[10px] leading-4 text-primary-foreground"
74-
>
75-
{count > maxCount ? `${maxCount}+` : count}
76-
</span>
78+
marker === "dot" ? (
79+
<span aria-hidden className="absolute top-1.5 right-1.5 size-2 rounded-full bg-primary" />
80+
) : (
81+
<span
82+
aria-hidden
83+
className="absolute top-1 right-1 min-w-4 rounded-full bg-primary px-1 text-center font-mono text-[10px] leading-4 text-primary-foreground"
84+
>
85+
{count > maxCount ? `${maxCount}+` : count}
86+
</span>
87+
)
7788
) : null}
7889
</button>
7990

0 commit comments

Comments
 (0)