@alpic-ai/ui 0.0.0-dev.edeed3c → 0.0.0-dev.ee56854

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/components/button.d.mts +3 -1
  2. package/dist/components/button.mjs +20 -6
  3. package/dist/components/form.d.mts +36 -1
  4. package/dist/components/form.mjs +114 -4
  5. package/dist/components/github-button.d.mts +13 -0
  6. package/dist/components/github-button.mjs +24 -0
  7. package/dist/components/page-loader.d.mts +11 -0
  8. package/dist/components/page-loader.mjs +122 -0
  9. package/dist/components/shimmer-text.d.mts +12 -0
  10. package/dist/components/shimmer-text.mjs +22 -0
  11. package/dist/components/sidebar.mjs +61 -17
  12. package/dist/components/table.d.mts +10 -1
  13. package/dist/components/table.mjs +4 -4
  14. package/dist/components/tabs.mjs +4 -4
  15. package/dist/components/task-progress.d.mts +27 -0
  16. package/dist/components/task-progress.mjs +66 -0
  17. package/dist/components/tooltip.mjs +1 -1
  18. package/dist/components/wizard.d.mts +34 -0
  19. package/dist/components/wizard.mjs +46 -0
  20. package/package.json +13 -13
  21. package/src/components/button.tsx +13 -9
  22. package/src/components/combobox.tsx +18 -6
  23. package/src/components/form.tsx +164 -3
  24. package/src/components/github-button.tsx +34 -0
  25. package/src/components/page-loader.tsx +59 -0
  26. package/src/components/shimmer-text.tsx +23 -0
  27. package/src/components/sidebar.tsx +59 -20
  28. package/src/components/table.tsx +17 -4
  29. package/src/components/tabs.tsx +4 -4
  30. package/src/components/task-progress.tsx +107 -0
  31. package/src/components/tooltip.tsx +1 -1
  32. package/src/components/wizard.tsx +69 -0
  33. package/src/hooks/use-copy-to-clipboard.ts +6 -2
  34. package/src/stories/button.stories.tsx +23 -1
  35. package/src/stories/form.stories.tsx +64 -2
  36. package/src/stories/sidebar.stories.tsx +6 -3
  37. package/src/stories/table.stories.tsx +2 -2
  38. package/src/stories/tabs.stories.tsx +4 -2
  39. package/src/stories/task-progress.stories.tsx +81 -0
  40. package/src/stories/wizard.stories.tsx +64 -0
  41. package/src/styles/tokens.css +217 -0
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import type { ComponentProps } from "react";
4
+
5
+ import { cn } from "../lib/cn";
6
+ import { Button } from "./button";
7
+
8
+ const GITHUB_ICON_PATH =
9
+ "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12";
10
+
11
+ function GitHubIcon() {
12
+ return (
13
+ <svg fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
14
+ <path d={GITHUB_ICON_PATH} />
15
+ </svg>
16
+ );
17
+ }
18
+
19
+ type GitHubButtonProps = Omit<ComponentProps<typeof Button>, "variant" | "icon">;
20
+
21
+ function GitHubButton({ className, children, ...props }: GitHubButtonProps) {
22
+ return (
23
+ <Button
24
+ {...props}
25
+ icon={<GitHubIcon />}
26
+ className={cn("bg-foreground text-background [@media(hover:hover)]:hover:bg-foreground/90", className)}
27
+ >
28
+ {children}
29
+ </Button>
30
+ );
31
+ }
32
+
33
+ export type { GitHubButtonProps };
34
+ export { GitHubButton };
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ import { cn } from "../lib/cn";
4
+
5
+ const CABLE_CAR_SVG = (
6
+ <svg
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ viewBox="0 0 120 130"
9
+ width="240"
10
+ height="260"
11
+ aria-hidden="true"
12
+ className="block h-auto w-full"
13
+ >
14
+ <line x1="60" y1="3" x2="60" y2="58" stroke="#333" strokeWidth="4" />
15
+ <circle cx="60" cy="11" r="10" fill="#555" />
16
+ <rect x="5" y="58" width="110" height="64" rx="4" fill="#e90060" />
17
+ <rect x="5" y="58" width="110" height="20" rx="4" fill="#F5F0E8" />
18
+ <rect x="5" y="68" width="110" height="10" fill="#F5F0E8" />
19
+ <rect x="14" y="66" width="26" height="30" rx="2" fill="#5B8EC9" stroke="#C4B9A8" strokeWidth="1.5" />
20
+ <rect x="47" y="66" width="26" height="30" rx="2" fill="#5B8EC9" stroke="#C4B9A8" strokeWidth="1.5" />
21
+ <rect x="80" y="66" width="26" height="30" rx="2" fill="#5B8EC9" stroke="#C4B9A8" strokeWidth="1.5" />
22
+ <rect x="5" y="115" width="110" height="7" rx="3" fill="#9f0042" />
23
+ </svg>
24
+ );
25
+
26
+ interface PageLoaderProps {
27
+ className?: string;
28
+ }
29
+
30
+ function PageLoader({ className }: PageLoaderProps) {
31
+ return (
32
+ <div
33
+ className={cn("flex min-h-screen items-center justify-center bg-background", className)}
34
+ role="status"
35
+ aria-label="Loading Alpic…"
36
+ >
37
+ <div className="relative h-[120px] w-[200px]">
38
+ <div
39
+ className="absolute top-[30px] left-0 h-[3px] w-full rounded-sm bg-[#6c6c77]"
40
+ style={{ transform: "rotate(-15deg)", transformOrigin: "left center" }}
41
+ />
42
+ <div className="absolute top-[33px] -left-[45px] w-[45px] motion-safe:animate-[alpic-ride_4s_linear_infinite]">
43
+ {CABLE_CAR_SVG}
44
+ </div>
45
+ <div
46
+ className="pointer-events-none absolute -top-[40px] -left-[45px] z-10 h-[200px] w-[95px]"
47
+ style={{ background: "linear-gradient(to right, var(--color-background) 47%, transparent)" }}
48
+ />
49
+ <div
50
+ className="pointer-events-none absolute -top-[40px] left-[142px] z-10 h-[200px] w-[95px]"
51
+ style={{ background: "linear-gradient(to right, transparent, var(--color-background) 53%)" }}
52
+ />
53
+ </div>
54
+ </div>
55
+ );
56
+ }
57
+
58
+ export type { PageLoaderProps };
59
+ export { PageLoader };
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ import type React from "react";
4
+
5
+ import { cn } from "../lib/cn";
6
+
7
+ const shimmerStyle: React.CSSProperties = {
8
+ background: `linear-gradient(90deg, #0000 calc(50% - 60px), var(--color-foreground), #0000 calc(50% + 60px)), linear-gradient(color-mix(in oklab, var(--color-muted-foreground) 70%, transparent), color-mix(in oklab, var(--color-muted-foreground) 70%, transparent))`,
9
+ backgroundSize: "250% 100%, auto",
10
+ backgroundRepeat: "no-repeat, padding-box",
11
+ WebkitBackgroundClip: "text",
12
+ WebkitTextFillColor: "transparent",
13
+ backgroundClip: "text",
14
+ animation: "shimmer-text 2.5s linear infinite",
15
+ };
16
+
17
+ export function ShimmerText({ children, className }: { children: string | number; className?: string }) {
18
+ return (
19
+ <span className={cn(className)} style={shimmerStyle}>
20
+ {children}
21
+ </span>
22
+ );
23
+ }
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { Slot } from "@radix-ui/react-slot";
4
4
  import { cva, type VariantProps } from "class-variance-authority";
