@alpic-ai/ui 0.0.0

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 (134) hide show
  1. package/dist/components/accordion-card.d.mts +41 -0
  2. package/dist/components/accordion-card.mjs +61 -0
  3. package/dist/components/accordion.d.mts +29 -0
  4. package/dist/components/accordion.mjs +48 -0
  5. package/dist/components/alert.d.mts +40 -0
  6. package/dist/components/alert.mjs +63 -0
  7. package/dist/components/attachment-tile.d.mts +26 -0
  8. package/dist/components/attachment-tile.mjs +35 -0
  9. package/dist/components/avatar.d.mts +52 -0
  10. package/dist/components/avatar.mjs +81 -0
  11. package/dist/components/badge.d.mts +20 -0
  12. package/dist/components/badge.mjs +36 -0
  13. package/dist/components/breadcrumb.d.mts +42 -0
  14. package/dist/components/breadcrumb.mjs +79 -0
  15. package/dist/components/button.d.mts +29 -0
  16. package/dist/components/button.mjs +67 -0
  17. package/dist/components/card.d.mts +37 -0
  18. package/dist/components/card.mjs +54 -0
  19. package/dist/components/checkbox.d.mts +11 -0
  20. package/dist/components/checkbox.mjs +26 -0
  21. package/dist/components/collapsible.d.mts +16 -0
  22. package/dist/components/collapsible.mjs +24 -0
  23. package/dist/components/combobox.d.mts +86 -0
  24. package/dist/components/combobox.mjs +207 -0
  25. package/dist/components/command.d.mts +38 -0
  26. package/dist/components/command.mjs +68 -0
  27. package/dist/components/copyable.d.mts +22 -0
  28. package/dist/components/copyable.mjs +33 -0
  29. package/dist/components/description-list.d.mts +22 -0
  30. package/dist/components/description-list.mjs +34 -0
  31. package/dist/components/dialog.d.mts +61 -0
  32. package/dist/components/dialog.mjs +110 -0
  33. package/dist/components/dropdown-menu.d.mts +72 -0
  34. package/dist/components/dropdown-menu.mjs +122 -0
  35. package/dist/components/input-group.d.mts +25 -0
  36. package/dist/components/input-group.mjs +43 -0
  37. package/dist/components/input.d.mts +27 -0
  38. package/dist/components/input.mjs +90 -0
  39. package/dist/components/label.d.mts +11 -0
  40. package/dist/components/label.mjs +14 -0
  41. package/dist/components/pagination.d.mts +18 -0
  42. package/dist/components/pagination.mjs +42 -0
  43. package/dist/components/popover.d.mts +22 -0
  44. package/dist/components/popover.mjs +34 -0
  45. package/dist/components/radio-group.d.mts +15 -0
  46. package/dist/components/radio-group.mjs +26 -0
  47. package/dist/components/scroll-area.d.mts +17 -0
  48. package/dist/components/scroll-area.mjs +35 -0
  49. package/dist/components/select-trigger-variants.d.mts +13 -0
  50. package/dist/components/select-trigger-variants.mjs +23 -0
  51. package/dist/components/select.d.mts +51 -0
  52. package/dist/components/select.mjs +95 -0
  53. package/dist/components/separator.d.mts +13 -0
  54. package/dist/components/separator.mjs +16 -0
  55. package/dist/components/sheet.d.mts +43 -0
  56. package/dist/components/sheet.mjs +74 -0
  57. package/dist/components/sidebar.d.mts +163 -0
  58. package/dist/components/sidebar.mjs +378 -0
  59. package/dist/components/skeleton.d.mts +16 -0
  60. package/dist/components/skeleton.mjs +21 -0
  61. package/dist/components/sonner.d.mts +29 -0
  62. package/dist/components/sonner.mjs +76 -0
  63. package/dist/components/spinner.d.mts +30 -0
  64. package/dist/components/spinner.mjs +46 -0
  65. package/dist/components/status-dot.d.mts +19 -0
  66. package/dist/components/status-dot.mjs +34 -0
  67. package/dist/components/switch.d.mts +11 -0
  68. package/dist/components/switch.mjs +18 -0
  69. package/dist/components/table.d.mts +38 -0
  70. package/dist/components/table.mjs +65 -0
  71. package/dist/components/tabs.d.mts +58 -0
  72. package/dist/components/tabs.mjs +119 -0
  73. package/dist/components/tag.d.mts +35 -0
  74. package/dist/components/tag.mjs +65 -0
  75. package/dist/components/textarea.d.mts +21 -0
  76. package/dist/components/textarea.mjs +44 -0
  77. package/dist/components/toggle-group.d.mts +28 -0
  78. package/dist/components/toggle-group.mjs +72 -0
  79. package/dist/components/tooltip-icon-button.d.mts +12 -0
  80. package/dist/components/tooltip-icon-button.mjs +27 -0
  81. package/dist/components/tooltip.d.mts +23 -0
  82. package/dist/components/tooltip.mjs +35 -0
  83. package/dist/hooks/use-copy-to-clipboard.d.mts +11 -0
  84. package/dist/hooks/use-copy-to-clipboard.mjs +24 -0
  85. package/dist/hooks/use-mobile.d.mts +4 -0
  86. package/dist/hooks/use-mobile.mjs +16 -0
  87. package/dist/lib/cn.d.mts +6 -0
  88. package/dist/lib/cn.mjs +8 -0
  89. package/package.json +88 -0
  90. package/src/components/accordion-card.tsx +103 -0
  91. package/src/components/accordion.tsx +63 -0
  92. package/src/components/alert.tsx +74 -0
  93. package/src/components/attachment-tile.tsx +68 -0
  94. package/src/components/avatar.tsx +127 -0
  95. package/src/components/badge.tsx +41 -0
  96. package/src/components/breadcrumb.tsx +98 -0
  97. package/src/components/button.tsx +106 -0
  98. package/src/components/card.tsx +62 -0
  99. package/src/components/checkbox.tsx +35 -0
  100. package/src/components/collapsible.tsx +18 -0
  101. package/src/components/combobox.tsx +393 -0
  102. package/src/components/command.tsx +112 -0
  103. package/src/components/copyable.tsx +47 -0
  104. package/src/components/description-list.tsx +36 -0
  105. package/src/components/dialog.tsx +161 -0
  106. package/src/components/dropdown-menu.tsx +234 -0
  107. package/src/components/input-group.tsx +97 -0
  108. package/src/components/input.tsx +145 -0
  109. package/src/components/label.tsx +20 -0
  110. package/src/components/pagination.tsx +53 -0
  111. package/src/components/popover.tsx +49 -0
  112. package/src/components/radio-group.tsx +33 -0
  113. package/src/components/scroll-area.tsx +48 -0
  114. package/src/components/select-trigger-variants.ts +28 -0
  115. package/src/components/select.tsx +186 -0
  116. package/src/components/separator.tsx +30 -0
  117. package/src/components/sheet.tsx +112 -0
  118. package/src/components/sidebar.tsx +682 -0
  119. package/src/components/skeleton.tsx +24 -0
  120. package/src/components/sonner.tsx +91 -0
  121. package/src/components/spinner.tsx +62 -0
  122. package/src/components/status-dot.tsx +33 -0
  123. package/src/components/switch.tsx +33 -0
  124. package/src/components/table.tsx +89 -0
  125. package/src/components/tabs.tsx +226 -0
  126. package/src/components/tag.tsx +82 -0
  127. package/src/components/textarea.tsx +70 -0
  128. package/src/components/toggle-group.tsx +96 -0
  129. package/src/components/tooltip-icon-button.tsx +33 -0
  130. package/src/components/tooltip.tsx +54 -0
  131. package/src/hooks/use-copy-to-clipboard.ts +27 -0
  132. package/src/hooks/use-mobile.ts +17 -0
  133. package/src/lib/cn.ts +6 -0
  134. package/src/styles/tokens.css +352 -0
