@alpic-ai/ui 0.0.0-dev.f10d392 → 0.0.0-dev.f117100
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 +1 -1
- package/dist/components/dropdown-menu.d.mts +1 -1
- package/dist/components/form.d.mts +36 -1
- package/dist/components/form.mjs +111 -1
- 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/skeleton.d.mts +1 -1
- package/dist/components/spinner.d.mts +1 -1
- 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/wizard.d.mts +34 -0
- package/dist/components/wizard.mjs +46 -0
- package/package.json +12 -12
- package/src/components/combobox.tsx +18 -6
- package/src/components/form.tsx +161 -0
- package/src/components/page-loader.tsx +59 -0
- package/src/components/shimmer-text.tsx +23 -0
- 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/wizard.tsx +69 -0
- package/src/hooks/use-copy-to-clipboard.ts +6 -2
- package/src/stories/form.stories.tsx +64 -2
- 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 +20 -4
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.mjs";
|
|
3
|
+
import { TabsNav, TabsNavList, TabsNavTrigger } from "./tabs.mjs";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/components/wizard.tsx
|
|
6
|
+
function WizardSteps({ steps, activeIdx, onSelect, ariaLabel = "Wizard steps", className }) {
|
|
7
|
+
return /* @__PURE__ */ jsx(TabsNav, {
|
|
8
|
+
orientation: "vertical",
|
|
9
|
+
"aria-label": ariaLabel,
|
|
10
|
+
className,
|
|
11
|
+
children: /* @__PURE__ */ jsx(TabsNavList, { children: steps.map((step, idx) => /* @__PURE__ */ jsx(TabsNavTrigger, {
|
|
12
|
+
active: idx === activeIdx,
|
|
13
|
+
asChild: true,
|
|
14
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
15
|
+
type: "button",
|
|
16
|
+
onClick: () => onSelect(idx),
|
|
17
|
+
className: "w-full justify-start text-left",
|
|
18
|
+
children: step.label
|
|
19
|
+
})
|
|
20
|
+
}, step.id)) })
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function WizardProgress({ current, total, className, ...props }) {
|
|
24
|
+
const percent = total > 0 ? Math.round(current / total * 100) : 0;
|
|
25
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
26
|
+
className: cn("flex flex-col gap-1.5 px-2", className),
|
|
27
|
+
...props,
|
|
28
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
29
|
+
className: "text-muted-foreground flex items-center justify-between text-xs",
|
|
30
|
+
children: [/* @__PURE__ */ jsxs("span", { children: [
|
|
31
|
+
"Step ",
|
|
32
|
+
current,
|
|
33
|
+
"/",
|
|
34
|
+
total
|
|
35
|
+
] }), /* @__PURE__ */ jsxs("span", { children: [percent, "%"] })]
|
|
36
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
37
|
+
className: "bg-muted h-1.5 overflow-hidden rounded-full",
|
|
38
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
39
|
+
className: "bg-primary h-full transition-all",
|
|
40
|
+
style: { width: `${percent}%` }
|
|
41
|
+
})
|
|
42
|
+
})]
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { WizardProgress, WizardSteps };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alpic-ai/ui",
|
|
3
|
-
"version": "0.0.0-dev.
|
|
3
|
+
"version": "0.0.0-dev.f117100",
|
|
4
4
|
"description": "Alpic design system — shared UI components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
"src"
|
|
24
24
|
],
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"lucide-react": "^1.
|
|
27
|
-
"react": "^19.2.
|
|
28
|
-
"react-dom": "^19.2.
|
|
29
|
-
"react-hook-form": "^7.
|
|
26
|
+
"lucide-react": "^1.14.0",
|
|
27
|
+
"react": "^19.2.6",
|
|
28
|
+
"react-dom": "^19.2.6",
|
|
29
|
+
"react-hook-form": "^7.75.0",
|
|
30
30
|
"sonner": "^2.0.7",
|
|
31
|
-
"tailwindcss": "^4.
|
|
31
|
+
"tailwindcss": "^4.3.0",
|
|
32
32
|
"tw-animate-css": "^1.4.0"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
@@ -52,19 +52,19 @@
|
|
|
52
52
|
"class-variance-authority": "^0.7.1",
|
|
53
53
|
"clsx": "^2.1.1",
|
|
54
54
|
"cmdk": "^1.1.1",
|
|
55
|
-
"tailwind-merge": "^3.
|
|
55
|
+
"tailwind-merge": "^3.6.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@ladle/react": "^5.1.1",
|
|
59
|
-
"@tailwindcss/postcss": "^4.
|
|
59
|
+
"@tailwindcss/postcss": "^4.3.0",
|
|
60
60
|
"@types/react": "19.2.14",
|
|
61
61
|
"@types/react-dom": "19.2.3",
|
|
62
|
-
"lucide-react": "^1.
|
|
63
|
-
"react-hook-form": "^7.
|
|
62
|
+
"lucide-react": "^1.14.0",
|
|
63
|
+
"react-hook-form": "^7.75.0",
|
|
64
64
|
"shx": "^0.4.0",
|
|
65
65
|
"sonner": "^2.0.7",
|
|
66
|
-
"tailwindcss": "^4.
|
|
67
|
-
"tsdown": "^0.
|
|
66
|
+
"tailwindcss": "^4.3.0",
|
|
67
|
+
"tsdown": "^0.22.0",
|
|
68
68
|
"tw-animate-css": "^1.4.0",
|
|
69
69
|
"typescript": "^6.0.3"
|
|
70
70
|
},
|
|
@@ -84,7 +84,9 @@ function Combobox(props: ComboboxProps) {
|
|
|
84
84
|
|
|
85
85
|
const onOpenChange = useCallback(
|
|
86
86
|
(newOpen: boolean) => {
|
|
87
|
-
if (!isOpenControlled)
|
|
87
|
+
if (!isOpenControlled) {
|
|
88
|
+
setUncontrolledOpen(newOpen);
|
|
89
|
+
}
|
|
88
90
|
controlledOnOpenChange?.(newOpen);
|
|
89
91
|
},
|
|
90
92
|
[isOpenControlled, controlledOnOpenChange],
|
|
@@ -105,7 +107,9 @@ function Combobox(props: ComboboxProps) {
|
|
|
105
107
|
|
|
106
108
|
const isSelected = useCallback(
|
|
107
109
|
(itemValue: string) => {
|
|
108
|
-
if (multiple)
|
|
110
|
+
if (multiple) {
|
|
111
|
+
return multiValues.includes(itemValue);
|
|
112
|
+
}
|
|
109
113
|
return singleValue === itemValue;
|
|
110
114
|
},
|
|
111
115
|
[multiple, singleValue, multiValues],
|
|
@@ -123,11 +127,15 @@ function Combobox(props: ComboboxProps) {
|
|
|
123
127
|
const next = multiValues.includes(itemValue)
|
|
124
128
|
? multiValues.filter((val) => val !== itemValue)
|
|
125
129
|
: [...multiValues, itemValue];
|
|
126
|
-
if (!isControlledMulti)
|
|
130
|
+
if (!isControlledMulti) {
|
|
131
|
+
setUncontrolledMultiValue(next);
|
|
132
|
+
}
|
|
127
133
|
onValueChangeMulti?.(next);
|
|
128
134
|
// Stay open in multi mode
|
|
129
135
|
} else {
|
|
130
|
-
if (!isControlledSingle)
|
|
136
|
+
if (!isControlledSingle) {
|
|
137
|
+
setUncontrolledSingleValue(itemValue);
|
|
138
|
+
}
|
|
131
139
|
onValueChangeSingle?.(itemValue);
|
|
132
140
|
onOpenChange(false);
|
|
133
141
|
}
|
|
@@ -145,9 +153,13 @@ function Combobox(props: ComboboxProps) {
|
|
|
145
153
|
|
|
146
154
|
const onDeselect = useCallback(
|
|
147
155
|
(itemValue: string) => {
|
|
148
|
-
if (!multiple)
|
|
156
|
+
if (!multiple) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
149
159
|
const next = multiValues.filter((val) => val !== itemValue);
|
|
150
|
-
if (!isControlledMulti)
|
|
160
|
+
if (!isControlledMulti) {
|
|
161
|
+
setUncontrolledMultiValue(next);
|
|
162
|
+
}
|
|
151
163
|
onValueChangeMulti?.(next);
|
|
152
164
|
},
|
|
153
165
|
[multiple, isControlledMulti, onValueChangeMulti, multiValues],
|
package/src/components/form.tsx
CHANGED
|
@@ -14,8 +14,10 @@ import {
|
|
|
14
14
|
} from "react-hook-form";
|
|
15
15
|
|
|
16
16
|
import { cn } from "../lib/cn";
|
|
17
|
+
import { Checkbox } from "./checkbox";
|
|
17
18
|
import { Input, type InputProps } from "./input";
|
|
18
19
|
import { Label } from "./label";
|
|
20
|
+
import { RadioGroup, RadioGroupItem } from "./radio-group";
|
|
19
21
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
|
|
20
22
|
import { Textarea, type TextareaProps } from "./textarea";
|
|
21
23
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip";
|
|
@@ -325,8 +327,166 @@ function SelectField<TFieldValues extends FieldValues, TName extends FieldPath<T
|
|
|
325
327
|
);
|
|
326
328
|
}
|
|
327
329
|
|
|
330
|
+
interface RadioFieldProps<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>
|
|
331
|
+
extends FormFieldBaseProps<TFieldValues, TName> {
|
|
332
|
+
options: SelectFieldOption[];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function RadioField<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>({
|
|
336
|
+
control,
|
|
337
|
+
name,
|
|
338
|
+
rules,
|
|
339
|
+
required,
|
|
340
|
+
label,
|
|
341
|
+
description,
|
|
342
|
+
tooltip,
|
|
343
|
+
options,
|
|
344
|
+
}: RadioFieldProps<TFieldValues, TName>) {
|
|
345
|
+
return (
|
|
346
|
+
<FormField
|
|
347
|
+
control={control}
|
|
348
|
+
name={name}
|
|
349
|
+
rules={rules}
|
|
350
|
+
render={({ field }) => (
|
|
351
|
+
<FormItem>
|
|
352
|
+
{label && (
|
|
353
|
+
<FormLabel required={required} tooltip={tooltip}>
|
|
354
|
+
{label}
|
|
355
|
+
</FormLabel>
|
|
356
|
+
)}
|
|
357
|
+
<FormControl>
|
|
358
|
+
<RadioGroup value={field.value ?? ""} onValueChange={field.onChange} onBlur={field.onBlur}>
|
|
359
|
+
{options.map((option) => {
|
|
360
|
+
const itemId = `${field.name}-${option.value}`;
|
|
361
|
+
return (
|
|
362
|
+
<div key={option.value} className="flex items-center gap-2">
|
|
363
|
+
<RadioGroupItem id={itemId} value={option.value} disabled={option.disabled} />
|
|
364
|
+
<Label htmlFor={itemId} className="type-text-sm font-normal">
|
|
365
|
+
{option.label}
|
|
366
|
+
</Label>
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
})}
|
|
370
|
+
</RadioGroup>
|
|
371
|
+
</FormControl>
|
|
372
|
+
{description && <FormDescription>{description}</FormDescription>}
|
|
373
|
+
<FormMessage />
|
|
374
|
+
</FormItem>
|
|
375
|
+
)}
|
|
376
|
+
/>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
interface ChecklistFieldProps<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>
|
|
381
|
+
extends FormFieldBaseProps<TFieldValues, TName> {
|
|
382
|
+
options: SelectFieldOption[];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function ChecklistField<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>({
|
|
386
|
+
control,
|
|
387
|
+
name,
|
|
388
|
+
rules,
|
|
389
|
+
required,
|
|
390
|
+
label,
|
|
391
|
+
description,
|
|
392
|
+
tooltip,
|
|
393
|
+
options,
|
|
394
|
+
}: ChecklistFieldProps<TFieldValues, TName>) {
|
|
395
|
+
return (
|
|
396
|
+
<FormField
|
|
397
|
+
control={control}
|
|
398
|
+
name={name}
|
|
399
|
+
rules={rules}
|
|
400
|
+
render={({ field }) => {
|
|
401
|
+
const selected: string[] = Array.isArray(field.value) ? field.value : [];
|
|
402
|
+
const toggle = (value: string, checked: boolean) => {
|
|
403
|
+
if (checked) {
|
|
404
|
+
field.onChange([...selected, value]);
|
|
405
|
+
} else {
|
|
406
|
+
field.onChange(selected.filter((existing) => existing !== value));
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
return (
|
|
410
|
+
<FormItem>
|
|
411
|
+
{label && (
|
|
412
|
+
<FormLabel required={required} tooltip={tooltip}>
|
|
413
|
+
{label}
|
|
414
|
+
</FormLabel>
|
|
415
|
+
)}
|
|
416
|
+
<FormControl>
|
|
417
|
+
<fieldset className="m-0 flex flex-col gap-2 border-0 p-0" onBlur={field.onBlur}>
|
|
418
|
+
{options.map((option) => {
|
|
419
|
+
const itemId = `${field.name}-${option.value}`;
|
|
420
|
+
const checked = selected.includes(option.value);
|
|
421
|
+
return (
|
|
422
|
+
<div key={option.value} className="flex items-start gap-2">
|
|
423
|
+
<Checkbox
|
|
424
|
+
id={itemId}
|
|
425
|
+
checked={checked}
|
|
426
|
+
onCheckedChange={(next) => toggle(option.value, Boolean(next))}
|
|
427
|
+
disabled={option.disabled}
|
|
428
|
+
/>
|
|
429
|
+
<Label htmlFor={itemId} className="type-text-sm font-normal leading-tight">
|
|
430
|
+
{option.label}
|
|
431
|
+
</Label>
|
|
432
|
+
</div>
|
|
433
|
+
);
|
|
434
|
+
})}
|
|
435
|
+
</fieldset>
|
|
436
|
+
</FormControl>
|
|
437
|
+
{description && <FormDescription>{description}</FormDescription>}
|
|
438
|
+
<FormMessage />
|
|
439
|
+
</FormItem>
|
|
440
|
+
);
|
|
441
|
+
}}
|
|
442
|
+
/>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
interface CheckboxFieldProps<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>
|
|
447
|
+
extends Omit<FormFieldBaseProps<TFieldValues, TName>, "required"> {}
|
|
448
|
+
|
|
449
|
+
function CheckboxField<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>({
|
|
450
|
+
control,
|
|
451
|
+
name,
|
|
452
|
+
rules,
|
|
453
|
+
label,
|
|
454
|
+
description,
|
|
455
|
+
tooltip,
|
|
456
|
+
}: CheckboxFieldProps<TFieldValues, TName>) {
|
|
457
|
+
return (
|
|
458
|
+
<FormField
|
|
459
|
+
control={control}
|
|
460
|
+
name={name}
|
|
461
|
+
rules={rules}
|
|
462
|
+
render={({ field }) => (
|
|
463
|
+
<FormItem className="gap-1.5">
|
|
464
|
+
<div className="flex items-start gap-2">
|
|
465
|
+
<FormControl>
|
|
466
|
+
<Checkbox
|
|
467
|
+
checked={field.value ?? false}
|
|
468
|
+
onCheckedChange={(next) => field.onChange(Boolean(next))}
|
|
469
|
+
onBlur={field.onBlur}
|
|
470
|
+
/>
|
|
471
|
+
</FormControl>
|
|
472
|
+
{label && (
|
|
473
|
+
<FormLabel tooltip={tooltip} className="font-normal">
|
|
474
|
+
{label}
|
|
475
|
+
</FormLabel>
|
|
476
|
+
)}
|
|
477
|
+
</div>
|
|
478
|
+
{description && <FormDescription className="ml-6">{description}</FormDescription>}
|
|
479
|
+
<FormMessage className="ml-6" />
|
|
480
|
+
</FormItem>
|
|
481
|
+
)}
|
|
482
|
+
/>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
328
486
|
export type { SelectFieldOption };
|
|
329
487
|
export {
|
|
488
|
+
CheckboxField,
|
|
489
|
+
ChecklistField,
|
|
330
490
|
Form,
|
|
331
491
|
FormControl,
|
|
332
492
|
FormDescription,
|
|
@@ -337,6 +497,7 @@ export {
|
|
|
337
497
|
FormLabel,
|
|
338
498
|
FormMessage,
|
|
339
499
|
InputField,
|
|
500
|
+
RadioField,
|
|
340
501
|
SelectField,
|
|
341
502
|
TextareaField,
|
|
342
503
|
useFormField,
|
|
@@ -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
|
+
}
|
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 };
|
|
@@ -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
|
},
|