@alpic-ai/ui 0.0.0-staging.d7342d0 → 0.0.0-staging.da92e17

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.
@@ -6,7 +6,7 @@ import * as _$class_variance_authority_types0 from "class-variance-authority/typ
6
6
 
7
7
  //#region src/components/avatar.d.ts
8
8
  declare const avatarVariants: (props?: ({
9
- size?: "sm" | "lg" | "xs" | "md" | "xl" | null | undefined;
9
+ size?: "xs" | "sm" | "md" | "lg" | "xl" | null | undefined;
10
10
  } & _$class_variance_authority_types0.ClassProp) | undefined) => string;
11
11
  type AvatarSize = NonNullable<VariantProps<typeof avatarVariants>["size"]>;
12
12
  type AvatarStatus = "online";
@@ -4,7 +4,7 @@ import * as _$class_variance_authority_types0 from "class-variance-authority/typ
4
4
 
5
5
  //#region src/components/badge.d.ts
6
6
  declare const badgeVariants: (props?: ({
7
- variant?: "warning" | "success" | "primary" | "secondary" | "error" | null | undefined;
7
+ variant?: "warning" | "success" | "secondary" | "primary" | "error" | null | undefined;
8
8
  size?: "sm" | "md" | null | undefined;
9
9
  } & _$class_variance_authority_types0.ClassProp) | undefined) => string;
10
10
  interface BadgeProps extends React.ComponentProps<"span">, VariantProps<typeof badgeVariants> {}
@@ -5,13 +5,14 @@ import * as _$class_variance_authority_types0 from "class-variance-authority/typ
5
5
 
6
6
  //#region src/components/button.d.ts
7
7
  declare const buttonVariants: (props?: ({
8
- variant?: "destructive" | "link" | "primary" | "secondary" | "tertiary" | "link-muted" | null | undefined;
8
+ variant?: "destructive" | "secondary" | "primary" | "tertiary" | "link" | "link-muted" | "cta" | null | undefined;
9
9
  size?: "default" | "icon" | "icon-rounded" | "pill" | null | undefined;
10
10
  } & _$class_variance_authority_types0.ClassProp) | undefined) => string;
11
11
  interface ButtonProps extends React.ComponentProps<"button">, VariantProps<typeof buttonVariants> {
12
12
  asChild?: boolean;
13
13
  loading?: boolean;
14
14
  icon?: React.ReactNode;
15
+ iconTrailing?: React.ReactNode;
15
16
  }