@@ -0,0 +1,91 @@
1
+ "use client";
2
+
3
+ import { CheckCircle2, Info, Loader2, TriangleAlert, X } from "lucide-react";
4
+ import type * as React from "react";
5
+ import { Toaster as Sonner, toast } from "sonner";
6
+ import { Button } from "./button";
7
+
8
+ type ToasterProps = React.ComponentProps<typeof Sonner>;
9
+
10
+ function Toaster(props: ToasterProps) {
11
+ return (
12
+ <Sonner
13
+ className="toaster group"
14
+ icons={{
15
+ success: <CheckCircle2 className="size-5 text-badge-success" />,
16
+ error: <TriangleAlert className="size-5 text-destructive" />,
17
+ warning: <TriangleAlert className="size-5 text-badge-warning" />,
18
+ info: <Info className="size-5 text-foreground" />,
19
+ loading: <Loader2 className="size-5 text-muted-foreground motion-safe:animate-spin" />,
20
+ close: <X className="size-3.5" />,
21
+ }}
22
+ toastOptions={{
23
+ unstyled: true,
24
+ classNames: {
25
+ toast: [
26
+ "group toast w-full",
27
+ "flex items-start gap-3 rounded-lg border border-border bg-background p-4 shadow-md",
28
+ "type-text-sm text-foreground",
29
+ ].join(" "),
30
+ title: "font-semibold",
31
+ description: "text-muted-foreground type-text-sm mt-0.5",
32
+ closeButton: [
33
+ "!static !transform-none shrink-0 ml-auto -mr-1 -mt-1",
34
+ "flex items-center justify-center size-6 rounded-sm",
35
+ "text-muted-foreground",
36
+ "[@media(hover:hover)]:hover:text-foreground",
37
+ "transition-colors",
38
+ ].join(" "),
39
+ },
40
+ }}
41
+ {...props}
42
+ />
43
+ );
44
+ }
45
+
46
+ interface ActionToastProps {
47
+ toastId: string | number;
48
+ title: string;
49
+ description: string;
50
+ onDismiss?: () => void;
51
+ confirm: {
52
+ label: string;
53
+ icon?: React.ReactNode;
54
+ onClick: () => void;
55
+ loading?: boolean;
56
+ disabled?: boolean;
57
+ };
58
+ }
59
+
60
+ function ActionToast({ toastId, title, description, onDismiss, confirm }: ActionToastProps) {
61
+ const handleDismiss = () => {
62
+ onDismiss?.();
63
+ toast.dismiss(toastId);
64
+ };
65
+
66
+ return (
67
+ <div className="flex flex-col gap-4">
68
+ <div className="flex flex-col gap-1">
69
+ <p className="type-text-lg font-semibold leading-none">{title}</p>
70
+ <p className="type-text-sm text-muted-foreground">{description}</p>
71
+ </div>
72
+ <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
73
+ <Button variant="secondary" disabled={confirm.loading} onClick={handleDismiss} icon={<X />}>
74
+ Dismiss
75
+ </Button>
76
+ <Button
77
+ variant="primary"
78
+ loading={confirm.loading}
79
+ disabled={confirm.disabled}
80
+ onClick={confirm.onClick}
81
+ icon={confirm.icon}
82
+ >
83
+ {confirm.label}
84
+ </Button>
85
+ </div>
86
+ </div>
87
+ );
88
+ }
89
+
90
+ export type { ActionToastProps, ToasterProps };
91
+ export { ActionToast, Toaster, toast };
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { Loader } from "lucide-react";
5
+ import { cn } from "../lib/cn";
6
+
7
+ const spinnerVariants = cva("motion-safe:animate-spin", {
8
+ variants: {
9
+ variant: {
10
+ primary: "text-primary",
11
+ secondary: "text-muted-foreground",
12
+ },
13
+ size: {
14
+ sm: "size-3.5",
15
+ md: "size-4",
16
+ lg: "size-5",
17
+ xl: "size-8",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "primary",
22
+ size: "md",
23
+ },
24
+ });
25
+
26
+ interface SpinnerProps extends Omit<React.ComponentProps<"svg">, "children">, VariantProps<typeof spinnerVariants> {}
27
+
28
+ function Spinner({ className, variant, size, ...props }: SpinnerProps) {
29
+ return (
30
+ <Loader
31
+ role="status"
32
+ aria-label="Loading"
33
+ className={cn(spinnerVariants({ variant, size }), className)}
34
+ {...props}
35
+ />
36
+ );
37
+ }
38
+
39
+ interface SpinnerBlockProps extends VariantProps<typeof spinnerVariants> {
40
+ className?: string;
41
+ fullscreen?: boolean;
42
+ withLabel?: boolean;
43
+ }
44
+
45
+ function SpinnerBlock({ className, variant, size, fullscreen, withLabel }: SpinnerBlockProps) {
46
+ return (
47
+ <div
48
+ className={cn(
49
+ "flex items-center justify-center",
50
+ fullscreen ? "min-h-screen" : "py-6",
51
+ withLabel && "gap-2 text-muted-foreground",
52
+ className,
53
+ )}
54
+ >
55
+ <Spinner variant={variant} size={size} />
56
+ {withLabel && <span>Loading...</span>}
57
+ </div>
58
+ );
59
+ }
60
+
61
+ export type { SpinnerBlockProps, SpinnerProps };
62
+ export { Spinner, SpinnerBlock, spinnerVariants };
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import { cn } from "@alpic-ai/ui/lib/cn";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+
6
+ const statusDotVariants = cva("size-2.5 rounded-full", {
7
+ variants: {
8
+ variant: {
9
+ success: "bg-success",
10
+ destructive: "bg-destructive",
11
+ warning: "bg-warning",
12
+ muted: "bg-muted-foreground",
13
+ },
14
+ pulse: {
15
+ true: "animate-pulse",
16
+ false: "",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ variant: "muted",
21
+ pulse: false,
22
+ },
23
+ });
24
+
25
+ interface StatusDotProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof statusDotVariants> {}
26
+
27
+ function StatusDot({ className, variant, pulse, ...props }: StatusDotProps) {
28
+ return <div className={cn(statusDotVariants({ variant, pulse }), className)} {...props} />;
29
+ }
30
+
31
+ type StatusDotVariantProps = Required<Pick<VariantProps<typeof statusDotVariants>, "variant" | "pulse">>;
32
+
33
+ export { StatusDot, type StatusDotVariantProps, statusDotVariants };
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import * as SwitchPrimitive from "@radix-ui/react-switch";
4
+ import type * as React from "react";
5
+
6
+ import { cn } from "../lib/cn";
7
+
8
+ function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
9
+ return (
10
+ <SwitchPrimitive.Root
11
+ data-slot="switch"
12
+ className={cn(
13
+ "peer inline-flex h-5 w-9 shrink-0 items-center rounded-full p-0.5",
14
+ "bg-subtle data-[state=checked]:not-disabled:bg-primary",
15
+ "transition-colors",
16
+ "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
17
+ "disabled:pointer-events-none disabled:opacity-50",
18
+ className,
19
+ )}
20
+ {...props}
21
+ >
22
+ <SwitchPrimitive.Thumb
23
+ data-slot="switch-thumb"
24
+ className={cn(
25
+ "pointer-events-none block size-4 rounded-full bg-primary-foreground shadow-xs",
26
+ "transition-transform data-[state=unchecked]:translate-x-0 data-[state=checked]:translate-x-4",
27
+ )}
28
+ />
29
+ </SwitchPrimitive.Root>
30
+ );
31
+ }
32
+
33
+ export { Switch };
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ import type * as React from "react";
4
+
5
+ import { cn } from "../lib/cn";
6
+
7
+ function Table({ className, ...props }: React.ComponentProps<"table">) {
8
+ return (
9
+ <div className="relative w-full overflow-auto rounded-xl border border-border-secondary bg-background">
10
+ <table data-slot="table" className={cn("w-full caption-bottom type-text-sm", className)} {...props} />
11
+ </div>
12
+ );
13
+ }
14
+
15
+ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
16
+ return (
17
+ <thead
18
+ data-slot="table-header"
19
+ className={cn("[&_tr]:border-b [&_tr]:border-border-secondary", className)}
20
+ {...props}
21
+ />
22
+ );
23
+ }
24
+
25
+ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
26
+ return <tbody data-slot="table-body" className={cn("[&_tr:last-child]:border-0", className)} {...props} />;
27
+ }
28
+
29
+ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
30
+ return (
31
+ <tfoot
32
+ data-slot="table-footer"
33
+ className={cn("border-t border-border-secondary type-text-sm font-medium [&>tr]:last:border-b-0", className)}
34
+ {...props}
35
+ />
36
+ );
37
+ }
38
+
39
+ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
40
+ return (
41
+ <tr
42
+ data-slot="table-row"
43
+ className={cn(
44
+ "border-b border-border-secondary transition-colors",
45
+ "data-[state=selected]:bg-muted",
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
+ className,
49
+ )}
50
+ {...props}
51
+ />
52
+ );
53
+ }
54
+
55
+ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
56
+ return (
57
+ <th
58
+ data-slot="table-head"
59
+ className={cn(
60
+ "h-11 px-6 py-3 bg-muted text-left align-middle type-text-xs font-semibold text-placeholder dark:text-subtle-foreground whitespace-nowrap",
61
+ "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:pr-3",
62
+ className,
63
+ )}
64
+ {...props}
65
+ />
66
+ );
67
+ }
68
+
69
+ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
70
+ return (
71
+ <td
72
+ data-slot="table-cell"
73
+ className={cn("px-6 py-2 align-middle", "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:pr-3", className)}
74
+ {...props}
75
+ />
76
+ );
77
+ }
78
+
79
+ function TableCaption({ className, ...props }: React.ComponentProps<"caption">) {
80
+ return (
81
+ <caption
82
+ data-slot="table-caption"
83
+ className={cn("mt-4 type-text-sm text-muted-foreground", className)}
84
+ {...props}
85
+ />
86
+ );
87
+ }
88
+
89
+ export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };
@@ -0,0 +1,226 @@
1
+ "use client";
2
+
3
+ /*
4
+ * Two tab families, one visual system:
5
+ *
6
+ * - Tabs / TabsList / TabsTrigger / TabsContent — state-managed.
7
+ * Use when tab content lives in the same page (e.g. "Code" vs "Preview").
8
+ *
9
+ * - TabsNav / TabsNavList / TabsNavTrigger — semantic <nav>/<ul>/<a>, URL-based.
10
+ * Use when each tab is a route (e.g. settings pages). Pair with <Link> via asChild.
11
+ *
12
+ * Both share `tabsTriggerVariants` for visual styles (colors, spacing, active state).
13
+ * Layout concerns (flex-1, orientation group selectors) stay component-specific.
14
+ */
15
+
16
+ import { Slot } from "@radix-ui/react-slot";
17
+ import * as TabsPrimitive from "@radix-ui/react-tabs";
18
+ import { cva, type VariantProps } from "class-variance-authority";
19
+ import type * as React from "react";
20
+ import { createContext, use } from "react";
21
+
22
+ import { cn } from "../lib/cn";
23
+
24
+ /* ─── Shared trigger styles ──────────────────────────────────────────────── */
25
+
26
+ const tabsTriggerVariants = cva(
27
+ [
28
+ "type-text-sm items-center gap-1.5 font-medium",
29
+ "disabled:pointer-events-none disabled:opacity-50",
30
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
31
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
32
+ ],
33
+ {
34
+ variants: {
35
+ variant: {
36
+ default: [
37
+ "rounded-md border border-transparent px-2 py-1",
38
+ "data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
39
+ ],
40
+ line: [
41
+ "h-8 pb-3 px-1",
42
+ "text-quaternary-foreground",
43
+ "[@media(hover:hover)]:hover:text-muted-foreground-hover",
44
+ "data-[state=active]:border-b-2 data-[state=active]:border-foreground data-[state=active]:text-foreground",
45
+ ],
46
+ pill: [
47
+ "rounded-lg px-4 py-2",
48
+ "text-muted-foreground",
49
+ "[@media(hover:hover)]:hover:bg-accent [@media(hover:hover)]:hover:text-accent-foreground",
50
+ "data-[state=active]:bg-accent data-[state=active]:text-accent-foreground",
51
+ ],
52
+ },
53
+ },
54
+ defaultVariants: {
55
+ variant: "default",
56
+ },
57
+ },
58
+ );
59
+
60
+ /* ─── Tabs (state-based, in-page content switching) ────────────────── */
61
+
62
+ function Tabs({ className, orientation = "horizontal", ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
63
+ return (
64
+ <TabsPrimitive.Root
65
+ data-slot="tabs"
66
+ data-orientation={orientation}
67
+ orientation={orientation}
68
+ className={cn(
69
+ "group/tabs flex gap-4",
70
+ "data-[orientation=horizontal]:flex-col",
71
+ "data-[orientation=vertical]:flex-row",
72
+ className,
73
+ )}
74
+ {...props}
75
+ />
76
+ );
77
+ }
78
+
79
+ type TabsListVariant = "default" | "line";
80
+
81
+ const TabsListVariantContext = createContext<TabsListVariant>("default");
82
+
83
+ const tabsListVariants = cva(
84
+ "inline-flex w-fit items-center justify-center text-muted-foreground group-data-[orientation=vertical]/tabs:flex-col",
85
+ {
86
+ variants: {
87
+ variant: {
88
+ default:
89
+ "rounded-lg bg-muted p-[3px] group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:bg-transparent group-data-[orientation=vertical]/tabs:p-0 group-data-[orientation=vertical]/tabs:rounded-none",
90
+ line: "gap-3 rounded-none bg-transparent border-b border-subtle group-data-[orientation=horizontal]/tabs:flex-wrap",
91
+ },
92
+ },
93
+ defaultVariants: {
94
+ variant: "default",
95
+ },
96
+ },
97
+ );
98
+
99
+ function TabsList({
100
+ className,
101
+ variant = "default",
102
+ ...props
103
+ }: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>) {
104
+ return (
105
+ <TabsListVariantContext value={variant ?? "default"}>
106
+ <TabsPrimitive.List data-slot="tabs-list" className={cn(tabsListVariants({ variant }), className)} {...props} />
107
+ </TabsListVariantContext>
108
+ );
109
+ }
110
+
111
+ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
112
+ const variant = use(TabsListVariantContext);
113
+
114
+ return (
115
+ <TabsPrimitive.Trigger
116
+ data-slot="tabs-trigger"
117
+ className={cn(
118
+ tabsTriggerVariants({ variant }),
119
+ "relative inline-flex flex-1 justify-center whitespace-nowrap transition-all",
120
+ "group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start",
121
+ variant === "default" && [
122
+ "h-[calc(100%-1px)]",
123
+ "group-data-[orientation=vertical]/tabs:data-[state=active]:bg-muted group-data-[orientation=vertical]/tabs:data-[state=active]:shadow-none dark:group-data-[orientation=vertical]/tabs:data-[state=active]:bg-subtle",
124
+ "group-data-[orientation=vertical]/tabs:p-2",
125
+ ],
126
+ className,
127
+ )}
128
+ {...props}
129
+ />
130
+ );
131
+ }
132
+
133
+ function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
134
+ return <TabsPrimitive.Content data-slot="tabs-content" className={cn("flex-1 outline-none", className)} {...props} />;
135
+ }
136
+
137
+ /* ─── Navigation tabs (semantic <nav>/<ul>/<a>, for URL-based routing) ──── */
138
+
139
+ type TabsNavVariant = "line" | "pill";
140
+ type TabsNavOrientation = "horizontal" | "vertical";
141
+
142
+ type TabsNavContextValue = {
143
+ orientation: TabsNavOrientation;
144
+ variant: TabsNavVariant;
145
+ };
146
+
147
+ const TabsNavContext = createContext<TabsNavContextValue>({ orientation: "horizontal", variant: "line" });
148
+
149
+ function TabsNav({
150
+ orientation = "horizontal",
151
+ variant,
152
+ className,
153
+ ...props
154
+ }: React.ComponentProps<"nav"> & {
155
+ orientation?: TabsNavOrientation;
156
+ variant?: TabsNavVariant;
157
+ }) {
158
+ const resolvedVariant = variant ?? (orientation === "vertical" ? "pill" : "line");
159
+
160
+ return (
161
+ <TabsNavContext value={{ orientation, variant: resolvedVariant }}>
162
+ <nav data-slot="tabs-nav" data-orientation={orientation} className={className} {...props} />
163
+ </TabsNavContext>
164
+ );
165
+ }
166
+
167
+ function TabsNavList({ className, ...props }: React.ComponentProps<"ul">) {
168
+ const { orientation, variant } = use(TabsNavContext);
169
+
170
+ return (
171
+ <ul
172
+ data-slot="tabs-nav-list"
173
+ className={cn(
174
+ orientation === "vertical" ? "flex flex-col gap-1" : "flex flex-wrap",
175
+ variant === "line" && orientation === "horizontal" && "gap-3 border-b border-subtle",
176
+ className,
177
+ )}
178
+ {...props}
179
+ />
180
+ );
181
+ }
182
+
183
+ function TabsNavTrigger({
184
+ className,
185
+ active = false,
186
+ asChild = false,
187
+ children,
188
+ ...props
189
+ }: React.ComponentProps<"a"> & {
190
+ active?: boolean;
191
+ asChild?: boolean;
192
+ }) {
193
+ const Comp = asChild ? Slot : "a";
194
+ const { variant } = use(TabsNavContext);
195
+
196
+ return (
197
+ <li data-slot="tabs-nav-trigger-item">
198
+ <Comp
199
+ data-state={active ? "active" : "inactive"}
200
+ className={cn(
201
+ tabsTriggerVariants({ variant }),
202
+ "flex transition-colors",
203
+ // TODO(mobile): line h-8 (32px) is below 44px touch target minimum — desktop-primary nav per Figma spec
204
+ variant === "line" && "cursor-pointer",
205
+ className,
206
+ )}
207
+ aria-current={active ? "page" : undefined}
208
+ {...props}
209
+ >
210
+ {children}
211
+ </Comp>
212
+ </li>
213
+ );
214
+ }
215
+
216
+ export {
217
+ Tabs,
218
+ TabsContent,
219
+ TabsList,
220
+ TabsNav,
221
+ TabsNavList,
222
+ TabsNavTrigger,
223
+ TabsTrigger,
224
+ tabsListVariants,
225
+ tabsTriggerVariants,
226
+ };
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ import { cn } from "@alpic-ai/ui/lib/cn";
4
+
5
+ const tagBase =
6
+ "inline-flex items-center justify-center rounded-sm border border-border bg-background py-0.5 type-text-sm font-medium text-muted-foreground";
7
+
8
+ interface TagBaseProps {
9
+ icon?: React.ReactNode;
10
+ }
11
+
12
+ /* ── Tag (text only) ──────────────────────────────────────────── */
13
+
14
+ interface TagProps extends React.ComponentProps<"span">, TagBaseProps {}
15
+
16
+ function Tag({ className, icon, children, ...props }: TagProps) {
17
+ return (
18
+ <span className={cn(tagBase, icon ? "gap-[5px] pl-1 pr-[9px]" : "px-[9px]", className)} {...props}>
19
+ {icon && <span className="inline-flex shrink-0">{icon}</span>}
20
+ <span>{children}</span>
21
+ </span>
22
+ );
23
+ }
24
+
25
+ /* ── TagDismissible (with × button) ───────────────────────────── */
26
+
27
+ interface TagDismissibleProps extends React.ComponentProps<"span">, TagBaseProps {
28
+ onDismiss?: () => void;
29
+ }
30
+
31
+ function TagDismissible({ className, icon, children, onDismiss, ...props }: TagDismissibleProps) {
32
+ return (
33
+ <span className={cn(tagBase, "gap-[3px]", icon ? "px-1" : "pl-[9px] pr-1", className)} {...props}>
34
+ <span className={cn("inline-flex items-center", icon && "gap-[5px]")}>
35
+ {icon && <span className="inline-flex shrink-0">{icon}</span>}
36
+ <span>{children}</span>
37
+ </span>
38
+ <button
39
+ type="button"
40
+ aria-label="Remove"
41
+ onClick={onDismiss}
42
+ className="inline-flex items-center justify-center rounded-[3px] p-0.5 hover:bg-accent"
43
+ >
44
+ <svg
45
+ xmlns="http://www.w3.org/2000/svg"
46
+ width="12"
47
+ height="12"
48
+ viewBox="0 0 24 24"
49
+ fill="none"
50
+ stroke="currentColor"
51
+ strokeWidth="2"
52
+ strokeLinecap="round"
53
+ strokeLinejoin="round"
54
+ aria-hidden="true"
55
+ >
56
+ <path d="M18 6 6 18" />
57
+ <path d="m6 6 12 12" />
58
+ </svg>
59
+ </button>
60
+ </span>
61
+ );
62
+ }
63
+
64
+ /* ── TagCount (with count badge) ──────────────────────────────── */
65
+
66
+ interface TagCountProps extends React.ComponentProps<"span">, TagBaseProps {
67
+ count: number;
68
+ }
69
+
70
+ function TagCount({ className, icon, children, count, ...props }: TagCountProps) {
71
+ return (
72
+ <span className={cn(tagBase, "gap-[5px]", icon ? "pl-1 pr-[3px]" : "pl-[9px] pr-[3px]", className)} {...props}>
73
+ {icon && <span className="inline-flex shrink-0">{icon}</span>}
74
+ <span>{children}</span>
75
+ <span className="inline-flex min-w-[18px] h-[18px] items-center justify-center rounded-[3px] bg-subtle px-[5px] type-text-xs font-medium text-muted-foreground">
76
+ {count}
77
+ </span>
78
+ </span>
79
+ );
80
+ }
81
+
82
+ export { Tag, TagCount, TagDismissible };
@@ -0,0 +1,70 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import { cn } from "../lib/cn";
6
+ import { Label } from "./label";
7
+
8
+ interface TextareaProps extends React.ComponentProps<"textarea"> {
9
+ label?: string;
10
+ required?: boolean;
11
+ hint?: string;
12
+ error?: string;
13
+ }
14
+
15
+ function Textarea({ className, id, label, required, hint, error, ...props }: TextareaProps) {
16
+ const generatedId = React.useId();
17
+ const fieldId = id ?? generatedId;
18
+
19
+ const textarea = (
20
+ <textarea
21
+ id={fieldId}
22
+ data-slot="textarea"
23
+ className={cn(
24
+ "block w-full min-h-[120px] resize-none",
25
+ "px-3.5 py-3",
26
+ "type-text-md text-foreground placeholder:text-placeholder",
27
+ "bg-background border border-border rounded-md",
28
+ "transition-colors",
29
+ "outline-none focus-visible:border-ring focus-visible:border-2",
30
+ "disabled:bg-disabled disabled:text-disabled-foreground disabled:cursor-not-allowed",
31
+ "aria-invalid:border-border-error [@media(hover:hover)]:aria-invalid:hover:border-border-error",
32
+ error && "border-border-error [@media(hover:hover)]:hover:border-border-error",
33
+ className,
34
+ )}
35
+ required={required}
36
+ aria-invalid={error ? true : undefined}
37
+ aria-describedby={fieldId && (hint || error) ? `${fieldId}-description` : undefined}
38
+ {...props}
39
+ />
40
+ );
41
+
42
+ return (
43
+ <div className="flex flex-col gap-1.5">
44
+ {label && (
45
+ <div className="flex items-center gap-0.5">
46
+ <Label htmlFor={fieldId} className="type-text-sm font-medium text-muted-foreground">
47
+ {label}
48
+ </Label>
49
+ {required && (
50
+ <span aria-hidden className="type-text-sm font-medium text-required">
51
+ *
52
+ </span>
53
+ )}
54
+ </div>
55
+ )}
56
+ {textarea}
57
+ {(hint || error) && (
58
+ <p
59
+ id={fieldId ? `${fieldId}-description` : undefined}
60
+ className={cn("type-text-sm", error ? "text-destructive" : "text-subtle-foreground")}
61
+ >
62
+ {error ?? hint}
63
+ </p>
64
+ )}
65
+ </div>
66
+ );
67
+ }
68
+
69
+ export type { TextareaProps };
70
+ export { Textarea };