@alpic-ai/ui 0.0.0-dev.f61bbc9 → 0.0.0-dev.f6cd325
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/button.d.mts +3 -1
- package/dist/components/button.mjs +20 -6
- package/dist/components/form.d.mts +36 -1
- package/dist/components/form.mjs +111 -1
- 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.mjs +61 -17
- 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/wizard.d.mts +34 -0
- package/dist/components/wizard.mjs +46 -0
- package/package.json +13 -13
- package/src/components/button.tsx +13 -9
- package/src/components/combobox.tsx +18 -6
- package/src/components/form.tsx +161 -0
- 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 +59 -20
- package/src/components/table.tsx +17 -4
- package/src/components/tabs.tsx +4 -4
- 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/wizard.stories.tsx +64 -0
- package/src/styles/tokens.css +217 -0
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
-
<
|
|
324
|
-
<span className="transition-opacity group-data-[collapsible=icon]:group-hover:opacity-0">
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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>
|
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,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>
|
|
@@ -190,8 +190,8 @@ export const AllVariants: Story = () => (
|
|
|
190
190
|
</TableRow>
|
|
191
191
|
</TableHeader>
|
|
192
192
|
<TableBody>
|
|
193
|
-
{USERS.slice(0, 3).map((user,
|
|
194
|
-
<TableRow key={user.id} data-state={
|
|
193
|
+
{USERS.slice(0, 3).map((user, index) => (
|
|
194
|
+
<TableRow key={user.id} data-state={index === 1 ? "selected" : undefined}>
|
|
195
195
|
<TableCell className="font-medium text-foreground">{user.name}</TableCell>
|
|
196
196
|
<TableCell className="text-subtle-foreground">{user.role}</TableCell>
|
|
197
197
|
<TableCell className="text-subtle-foreground">{user.email}</TableCell>
|
|
@@ -18,9 +18,11 @@ const navTabs = [
|
|
|
18
18
|
|
|
19
19
|
const sideNavTabs = [
|
|
20
20
|
{ id: "general", label: "General" },
|
|
21
|
+
{ id: "build-settings", label: "Build settings" },
|
|
22
|
+
{ id: "environment-variables", label: "Environment variables" },
|
|
23
|
+
{ id: "domains", label: "Domains" },
|
|
21
24
|
{ id: "authentication", label: "Authentication" },
|
|
22
|
-
{ id: "
|
|
23
|
-
{ id: "billing", label: "Billing" },
|
|
25
|
+
{ id: "marketplace", label: "Marketplace" },
|
|
24
26
|
];
|
|
25
27
|
|
|
26
28
|
function useHashRoute(fallback: string) {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Story } from "@ladle/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { WizardProgress, type WizardStep, WizardSteps } from "../components/wizard";
|
|
5
|
+
|
|
6
|
+
const SECTION_HEADER = "type-text-xs font-medium text-muted-foreground uppercase tracking-wide pt-4";
|
|
7
|
+
|
|
8
|
+
const steps: WizardStep[] = [
|
|
9
|
+
{ id: "overview", label: "Overview" },
|
|
10
|
+
{ id: "branding", label: "Branding & metadata" },
|
|
11
|
+
{ id: "auth", label: "Authentication" },
|
|
12
|
+
{ id: "tools", label: "Tools & test cases" },
|
|
13
|
+
{ id: "review", label: "Review & submit" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export const AllVariants: Story = () => {
|
|
17
|
+
const [activeIdx, setActiveIdx] = useState(1);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex flex-col gap-10 p-8">
|
|
21
|
+
{/* Full rail (steps + progress) */}
|
|
22
|
+
<div>
|
|
23
|
+
<p className={SECTION_HEADER}>Full rail — steps + progress</p>
|
|
24
|
+
<div className="mt-4 flex gap-6">
|
|
25
|
+
<aside className="basis-56 shrink-0 flex flex-col gap-4 self-start">
|
|
26
|
+
<WizardSteps steps={steps} activeIdx={activeIdx} onSelect={setActiveIdx} ariaLabel="Submission steps" />
|
|
27
|
+
<WizardProgress current={activeIdx + 1} total={steps.length} />
|
|
28
|
+
</aside>
|
|
29
|
+
<div className="flex-1 rounded-md border p-4">
|
|
30
|
+
<p className="type-text-sm text-muted-foreground">Content for step "{steps[activeIdx]?.label}"</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
{/* Steps only */}
|
|
36
|
+
<div>
|
|
37
|
+
<p className={SECTION_HEADER}>Steps only</p>
|
|
38
|
+
<div className="mt-4 max-w-56">
|
|
39
|
+
<WizardSteps steps={steps} activeIdx={activeIdx} onSelect={setActiveIdx} />
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* Progress only — various positions */}
|
|
44
|
+
<div>
|
|
45
|
+
<p className={SECTION_HEADER}>Progress — first step</p>
|
|
46
|
+
<div className="mt-4 max-w-56">
|
|
47
|
+
<WizardProgress current={1} total={5} />
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div>
|
|
51
|
+
<p className={SECTION_HEADER}>Progress — mid-flow</p>
|
|
52
|
+
<div className="mt-4 max-w-56">
|
|
53
|
+
<WizardProgress current={3} total={5} />
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div>
|
|
57
|
+
<p className={SECTION_HEADER}>Progress — complete</p>
|
|
58
|
+
<div className="mt-4 max-w-56">
|
|
59
|
+
<WizardProgress current={5} total={5} />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
};
|