@kinetixui/ui 0.6.5 → 0.8.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 (51) hide show
  1. package/dist/index.d.ts +47 -2
  2. package/dist/index.js +472 -380
  3. package/package.json +2 -2
  4. package/src/components/accordion.tsx +1 -1
  5. package/src/components/alert-dialog.tsx +2 -2
  6. package/src/components/app-bar.tsx +1 -1
  7. package/src/components/audio-player.tsx +3 -0
  8. package/src/components/badge.tsx +13 -4
  9. package/src/components/button.tsx +2 -2
  10. package/src/components/calendar.tsx +9 -5
  11. package/src/components/checkbox.tsx +1 -1
  12. package/src/components/circular-progress.tsx +1 -1
  13. package/src/components/command.tsx +4 -2
  14. package/src/components/context-menu.tsx +2 -2
  15. package/src/components/dialog.tsx +3 -3
  16. package/src/components/drawer.tsx +2 -2
  17. package/src/components/dropdown-menu.tsx +4 -2
  18. package/src/components/empty.tsx +76 -0
  19. package/src/components/fab.tsx +1 -1
  20. package/src/components/file-upload.tsx +1 -1
  21. package/src/components/hover-card.tsx +1 -1
  22. package/src/components/image.tsx +2 -2
  23. package/src/components/inform.tsx +1 -1
  24. package/src/components/input-group.tsx +2 -2
  25. package/src/components/input-otp.tsx +3 -3
  26. package/src/components/input.tsx +2 -2
  27. package/src/components/kbd.tsx +40 -0
  28. package/src/components/label.tsx +1 -1
  29. package/src/components/list.tsx +1 -1
  30. package/src/components/menubar.tsx +2 -2
  31. package/src/components/modal.tsx +2 -2
  32. package/src/components/navigation-menu.tsx +4 -4
  33. package/src/components/number-input.tsx +3 -3
  34. package/src/components/popover.tsx +1 -1
  35. package/src/components/radio-group.tsx +1 -1
  36. package/src/components/resizable.tsx +1 -1
  37. package/src/components/select.tsx +5 -3
  38. package/src/components/sheet.tsx +3 -3
  39. package/src/components/sidebar.tsx +5 -5
  40. package/src/components/slider.tsx +1 -1
  41. package/src/components/switch.tsx +1 -1
  42. package/src/components/tabs.tsx +1 -1
  43. package/src/components/tag.tsx +27 -15
  44. package/src/components/textarea.tsx +2 -2
  45. package/src/components/toggle.tsx +1 -1
  46. package/src/components/tooltip.tsx +1 -1
  47. package/src/components-behavior.test.tsx +37 -0
  48. package/src/index.ts +13 -0
  49. package/src/stories/Empty.stories.tsx +35 -0
  50. package/src/stories/Kbd.stories.tsx +27 -0
  51. package/tailwind.config.ts +29 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinetixui/ui",
3
- "version": "0.6.5",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
5
  "description": "KinetixUI React components — CVA + Radix + Tailwind on the KinetixUI token contract. Also distributed via the kinetixui CLI as a component registry.",
6
6
  "keywords": [
@@ -90,7 +90,7 @@
90
90
  "react-dom": ">=18"
91
91
  },
92
92
  "devDependencies": {
93
- "@kinetixui/tokens": "0.6.5",
93
+ "@kinetixui/tokens": "0.8.0",
94
94
  "@testing-library/jest-dom": "^7.0.1",
95
95
  "@testing-library/react": "^16.3.3",
96
96
  "@testing-library/user-event": "^14.6.7",
@@ -29,7 +29,7 @@ const AccordionTrigger = React.forwardRef<
29
29
  {...props}
30
30
  >
31
31
  {children}
32
- <ChevronDown className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
32
+ <ChevronDown className="size-4 shrink-0 text-muted-foreground transition-transform duration-fast" />
33
33
  </AccordionPrimitive.Trigger>
34
34
  </AccordionPrimitive.Header>
35
35
  ));
@@ -16,7 +16,7 @@ const AlertDialogOverlay = React.forwardRef<
16
16
  <AlertDialogPrimitive.Overlay
17
17
  ref={ref}
