@alpic-ai/ui 0.0.0-dev.fb86846 → 0.0.0-dev.fbc07c5

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 (39) 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 +111 -1
  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/wizard.d.mts +34 -0
  18. package/dist/components/wizard.mjs +46 -0
  19. package/package.json +13 -13
  20. package/src/components/button.tsx +13 -9
  21. package/src/components/combobox.tsx +18 -6
  22. package/src/components/form.tsx +161 -0
  23. package/src/components/github-button.tsx +34 -0
  24. package/src/components/page-loader.tsx +59 -0
  25. package/src/components/shimmer-text.tsx +23 -0
  26. package/src/components/sidebar.tsx +59 -20
  27. package/src/components/table.tsx +17 -4
  28. package/src/components/tabs.tsx +4 -4
  29. package/src/components/task-progress.tsx +107 -0
  30. package/src/components/wizard.tsx +69 -0
  31. package/src/hooks/use-copy-to-clipboard.ts +6 -2
  32. package/src/stories/button.stories.tsx +23 -1
  33. package/src/stories/form.stories.tsx +64 -2
  34. package/src/stories/sidebar.stories.tsx +6 -3
  35. package/src/stories/table.stories.tsx +2 -2
  36. package/src/stories/tabs.stories.tsx +4 -2
  37. package/src/stories/task-progress.stories.tsx +81 -0
  38. package/src/stories/wizard.stories.tsx +64 -0
  39. package/src/styles/tokens.css +217 -0
@@ -21,10 +21,10 @@ const tabsTriggerVariants = cva([
21
21
  "data-[state=active]:border-b-2 data-[state=active]:border-foreground data-[state=active]:text-foreground"
22
22
  ],
23
23
  pill: [
24
- "rounded-lg px-4 py-2",
25
- "text-muted-foreground",
26
- "[@media(hover:hover)]:hover:bg-accent [@media(hover:hover)]:hover:text-accent-foreground",
27
- "data-[state=active]:bg-accent data-[state=active]:text-accent-foreground"
24
+ "rounded-md px-2 py-1.5",
25
+ "text-quaternary-foreground",
26
+ "[@media(hover:hover)]:hover:bg-accent [@media(hover:hover)]:hover:text-muted-foreground",
27
+ "data-[state=active]:bg-accent data-[state=active]:text-muted-foreground"
28
28
  ]
29
29
  } },
30
30
  defaultVariants: { variant: "default" }
