@alpic-ai/ui 0.0.0-dev.b18492d → 0.0.0-dev.b1956a0
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.
- package/dist/components/alert.d.mts +1 -1
- package/dist/components/avatar.d.mts +1 -1
- package/dist/components/badge.d.mts +1 -1
- package/dist/components/button.d.mts +3 -1
- package/dist/components/button.mjs +20 -6
- package/dist/components/dropdown-menu.d.mts +1 -1
- package/dist/components/form.d.mts +36 -1
- package/dist/components/form.mjs +114 -4
- package/dist/components/github-button.d.mts +13 -0
- package/dist/components/github-button.mjs +24 -0
- package/dist/components/page-loader.d.mts +11 -0
- package/dist/components/page-loader.mjs +122 -0
- package/dist/components/shimmer-text.d.mts +12 -0
- package/dist/components/shimmer-text.mjs +22 -0
- package/dist/components/sidebar.d.mts +1 -1
- package/dist/components/sidebar.mjs +39 -10
- package/dist/components/status-dot.d.mts +1 -1
- package/dist/components/table.d.mts +10 -1
- package/dist/components/table.mjs +4 -4
- package/dist/components/tabs.mjs +4 -4
- package/dist/components/task-progress.d.mts +27 -0
- package/dist/components/task-progress.mjs +66 -0
- package/dist/components/toggle-group.d.mts +1 -1
- package/dist/components/tooltip.mjs +1 -1
- package/dist/components/wizard.d.mts +34 -0
- package/dist/components/wizard.mjs +46 -0
- package/package.json +12 -12
- package/src/components/button.tsx +13 -9
- package/src/components/combobox.tsx +18 -6
- package/src/components/form.tsx +164 -3
- package/src/components/github-button.tsx +34 -0
- package/src/components/page-loader.tsx +59 -0
- package/src/components/shimmer-text.tsx +23 -0
- package/src/components/sidebar.tsx +48 -10
- package/src/components/table.tsx +17 -4
- package/src/components/tabs.tsx +4 -4
- package/src/components/task-progress.tsx +107 -0
- package/src/components/tooltip.tsx +1 -1
- package/src/components/wizard.tsx +69 -0
- package/src/hooks/use-copy-to-clipboard.ts +6 -2
- package/src/stories/button.stories.tsx +23 -1
- package/src/stories/form.stories.tsx +64 -2
- package/src/stories/sidebar.stories.tsx +6 -3
- package/src/stories/table.stories.tsx +2 -2
- package/src/stories/tabs.stories.tsx +4 -2
- package/src/stories/task-progress.stories.tsx +81 -0
- package/src/stories/wizard.stories.tsx +64 -0
- package/src/styles/tokens.css +193 -4
|
@@ -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
|
+
}
|
|
@@ -19,6 +19,19 @@ const SIDEBAR_WIDTH_MOBILE = "16rem";
|
|
|
19
19
|
const SIDEBAR_WIDTH_ICON = "3.5rem";
|
|
20
20
|
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
|
21
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
|
+
|
|
22
35
|
type SidebarContextProps = {
|
|
23
36
|
state: "expanded" | "collapsed";
|
|
24
37
|
open: boolean;
|
|
@@ -142,13 +155,31 @@ function Sidebar({
|
|
|
142
155
|
variant?: "sidebar" | "floating" | "inset";
|
|
143
156
|
collapsible?: "offcanvas" | "icon" | "none";
|
|
144
157
|
}) {
|
|
145
|
-
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
|
+
}
|
|
146
177
|
|
|
147
178
|
if (collapsible === "none") {
|
|
148
179
|
return (
|
|
149
180
|
<div
|
|
150
181
|
data-slot="sidebar"
|
|
151
|
-
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)}
|
|
152
183
|
{...props}
|
|
153
184
|
>
|
|
154
185
|
{children}
|
|
@@ -163,7 +194,7 @@ function Sidebar({
|
|
|
163
194
|
data-sidebar="sidebar"
|
|
164
195
|
data-slot="sidebar"
|
|
165
196
|
data-mobile="true"
|
|
166
|
-
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"
|
|
167
198
|
style={
|
|
168
199
|
{
|
|
169
200
|
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
|
@@ -212,7 +243,7 @@ function Sidebar({
|
|
|
212
243
|
// Adjust the padding for floating and inset variants.
|
|
213
244
|
variant === "floating" || variant === "inset"
|
|
214
245
|
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
|
215
|
-
: "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",
|
|
216
247
|
className,
|
|
217
248
|
)}
|
|
218
249
|
{...props}
|
|
@@ -220,7 +251,8 @@ function Sidebar({
|
|
|
220
251
|
<div
|
|
221
252
|
data-sidebar="sidebar"
|
|
222
253
|
data-slot="sidebar-inner"
|
|
223
|
-
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}
|
|
224
256
|
>
|
|
225
257
|
{children}
|
|
226
258
|
</div>
|
|
@@ -272,9 +304,7 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
|
|
272
304
|
onClick={toggleSidebar}
|
|
273
305
|
title="Toggle Sidebar"
|
|
274
306
|
className={cn(
|
|
275
|
-
"
|
|
276
|
-
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
|
277
|
-
"[[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",
|
|
278
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",
|
|
279
309
|
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
|
280
310
|
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
|
@@ -322,11 +352,19 @@ function SidebarHeader({ className, icon, title, children, ...props }: SidebarHe
|
|
|
322
352
|
<div
|
|
323
353
|
data-slot="sidebar-header"
|
|
324
354
|
data-sidebar="header"
|
|
325
|
-
className={cn("flex flex-col gap-2
|
|
355
|
+
className={cn("flex flex-col gap-2 py-2", className)}
|
|
326
356
|
{...props}
|
|
327
357
|
>
|
|
328
358
|
<div className="flex h-8 items-center gap-2 px-3">
|
|
329
|
-
<
|
|
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>
|
|
330
368
|
<span className="text-foreground text-md min-w-0 truncate font-medium group-data-[collapsible=icon]:hidden">
|
|
331
369
|
{title}
|
|
332
370
|
</span>
|
package/src/components/table.tsx
CHANGED
|
@@ -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])]:
|
|
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
|
-
|
|
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(
|
|
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
|
);
|
package/src/components/tabs.tsx
CHANGED
|
@@ -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-
|
|
48
|
-
"text-
|
|
49
|
-
"[@media(hover:hover)]:hover:bg-accent [@media(hover:hover)]:hover:text-
|
|
50
|
-
"data-[state=active]:bg-accent data-[state=active]:text-
|
|
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)
|
|
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)
|
|
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
|
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { Story } from "@ladle/react";
|
|
2
2
|
import { useForm } from "react-hook-form";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CheckboxField,
|
|
6
|
+
ChecklistField,
|
|
7
|
+
Form,
|
|
8
|
+
FormFields,
|
|
9
|
+
FormHeader,
|
|
10
|
+
InputField,
|
|
11
|
+
RadioField,
|
|
12
|
+
SelectField,
|
|
13
|
+
TextareaField,
|
|
14
|
+
} from "../components/form";
|
|
4
15
|
|
|
5
16
|
/* ── Types ───────────────────────────────────────────────────────────────── */
|
|
6
17
|
|
|
@@ -8,6 +19,9 @@ interface CreateProjectForm {
|
|
|
8
19
|
email: string;
|
|
9
20
|
role: string;
|
|
10
21
|
description: string;
|
|
22
|
+
visibility: string;
|
|
23
|
+
capabilities: string[];
|
|
24
|
+
acceptTerms: boolean;
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
const roles = [
|
|
@@ -16,11 +30,31 @@ const roles = [
|
|
|
16
30
|
{ value: "viewer", label: "Viewer" },
|
|
17
31
|
];
|
|
18
32
|
|
|
33
|
+
const visibilities = [
|
|
34
|
+
{ value: "public", label: "Public" },
|
|
35
|
+
{ value: "private", label: "Private" },
|
|
36
|
+
{ value: "internal", label: "Internal" },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const capabilities = [
|
|
40
|
+
{ value: "read", label: "Read" },
|
|
41
|
+
{ value: "write", label: "Write" },
|
|
42
|
+
{ value: "deploy", label: "Deploy" },
|
|
43
|
+
{ value: "admin", label: "Admin" },
|
|
44
|
+
];
|
|
45
|
+
|
|
19
46
|
/* ── Composed Form ───────────────────────────────────────────────────────── */
|
|
20
47
|
|
|
21
48
|
function ComposedForm() {
|
|
22
49
|
const form = useForm<CreateProjectForm>({
|
|
23
|
-
defaultValues: {
|
|
50
|
+
defaultValues: {
|
|
51
|
+
email: "",
|
|
52
|
+
role: "",
|
|
53
|
+
description: "",
|
|
54
|
+
visibility: "",
|
|
55
|
+
capabilities: [],
|
|
56
|
+
acceptTerms: false,
|
|
57
|
+
},
|
|
24
58
|
});
|
|
25
59
|
|
|
26
60
|
const onSubmit = (data: CreateProjectForm) => {
|
|
@@ -64,6 +98,34 @@ function ComposedForm() {
|
|
|
64
98
|
placeholder="A short description of your project..."
|
|
65
99
|
required
|
|
66
100
|
/>
|
|
101
|
+
|
|
102
|
+
<RadioField
|
|
103
|
+
control={form.control}
|
|
104
|
+
name="visibility"
|
|
105
|
+
rules={{ required: "Pick a visibility." }}
|
|
106
|
+
label="Visibility"
|
|
107
|
+
description="Who can see and access this project."
|
|
108
|
+
options={visibilities}
|
|
109
|
+
required
|
|
110
|
+
/>
|
|
111
|
+
|
|
112
|
+
<ChecklistField
|
|
113
|
+
control={form.control}
|
|
114
|
+
name="capabilities"
|
|
115
|
+
rules={{ validate: (value) => (value?.length ? true : "Pick at least one capability.") }}
|
|
116
|
+
label="Capabilities"
|
|
117
|
+
description="Select all that apply."
|
|
118
|
+
options={capabilities}
|
|
119
|
+
required
|
|
120
|
+
/>
|
|
121
|
+
|
|
122
|
+
<CheckboxField
|
|
123
|
+
control={form.control}
|
|
124
|
+
name="acceptTerms"
|
|
125
|
+
rules={{ validate: (value) => (value ? true : "You must accept the terms.") }}
|
|
126
|
+
label="I accept the terms and conditions"
|
|
127
|
+
description="Required to create the project."
|
|
128
|
+
/>
|
|
67
129
|
</FormFields>
|
|
68
130
|
|
|
69
131
|
<button
|
|
@@ -107,7 +107,8 @@ export const AllVariants: Story = () => (
|
|
|
107
107
|
<UserFooter />
|
|
108
108
|
</Sidebar>
|
|
109
109
|
<SidebarInset>
|
|
110
|
-
<div className="p-4">
|
|
110
|
+
<div className="flex items-center gap-2 p-4">
|
|
111
|
+
<SidebarTrigger />
|
|
111
112
|
<span className="type-text-sm text-muted-foreground">Sub-menus expand inline under their parent</span>
|
|
112
113
|
</div>
|
|
113
114
|
</SidebarInset>
|
|
@@ -169,7 +170,8 @@ export const AllVariants: Story = () => (
|
|
|
169
170
|
<UserFooter />
|
|
170
171
|
</Sidebar>
|
|
171
172
|
<SidebarInset>
|
|
172
|
-
<div className="p-4">
|
|
173
|
+
<div className="flex items-center gap-2 p-4">
|
|
174
|
+
<SidebarTrigger />
|
|
173
175
|
<span className="type-text-sm text-muted-foreground">Groups with labels, badges, and separator</span>
|
|
174
176
|
</div>
|
|
175
177
|
</SidebarInset>
|
|
@@ -195,7 +197,8 @@ export const AllVariants: Story = () => (
|
|
|
195
197
|
<UserFooter />
|
|
196
198
|
</Sidebar>
|
|
197
199
|
<SidebarInset>
|
|
198
|
-
<div className="p-4">
|
|
200
|
+
<div className="flex items-center gap-2 p-4">
|
|
201
|
+
<SidebarTrigger />
|
|
199
202
|
<span className="type-text-sm text-muted-foreground">Loading state with skeleton placeholders</span>
|
|
200
203
|
</div>
|
|
201
204
|
</SidebarInset>
|