18
18
  className={cn(
19
- "fixed inset-0 z-50 bg-foreground/40 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
19
+ "fixed inset-0 z-overlay bg-foreground/40 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
20
20
  className,
21
21
  )}
22
22
  {...props}
@@ -33,7 +33,7 @@ const AlertDialogContent = React.forwardRef<
33
33
  <AlertDialogPrimitive.Content
34
34
  ref={ref}
35
35
  className={cn(
36
- "fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg sm:rounded-xl font-sans",
36
+ "fixed left-1/2 top-1/2 z-overlay grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg sm:rounded-xl font-sans",
37
37
  "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
38
38
  className,
39
39
  )}
@@ -48,7 +48,7 @@ const AppBar = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement>>(
48
48
  <header
49
49
  ref={ref}
50
50
  className={cn(
51
- "sticky top-0 z-40 w-full border-b border-border bg-background/95 font-sans",
51
+ "sticky top-0 z-sticky w-full border-b border-border bg-background/95 font-sans",
52
52
  "backdrop-blur supports-[backdrop-filter]:bg-background/75",
53
53
  className,
54
54
  )}
@@ -73,6 +73,9 @@ const AudioPlayer = React.forwardRef<HTMLDivElement, AudioPlayerProps>(
73
73
  </button>
74
74
  );
75
75
 
76
+ // disabled:opacity-40 (not --opacity-disabled's 0.5) is a real, pre-existing
77
+ // inconsistency — left as a literal rather than silently changing the
78
+ // rendered value; a design call, not a rename.
76
79
  const iconBtn =
77
80
  "grid size-8 shrink-0 place-items-center rounded-full text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:text-foreground disabled:opacity-40 [&>svg]:size-4";
78
81
 
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
+ import { Slot } from "@radix-ui/react-slot";
4
5
  import { cva, type VariantProps } from "class-variance-authority";
5
6
  import { cn } from "../lib/utils";
6
7
 
@@ -31,10 +32,18 @@ const badgeVariants = cva(
31
32
 
32
33
  export interface BadgeProps
33
34
  extends React.HTMLAttributes<HTMLDivElement>,
34
- VariantProps<typeof badgeVariants> {}
35
-
36
- function Badge({ className, variant, ...props }: BadgeProps) {
37
- return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
35
+ VariantProps<typeof badgeVariants> {
36
+ /** render as the single child element (Radix Slot) instead of <div> —
37
+ * e.g. `<Badge asChild><a href="/new">New</a></Badge>` for a linked badge */
38
+ asChild?: boolean;
38
39
  }
39
40
 
41
+ const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
42
+ ({ className, variant, asChild = false, ...props }, ref) => {
43
+ const Comp = asChild ? Slot : "div";
44
+ return <Comp ref={ref} className={cn(badgeVariants({ variant }), className)} {...props} />;
45
+ },
46
+ );
47
+ Badge.displayName = "Badge";
48
+
40
49
  export { Badge, badgeVariants };
@@ -28,7 +28,7 @@ const buttonVariants = cva(
28
28
  "font-sans font-medium whitespace-nowrap select-none",
29
29
  "rounded-md transition-colors outline-none",
30
30
  "focus-visible:shadow-focus",
31
- "disabled:pointer-events-none disabled:opacity-50",
31
+ "disabled:pointer-events-none disabled:opacity-disabled",
32
32
  "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:size-[18px]",
33
33
  ],
34
34
  {
@@ -72,7 +72,7 @@ const buttonVariants = cva(
72
72
  Hover: "",
73
73
  Focus: "shadow-focus",
74
74
  Active: "",
75
- Disabled: "pointer-events-none opacity-50",
75
+ Disabled: "pointer-events-none opacity-disabled",
76
76
  },
77
77
  /** corner style — matches the design source's Corners property */
78
78
  corners: {
@@ -19,13 +19,17 @@ function Calendar({ className, classNames, showOutsideDays = true, ...props }: C
19
19
  month_caption: "flex justify-center pt-1 relative items-center w-full",
20
20
  caption_label: "text-sm font-medium",
21
21
  nav: "flex items-center gap-1",
22
+ // Resting-dim/hover-brighten nav icons — numerically the disabled
23
+ // value (0.5) but not a disabled state, so left as a literal rather
24
+ // than forced into --opacity-disabled or --opacity-muted (0.7, which
25
+ // would change the rendered value).
22
26
  button_previous: cn(
23
27
  buttonVariants({ variant: "Outline", size: "icon" }),
24
- "absolute left-1 size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
28
+ "absolute left-1 size-7 bg-transparent p-0 opacity-50 hover:opacity-visible",
25
29
  ),
26
30
  button_next: cn(
27
31
  buttonVariants({ variant: "Outline", size: "icon" }),
28
- "absolute right-1 size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
32
+ "absolute right-1 size-7 bg-transparent p-0 opacity-50 hover:opacity-visible",
29
33
  ),
30
34
  month_grid: "w-full border-collapse space-x-1",
31
35
  weekdays: "flex",
@@ -36,19 +40,19 @@ function Calendar({ className, classNames, showOutsideDays = true, ...props }: C
36
40
  // accent background + corner rounding and hands the primary-colored
37
41
  // "selected" circle down to `day_button` via a `group` class.
38
42
  day: cn(
39
- "group relative p-0 text-center text-sm focus-within:relative focus-within:z-20 aria-selected:bg-accent",
43
+ "group relative p-0 text-center text-sm focus-within:relative focus-within:z-focus aria-selected:bg-accent",
40
44
  props.mode === "range" ? "" : "aria-selected:rounded-md",
41
45
  ),
42
46
  day_button: cn(
43
47
  buttonVariants({ variant: "Ghost", size: "icon" }),
44
- "size-8 p-0 font-normal group-aria-selected:opacity-100 group-data-[selected=true]:bg-primary group-data-[selected=true]:text-primary-foreground group-data-[selected=true]:hover:bg-primary group-data-[selected=true]:hover:text-primary-foreground group-data-[selected=true]:focus:bg-primary group-data-[selected=true]:focus:text-primary-foreground",
48
+ "size-8 p-0 font-normal group-aria-selected:opacity-visible group-data-[selected=true]:bg-primary group-data-[selected=true]:text-primary-foreground group-data-[selected=true]:hover:bg-primary group-data-[selected=true]:hover:text-primary-foreground group-data-[selected=true]:focus:bg-primary group-data-[selected=true]:focus:text-primary-foreground",
45
49
  ),
46
50
  range_start: "day-range-start rounded-l-md",
47
51
  range_end: "day-range-end rounded-r-md",
48
52
  range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
49
53
  today: "bg-accent text-accent-foreground",
50
54
  outside: "day-outside text-muted-foreground aria-selected:text-muted-foreground",
51
- disabled: "text-muted-foreground opacity-50",
55
+ disabled: "text-muted-foreground opacity-disabled",
52
56
  hidden: "invisible",
53
57
  ...classNames,
54
58
  }}
@@ -20,7 +20,7 @@ const Checkbox = React.forwardRef<
20
20
  className={cn(
21
21
  "peer size-[18px] shrink-0 rounded-sm border-2 border-input outline-none transition-colors",
22
22
  "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
23
- "disabled:cursor-not-allowed disabled:opacity-50",
23
+ "disabled:cursor-not-allowed disabled:opacity-disabled",
24
24
  "data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
25
25
  "data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground",
26
26
  "aria-invalid:border-destructive aria-invalid:data-[state=checked]:border-destructive aria-invalid:data-[state=checked]:bg-destructive",
@@ -52,7 +52,7 @@ const CircularProgress = React.forwardRef<HTMLDivElement, CircularProgressProps>
52
52
  strokeLinecap="round"
53
53
  strokeDasharray={c}
54
54
  strokeDashoffset={c - (v / 100) * c}
55
- className="stroke-primary transition-[stroke-dashoffset] duration-300"
55
+ className="stroke-primary transition-[stroke-dashoffset] duration-base"
56
56
  />
57
57
  </svg>
58
58
  {(showValue || label != null) && (
@@ -36,11 +36,13 @@ const CommandInput = React.forwardRef<
36
36
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
37
37
  >(({ className, ...props }, ref) => (
38
38
  <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
39
+ {/* Resting-dim icon, no hover/disabled state — numerically the disabled
40
+ value (0.5) but not that role; left as a literal, see calendar.tsx */}
39
41
  <Search className="mr-2 size-4 shrink-0 opacity-50" />
40
42
  <CommandPrimitive.Input
41
43
  ref={ref}
42
44
  className={cn(
43
- "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
45
+ "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-disabled",
44
46
  className,
45
47
  )}
46
48
  {...props}
@@ -97,7 +99,7 @@ const CommandItem = React.forwardRef<
97
99
  <CommandPrimitive.Item
98
100
  ref={ref}
99
101
  className={cn(
100
- "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0",
102
+ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-disabled [&_svg]:size-4 [&_svg]:shrink-0",
101
103
  className,
102
104
  )}
103
105
  {...props}
@@ -13,9 +13,9 @@ const ContextMenuSub = ContextMenuPrimitive.Sub;
13
13
  const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
14
14
 
15
15
  const itemCls =
16
- "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground focus:ring-1 focus:ring-inset focus:ring-ring data-[disabled]:pointer-events-none data-[disabled]:opacity-50";
16
+ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground focus:ring-1 focus:ring-inset focus:ring-ring data-[disabled]:pointer-events-none data-[disabled]:opacity-disabled";
17
17
  const contentCls =
18
- "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md font-sans data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95";
18
+ "z-overlay min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md font-sans data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95";
19
19
 
20
20
  const ContextMenuSubTrigger = React.forwardRef<
21
21
  React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
@@ -17,7 +17,7 @@ const DialogOverlay = React.forwardRef<
17
17
  <DialogPrimitive.Overlay
18
18
  ref={ref}
19
19
  className={cn(
20
- "fixed inset-0 z-50 bg-foreground/40 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
20
+ "fixed inset-0 z-overlay bg-foreground/40 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
21
21
  className,
22
22
  )}
23
23
  {...props}
@@ -34,14 +34,14 @@ const DialogContent = React.forwardRef<
34
34
  <DialogPrimitive.Content
35
35
  ref={ref}
36
36
  className={cn(
37
- "fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg sm:rounded-xl font-sans",
37
+ "fixed left-1/2 top-1/2 z-overlay grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg sm:rounded-xl font-sans",
38
38
  "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
39
39
  className,
40
40
  )}
41
41
  {...props}
42
42
  >
43
43
  {children}
44
- <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring disabled:pointer-events-none">
44
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-muted transition-opacity hover:opacity-visible focus:outline-none focus:ring-2 focus:ring-ring disabled:pointer-events-none">
45
45
  <X className="size-4" />
46
46
  <span className="sr-only">Close</span>
47
47
  </DialogPrimitive.Close>
@@ -17,7 +17,7 @@ const DrawerOverlay = React.forwardRef<
17
17
  React.ElementRef<typeof DrawerPrimitive.Overlay>,
18
18
  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
19
19
  >(({ className, ...props }, ref) => (
20
- <DrawerPrimitive.Overlay ref={ref} className={cn("fixed inset-0 z-50 bg-foreground/40", className)} {...props} />
20
+ <DrawerPrimitive.Overlay ref={ref} className={cn("fixed inset-0 z-overlay bg-foreground/40", className)} {...props} />
21
21
  ));
22
22
  DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
23
23
 
@@ -30,7 +30,7 @@ const DrawerContent = React.forwardRef<
30
30
  <DrawerPrimitive.Content
31
31
  ref={ref}
32
32
  className={cn(
33
- "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background font-sans",
33
+ "fixed inset-x-0 bottom-0 z-overlay mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background font-sans",
34
34
  className,
35
35
  )}
36
36
  {...props}
@@ -13,9 +13,9 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
13
13
  const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
14
14
 
15
15
  const contentCls =
16
- "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md font-sans data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95";
16
+ "z-overlay min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md font-sans data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95";
17
17
  const itemCls =
18
- "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground focus:ring-1 focus:ring-inset focus:ring-ring data-[disabled]:pointer-events-none data-[disabled]:opacity-50";
18
+ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground focus:ring-1 focus:ring-inset focus:ring-ring data-[disabled]:pointer-events-none data-[disabled]:opacity-disabled";
19
19
 
20
20
  const DropdownMenuContent = React.forwardRef<
21
21
  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -85,6 +85,8 @@ const DropdownMenuSeparator = React.forwardRef<
85
85
  ));
86
86
  DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
87
87
 
88
+ // opacity-60 (not --opacity-muted's 0.7) is a pre-existing inconsistency —
89
+ // left as a literal rather than silently changing the rendered value.
88
90
  const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
89
91
  <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
90
92
  );
@@ -0,0 +1,76 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * Empty — a placeholder for a zero-results state (an empty table, an empty
9
+ * search, a fresh workspace with nothing in it yet). Composition-based, like
10
+ * Card: `Empty` is an unstyled-beyond-layout root so it composes cleanly
11
+ * inside whatever already has a border (a Table cell, a Card, a bare page
12
+ * section) instead of forcing its own. Gap-fill addition (not in the
13
+ * original Figma source) — matches the shadcn/ui Empty API shape.
14
+ */
15
+ const Empty = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
16
+ ({ className, ...props }, ref) => (
17
+ <div
18
+ ref={ref}
19
+ className={cn("flex min-w-0 flex-1 flex-col items-center justify-center gap-6 p-10 text-center font-sans", className)}
20
+ {...props}
21
+ />
22
+ ),
23
+ );
24
+ Empty.displayName = "Empty";
25
+
26
+ const EmptyHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
27
+ ({ className, ...props }, ref) => (
28
+ <div ref={ref} className={cn("flex max-w-sm flex-col items-center gap-2", className)} {...props} />
29
+ ),
30
+ );
31
+ EmptyHeader.displayName = "EmptyHeader";
32
+
33
+ const emptyMediaVariants = cva("flex shrink-0 items-center justify-center", {
34
+ variants: {
35
+ /** `icon` gives a muted circular badge behind the icon; `default` renders the child as-is (an illustration, an avatar, …) */
36
+ variant: {
37
+ default: "",
38
+ icon: "size-10 rounded-full bg-muted text-muted-foreground [&_svg]:size-5",
39
+ },
40
+ },
41
+ defaultVariants: { variant: "default" },
42
+ });
43
+
44
+ export interface EmptyMediaProps
45
+ extends React.HTMLAttributes<HTMLDivElement>,
46
+ VariantProps<typeof emptyMediaVariants> {}
47
+
48
+ const EmptyMedia = React.forwardRef<HTMLDivElement, EmptyMediaProps>(
49
+ ({ className, variant, ...props }, ref) => (
50
+ <div ref={ref} className={cn(emptyMediaVariants({ variant }), className)} {...props} />
51
+ ),
52
+ );
53
+ EmptyMedia.displayName = "EmptyMedia";
54
+
55
+ const EmptyTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
56
+ ({ className, ...props }, ref) => (
57
+ <p ref={ref} className={cn("text-title-sm font-medium text-foreground", className)} {...props} />
58
+ ),
59
+ );
60
+ EmptyTitle.displayName = "EmptyTitle";
61
+
62
+ const EmptyDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
63
+ ({ className, ...props }, ref) => (
64
+ <p ref={ref} className={cn("text-body-sm text-muted-foreground", className)} {...props} />
65
+ ),
66
+ );
67
+ EmptyDescription.displayName = "EmptyDescription";
68
+
69
+ const EmptyContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
70
+ ({ className, ...props }, ref) => (
71
+ <div ref={ref} className={cn("flex flex-col items-center gap-3", className)} {...props} />
72
+ ),
73
+ );
74
+ EmptyContent.displayName = "EmptyContent";
75
+
76
+ export { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent, emptyMediaVariants };
@@ -13,7 +13,7 @@ const fabVariants = cva(
13
13
  [
14
14
  "inline-flex shrink-0 items-center justify-center gap-2 rounded-full font-sans font-medium",
15
15
  "shadow-lg outline-none transition-colors",
16
- "focus-visible:shadow-focus disabled:pointer-events-none disabled:opacity-50",
16
+ "focus-visible:shadow-focus disabled:pointer-events-none disabled:opacity-disabled",
17
17
  "[&_svg]:pointer-events-none [&_svg]:shrink-0",
18
18
  ],
19
19
  {
@@ -84,7 +84,7 @@ const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
84
84
  "flex flex-col items-center gap-2 rounded-md border border-dashed border-input p-6 text-center outline-none transition-colors",
85
85
  !disabled && "cursor-pointer hover:bg-accent/50 focus-visible:shadow-focus",
86
86
  dragging && "border-primary bg-accent",
87
- disabled && "pointer-events-none opacity-50",
87
+ disabled && "pointer-events-none opacity-disabled",
88
88
  )}
89
89
  >
90
90
  <Upload className="size-6 text-muted-foreground" aria-hidden />
@@ -16,7 +16,7 @@ const HoverCardContent = React.forwardRef<
16
16
  align={align}
17
17
  sideOffset={sideOffset}
18
18
  className={cn(
19
- "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none font-sans",
19
+ "z-overlay w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none font-sans",
20
20
  "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
21
21
  className,
22
22
  )}
@@ -64,8 +64,8 @@ const Image = React.forwardRef<HTMLImageElement, ImageProps>(
64
64
  ref={setRef}
65
65
  alt={alt}
66
66
  className={cn(
67
- "size-full object-cover transition-opacity duration-300",
68
- status === "loaded" ? "opacity-100" : "opacity-0",
67
+ "size-full object-cover transition-opacity duration-base",
68
+ status === "loaded" ? "opacity-visible" : "opacity-hidden",
69
69
  )}
70
70
  onLoad={(e) => {
71
71
  setStatus("loaded");
@@ -68,7 +68,7 @@ const Inform = React.forwardRef<HTMLDivElement, InformProps>(
68
68
  type="button"
69
69
  onClick={onDismiss}
70
70
  aria-label="Dismiss"
71
- className="-m-0.5 shrink-0 rounded-[2px] p-0.5 opacity-70 outline-none transition-opacity hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-1 focus-visible:ring-current"
71
+ className="-m-0.5 shrink-0 rounded-[2px] p-0.5 opacity-muted outline-none transition-opacity hover:opacity-visible focus-visible:opacity-visible focus-visible:ring-1 focus-visible:ring-current"
72
72
  >
73
73
  <X className="size-3.5" />
74
74
  </button>
@@ -24,7 +24,7 @@ const InputGroup = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDiv
24
24
  "flex w-full items-center overflow-hidden rounded-sm border border-input bg-background font-sans transition-colors",
25
25
  "focus-within:border-primary focus-within:shadow-focus",
26
26
  "has-[[aria-invalid=true]]:border-destructive has-[[aria-invalid=true]]:focus-within:shadow-focus-destructive",
27
- "has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50",
27
+ "has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-disabled",
28
28
  className,
29
29
  )}
30
30
  {...props}
@@ -85,7 +85,7 @@ const InputGroupButton = React.forwardRef<
85
85
  className={cn(
86
86
  "flex h-full shrink-0 items-center gap-1.5 self-stretch border-l border-input bg-muted px-3 text-label-md text-foreground",
87
87
  // base is bg-muted and muted==accent on dark, so the fill is a no-op there — the ring carries it
88
- "outline-none transition-colors hover:bg-accent hover:ring-1 hover:ring-inset hover:ring-ring focus-visible:bg-accent focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
88
+ "outline-none transition-colors hover:bg-accent hover:ring-1 hover:ring-inset hover:ring-ring focus-visible:bg-accent focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-disabled",
89
89
  "[&_svg]:size-4 [&_svg]:shrink-0",
90
90
  className,
91
91
  )}
@@ -11,7 +11,7 @@ const InputOTP = React.forwardRef<
11
11
  >(({ className, containerClassName, ...props }, ref) => (
12
12
  <OTPInput
13
13
  ref={ref}
14
- containerClassName={cn("flex items-center gap-2 has-[:disabled]:opacity-50", containerClassName)}
14
+ containerClassName={cn("flex items-center gap-2 has-[:disabled]:opacity-disabled", containerClassName)}
15
15
  className={cn("disabled:cursor-not-allowed", className)}
16
16
  {...props}
17
17
  />
@@ -35,7 +35,7 @@ const InputOTPSlot = React.forwardRef<
35
35
  ref={ref}
36
36
  className={cn(
37
37
  "relative flex size-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
38
- isActive && "z-10 ring-1 ring-ring",
38
+ isActive && "z-docked ring-1 ring-ring",
39
39
  className,
40
40
  )}
41
41
  {...props}
@@ -43,7 +43,7 @@ const InputOTPSlot = React.forwardRef<
43
43
  {char}
44
44
  {hasFakeCaret && (
45
45
  <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
46
- <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
46
+ <div className="h-4 w-px animate-caret-blink bg-foreground duration-slower" />
47
47
  </div>
48
48
  )}
49
49
  </div>
@@ -26,7 +26,7 @@ const inputVariants = cva(
26
26
  "placeholder:text-muted-foreground",
27
27
  "outline-none transition-colors",
28
28
  "focus-visible:border-primary focus-visible:shadow-focus",
29
- "disabled:cursor-not-allowed disabled:opacity-50",
29
+ "disabled:cursor-not-allowed disabled:opacity-disabled",
30
30
  "aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-visible:border-destructive aria-[invalid=true]:focus-visible:shadow-focus-destructive",
31
31
  ],
32
32
  {
@@ -35,7 +35,7 @@ const inputVariants = cva(
35
35
  Default: "",
36
36
  Focus: "border-primary shadow-focus",
37
37
  Error: "border-destructive",
38
- Disabled: "opacity-50 pointer-events-none",
38
+ Disabled: "opacity-disabled pointer-events-none",
39
39
  },
40
40
  /** corner style — matches the design source's Corners property */
41
41
  corners: {
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Slot } from "@radix-ui/react-slot";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * Kbd — a single keyboard key glyph, e.g. `<Kbd>⌘</Kbd>`. Wrap several in
9
+ * `KbdGroup` for a shortcut combo (`⌘` `K`). Gap-fill addition (not in the
10
+ * original Figma source) — matches the shadcn/ui Kbd/KbdGroup API shape.
11
+ */
12
+ export interface KbdProps extends React.HTMLAttributes<HTMLElement> {
13
+ /** render as the single child element (Radix Slot) instead of <kbd> */
14
+ asChild?: boolean;
15
+ }
16
+
17
+ const Kbd = React.forwardRef<HTMLElement, KbdProps>(({ className, asChild = false, ...props }, ref) => {
18
+ const Comp = asChild ? Slot : "kbd";
19
+ return (
20
+ <Comp
21
+ ref={ref}
22
+ className={cn(
23
+ "inline-flex h-5 min-w-5 items-center justify-center rounded-sm border border-input bg-muted px-1.5 font-sans text-[11px] font-medium text-muted-foreground",
24
+ className,
25
+ )}
26
+ {...props}
27
+ />
28
+ );
29
+ });
30
+ Kbd.displayName = "Kbd";
31
+
32
+ export interface KbdGroupProps extends React.HTMLAttributes<HTMLElement> {}
33
+
34
+ /** Lays out multiple `Kbd` for a shortcut combo — `<KbdGroup><Kbd>⌘</Kbd><Kbd>K</Kbd></KbdGroup>` */
35
+ const KbdGroup = React.forwardRef<HTMLElement, KbdGroupProps>(({ className, ...props }, ref) => (
36
+ <kbd ref={ref} className={cn("inline-flex items-center gap-1", className)} {...props} />
37
+ ));
38
+ KbdGroup.displayName = "KbdGroup";
39
+
40
+ export { Kbd, KbdGroup };
@@ -6,7 +6,7 @@ import { cva, type VariantProps } from "class-variance-authority";
6
6
  import { cn } from "../lib/utils";
7
7
 
8
8
  const labelVariants = cva(
9
- "text-sm font-medium leading-none font-sans peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
9
+ "text-sm font-medium leading-none font-sans peer-disabled:cursor-not-allowed peer-disabled:opacity-muted",
10
10
  );
11
11
 
12
12
  const Label = React.forwardRef<
@@ -51,7 +51,7 @@ const ListItem = React.forwardRef<HTMLDivElement, ListItemProps>(
51
51
  // ring keeps the highlighted row perceptible where the accent fill is <3:1 (dark)
52
52
  interactive &&
53
53
  "cursor-pointer hover:bg-accent hover:ring-1 hover:ring-inset hover:ring-ring focus-visible:bg-accent focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring",
54
- disabled && "pointer-events-none opacity-50",
54
+ disabled && "pointer-events-none opacity-disabled",
55
55
  className,
56
56
  )}
57
57
  {...props}
@@ -22,9 +22,9 @@ const MenubarRadioGroup = (props: React.ComponentProps<typeof MenubarPrimitive.R
22
22
  );
23
23
 
24
24
  const itemCls =
25
- "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground focus:ring-1 focus:ring-inset focus:ring-ring data-[disabled]:pointer-events-none data-[disabled]:opacity-50";
25
+ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground focus:ring-1 focus:ring-inset focus:ring-ring data-[disabled]:pointer-events-none data-[disabled]:opacity-disabled";
26
26
  const contentCls =
27
- "z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md font-sans data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95";
27
+ "z-overlay min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md font-sans data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95";
28
28
 
29
29
  const Menubar = React.forwardRef<
30
30
  React.ElementRef<typeof MenubarPrimitive.Root>,
@@ -68,7 +68,7 @@ function Modal({
68
68
  <DialogOverlay />
69
69
  <DialogPrimitive.Content
70
70
  className={cn(
71
- "fixed left-1/2 top-1/2 z-50 flex w-full max-w-[480px] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-xl bg-background font-sans",
71
+ "fixed left-1/2 top-1/2 z-overlay flex w-full max-w-[480px] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-xl bg-background font-sans",
72
72
  "shadow-[0_4px_24px_rgba(0,0,0,0.12)]",
73
73
  "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
74
74
  className,
@@ -79,7 +79,7 @@ function Modal({
79
79
  {title}
80
80
  </DialogPrimitive.Title>
81
81
  <DialogPrimitive.Close
82
- className="rounded-sm opacity-70 outline-none transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring"
82
+ className="rounded-sm opacity-muted outline-none transition-opacity hover:opacity-visible focus-visible:ring-2 focus-visible:ring-ring"
83
83
  aria-label="Close"
84
84
  >
85
85
  <X className="size-5" />
@@ -12,7 +12,7 @@ const NavigationMenu = React.forwardRef<
12
12
  >(({ className, children, ...props }, ref) => (
13
13
  <NavigationMenuPrimitive.Root
14
14
  ref={ref}
15
- className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center font-sans", className)}
15
+ className={cn("relative z-docked flex max-w-max flex-1 items-center justify-center font-sans", className)}
16
16
  {...props}
17
17
  >
18
18
  {children}
@@ -36,7 +36,7 @@ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
36
36
  const NavigationMenuItem = NavigationMenuPrimitive.Item;
37
37
 
38
38
  const navigationMenuTriggerStyle = cva(
39
- "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-accent/50",
39
+ "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-disabled data-[state=open]:bg-accent/50",
40
40
  );
41
41
 
42
42
  const NavigationMenuTrigger = React.forwardRef<
@@ -46,7 +46,7 @@ const NavigationMenuTrigger = React.forwardRef<
46
46
  <NavigationMenuPrimitive.Trigger ref={ref} className={cn(navigationMenuTriggerStyle(), "group", className)} {...props}>
47
47
  {children}
48
48
  <ChevronDown
49
- className="relative top-px ml-1 size-3 transition-transform duration-200 group-data-[state=open]:rotate-180"
49
+ className="relative top-px ml-1 size-3 transition-transform duration-fast group-data-[state=open]:rotate-180"
50
50
  aria-hidden
51
51
  />
52
52
  </NavigationMenuPrimitive.Trigger>
@@ -94,7 +94,7 @@ const NavigationMenuIndicator = React.forwardRef<
94
94
  <NavigationMenuPrimitive.Indicator
95
95
  ref={ref}
96
96
  className={cn(
97
- "top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
97
+ "top-full z-raised flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
98
98
  className,
99
99
  )}
100
100
  {...props}