16
17
  declare function Button({
17
18
  className,
@@ -21,6 +22,7 @@ declare function Button({
21
22
  asChild,
22
23
  loading,
23
24
  icon,
25
+ iconTrailing,
24
26
  disabled,
25
27
  children,
26
28
  ...props
@@ -35,7 +35,14 @@ const buttonVariants = cva([
35
35
  "[@media(hover:hover)]:hover:underline",
36
36
  "focus-visible:bg-background"
37
37
  ].join(" "),
38
- destructive: ["bg-destructive text-destructive-foreground", "[@media(hover:hover)]:hover:bg-destructive-hover"].join(" ")
38
+ destructive: ["bg-destructive text-destructive-foreground", "[@media(hover:hover)]:hover:bg-destructive-hover"].join(" "),
39
+ cta: [
40
+ "button-cta",
41
+ "h-9 px-4 gap-2 rounded-md",
42
+ "dark:bg-inverted text-foreground",
43
+ "transition-[transform,filter] duration-300 ease-out",
44
+ "active:scale-[0.99]"
45
+ ].join(" ")
39
46
  },
40
47
  size: {
41
48
  default: "type-text-sm",
@@ -49,7 +56,7 @@ const buttonVariants = cva([
49
56
  size: "default"
50
57
  }
51
58
  });
52
- function Button({ className, variant, size, type = "button", asChild = false, loading = false, icon, disabled, children, ...props }) {
59
+ function Button({ className, variant, size, type = "button", asChild = false, loading = false, icon, iconTrailing, disabled, children, ...props }) {
53
60
  return /* @__PURE__ */ jsxs(asChild ? Slot : "button", {
54
61
  "data-slot": "button",
55
62
  className: cn(buttonVariants({
@@ -60,7 +67,14 @@ function Button({ className, variant, size, type = "button", asChild = false, lo
60
67
  disabled: disabled || loading,
61
68
  "aria-busy": loading || void 0,
62
69
  ...props,
63
- children: [loading ? /* @__PURE__ */ jsx(Loader2, { className: "motion-safe:animate-spin" }) : icon, asChild ? /* @__PURE__ */ jsx(Slottable, { children }) : children]
70
+ children: [
71
+ loading ? /* @__PURE__ */ jsx(Loader2, { className: "motion-safe:animate-spin" }) : icon,
72
+ asChild ? /* @__PURE__ */ jsx(Slottable, { children }) : children,
73
+ !loading && iconTrailing ? /* @__PURE__ */ jsx("span", {
74
+ "data-cta-icon-trailing": true,
75
+ children: iconTrailing
76
+ }) : null
77
+ ]
64
78
  });
65
79
  }
66
80
  //#endregion
@@ -0,0 +1,11 @@
1
+ import * as _$react_jsx_runtime0 from "react/jsx-runtime";
2
+
3
+ //#region src/components/page-loader.d.ts
4
+ interface PageLoaderProps {
5
+ className?: string;
6
+ }
7
+ declare function PageLoader({
8
+ className
9
+ }: PageLoaderProps): _$react_jsx_runtime0.JSX.Element;
10
+ //#endregion
11
+ export { PageLoader, type PageLoaderProps };
@@ -0,0 +1,122 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.mjs";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/components/page-loader.tsx
5
+ const CABLE_CAR_SVG = /* @__PURE__ */ jsxs("svg", {
6
+ xmlns: "http://www.w3.org/2000/svg",
7
+ viewBox: "0 0 120 130",
8
+ width: "240",
9
+ height: "260",
10
+ "aria-hidden": "true",
11
+ className: "block h-auto w-full",
12
+ children: [
13
+ /* @__PURE__ */ jsx("line", {
14
+ x1: "60",
15
+ y1: "3",
16
+ x2: "60",
17
+ y2: "58",
18
+ stroke: "#333",
19
+ strokeWidth: "4"
20
+ }),
21
+ /* @__PURE__ */ jsx("circle", {
22
+ cx: "60",
23
+ cy: "11",
24
+ r: "10",
25
+ fill: "#555"
26
+ }),
27
+ /* @__PURE__ */ jsx("rect", {
28
+ x: "5",
29
+ y: "58",
30
+ width: "110",
31
+ height: "64",
32
+ rx: "4",
33
+ fill: "#e90060"
34
+ }),
35
+ /* @__PURE__ */ jsx("rect", {
36
+ x: "5",
37
+ y: "58",
38
+ width: "110",
39
+ height: "20",
40
+ rx: "4",
41
+ fill: "#F5F0E8"
42
+ }),
43
+ /* @__PURE__ */ jsx("rect", {
44
+ x: "5",
45
+ y: "68",
46
+ width: "110",
47
+ height: "10",
48
+ fill: "#F5F0E8"
49
+ }),
50
+ /* @__PURE__ */ jsx("rect", {
51
+ x: "14",
52
+ y: "66",
53
+ width: "26",
54
+ height: "30",
55
+ rx: "2",
56
+ fill: "#5B8EC9",
57
+ stroke: "#C4B9A8",
58
+ strokeWidth: "1.5"
59
+ }),
60
+ /* @__PURE__ */ jsx("rect", {
61
+ x: "47",
62
+ y: "66",
63
+ width: "26",
64
+ height: "30",
65
+ rx: "2",
66
+ fill: "#5B8EC9",
67
+ stroke: "#C4B9A8",
68
+ strokeWidth: "1.5"
69
+ }),
70
+ /* @__PURE__ */ jsx("rect", {
71
+ x: "80",
72
+ y: "66",
73
+ width: "26",
74
+ height: "30",
75
+ rx: "2",
76
+ fill: "#5B8EC9",
77
+ stroke: "#C4B9A8",
78
+ strokeWidth: "1.5"
79
+ }),
80
+ /* @__PURE__ */ jsx("rect", {
81
+ x: "5",
82
+ y: "115",
83
+ width: "110",
84
+ height: "7",
85
+ rx: "3",
86
+ fill: "#9f0042"
87
+ })
88
+ ]
89
+ });
90
+ function PageLoader({ className }) {
91
+ return /* @__PURE__ */ jsx("div", {
92
+ className: cn("flex min-h-screen items-center justify-center bg-background", className),
93
+ role: "status",
94
+ "aria-label": "Loading Alpic…",
95
+ children: /* @__PURE__ */ jsxs("div", {
96
+ className: "relative h-[120px] w-[200px]",
97
+ children: [
98
+ /* @__PURE__ */ jsx("div", {
99
+ className: "absolute top-[30px] left-0 h-[3px] w-full rounded-sm bg-[#6c6c77]",
100
+ style: {
101
+ transform: "rotate(-15deg)",
102
+ transformOrigin: "left center"
103
+ }
104
+ }),
105
+ /* @__PURE__ */ jsx("div", {
106
+ className: "absolute top-[33px] -left-[45px] w-[45px] motion-safe:animate-[alpic-ride_4s_linear_infinite]",
107
+ children: CABLE_CAR_SVG
108
+ }),
109
+ /* @__PURE__ */ jsx("div", {
110
+ className: "pointer-events-none absolute -top-[40px] -left-[45px] z-10 h-[200px] w-[95px]",
111
+ style: { background: "linear-gradient(to right, var(--color-background) 47%, transparent)" }
112
+ }),
113
+ /* @__PURE__ */ jsx("div", {
114
+ className: "pointer-events-none absolute -top-[40px] left-[142px] z-10 h-[200px] w-[95px]",
115
+ style: { background: "linear-gradient(to right, transparent, var(--color-background) 53%)" }
116
+ })
117
+ ]
118
+ })
119
+ });
120
+ }
121
+ //#endregion
122
+ export { PageLoader };
@@ -108,7 +108,7 @@ declare function SidebarMenuItem({
108
108
  }: React.ComponentProps<"li">): _$react_jsx_runtime0.JSX.Element;
109
109
  declare const sidebarMenuButtonVariants: (props?: ({
110
110
  variant?: "default" | "outline" | null | undefined;
111
- size?: "sm" | "lg" | "default" | null | undefined;
111
+ size?: "default" | "sm" | "lg" | null | undefined;
112
112
  } & _$class_variance_authority_types0.ClassProp) | undefined) => string;
113
113
  declare function SidebarMenuButton({
114
114
  asChild,
@@ -105,7 +105,7 @@ function Sidebar({ side = "left", variant = "sidebar", collapsible = "offcanvas"
105
105
  }
106
106
  if (collapsible === "none") return /* @__PURE__ */ jsx("div", {
107
107
  "data-slot": "sidebar",
108
- className: cn("bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col", className),
108
+ className: cn("bg-sidebar-surface text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col", className),
109
109
  ...props,
110
110
  children
111
111
  });
@@ -117,7 +117,7 @@ function Sidebar({ side = "left", variant = "sidebar", collapsible = "offcanvas"
117
117
  "data-sidebar": "sidebar",
118
118
  "data-slot": "sidebar",
119
119
  "data-mobile": "true",
120
- className: "bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden",
120
+ className: "bg-sidebar-surface text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden",
121
121
  style: { "--sidebar-width": SIDEBAR_WIDTH_MOBILE },
122
122
  side,
123
123
  children: [/* @__PURE__ */ jsxs(SheetHeader, {
@@ -141,12 +141,12 @@ function Sidebar({ side = "left", variant = "sidebar", collapsible = "offcanvas"
141
141
  className: cn("relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-out", "group-data-[collapsible=offcanvas]:w-0", "group-data-[side=right]:rotate-180", variant === "floating" || variant === "inset" ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]" : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)")
142
142
  }), /* @__PURE__ */ jsx("div", {
143
143
  "data-slot": "sidebar-container",
144
- className: cn("fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-out md:flex", side === "left" ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", variant === "floating" || variant === "inset" ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l", className),
144
+ className: cn("fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-out md:flex", side === "left" ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", variant === "floating" || variant === "inset" ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" : "border-sidebar-border group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l", className),
145
145
  ...props,
146
146
  children: /* @__PURE__ */ jsx("div", {
147
147
  "data-sidebar": "sidebar",
148
148
  "data-slot": "sidebar-inner",
149
- className: "bg-sidebar 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",
149
+ 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",
150
150
  onClickCapture: handleSurfaceClickCapture,
151
151
  children
152
152
  })
@@ -4,8 +4,8 @@ import * as _$class_variance_authority_types0 from "class-variance-authority/typ
4
4
 
5
5
  //#region src/components/spinner.d.ts
6
6
  declare const spinnerVariants: (props?: ({
7
- variant?: "primary" | "secondary" | null | undefined;
8
- size?: "sm" | "lg" | "md" | "xl" | null | undefined;
7
+ variant?: "secondary" | "primary" | null | undefined;
8
+ size?: "sm" | "md" | "lg" | "xl" | null | undefined;
9
9
  } & _$class_variance_authority_types0.ClassProp) | undefined) => string;
10
10
  interface SpinnerProps extends Omit<React.ComponentProps<"svg">, "children">, VariantProps<typeof spinnerVariants> {}
11
11
  declare function Spinner({
@@ -26,10 +26,19 @@ declare function TableHead({
26
26
  className,
27
27
  ...props
28
28
  }: React.ComponentProps<"th">): _$react_jsx_runtime0.JSX.Element;
29
+ interface TableCellProps extends React.ComponentProps<"td"> {
30
+ /**
31
+ * When true, the cell renders edge-to-edge so the child can act as the
32
+ * interactive surface (e.g. a button or popover trigger filling the cell).
33
+ * Defaults to false (standard padded cell).
34
+ */
35
+ interactive?: boolean;
36
+ }
29
37
  declare function TableCell({
30
38
  className,
39
+ interactive,
31
40
  ...props
32
- }: React.ComponentProps<"td">): _$react_jsx_runtime0.JSX.Element;
41
+ }: TableCellProps): _$react_jsx_runtime0.JSX.Element;
33
42
  declare function TableCaption({
34
43
  className,
35
44
  ...props
@@ -47,10 +47,10 @@ function TableHead({ className, ...props }) {
47
47
  ...props
48
48
  });
49
49
  }
50
- function TableCell({ className, ...props }) {
50
+ function TableCell({ className, interactive = false, ...props }) {
51
51
  return /* @__PURE__ */ jsx("td", {
52
52
  "data-slot": "table-cell",
53
- className: cn("px-6 py-2 align-middle", "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:pr-3", className),
53
+ className: cn("align-middle", interactive ? "h-px p-0" : "px-6 py-2", "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:pr-3", className),
54
54
  ...props
55
55
  });
56
56
  }
@@ -6,7 +6,7 @@ import * as _$class_variance_authority_types0 from "class-variance-authority/typ
6
6
 
7
7
  //#region src/components/tabs.d.ts
8
8
  declare const tabsTriggerVariants: (props?: ({
9
- variant?: "default" | "line" | "pill" | null | undefined;
9
+ variant?: "default" | "pill" | "line" | null | undefined;
10
10
  } & _$class_variance_authority_types0.ClassProp) | undefined) => string;
11
11
  declare function Tabs({
12
12
  className,
@@ -7,7 +7,7 @@ import * as _$class_variance_authority_types0 from "class-variance-authority/typ
7
7
  //#region src/components/toggle-group.d.ts
8
8
  declare const toggleGroupItemVariants: (props?: ({
9
9
  variant?: "default" | "outline" | null | undefined;
10
- size?: "sm" | "default" | null | undefined;
10
+ size?: "default" | "sm" | null | undefined;
11
11
  } & _$class_variance_authority_types0.ClassProp) | undefined) => string;
12
12
  type ToggleGroupContextValue = VariantProps<typeof toggleGroupItemVariants>;
13
13
  declare function ToggleGroup({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alpic-ai/ui",
3
- "version": "0.0.0-staging.d7342d0",
3
+ "version": "0.0.0-staging.da92e17",
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.8.0",
26
+ "lucide-react": "^1.11.0",
27
27
  "react": "^19.2.5",
28
28
  "react-dom": "^19.2.5",
29
- "react-hook-form": "^7.72.1",
29
+ "react-hook-form": "^7.74.0",
30
30
  "sonner": "^2.0.7",
31
- "tailwindcss": "^4.2.2",
31
+ "tailwindcss": "^4.2.4",
32
32
  "tw-animate-css": "^1.4.0"
33
33
  },
34
34
  "dependencies": {
@@ -56,15 +56,15 @@
56
56
  },
57
57
  "devDependencies": {
58
58
  "@ladle/react": "^5.1.1",
59
- "@tailwindcss/postcss": "^4.2.2",
59
+ "@tailwindcss/postcss": "^4.2.4",
60
60
  "@types/react": "19.2.14",
61
61
  "@types/react-dom": "19.2.3",
62
- "lucide-react": "^1.8.0",
63
- "react-hook-form": "^7.72.1",
62
+ "lucide-react": "^1.11.0",
63
+ "react-hook-form": "^7.74.0",
64
64
  "shx": "^0.4.0",
65
65
  "sonner": "^2.0.7",
66
- "tailwindcss": "^4.2.2",
67
- "tsdown": "^0.21.9",
66
+ "tailwindcss": "^4.2.4",
67
+ "tsdown": "^0.21.10",
68
68
  "tw-animate-css": "^1.4.0",
69
69
  "typescript": "^6.0.3"
70
70
  },
@@ -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
@@ -92,6 +101,7 @@ function Button({
92
101
  >
93
102
  {loading ? <Loader2 className="motion-safe:animate-spin" /> : icon}
94
103
  {asChild ? <Slottable>{children}</Slottable> : children}
104
+ {!loading && iconTrailing ? <span data-cta-icon-trailing>{iconTrailing}</span> : null}
95
105
  </Comp>
96
106
  );
97
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],
@@ -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 };
@@ -179,7 +179,7 @@ function Sidebar({
179
179
  return (
180
180
  <div
181
181
  data-slot="sidebar"
182
- 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)}
183
183
  {...props}
184
184
  >
185
185
  {children}
@@ -194,7 +194,7 @@ function Sidebar({
194
194
  data-sidebar="sidebar"
195
195
  data-slot="sidebar"
196
196
  data-mobile="true"
197
- 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"
198
198
  style={
199
199
  {
200
200
  "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
@@ -243,7 +243,7 @@ function Sidebar({
243
243
  // Adjust the padding for floating and inset variants.
244
244
  variant === "floating" || variant === "inset"
245
245
  ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
246
- : "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",
247
247
  className,
248
248
  )}
249
249
  {...props}
@@ -251,7 +251,7 @@ function Sidebar({
251
251
  <div
252
252
  data-sidebar="sidebar"
253
253
  data-slot="sidebar-inner"
254
- className="bg-sidebar 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"
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
255
  onClickCapture={handleSurfaceClickCapture}
256
256
  >
257
257
  {children}
@@ -66,11 +66,25 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
66
66
  );
67
67
  }
68
68
 
69
- function TableCell({ className, ...props }: React.ComponentProps<"td">) {
69
+ interface TableCellProps extends React.ComponentProps<"td"> {
70
+ /**
71
+ * When true, the cell renders edge-to-edge so the child can act as the
72
+ * interactive surface (e.g. a button or popover trigger filling the cell).
73
+ * Defaults to false (standard padded cell).
74
+ */
75
+ interactive?: boolean;
76
+ }
77
+
78
+ function TableCell({ className, interactive = false, ...props }: TableCellProps) {
70
79
  return (
71
80
  <td
72
81
  data-slot="table-cell"
73
- className={cn("px-6 py-2 align-middle", "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:pr-3", className)}
82
+ className={cn(
83
+ "align-middle",
84
+ interactive ? "h-px p-0" : "px-6 py-2",
85
+ "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:pr-3",
86
+ className,
87
+ )}
74
88
  {...props}
75
89
  />
76
90
  );
@@ -8,7 +8,9 @@ export function useCopyToClipboard({ resetDelay = 2000 }: { resetDelay?: number
8
8
 
9
9
  useEffect(() => {
10
10
  return () => {
11
- if (timeoutRef.current) clearTimeout(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) clearTimeout(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
 
@@ -190,8 +190,8 @@ export const AllVariants: Story = () => (
190
190
  </TableRow>
191
191
  </TableHeader>
192
192
  <TableBody>
193
- {USERS.slice(0, 3).map((user, i) => (
194
- <TableRow key={user.id} data-state={i === 1 ? "selected" : undefined}>
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>
@@ -39,6 +39,9 @@
39
39
 
40
40
  --color-ring: #f22b79; /* Figma: focus-ring */
41
41
 
42
+ /* cta — decorative gradient accent, used with --color-primary in the CTA button ring */
43
+ --color-cta-accent: #6eece7; /* Figma: CTA border accent (cyan) */
44
+
42
45
  /* sidebar */
43
46
  --color-sidebar: #f8fafa; /* Figma: bg-secondary-subtle */
44
47
  --color-sidebar-foreground: #3a4848; /* Figma: fg-secondary */
@@ -110,6 +113,15 @@
110
113
  --animate-beacon-ring-pulse: beacon-ring-pulse 2.2s ease-in-out infinite;
111
114
  }
112
115
 
116
+ @keyframes alpic-ride {
117
+ 0% {
118
+ transform: translate(0px, 0px);
119
+ }
120
+ 100% {
121
+ transform: translate(237px, -64px);
122
+ }
123
+ }
124
+
113
125
  @keyframes accordion-down {
114
126
  from {
115
127
  height: 0;
@@ -154,6 +166,160 @@
154
166
  }
155
167
  }
156
168
 
169
+ /* ─── CTA button — animated conic gradient ring ───────────────────────────── */
170
+
171
+ @property --cta-angle {
172
+ syntax: "<angle>";
173
+ inherits: false;
174
+ initial-value: 135deg;
175
+ }
176
+
177
+ @keyframes cta-rotate {
178
+ to {
179
+ --cta-angle: 495deg;
180
+ }
181
+ }
182
+
183
+ .button-cta {
184
+ position: relative;
185
+ isolation: isolate;
186
+ /* light mode: whisper-of-gradient surface tint + soft rose drop-shadow */
187
+ background-image: linear-gradient(
188
+ 135deg,
189
+ color-mix(in oklab, var(--color-primary) 5%, transparent) 0%,
190
+ color-mix(in oklab, var(--color-cta-accent) 5%, transparent) 100%
191
+ );
192
+ box-shadow:
193
+ 0 6px 24px -10px color-mix(in oklab, var(--color-primary) 38%, transparent),
194
+ 0 2px 6px -4px color-mix(in oklab, var(--color-cta-accent) 30%, transparent);
195
+ transition:
196
+ box-shadow 400ms ease,
197
+ transform 300ms ease,
198
+ filter 300ms ease;
199
+ }
200
+
201
+ @media (hover: hover) {
202
+ .button-cta:hover {
203
+ box-shadow:
204
+ 0 10px 30px -8px color-mix(in oklab, var(--color-primary) 52%, transparent),
205
+ 0 3px 10px -3px color-mix(in oklab, var(--color-cta-accent) 40%, transparent);
206
+ }
207
+ }
208
+
209
+ /* dark mode: solid inverted surface, no tint or shadow — let the halo do the work */
210
+ .dark .button-cta {
211
+ background-image: none;
212
+ box-shadow: none;
213
+ }
214
+
215
+ .button-cta::before,
216
+ .button-cta::after {
217
+ content: "";
218
+ position: absolute;
219
+ inset: 0;
220
+ border-radius: inherit;
221
+ pointer-events: none;
222
+ /* Always "running" in browser terms, but paused at rest — freezes at current
223
+ angle on unhover instead of snapping back. */
224
+ animation: cta-rotate 3.2s linear infinite;
225
+ animation-play-state: paused;
226
+ }
227
+
228
+ /* Gradient ring (masked so only the border shows) */
229
+ .button-cta::before {
230
+ padding: 1.5px;
231
+ background: conic-gradient(
232
+ from var(--cta-angle),
233
+ var(--color-cta-accent) 0deg,
234
+ var(--color-primary) 150deg,
235
+ var(--color-cta-accent) 300deg,
236
+ var(--color-cta-accent) 360deg
237
+ );
238
+ -webkit-mask:
239
+ linear-gradient(#000 0 0) content-box,
240
+ linear-gradient(#000 0 0);
241
+ -webkit-mask-composite: xor;
242
+ mask-composite: exclude;
243
+ transition: filter 400ms ease;
244
+ }
245
+
246
+ /* Blurred glow halo behind the button — subtle in light, bolder in dark */
247
+ .button-cta::after {
248
+ z-index: -1;
249
+ background: conic-gradient(
250
+ from var(--cta-angle),
251
+ var(--color-cta-accent) 0deg,
252
+ var(--color-primary) 150deg,
253
+ var(--color-cta-accent) 300deg,
254
+ var(--color-cta-accent) 360deg
255
+ );
256
+ filter: blur(12px);
257
+ opacity: 0.05;
258
+ transition: opacity 400ms ease;
259
+ }
260
+
261
+ .dark .button-cta::after {
262
+ opacity: 0.14;
263
+ }
264
+
265
+ @media (hover: hover) {
266
+ .button-cta:hover::before,
267
+ .button-cta:hover::after {
268
+ animation-play-state: running;
269
+ }
270
+ .button-cta:hover::before {
271
+ filter: saturate(1.15) brightness(1.05);
272
+ }
273
+ .button-cta:hover::after {
274
+ opacity: 0.18;
275
+ }
276
+ .dark .button-cta:hover::after {
277
+ opacity: 0.32;
278
+ }
279
+ }
280
+
281
+ .button-cta:focus-visible::before,
282
+ .button-cta:focus-visible::after {
283
+ animation-play-state: running;
284
+ }
285
+
286
+ .button-cta:disabled::before,
287
+ .button-cta:disabled::after,
288
+ [aria-busy="true"].button-cta::before,
289
+ [aria-busy="true"].button-cta::after {
290
+ animation-play-state: paused;
291
+ }
292
+ .button-cta:disabled::after {
293
+ opacity: 0;
294
+ }
295
+
296
+ @media (prefers-reduced-motion: reduce) {
297
+ .button-cta::before,
298
+ .button-cta::after {
299
+ animation: none;
300
+ }
301
+ }
302
+
303
+ /* Icon slide on hover (applied to [data-cta-icon-trailing]) */
304
+ .button-cta [data-cta-icon-trailing] {
305
+ transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
306
+ }
307
+
308
+ @media (hover: hover) {
309
+ .button-cta:hover [data-cta-icon-trailing] {
310
+ transform: translateX(2px);
311
+ }
312
+ }
313
+
314
+ @media (prefers-reduced-motion: reduce) {
315
+ .button-cta [data-cta-icon-trailing] {
316
+ transition: none;
317
+ }
318
+ .button-cta:hover [data-cta-icon-trailing] {
319
+ transform: none;
320
+ }
321
+ }
322
+
157
323
  /* ─── Dark mode ───────────────────────────────────────────────────────────── */
158
324
 
159
325
  .dark {
@@ -191,6 +357,9 @@
191
357
 
192
358
  --color-ring: #f22b79; /* Figma: focus-ring */
193
359
 
360
+ /* cta — decorative gradient accent, used with --color-primary in the CTA button ring */
361
+ --color-cta-accent: #6eece7; /* Figma: CTA border accent (cyan) */
362
+
194
363
  /* sidebar */
195
364
  --color-sidebar: #0c1c1c; /* Figma: bg-secondary */
196
365
  --color-sidebar-foreground: #90a4a4; /* Figma: fg-secondary */
@@ -249,6 +418,14 @@
249
418
 
250
419
  @custom-variant dark (&:where(.dark, .dark *));
251
420
 
421
+ :root {
422
+ --gradient-sidebar: linear-gradient(0deg, #c9e2e280 0%, #ffffff 70%); /* Figma: bg-nav-gradiant-light */
423
+ }
424
+
425
+ .dark {
426
+ --gradient-sidebar: linear-gradient(0deg, #213535 0%, #121e1e 70%); /* Figma: bg-nav-gradiant-dark */
427
+ }
428
+
252
429
  @layer base {
253
430
  * {
254
431
  @apply border-border shadow-shadow;
@@ -302,6 +479,11 @@
302
479
 
303
480
  /* ─── Type preset utilities ───────────────────────────────────────────────── */
304
481
 
482
+ @utility bg-sidebar-surface {
483
+ background-color: var(--color-background);
484
+ background-image: var(--gradient-sidebar);
485
+ }
486
+
305
487
  @utility type-display-2xl {
306
488
  font-family: var(--font-display);
307
489
  font-size: var(--font-size-display-2xl);