@@ -0,0 +1,27 @@
1
+ import * as _$react_jsx_runtime0 from "react/jsx-runtime";
2
+
3
+ //#region src/components/task-progress.d.ts
4
+ type TaskProgressStatus = "pending" | "running" | "done";
5
+ interface TaskProgressStep {
6
+ id: string;
7
+ label: string;
8
+ status: TaskProgressStatus;
9
+ }
10
+ interface TaskProgressProps extends Omit<React.ComponentProps<"div">, "children"> {
11
+ steps: readonly TaskProgressStep[];
12
+ trailingLabel?: string;
13
+ /**
14
+ * Optional mount-time stagger: each row fades + slides in with `animation-delay = index × stepRevealDelayMs`.
15
+ * Pure CSS, runs once on mount. Omit for no animation.
16
+ */
17
+ stepRevealDelayMs?: number;
18
+ }
19
+ declare function TaskProgress({
20
+ steps,
21
+ trailingLabel,
22
+ stepRevealDelayMs,
23
+ className,
24
+ ...props
25
+ }: TaskProgressProps): _$react_jsx_runtime0.JSX.Element;
26
+ //#endregion
27
+ export { TaskProgress, type TaskProgressStatus, type TaskProgressStep };
@@ -0,0 +1,66 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.mjs";
3
+ import { Separator } from "./separator.mjs";
4
+ import { CheckIcon } from "lucide-react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/components/task-progress.tsx
7
+ const REVEAL_CLASSES = "animate-in fade-in slide-in-from-bottom-2 duration-300";
8
+ function TaskProgress({ steps, trailingLabel, stepRevealDelayMs, className, ...props }) {
9
+ const total = steps.length;
10
+ const doneCount = steps.filter((step) => step.status === "done").length;
11
+ const percent = total > 0 ? Math.round(doneCount / total * 100) : 0;
12
+ const stagger = stepRevealDelayMs != null;
13
+ return /* @__PURE__ */ jsxs("div", {
14
+ className: cn("flex flex-col gap-4", className),
15
+ ...props,
16
+ children: [/* @__PURE__ */ jsx("div", {
17
+ className: "bg-muted h-2 w-full overflow-hidden rounded-full",
18
+ children: /* @__PURE__ */ jsx("div", {
19
+ className: "h-full rounded-full bg-success transition-all duration-500",
20
+ style: { width: `${percent}%` }
21
+ })
22
+ }), /* @__PURE__ */ jsxs("div", { children: [steps.map((step, idx) => /* @__PURE__ */ jsxs("div", {
23
+ className: stagger ? REVEAL_CLASSES : void 0,
24
+ style: stagger ? {
25
+ animationDelay: `${idx * (stepRevealDelayMs ?? 0)}ms`,
26
+ animationFillMode: "both"
27
+ } : void 0,
28
+ children: [/* @__PURE__ */ jsx(TaskProgressRow, {
29
+ label: step.label,
30
+ status: step.status
31
+ }), idx < steps.length - 1 && /* @__PURE__ */ jsx(Separator, {})]
32
+ }, step.id)), trailingLabel && /* @__PURE__ */ jsxs("div", {
33
+ className: stagger ? REVEAL_CLASSES : void 0,
34
+ children: [/* @__PURE__ */ jsx(Separator, {}), /* @__PURE__ */ jsx(TaskProgressRow, {
35
+ label: trailingLabel,
36
+ status: "running",
37
+ muted: true
38
+ })]
39
+ })] })]
40
+ });
41
+ }
42
+ function TaskProgressRow({ label, status, muted }) {
43
+ return /* @__PURE__ */ jsxs("div", {
44
+ className: "flex items-center justify-between py-2",
45
+ children: [
46
+ /* @__PURE__ */ jsx("span", {
47
+ className: cn("type-text-sm", muted && "text-muted-foreground"),
48
+ children: label
49
+ }),
50
+ status === "done" && /* @__PURE__ */ jsxs("span", {
51
+ className: "flex items-center gap-1 type-text-sm text-success",
52
+ children: [/* @__PURE__ */ jsx(CheckIcon, { className: "size-3.5" }), /* @__PURE__ */ jsx("span", { children: "done" })]
53
+ }),
54
+ status === "running" && /* @__PURE__ */ jsxs("span", {
55
+ className: "flex items-center gap-1.5 type-text-sm text-warning",
56
+ children: [/* @__PURE__ */ jsx("span", { className: "size-2 rounded-full bg-warning" }), /* @__PURE__ */ jsx("span", { children: "running…" })]
57
+ }),
58
+ status === "pending" && /* @__PURE__ */ jsxs("span", {
59
+ className: "flex items-center gap-1.5 type-text-sm text-muted-foreground",
60
+ children: [/* @__PURE__ */ jsx("span", { className: "size-2 rounded-full border border-muted-foreground" }), /* @__PURE__ */ jsx("span", { children: "pending" })]
61
+ })
62
+ ]
63
+ });
64
+ }
65
+ //#endregion
66
+ export { TaskProgress };
@@ -0,0 +1,34 @@
1
+ import * as _$react_jsx_runtime0 from "react/jsx-runtime";
2
+ import * as React from "react";
3
+
4
+ //#region src/components/wizard.d.ts
5
+ interface WizardStep {
6
+ id: string;
7
+ label: string;
8
+ }
9
+ interface WizardStepsProps {
10
+ steps: readonly WizardStep[];
11
+ activeIdx: number;
12
+ onSelect: (idx: number) => void;
13
+ ariaLabel?: string;
14
+ className?: string;
15
+ }
16
+ declare function WizardSteps({
17
+ steps,
18
+ activeIdx,
19
+ onSelect,
20
+ ariaLabel,
21
+ className
22
+ }: WizardStepsProps): _$react_jsx_runtime0.JSX.Element;
23
+ interface WizardProgressProps extends React.ComponentProps<"div"> {
24
+ current: number;
25
+ total: number;
26
+ }
27
+ declare function WizardProgress({
28
+ current,
29
+ total,
30
+ className,
31
+ ...props
32
+ }: WizardProgressProps): _$react_jsx_runtime0.JSX.Element;
33
+ //#endregion
34
+ export { WizardProgress, type WizardStep, WizardSteps };
@@ -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.fb86846",
3
+ "version": "0.0.0-dev.fbc07c5",
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.7.0",
27
- "react": "^19.2.4",
28
- "react-dom": "^19.2.4",
29
- "react-hook-form": "^7.72.1",
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.2.2",
31
+ "tailwindcss": "^4.3.0",
32
32
  "tw-animate-css": "^1.4.0"
