@alpic-ai/ui 0.0.0-dev.fcb3084 → 0.0.0-dev.fdd73bf

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.
@@ -5,7 +5,7 @@ import * as _$class_variance_authority_types0 from "class-variance-authority/typ
5
5
 
6
6
  //#region src/components/alert.d.ts
7
7
  declare const alertVariants: (props?: ({
8
- variant?: "default" | "destructive" | "success" | "warning" | null | undefined;
8
+ variant?: "default" | "destructive" | "warning" | "success" | null | undefined;
9
9
  } & _$class_variance_authority_types0.ClassProp) | undefined) => string;
10
10
  interface AlertProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof alertVariants> {}
11
11
  declare function Alert({
@@ -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" | "md" | "lg" | "xl" | "xs" | 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?: "primary" | "secondary" | "success" | "warning" | "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" | "primary" | "secondary" | "tertiary" | "link" | "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
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.mjs";
3
3
  import { Loader2 } from "lucide-react";
4
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
5
  import { cva } from "class-variance-authority";
6
- import { Slot } from "@radix-ui/react-slot";
6
+ import { Slot, Slottable } from "@radix-ui/react-slot";
7
7
  //#region src/components/button.tsx
8
8
  const buttonVariants = cva([
9
9
  "inline-flex items-center justify-center gap-1 whitespace-nowrap",
@@ -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,8 +56,8 @@ 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 }) {
53
- return /* @__PURE__ */ jsx(asChild ? Slot : "button", {
59
+ function Button({ className, variant, size, type = "button", asChild = false, loading = false, icon, iconTrailing, disabled, children, ...props }) {
60
+ return /* @__PURE__ */ jsxs(asChild ? Slot : "button", {
54
61
  "data-slot": "button",
55
62
  className: cn(buttonVariants({
56
63
  variant,
@@ -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: asChild ? children : /* @__PURE__ */ jsxs(Fragment, { children: [loading ? /* @__PURE__ */ jsx(Loader2, { className: "motion-safe:animate-spin" }) : icon, 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,13 @@
1
+ import { Button } from "./button.mjs";
2
+ import * as _$react_jsx_runtime0 from "react/jsx-runtime";
3
+ import { ComponentProps } from "react";
4
+
5
+ //#region src/components/github-button.d.ts
6
+ type GitHubButtonProps = Omit<ComponentProps<typeof Button>, "variant" | "icon">;
7
+ declare function GitHubButton({
8
+ className,
9
+ children,
10
+ ...props
11
+ }: GitHubButtonProps): _$react_jsx_runtime0.JSX.Element;
12
+ //#endregion
13
+ export { GitHubButton, type GitHubButtonProps };
@@ -0,0 +1,24 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.mjs";
3
+ import { Button } from "./button.mjs";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region src/components/github-button.tsx
6
+ const GITHUB_ICON_PATH = "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";
7
+ function GitHubIcon() {
8
+ return /* @__PURE__ */ jsx("svg", {
9
+ fill: "currentColor",
10
+ viewBox: "0 0 24 24",
11
+ "aria-hidden": "true",
12
+ children: /* @__PURE__ */ jsx("path", { d: GITHUB_ICON_PATH })
13
+ });
14
+ }
15
+ function GitHubButton({ className, children, ...props }) {
16
+ return /* @__PURE__ */ jsx(Button, {
17
+ ...props,
18
+ icon: /* @__PURE__ */ jsx(GitHubIcon, {}),
19
+ className: cn("bg-foreground text-background [@media(hover:hover)]:hover:bg-foreground/90", className),
20
+ children
21
+ });
22
+ }
23
+ //#endregion
24
+ export { GitHubButton };
@@ -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 };
@@ -0,0 +1,12 @@
1
+ import * as _$react_jsx_runtime0 from "react/jsx-runtime";
2
+
3
+ //#region src/components/shimmer-text.d.ts
4
+ declare function ShimmerText({
5
+ children,
6
+ className
7
+ }: {
8
+ children: string | number;
9
+ className?: string;
10
+ }): _$react_jsx_runtime0.JSX.Element;
11
+ //#endregion
12
+ export { ShimmerText };
@@ -0,0 +1,22 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.mjs";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/components/shimmer-text.tsx
5
+ const shimmerStyle = {
6
+ 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))`,
7
+ backgroundSize: "250% 100%, auto",
8
+ backgroundRepeat: "no-repeat, padding-box",
9
+ WebkitBackgroundClip: "text",
10
+ WebkitTextFillColor: "transparent",
11
+ backgroundClip: "text",
12
+ animation: "shimmer-text 2.5s linear infinite"
13
+ };
14
+ function ShimmerText({ children, className }) {
15
+ return /* @__PURE__ */ jsx("span", {
16
+ className: cn(className),
17
+ style: shimmerStyle,
18
+ children
19
+ });
20
+ }
21
+ //#endregion
22
+ export { ShimmerText };
@@ -17,6 +17,18 @@ const SIDEBAR_WIDTH = "15rem";
17
17
  const SIDEBAR_WIDTH_MOBILE = "16rem";
18
18
  const SIDEBAR_WIDTH_ICON = "3.5rem";
19
19
  const SIDEBAR_KEYBOARD_SHORTCUT = "b";
20
+ const INTERACTIVE_SIDEBAR_ELEMENT_SELECTOR = [
21
+ "a",
22
+ "button",
23
+ "input",
24
+ "select",
25
+ "textarea",
26
+ "[role='button']",
27
+ "[role='link']",
28
+ "[role='menuitem']",
29
+ "[contenteditable='true']",
30
+ "[tabindex]:not([tabindex='-1'])"
31
+ ].join(", ");
20
32
  const SidebarContext = React.createContext(null);
21
33
  function useSidebar() {
22
34
  const context = React.useContext(SidebarContext);
@@ -80,10 +92,20 @@ function SidebarProvider({ defaultOpen = true, open: openProp, onOpenChange: set
80
92
  });
81
93
  }
82
94
  function Sidebar({ side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, ...props }) {
83
- const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
95
+ const { isMobile, state, openMobile, setOpenMobile, open, setOpen } = useSidebar();
96
+ function handleSurfaceClickCapture(event) {
97
+ if (event.target instanceof Element && event.target.closest(INTERACTIVE_SIDEBAR_ELEMENT_SELECTOR)) return;
98
+ if (!open) {
99
+ event.preventDefault();
100
+ event.stopPropagation();
101
+ setOpen(true);
102
+ return;
103
+ }
104
+ setOpen(false);
105
+ }
84
106
  if (collapsible === "none") return /* @__PURE__ */ jsx("div", {
85
107
  "data-slot": "sidebar",
86
- 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),
87
109
  ...props,
88
110
  children
89
111
  });
@@ -95,7 +117,7 @@ function Sidebar({ side = "left", variant = "sidebar", collapsible = "offcanvas"
95
117
  "data-sidebar": "sidebar",
96
118
  "data-slot": "sidebar",
97
119
  "data-mobile": "true",
98
- 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",
99
121
  style: { "--sidebar-width": SIDEBAR_WIDTH_MOBILE },
100
122
  side,
101
123
  children: [/* @__PURE__ */ jsxs(SheetHeader, {
@@ -119,12 +141,13 @@ function Sidebar({ side = "left", variant = "sidebar", collapsible = "offcanvas"
119
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)")
120
142
  }), /* @__PURE__ */ jsx("div", {
121
143
  "data-slot": "sidebar-container",
122
- 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),
123
145
  ...props,
124
146
  children: /* @__PURE__ */ jsx("div", {
125
147
  "data-sidebar": "sidebar",
126
148
  "data-slot": "sidebar-inner",
127
- className: "bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm",
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
+ onClickCapture: handleSurfaceClickCapture,
128
151
  children
129
152
  })
130
153
  })]
@@ -181,7 +204,7 @@ function SidebarRail({ className, ...props }) {
181
204
  tabIndex: -1,
182
205
  onClick: toggleSidebar,
183
206
  title: "Toggle Sidebar",
184
- className: cn("[@media(hover:hover)]:hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex", "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize", "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", "[@media(hover:hover)]:hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full", "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", className),
207
+ className: cn("absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex", "[@media(hover:hover)]:hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full", "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", className),
185
208
  ...props
186
209
  });
187
210
  }
@@ -203,14 +226,20 @@ function SidebarHeader({ className, icon, title, children, ...props }) {
203
226
  return /* @__PURE__ */ jsxs("div", {
204
227
  "data-slot": "sidebar-header",
205
228
  "data-sidebar": "header",
206
- className: cn("flex flex-col gap-2 p-2", className),
229
+ className: cn("flex flex-col gap-2 py-2", className),
207
230
  ...props,
208
231
  children: [/* @__PURE__ */ jsxs("div", {
209
232
  className: "flex h-8 items-center gap-2 px-3",
210
233
  children: [
211
- /* @__PURE__ */ jsx("div", {
212
- className: "shrink-0",
213
- children: icon
234
+ /* @__PURE__ */ jsxs("span", {
235
+ className: "relative flex size-8 shrink-0 items-center justify-center",
236
+ children: [/* @__PURE__ */ jsx("span", {
237
+ className: "transition-opacity duration-200 group-data-[collapsible=icon]:group-hover:opacity-0",
238
+ children: icon
239
+ }), /* @__PURE__ */ jsx(SidebarTrigger, {
240
+ tabIndex: -1,
241
+ className: "absolute inset-0 opacity-0 transition-opacity duration-200 group-data-[collapsible=icon]:group-hover:opacity-100"
242
+ })]
214
243
  }),
215
244
  /* @__PURE__ */ jsx("span", {
216
245
  className: "text-foreground text-md min-w-0 truncate font-medium group-data-[collapsible=icon]:hidden",
@@ -4,7 +4,7 @@ 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;
7
+ variant?: "secondary" | "primary" | null | undefined;
8
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> {}
@@ -4,7 +4,7 @@ import * as _$class_variance_authority_types0 from "class-variance-authority/typ
4
4
 
5
5
  //#region src/components/status-dot.d.ts
6
6
  declare const statusDotVariants: (props?: ({
7
- variant?: "destructive" | "success" | "warning" | "muted" | null | undefined;
7
+ variant?: "destructive" | "warning" | "success" | "muted" | null | undefined;
8
8
  pulse?: boolean | null | undefined;
9
9
  } & _$class_variance_authority_types0.ClassProp) | undefined) => string;
10
10
  interface StatusDotProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof statusDotVariants> {}
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alpic-ai/ui",
3
- "version": "0.0.0-dev.fcb3084",
3
+ "version": "0.0.0-dev.fdd73bf",
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.14.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.75.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.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.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
  },
@@ -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],
@@ -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
+ }
@@ -19,6 +19,19 @@ const SIDEBAR_WIDTH_MOBILE = "16rem";
19
19
  const SIDEBAR_WIDTH_ICON = "3.5rem";
20
20
  const SIDEBAR_KEYBOARD_SHORTCUT = "b";
21
21
 
22
+ const INTERACTIVE_SIDEBAR_ELEMENT_SELECTOR = [
23
+ "a",
24
+ "button",
25
+ "input",
26
+ "select",
27
+ "textarea",
28
+ "[role='button']",
29
+ "[role='link']",
30
+ "[role='menuitem']",
31
+ "[contenteditable='true']",
32
+ "[tabindex]:not([tabindex='-1'])",
33
+ ].join(", ");
34
+
22
35
  type SidebarContextProps = {
23
36
  state: "expanded" | "collapsed";
24
37
  open: boolean;
@@ -142,13 +155,31 @@ function Sidebar({
142
155
  variant?: "sidebar" | "floating" | "inset";
143
156
  collapsible?: "offcanvas" | "icon" | "none";
144
157
  }) {
145
- const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
158
+ const { isMobile, state, openMobile, setOpenMobile, open, setOpen } = useSidebar();
159
+
160
+ function handleSurfaceClickCapture(event: React.MouseEvent<HTMLDivElement>) {
161
+ const clickedInteractiveElement =
162
+ event.target instanceof Element && event.target.closest(INTERACTIVE_SIDEBAR_ELEMENT_SELECTOR);
163
+
164
+ if (clickedInteractiveElement) {
165
+ return;
166
+ }
167
+
168
+ if (!open) {
169
+ event.preventDefault();
170
+ event.stopPropagation();
171
+ setOpen(true);
172
+ return;
173
+ }
174
+
175
+ setOpen(false);
176
+ }
146
177
 
147
178
  if (collapsible === "none") {
148
179
  return (
149
180
  <div
150
181
  data-slot="sidebar"
151
- 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)}
152
183
  {...props}
153
184
  >
154
185
  {children}
@@ -163,7 +194,7 @@ function Sidebar({
163
194
  data-sidebar="sidebar"
164
195
  data-slot="sidebar"
165
196
  data-mobile="true"
166
- 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"
167
198
  style={
168
199
  {
169
200
  "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
@@ -212,7 +243,7 @@ function Sidebar({
212
243
  // Adjust the padding for floating and inset variants.
213
244
  variant === "floating" || variant === "inset"
214
245
  ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
215
- : "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",
216
247
  className,
217
248
  )}
218
249
  {...props}
@@ -220,7 +251,8 @@ function Sidebar({
220
251
  <div
221
252
  data-sidebar="sidebar"
222
253
  data-slot="sidebar-inner"
223
- className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
254
+ className="bg-sidebar-surface group-data-[variant=floating]:border-sidebar-border flex h-full w-full cursor-pointer flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
255
+ onClickCapture={handleSurfaceClickCapture}
224
256
  >
225
257
  {children}
226
258
  </div>
@@ -272,9 +304,7 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
272
304
  onClick={toggleSidebar}
273
305
  title="Toggle Sidebar"
274
306
  className={cn(
275
- "[@media(hover:hover)]:hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
276
- "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
277
- "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
307
+ "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
278
308
  "[@media(hover:hover)]:hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
279
309
  "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
280
310
  "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
@@ -322,11 +352,19 @@ function SidebarHeader({ className, icon, title, children, ...props }: SidebarHe
322
352
  <div
323
353
  data-slot="sidebar-header"
324
354
  data-sidebar="header"
325
- className={cn("flex flex-col gap-2 p-2", className)}
355
+ className={cn("flex flex-col gap-2 py-2", className)}
326
356
  {...props}
327
357
  >
328
358
  <div className="flex h-8 items-center gap-2 px-3">
329
- <div className="shrink-0">{icon}</div>
359
+ <span className="relative flex size-8 shrink-0 items-center justify-center">
360
+ <span className="transition-opacity duration-200 group-data-[collapsible=icon]:group-hover:opacity-0">
361
+ {icon}
362
+ </span>
363
+ <SidebarTrigger
364
+ tabIndex={-1}
365
+ className="absolute inset-0 opacity-0 transition-opacity duration-200 group-data-[collapsible=icon]:group-hover:opacity-100"
366
+ />
367
+ </span>
330
368
  <span className="text-foreground text-md min-w-0 truncate font-medium group-data-[collapsible=icon]:hidden">
331
369
  {title}
332
370
  </span>
@@ -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
 
@@ -107,7 +107,8 @@ export const AllVariants: Story = () => (
107
107
  <UserFooter />
108
108
  </Sidebar>
109
109
  <SidebarInset>
110
- <div className="p-4">
110
+ <div className="flex items-center gap-2 p-4">
111
+ <SidebarTrigger />
111
112
  <span className="type-text-sm text-muted-foreground">Sub-menus expand inline under their parent</span>
112
113
  </div>
113
114
  </SidebarInset>
@@ -169,7 +170,8 @@ export const AllVariants: Story = () => (
169
170
  <UserFooter />
170
171
  </Sidebar>
171
172
  <SidebarInset>
172
- <div className="p-4">
173
+ <div className="flex items-center gap-2 p-4">
174
+ <SidebarTrigger />
173
175
  <span className="type-text-sm text-muted-foreground">Groups with labels, badges, and separator</span>
174
176
  </div>
175
177
  </SidebarInset>
@@ -195,7 +197,8 @@ export const AllVariants: Story = () => (
195
197
  <UserFooter />
196
198
  </Sidebar>
197
199
  <SidebarInset>
198
- <div className="p-4">
200
+ <div className="flex items-center gap-2 p-4">
201
+ <SidebarTrigger />
199
202
  <span className="type-text-sm text-muted-foreground">Loading state with skeleton placeholders</span>
200
203
  </div>
201
204
  </SidebarInset>
@@ -190,8 +190,8 @@ export const AllVariants: Story = () => (
190
190
  </TableRow>
191
191
  </TableHeader>
192
192
  <TableBody>
193
- {USERS.slice(0, 3).map((user, 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,24 @@
110
113
  --animate-beacon-ring-pulse: beacon-ring-pulse 2.2s ease-in-out infinite;
111
114
  }
112
115
 
116
+ @keyframes shimmer-text {
117
+ from {
118
+ background-position: 100% center;
119
+ }
120
+ to {
121
+ background-position: 0% center;
122
+ }
123
+ }
124
+
125
+ @keyframes alpic-ride {
126
+ 0% {
127
+ transform: translate(0px, 0px);
128
+ }
129
+ 100% {
130
+ transform: translate(237px, -64px);
131
+ }
132
+ }
133
+
113
134
  @keyframes accordion-down {
114
135
  from {
115
136
  height: 0;
@@ -143,14 +164,166 @@
143
164
  @keyframes beacon-ring-pulse {
144
165
  0%,
145
166
  100% {
146
- opacity: 0.88;
167
+ opacity: 0.55;
147
168
  transform: scale(1);
148
- filter: drop-shadow(0 0 0 rgba(59, 130, 246, 0));
149
169
  }
150
170
  50% {
151
- opacity: 1;
171
+ opacity: 0.9;
152
172
  transform: scale(1.02);
153
- filter: drop-shadow(0 0 8px rgba(59, 130, 246, 0.24));
173
+ }
174
+ }
175
+
176
+ /* ─── CTA button — animated conic gradient ring ───────────────────────────── */
177
+
178
+ @property --cta-angle {
179
+ syntax: "<angle>";
180
+ inherits: false;
181
+ initial-value: 135deg;
182
+ }
183
+
184
+ @keyframes cta-rotate {
185
+ to {
186
+ --cta-angle: 495deg;
187
+ }
188
+ }
189
+
190
+ .button-cta {
191
+ position: relative;
192
+ isolation: isolate;
193
+ /* light mode: whisper-of-gradient surface tint + soft rose drop-shadow */
194
+ background-image: linear-gradient(
195
+ 135deg,
196
+ color-mix(in oklab, var(--color-primary) 5%, transparent) 0%,
197
+ color-mix(in oklab, var(--color-cta-accent) 5%, transparent) 100%
198
+ );
199
+ box-shadow:
200
+ 0 6px 24px -10px color-mix(in oklab, var(--color-primary) 38%, transparent),
201
+ 0 2px 6px -4px color-mix(in oklab, var(--color-cta-accent) 30%, transparent);
202
+ transition:
203
+ box-shadow 400ms ease,
204
+ transform 300ms ease,
205
+ filter 300ms ease;
206
+ }
207
+
208
+ @media (hover: hover) {
209
+ .button-cta:hover {
210
+ box-shadow:
211
+ 0 10px 30px -8px color-mix(in oklab, var(--color-primary) 52%, transparent),
212
+ 0 3px 10px -3px color-mix(in oklab, var(--color-cta-accent) 40%, transparent);
213
+ }
214
+ }
215
+
216
+ /* dark mode: solid inverted surface, no tint or shadow — let the halo do the work */
217
+ .dark .button-cta {
218
+ background-image: none;
219
+ box-shadow: none;
220
+ }
221
+
222
+ .button-cta::before,
223
+ .button-cta::after {
224
+ content: "";
225
+ position: absolute;
226
+ inset: 0;
227
+ border-radius: inherit;
228
+ pointer-events: none;
229
+ /* Always "running" in browser terms, but paused at rest — freezes at current
230
+ angle on unhover instead of snapping back. */
231
+ animation: cta-rotate 3.2s linear infinite;
232
+ animation-play-state: paused;
233
+ }
234
+
235
+ /* Gradient ring (masked so only the border shows) */
236
+ .button-cta::before {
237
+ padding: 1.5px;
238
+ background: conic-gradient(
239
+ from var(--cta-angle),
240
+ var(--color-cta-accent) 0deg,
241
+ var(--color-primary) 150deg,
242
+ var(--color-cta-accent) 300deg,
243
+ var(--color-cta-accent) 360deg
244
+ );
245
+ -webkit-mask:
246
+ linear-gradient(#000 0 0) content-box,
247
+ linear-gradient(#000 0 0);
248
+ -webkit-mask-composite: xor;
249
+ mask-composite: exclude;
250
+ transition: filter 400ms ease;
251
+ }
252
+
253
+ /* Blurred glow halo behind the button — subtle in light, bolder in dark */
254
+ .button-cta::after {
255
+ z-index: -1;
256
+ background: conic-gradient(
257
+ from var(--cta-angle),
258
+ var(--color-cta-accent) 0deg,
259
+ var(--color-primary) 150deg,
260
+ var(--color-cta-accent) 300deg,
261
+ var(--color-cta-accent) 360deg
262
+ );
263
+ filter: blur(12px);
264
+ opacity: 0.05;
265
+ transition: opacity 400ms ease;
266
+ }
267
+
268
+ .dark .button-cta::after {
269
+ opacity: 0.14;
270
+ }
271
+
272
+ @media (hover: hover) {
273
+ .button-cta:hover::before,
274
+ .button-cta:hover::after {
275
+ animation-play-state: running;
276
+ }
277
+ .button-cta:hover::before {
278
+ filter: saturate(1.15) brightness(1.05);
279
+ }
280
+ .button-cta:hover::after {
281
+ opacity: 0.18;
282
+ }
283
+ .dark .button-cta:hover::after {
284
+ opacity: 0.32;
285
+ }
286
+ }
287
+
288
+ .button-cta:focus-visible::before,
289
+ .button-cta:focus-visible::after {
290
+ animation-play-state: running;
291
+ }
292
+
293
+ .button-cta:disabled::before,
294
+ .button-cta:disabled::after,
295
+ [aria-busy="true"].button-cta::before,
296
+ [aria-busy="true"].button-cta::after {
297
+ animation-play-state: paused;
298
+ }
299
+ .button-cta:disabled::after {
300
+ opacity: 0;
301
+ }
302
+
303
+ @media (prefers-reduced-motion: reduce) {
304
+ .button-cta::before,
305
+ .button-cta::after {
306
+ animation: none;
307
+ }
308
+ }
309
+
310
+ /* Icon slide on hover (applied to [data-cta-icon-trailing]) */
311
+ .button-cta [data-cta-icon-trailing] {
312
+ transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
313
+ }
314
+
315
+ @media (hover: hover) {
316
+ .button-cta:hover [data-cta-icon-trailing] {
317
+ transform: translateX(2px);
318
+ }
319
+ }
320
+
321
+ @media (prefers-reduced-motion: reduce) {
322
+ .button-cta [data-cta-icon-trailing] {
323
+ transition: none;
324
+ }
325
+ .button-cta:hover [data-cta-icon-trailing] {
326
+ transform: none;
154
327
  }
155
328
  }
156
329
 
@@ -191,6 +364,9 @@
191
364
 
192
365
  --color-ring: #f22b79; /* Figma: focus-ring */
193
366
 
367
+ /* cta — decorative gradient accent, used with --color-primary in the CTA button ring */
368
+ --color-cta-accent: #6eece7; /* Figma: CTA border accent (cyan) */
369
+
194
370
  /* sidebar */
195
371
  --color-sidebar: #0c1c1c; /* Figma: bg-secondary */
196
372
  --color-sidebar-foreground: #90a4a4; /* Figma: fg-secondary */
@@ -249,6 +425,14 @@
249
425
 
250
426
  @custom-variant dark (&:where(.dark, .dark *));
251
427
 
428
+ :root {
429
+ --gradient-sidebar: linear-gradient(0deg, #c9e2e280 0%, #ffffff 70%); /* Figma: bg-nav-gradiant-light */
430
+ }
431
+
432
+ .dark {
433
+ --gradient-sidebar: linear-gradient(0deg, #213535 0%, #121e1e 70%); /* Figma: bg-nav-gradiant-dark */
434
+ }
435
+
252
436
  @layer base {
253
437
  * {
254
438
  @apply border-border shadow-shadow;
@@ -302,6 +486,11 @@
302
486
 
303
487
  /* ─── Type preset utilities ───────────────────────────────────────────────── */
304
488
 
489
+ @utility bg-sidebar-surface {
490
+ background-color: var(--color-background);
491
+ background-image: var(--gradient-sidebar);
492
+ }
493
+
305
494
  @utility type-display-2xl {
306
495
  font-family: var(--font-display);
307
496
  font-size: var(--font-size-display-2xl);