5
- import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
6
5
  import * as React from "react";
7
6
 
8
7
  import { useIsMobile } from "../hooks/use-mobile";
@@ -20,6 +19,19 @@ const SIDEBAR_WIDTH_MOBILE = "16rem";
20
19
  const SIDEBAR_WIDTH_ICON = "3.5rem";
21
20
  const SIDEBAR_KEYBOARD_SHORTCUT = "b";
22
21
 
22
+ const INTERACTIVE_SIDEBAR_ELEMENT_SELECTOR = [
23
+ "a",
24
+ "button",
25
+ "input",
26
+ "select",
27
+ "textarea",
28
+ "[role='button']",
29
+ "[role='link']",
30
+ "[role='menuitem']",
31
+ "[contenteditable='true']",
32
+ "[tabindex]:not([tabindex='-1'])",
33
+ ].join(", ");
34
+
23
35
  type SidebarContextProps = {
24
36
  state: "expanded" | "collapsed";
25
37
  open: boolean;
@@ -143,13 +155,31 @@ function Sidebar({
143
155
  variant?: "sidebar" | "floating" | "inset";
144
156
  collapsible?: "offcanvas" | "icon" | "none";
145
157
  }) {
146
- const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
158
+ const { isMobile, state, openMobile, setOpenMobile, open, setOpen } = useSidebar();
159
+
160
+ function handleSurfaceClickCapture(event: React.MouseEvent<HTMLDivElement>) {
161
+ const clickedInteractiveElement =
162
+ event.target instanceof Element && event.target.closest(INTERACTIVE_SIDEBAR_ELEMENT_SELECTOR);
163
+
164
+ if (clickedInteractiveElement) {
165
+ return;
166
+ }
167
+
168
+ if (!open) {
169
+ event.preventDefault();
170
+ event.stopPropagation();
171
+ setOpen(true);
172
+ return;
173
+ }
174
+
175
+ setOpen(false);
176
+ }
147
177
 
148
178
  if (collapsible === "none") {
149
179
  return (
150
180
  <div
151
181
  data-slot="sidebar"
152
- className={cn("bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col", className)}
182
+ className={cn("bg-sidebar-surface text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col", className)}
153
183
  {...props}
154
184
  >
155
185
  {children}
@@ -164,7 +194,7 @@ function Sidebar({
164
194
  data-sidebar="sidebar"
165
195
  data-slot="sidebar"
166
196
  data-mobile="true"
167
- className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
197
+ className="bg-sidebar-surface text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
168
198
  style={
169
199
  {
170
200
  "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
@@ -213,7 +243,7 @@ function Sidebar({
213
243
  // Adjust the padding for floating and inset variants.
214
244
  variant === "floating" || variant === "inset"
215
245
  ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
216
- : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
246
+ : "border-sidebar-border group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
217
247
  className,
218
248
  )}
219
249
  {...props}
@@ -221,7 +251,8 @@ function Sidebar({
221
251
  <div
222
252
  data-sidebar="sidebar"
223
253
  data-slot="sidebar-inner"
224
- className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
254
+ className="bg-sidebar-surface group-data-[variant=floating]:border-sidebar-border flex h-full w-full cursor-pointer flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
255
+ onClickCapture={handleSurfaceClickCapture}
225
256
  >
226
257
  {children}
227
258
  </div>
@@ -231,9 +262,7 @@ function Sidebar({
231
262
  }
232
263
 
233
264
  function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>) {
234
- const { state, isMobile, openMobile, toggleSidebar } = useSidebar();
235
-
236
- const isOpen = isMobile ? openMobile : state === "expanded";
265
+ const { toggleSidebar } = useSidebar();
237
266
 
238
267
  return (
239
268
  <Button
@@ -248,12 +277,21 @@ function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<t
248
277
  }}
249
278
  {...props}
250
279
  >
251
- {isOpen ? <PanelLeftClose className="size-4.5" /> : <PanelLeftOpen className="size-4.5" />}
280
+ <SidebarToggleIcon className="size-4.5" />
252
281
  <span className="sr-only">Toggle Sidebar</span>
253
282
  </Button>
254
283
  );
255
284
  }
256
285
 
286
+ function SidebarToggleIcon({ className, ...props }: React.ComponentProps<"svg">) {
287
+ return (
288
+ <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className={className} {...props}>
289
+ <rect x="2.75" y="2.75" width="14.5" height="14.5" rx="4" stroke="currentColor" strokeWidth="1.5" />
290
+ <path d="M10 4.75V15.25" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
291
+ </svg>
292
+ );
293
+ }
294
+
257
295
  function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
258
296
  const { toggleSidebar } = useSidebar();
259
297
 
@@ -266,9 +304,7 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
266
304
  onClick={toggleSidebar}
267
305
  title="Toggle Sidebar"
268
306
  className={cn(
269
- "[@media(hover:hover)]:hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
270
- "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
271
- "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
307
+ "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
272
308
  "[@media(hover:hover)]:hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
273
309
  "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
274
310
  "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
@@ -316,16 +352,19 @@ function SidebarHeader({ className, icon, title, children, ...props }: SidebarHe
316
352
  <div
317
353
  data-slot="sidebar-header"
318
354
  data-sidebar="header"
319
- className={cn("flex flex-col gap-2 p-2", className)}
355
+ className={cn("flex flex-col gap-2 py-2", className)}
320
356
  {...props}
321
357
  >
322
358
  <div className="flex h-8 items-center gap-2 px-3">
323
- <div className="relative shrink-0">
324
- <span className="transition-opacity group-data-[collapsible=icon]:group-hover:opacity-0">{icon}</span>
325
- <div className="absolute inset-0 flex items-center justify-center opacity-0 transition-opacity group-data-[collapsible=icon]:group-hover:opacity-100">
326
- <SidebarTrigger />
327
- </div>
328
- </div>
359
+ <span className="relative flex size-8 shrink-0 items-center justify-center">
360
+ <span className="transition-opacity duration-200 group-data-[collapsible=icon]:group-hover:opacity-0">
361
+ {icon}
362
+ </span>
363
+ <SidebarTrigger
364
+ tabIndex={-1}
365
+ className="absolute inset-0 opacity-0 transition-opacity duration-200 group-data-[collapsible=icon]:group-hover:opacity-100"
366
+ />
367
+ </span>
329
368
  <span className="text-foreground text-md min-w-0 truncate font-medium group-data-[collapsible=icon]:hidden">
330
369
  {title}
331
370
  </span>
@@ -44,7 +44,6 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
44
44
  "border-b border-border-secondary transition-colors",
45
45
  "data-[state=selected]:bg-muted",
46
46
  "[@media(hover:hover)]:hover:bg-background-hover dark:[@media(hover:hover)]:hover:bg-muted",
47
- "[@media(hover:hover)]:[&:hover_button:hover]:bg-subtle",
48
47
  className,
49
48
  )}
50
49
  {...props}
@@ -58,7 +57,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
58
57
  data-slot="table-head"
59
58
  className={cn(
60
59
  "h-11 px-6 py-3 bg-muted text-left align-middle type-text-xs font-semibold text-placeholder dark:text-subtle-foreground whitespace-nowrap",
61
- "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:pr-3",
60
+ "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:px-0",
62
61
  className,
63
62
  )}
64
63
  {...props}
@@ -66,11 +65,25 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
66
65
  );
67
66
  }
68
67
 
69
- function TableCell({ className, ...props }: React.ComponentProps<"td">) {
68
+ interface TableCellProps extends React.ComponentProps<"td"> {
69
+ /**
70
+ * When true, the cell renders edge-to-edge so the child can act as the
71
+ * interactive surface (e.g. a button or popover trigger filling the cell).
72
+ * Defaults to false (standard padded cell).
73
+ */
74
+ interactive?: boolean;
75
+ }
76
+
77
+ function TableCell({ className, interactive = false, ...props }: TableCellProps) {
70
78
  return (
71
79
  <td
72
80
  data-slot="table-cell"
73
- className={cn("px-6 py-2 align-middle", "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:pr-3", className)}
81
+ className={cn(
82
+ "align-middle",
83
+ interactive ? "h-px p-0" : "px-6 py-2",
84
+ "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:px-0",
85
+ className,
86
+ )}
74
87
  {...props}
75
88
  />
76
89
  );
@@ -44,10 +44,10 @@ const tabsTriggerVariants = cva(
44
44
  "data-[state=active]:border-b-2 data-[state=active]:border-foreground data-[state=active]:text-foreground",
45
45
  ],
46
46
  pill: [
47
- "rounded-lg px-4 py-2",
48
- "text-muted-foreground",
49
- "[@media(hover:hover)]:hover:bg-accent [@media(hover:hover)]:hover:text-accent-foreground",
50
- "data-[state=active]:bg-accent data-[state=active]:text-accent-foreground",
47
+ "rounded-md px-2 py-1.5",
48
+ "text-quaternary-foreground",
49
+ "[@media(hover:hover)]:hover:bg-accent [@media(hover:hover)]:hover:text-muted-foreground",
50
+ "data-[state=active]:bg-accent data-[state=active]:text-muted-foreground",
51
51
  ],
52
52
  },
53
53
  },
@@ -0,0 +1,107 @@
1
+ "use client";
2
+
3
+ /*
4
+ * TaskProgress — multi-step async progress primitive.
5
+ *
6
+ * Dumb component: the consumer owns orchestration (timer, websocket, polling) and
7
+ * passes `steps` with each step's status. Renders a percentage bar at the top
8
+ * and one row per step with done/running/pending state badges.
9
+ *
10
+ * `trailingLabel` is for "still working on something not in the step list" —
11
+ * shows after the last step as a running row that isn't counted in the percent.
12
+ */
13
+
14
+ import { CheckIcon } from "lucide-react";
15
+
16
+ import { cn } from "../lib/cn";
17
+ import { Separator } from "./separator";
18
+
19
+ type TaskProgressStatus = "pending" | "running" | "done";
20
+
21
+ interface TaskProgressStep {
22
+ id: string;
23
+ label: string;
24
+ status: TaskProgressStatus;
25
+ }
26
+
27
+ interface TaskProgressProps extends Omit<React.ComponentProps<"div">, "children"> {
28
+ steps: readonly TaskProgressStep[];
29
+ trailingLabel?: string;
30
+ /**
31
+ * Optional mount-time stagger: each row fades + slides in with `animation-delay = index × stepRevealDelayMs`.
32
+ * Pure CSS, runs once on mount. Omit for no animation.
33
+ */
34
+ stepRevealDelayMs?: number;
35
+ }
36
+
37
+ const REVEAL_CLASSES = "animate-in fade-in slide-in-from-bottom-2 duration-300";
38
+
39
+ function TaskProgress({ steps, trailingLabel, stepRevealDelayMs, className, ...props }: TaskProgressProps) {
40
+ const total = steps.length;
41
+ const doneCount = steps.filter((step) => step.status === "done").length;
42
+ const percent = total > 0 ? Math.round((doneCount / total) * 100) : 0;
43
+ const stagger = stepRevealDelayMs != null;
44
+
45
+ return (
46
+ <div className={cn("flex flex-col gap-4", className)} {...props}>
47
+ <div className="bg-muted h-2 w-full overflow-hidden rounded-full">
48
+ <div className="h-full rounded-full bg-success transition-all duration-500" style={{ width: `${percent}%` }} />
49
+ </div>
50
+ <div>
51
+ {steps.map((step, idx) => (
52
+ <div
53
+ key={step.id}
54
+ className={stagger ? REVEAL_CLASSES : undefined}
55
+ style={
56
+ stagger ? { animationDelay: `${idx * (stepRevealDelayMs ?? 0)}ms`, animationFillMode: "both" } : undefined
57
+ }
58
+ >
59
+ <TaskProgressRow label={step.label} status={step.status} />
60
+ {idx < steps.length - 1 && <Separator />}
61
+ </div>
62
+ ))}
63
+ {trailingLabel && (
64
+ <div className={stagger ? REVEAL_CLASSES : undefined}>
65
+ <Separator />
66
+ <TaskProgressRow label={trailingLabel} status="running" muted />
67
+ </div>
68
+ )}
69
+ </div>
70
+ </div>
71
+ );
72
+ }
73
+
74
+ interface TaskProgressRowProps {
75
+ label: string;
76
+ status: TaskProgressStatus;
77
+ muted?: boolean;
78
+ }
79
+
80
+ function TaskProgressRow({ label, status, muted }: TaskProgressRowProps) {
81
+ return (
82
+ <div className="flex items-center justify-between py-2">
83
+ <span className={cn("type-text-sm", muted && "text-muted-foreground")}>{label}</span>
84
+ {status === "done" && (
85
+ <span className="flex items-center gap-1 type-text-sm text-success">
86
+ <CheckIcon className="size-3.5" />
87
+ <span>done</span>
88
+ </span>
89
+ )}
90
+ {status === "running" && (
91
+ <span className="flex items-center gap-1.5 type-text-sm text-warning">
92
+ <span className="size-2 rounded-full bg-warning" />
93
+ <span>running…</span>
94
+ </span>
95
+ )}
96
+ {status === "pending" && (
97
+ <span className="flex items-center gap-1.5 type-text-sm text-muted-foreground">
98
+ <span className="size-2 rounded-full border border-muted-foreground" />
99
+ <span>pending</span>
100
+ </span>
101
+ )}
102
+ </div>
103
+ );
104
+ }
105
+
106
+ export type { TaskProgressStatus, TaskProgressStep };
107
+ export { TaskProgress };
@@ -35,7 +35,7 @@ function TooltipContent({
35
35
  className={cn(
36
36
  "bg-inverted text-inverted-foreground dark:bg-subtle dark:text-foreground",
37
37
  "z-50 w-fit rounded-md px-3 py-2 shadow-lg dark:shadow-none dark:drop-shadow-[0_0_0.5px_var(--color-border)]",
38
- "type-text-xs font-semibold text-balance text-center",
38
+ "type-text-xs font-semibold text-balance text-center whitespace-pre-line",
39
39
  "animate-in fade-in-0 zoom-in-95",
40
40
  "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
41
41
  "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ /*
4
+ * Wizard family — primitives for multi-step flows.
5
+ *
6
+ * - WizardSteps — vertical step rail (controlled by activeIdx + onSelect)
7
+ * - WizardProgress — step counter + progress bar
8
+ *
9
+ * Consumers compose these inside whatever container they need (sticky aside, modal, etc.).
10
+ */
11
+
12
+ import type * as React from "react";
13
+
14
+ import { cn } from "../lib/cn";
15
+ import { TabsNav, TabsNavList, TabsNavTrigger } from "./tabs";
16
+
17
+ interface WizardStep {
18
+ id: string;
19
+ label: string;
20
+ }
21
+
22
+ interface WizardStepsProps {
23
+ steps: readonly WizardStep[];
24
+ activeIdx: number;
25
+ onSelect: (idx: number) => void;
26
+ ariaLabel?: string;
27
+ className?: string;
28
+ }
29
+
30
+ function WizardSteps({ steps, activeIdx, onSelect, ariaLabel = "Wizard steps", className }: WizardStepsProps) {
31
+ return (
32
+ <TabsNav orientation="vertical" aria-label={ariaLabel} className={className}>
33
+ <TabsNavList>
34
+ {steps.map((step, idx) => (
35
+ <TabsNavTrigger key={step.id} active={idx === activeIdx} asChild>
36
+ <button type="button" onClick={() => onSelect(idx)} className="w-full justify-start text-left">
37
+ {step.label}
38
+ </button>
39
+ </TabsNavTrigger>
40
+ ))}
41
+ </TabsNavList>
42
+ </TabsNav>
43
+ );
44
+ }
45
+
46
+ interface WizardProgressProps extends React.ComponentProps<"div"> {
47
+ current: number;
48
+ total: number;
49
+ }
50
+
51
+ function WizardProgress({ current, total, className, ...props }: WizardProgressProps) {
52
+ const percent = total > 0 ? Math.round((current / total) * 100) : 0;
53
+ return (
54
+ <div className={cn("flex flex-col gap-1.5 px-2", className)} {...props}>
55
+ <div className="text-muted-foreground flex items-center justify-between text-xs">
56
+ <span>
57
+ Step {current}/{total}
58
+ </span>
59
+ <span>{percent}%</span>
60
+ </div>
61
+ <div className="bg-muted h-1.5 overflow-hidden rounded-full">
62
+ <div className="bg-primary h-full transition-all" style={{ width: `${percent}%` }} />
63
+ </div>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ export type { WizardStep };
69
+ export { WizardProgress, WizardSteps };
@@ -8,7 +8,9 @@ export function useCopyToClipboard({ resetDelay = 2000 }: { resetDelay?: number
8
8
 
9
9
  useEffect(() => {
10
10
  return () => {
11
- if (timeoutRef.current) clearTimeout(timeoutRef.current);
11
+ if (timeoutRef.current) {
12
+ clearTimeout(timeoutRef.current);
13
+ }
12
14
  };
13
15
  }, []);
14
16
 
@@ -16,7 +18,9 @@ export function useCopyToClipboard({ resetDelay = 2000 }: { resetDelay?: number
16
18
  (text: string) => {
17
19
  navigator.clipboard.writeText(text).then(() => {
18
20
  setIsCopied(true);
19
- if (timeoutRef.current) clearTimeout(timeoutRef.current);
21
+ if (timeoutRef.current) {
22
+ clearTimeout(timeoutRef.current);
23
+ }
20
24
  timeoutRef.current = setTimeout(() => setIsCopied(false), resetDelay);
21
25
  });
22
26
  },
@@ -1,4 +1,4 @@
1
- import { Plus } from "lucide-react";
1
+ import { ArrowRight, Plus, Sparkles } from "lucide-react";
2
2
 
3
3
  import { Button } from "../components/button";
4
4
 
@@ -323,6 +323,28 @@ export const AllVariants = () => {
323
323
  </Button>
324
324
  </div>
325
325
 
326
+ {/* ── CTA (animated gradient ring) ────────────────────────────────── */}
327
+ <span className={SECTION_HEADER}>CTA — animated</span>
328
+ <p className="type-text-xs text-muted-foreground -mt-2 max-w-md">
329
+ Hover to rotate the conic gradient around the border and ignite the soft halo.
330
+ </p>
331
+
332
+ <div className="flex items-center gap-6">
333
+ <Button variant="cta" iconTrailing={<ArrowRight />}>
334
+ Get started
335
+ </Button>
336
+ <Button variant="cta" icon={<Sparkles />} iconTrailing={<ArrowRight />}>
337
+ Launch server
338
+ </Button>
339
+ <Button variant="cta">Deploy now</Button>
340
+ <Button variant="cta" iconTrailing={<ArrowRight />} disabled>
341
+ Disabled
342
+ </Button>
343
+ <Button variant="cta" iconTrailing={<ArrowRight />} loading>
344
+ Deploying
345
+ </Button>
346
+ </div>
347
+
326
348
  {/* ── asChild ─────────────────────────────────────────────────────── */}
327
349
  <span className={SECTION_HEADER}>asChild</span>
328
350