33
33
  },
34
34
  "dependencies": {
@@ -52,21 +52,21 @@
52
52
  "class-variance-authority": "^0.7.1",
53
53
  "clsx": "^2.1.1",
54
54
  "cmdk": "^1.1.1",
55
- "tailwind-merge": "^3.5.0"
55
+ "tailwind-merge": "^3.6.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@ladle/react": "^5.1.1",
59
- "@tailwindcss/postcss": "^4.2.2",
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.7.0",
63
- "react-hook-form": "^7.72.1",
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.2.2",
67
- "tsdown": "^0.21.7",
66
+ "tailwindcss": "^4.3.0",
67
+ "tsdown": "^0.22.0",
68
68
  "tw-animate-css": "^1.4.0",
69
- "typescript": "^6.0.2"
69
+ "typescript": "^6.0.3"
70
70
  },
71
71
  "scripts": {
72
72
  "build": "shx rm -rf dist && tsdown",
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Slot } from "@radix-ui/react-slot";
3
+ import { Slot, Slottable } from "@radix-ui/react-slot";
4
4
  import { cva, type VariantProps } from "class-variance-authority";
5
5
  import { Loader2 } from "lucide-react";
6
6
  import type * as React from "react";
@@ -46,6 +46,13 @@ const buttonVariants = cva(
46
46
  "bg-destructive text-destructive-foreground",
47
47
  "[@media(hover:hover)]:hover:bg-destructive-hover",
48
48
  ].join(" "),
49
+ cta: [
50
+ "button-cta",
51
+ "h-9 px-4 gap-2 rounded-md",
52
+ "dark:bg-inverted text-foreground",
53
+ "transition-[transform,filter] duration-300 ease-out",
54
+ "active:scale-[0.99]",
55
+ ].join(" "),
49
56
  },
50
57
  size: {
51
58
  default: "type-text-sm",
@@ -65,6 +72,7 @@ interface ButtonProps extends React.ComponentProps<"button">, VariantProps<typeo
65
72
  asChild?: boolean;
66
73
  loading?: boolean;
67
74
  icon?: React.ReactNode;
75
+ iconTrailing?: React.ReactNode;
68
76
  }
69
77
 
70
78
  function Button({
@@ -75,6 +83,7 @@ function Button({
75
83
  asChild = false,
76
84
  loading = false,
77
85
  icon,
86
+ iconTrailing,
78
87
  disabled,
79
88
  children,
80
89
  ...props
@@ -90,14 +99,9 @@ function Button({
90
99
  aria-busy={loading || undefined}
91
100
  {...props}
92
101
  >
93
- {asChild ? (
94
- children
95
- ) : (
96
- <>
97
- {loading ? <Loader2 className="motion-safe:animate-spin" /> : icon}
98
- {children}
99
- </>
100
- )}
102
+ {loading ? <Loader2 className="motion-safe:animate-spin" /> : icon}
103
+ {asChild ? <Slottable>{children}</Slottable> : children}
104
+ {!loading && iconTrailing ? <span data-cta-icon-trailing>{iconTrailing}</span> : null}
101
105
  </Comp>
102
106
  );
103
107
  }
@@ -84,7 +84,9 @@ function Combobox(props: ComboboxProps) {
84
84
 
85
85
  const onOpenChange = useCallback(
86
86
  (newOpen: boolean) => {
87
- if (!isOpenControlled) setUncontrolledOpen(newOpen);
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) return multiValues.includes(itemValue);
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) setUncontrolledMultiValue(next);
130
+ if (!isControlledMulti) {
131
+ setUncontrolledMultiValue(next);
132
+ }
127
133
  onValueChangeMulti?.(next);
128
134
  // Stay open in multi mode
129
135
  } else {
130
- if (!isControlledSingle) setUncontrolledSingleValue(itemValue);
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) return;
156
+ if (!multiple) {
157
+ return;
158
+ }
149
159
  const next = multiValues.filter((val) => val !== itemValue);
150
- if (!isControlledMulti) setUncontrolledMultiValue(next);
160
+ if (!isControlledMulti) {
161
+ setUncontrolledMultiValue(next);
162
+ }
151
163
  onValueChangeMulti?.(next);
152
164
  },
153
165
  [multiple, isControlledMulti, onValueChangeMulti, multiValues],
@@ -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,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
+ }