@kinetixui/ui 0.1.0 → 0.4.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 (83) hide show
  1. package/dist/index.js +30 -21
  2. package/package.json +12 -4
  3. package/src/components/button.tsx +19 -12
  4. package/src/components/context-menu.tsx +1 -1
  5. package/src/components/dropdown-menu.tsx +1 -1
  6. package/src/components/input-group.tsx +2 -1
  7. package/src/components/list.tsx +3 -1
  8. package/src/components/menubar.tsx +2 -2
  9. package/src/components/navigation-bar.tsx +1 -1
  10. package/src/components/select.tsx +1 -1
  11. package/src/components/toggle.tsx +1 -1
  12. package/src/components-behavior.test.tsx +74 -0
  13. package/src/components-smoke.test.tsx +47 -0
  14. package/src/stories/Accordion.stories.tsx +49 -0
  15. package/src/stories/Alert.stories.tsx +38 -0
  16. package/src/stories/AlertDialog.stories.tsx +35 -0
  17. package/src/stories/AspectRatio.stories.tsx +32 -0
  18. package/src/stories/AudioPlayer.stories.tsx +26 -0
  19. package/src/stories/Avatar.stories.tsx +23 -0
  20. package/src/stories/AvatarGroup.stories.tsx +37 -0
  21. package/src/stories/Badge.stories.tsx +35 -0
  22. package/src/stories/Breadcrumb.stories.tsx +36 -0
  23. package/src/stories/Calendar.stories.tsx +25 -0
  24. package/src/stories/Card.stories.tsx +33 -0
  25. package/src/stories/Carousel.stories.tsx +33 -0
  26. package/src/stories/Chart.stories.tsx +44 -0
  27. package/src/stories/Checkbox.stories.tsx +29 -0
  28. package/src/stories/CircularProgress.stories.tsx +42 -0
  29. package/src/stories/CodeBlock.stories.tsx +25 -0
  30. package/src/stories/Collapsible.stories.tsx +50 -0
  31. package/src/stories/Combobox.stories.tsx +32 -0
  32. package/src/stories/Command.stories.tsx +32 -0
  33. package/src/stories/ContextMenu.stories.tsx +31 -0
  34. package/src/stories/DataTable.stories.tsx +31 -0
  35. package/src/stories/DatePicker.stories.tsx +30 -0
  36. package/src/stories/Dialog.stories.tsx +38 -0
  37. package/src/stories/Drawer.stories.tsx +39 -0
  38. package/src/stories/DropdownMenu.stories.tsx +32 -0
  39. package/src/stories/Fab.stories.tsx +47 -0
  40. package/src/stories/Field.stories.tsx +35 -0
  41. package/src/stories/FileUpload.stories.tsx +36 -0
  42. package/src/stories/Footer.stories.tsx +39 -0
  43. package/src/stories/Form.stories.tsx +28 -0
  44. package/src/stories/HoverCard.stories.tsx +41 -0
  45. package/src/stories/Image.stories.tsx +35 -0
  46. package/src/stories/Inform.stories.tsx +39 -0
  47. package/src/stories/InputGroup.stories.tsx +36 -0
  48. package/src/stories/InputOTP.stories.tsx +27 -0
  49. package/src/stories/Label.stories.tsx +24 -0
  50. package/src/stories/List.stories.tsx +26 -0
  51. package/src/stories/Menubar.stories.tsx +38 -0
  52. package/src/stories/Metric.stories.tsx +32 -0
  53. package/src/stories/Modal.stories.tsx +31 -0
  54. package/src/stories/NavigationBar.stories.tsx +32 -0
  55. package/src/stories/NavigationMenu.stories.tsx +34 -0
  56. package/src/stories/NumberInput.stories.tsx +29 -0
  57. package/src/stories/Pagination.stories.tsx +30 -0
  58. package/src/stories/PasswordInput.stories.tsx +23 -0
  59. package/src/stories/Popover.stories.tsx +47 -0
  60. package/src/stories/Progress.stories.tsx +31 -0
  61. package/src/stories/Quote.stories.tsx +32 -0
  62. package/src/stories/RadioGroup.stories.tsx +45 -0
  63. package/src/stories/Rating.stories.tsx +35 -0
  64. package/src/stories/Resizable.stories.tsx +29 -0
  65. package/src/stories/ScrollArea.stories.tsx +25 -0
  66. package/src/stories/Select.stories.tsx +46 -0
  67. package/src/stories/Separator.stories.tsx +41 -0
  68. package/src/stories/Sheet.stories.tsx +31 -0
  69. package/src/stories/Sidebar.stories.tsx +25 -0
  70. package/src/stories/Skeleton.stories.tsx +27 -0
  71. package/src/stories/Slider.stories.tsx +24 -0
  72. package/src/stories/Sonner.stories.tsx +26 -0
  73. package/src/stories/Spinner.stories.tsx +33 -0
  74. package/src/stories/Stepper.stories.tsx +35 -0
  75. package/src/stories/Switch.stories.tsx +29 -0
  76. package/src/stories/TabBar.stories.tsx +35 -0
  77. package/src/stories/Table.stories.tsx +42 -0
  78. package/src/stories/TableOfContents.stories.tsx +31 -0
  79. package/src/stories/Tabs.stories.tsx +50 -0
  80. package/src/stories/Tag.stories.tsx +42 -0
  81. package/src/stories/Toggle.stories.tsx +34 -0
  82. package/src/stories/ToggleGroup.stories.tsx +37 -0
  83. package/src/stories/Tooltip.stories.tsx +42 -0
package/dist/index.js CHANGED
@@ -27,13 +27,20 @@ var buttonVariants = cva(
27
27
  variants: {
28
28
  variant: {
29
29
  // bg primary / text on-primary; hover+active step down the blue ramp; disabled -> border grey
30
- Primary: "bg-primary text-primary-foreground hover:bg-[--color-blue-600] active:bg-[--color-blue-700] disabled:bg-border disabled:text-muted-foreground",
31
- // rest = secondaryContainer; hover/active promote to full secondary with on-secondary text
32
- Secondary: "bg-secondary text-secondary-foreground hover:bg-[--color-green-500] hover:text-[--color-green-50] active:bg-[--color-green-600] active:text-[--color-green-50] focus-visible:bg-secondary disabled:bg-border disabled:text-muted-foreground",
33
- // 1px outline; hover fills with accent (LightBlue); active pins primary border+text
34
- Outline: "border border-input bg-transparent text-foreground hover:bg-accent active:border-primary active:bg-accent active:text-primary disabled:border-input disabled:text-muted-foreground disabled:bg-transparent",
30
+ // rest = --primary; hover/active dim it via opacity so it works in BOTH themes.
31
+ // (the old hover:bg-[--color-blue-600] was a hard-coded dark navy on dark mode,
32
+ // where --primary is light, it inverted the button into the near-black background.)
33
+ Primary: "bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80 disabled:bg-border disabled:text-muted-foreground",
34
+ // rest = --secondary; hover/active promote to the --secondary-foreground fill with
35
+ // inverted text — theme-aware, unlike the previous hard-coded green primitives
36
+ Secondary: "bg-secondary text-secondary-foreground hover:bg-secondary-foreground hover:text-secondary active:bg-secondary-foreground/90 active:text-secondary focus-visible:bg-secondary disabled:bg-border disabled:text-muted-foreground",
37
+ // 1px outline; hover fills with accent + strengthens the edge to --ring so the
38
+ // state stays visible on the darkest surfaces (WCAG 2.2 SC 1.4.11); active pins primary
39
+ Outline: "border border-input bg-transparent text-foreground hover:border-ring hover:bg-accent active:border-primary active:bg-accent active:text-primary disabled:border-input disabled:text-muted-foreground disabled:bg-transparent",
35
40
  Destructive: "bg-destructive text-destructive-foreground hover:brightness-95 active:brightness-90 disabled:bg-border disabled:text-muted-foreground",
36
- Ghost: "bg-transparent text-foreground hover:bg-accent active:bg-accent active:text-primary disabled:text-muted-foreground disabled:bg-transparent",
41
+ // transparent rest; hover adds an inset --ring outline (the accent fill alone is
42
+ // <3:1 against the page on dark) plus the accent fill; active pins primary text
43
+ Ghost: "bg-transparent text-foreground hover:bg-accent hover:ring-1 hover:ring-inset hover:ring-ring active:bg-accent active:text-primary active:ring-0 disabled:text-muted-foreground disabled:bg-transparent",
37
44
  Link: "bg-transparent text-primary underline-offset-4 hover:underline hover:text-foreground rounded-none px-0 disabled:text-muted-foreground disabled:no-underline"
38
45
  },
39
46
  size: {
@@ -63,13 +70,13 @@ var buttonVariants = cva(
63
70
  },
64
71
  compoundVariants: [
65
72
  { variant: "Destructive", class: "focus-visible:shadow-focus-destructive" },
66
- { variant: "Primary", state: "Hover", class: "bg-[--color-blue-600]" },
67
- { variant: "Primary", state: "Active", class: "bg-[--color-blue-700]" },
68
- { variant: "Secondary", state: "Hover", class: "bg-[--color-green-500] text-[--color-green-50]" },
69
- { variant: "Secondary", state: "Active", class: "bg-[--color-green-600] text-[--color-green-50]" },
70
- { variant: "Outline", state: "Hover", class: "bg-accent" },
73
+ { variant: "Primary", state: "Hover", class: "bg-primary/90" },
74
+ { variant: "Primary", state: "Active", class: "bg-primary/80" },
75
+ { variant: "Secondary", state: "Hover", class: "bg-secondary-foreground text-secondary" },
76
+ { variant: "Secondary", state: "Active", class: "bg-secondary-foreground/90 text-secondary" },
77
+ { variant: "Outline", state: "Hover", class: "border-ring bg-accent" },
71
78
  { variant: "Outline", state: "Active", class: "border-primary bg-accent text-primary" },
72
- { variant: "Ghost", state: "Hover", class: "bg-accent" },
79
+ { variant: "Ghost", state: "Hover", class: "bg-accent ring-1 ring-inset ring-ring" },
73
80
  { variant: "Ghost", state: "Active", class: "bg-accent text-primary" },
74
81
  { variant: "Link", state: "Hover", class: "underline text-foreground" },
75
82
  { variant: "Link", size: ["sm", "md", "lg"], class: "py-2 px-0 h-auto" }
@@ -596,7 +603,7 @@ var DropdownMenuPortal = DropdownMenuPrimitive.Portal;
596
603
  var DropdownMenuSub = DropdownMenuPrimitive.Sub;
597
604
  var DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
598
605
  var contentCls = "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";
599
- var itemCls = "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 data-[disabled]:pointer-events-none data-[disabled]:opacity-50";
606
+ var itemCls = "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";
600
607
  var DropdownMenuContent = React12.forwardRef(({ className, sideOffset = 4, ...props }, ref) => /* @__PURE__ */ jsx13(DropdownMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx13(DropdownMenuPrimitive.Content, { ref, sideOffset, className: cn(contentCls, className), ...props }) }));
601
608
  DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
602
609
  var DropdownMenuItem = React12.forwardRef(({ className, inset, ...props }, ref) => /* @__PURE__ */ jsx13(DropdownMenuPrimitive.Item, { ref, className: cn(itemCls, inset && "pl-8", className), ...props }));
@@ -886,7 +893,7 @@ var SelectItem = React20.forwardRef(({ className, children, ...props }, ref) =>
886
893
  ref,
887
894
  className: cn(
888
895
  "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none",
889
- "focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
896
+ "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",
890
897
  className
891
898
  ),
892
899
  ...props,
@@ -1262,7 +1269,7 @@ import * as TogglePrimitive from "@radix-ui/react-toggle";
1262
1269
  import { cva as cva9 } from "class-variance-authority";
1263
1270
  import { jsx as jsx32 } from "react/jsx-runtime";
1264
1271
  var toggleVariants = cva9(
1265
- "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium font-sans transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0",
1272
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium font-sans transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground data-[state=on]:ring-1 data-[state=on]:ring-inset data-[state=on]:ring-ring [&_svg]:size-4 [&_svg]:shrink-0",
1266
1273
  {
1267
1274
  variants: {
1268
1275
  variant: {
@@ -1729,7 +1736,7 @@ var ContextMenuGroup = ContextMenuPrimitive.Group;
1729
1736
  var ContextMenuPortal = ContextMenuPrimitive.Portal;
1730
1737
  var ContextMenuSub = ContextMenuPrimitive.Sub;
1731
1738
  var ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
1732
- var itemCls2 = "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 data-[disabled]:pointer-events-none data-[disabled]:opacity-50";
1739
+ var itemCls2 = "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";
1733
1740
  var contentCls2 = "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";
1734
1741
  var ContextMenuSubTrigger = React34.forwardRef(({ className, inset, children, ...props }, ref) => /* @__PURE__ */ jsxs17(
1735
1742
  ContextMenuPrimitive.SubTrigger,
@@ -2070,7 +2077,8 @@ var InputGroupButton = React39.forwardRef(({ className, type = "button", ...prop
2070
2077
  type,
2071
2078
  className: cn(
2072
2079
  "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",
2073
- "outline-none transition-colors hover:bg-accent focus-visible:bg-accent disabled:cursor-not-allowed disabled:opacity-50",
2080
+ // base is bg-muted and muted==accent on dark, so the fill is a no-op there — the ring carries it
2081
+ "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",
2074
2082
  "[&_svg]:size-4 [&_svg]:shrink-0",
2075
2083
  className
2076
2084
  ),
@@ -2157,7 +2165,7 @@ var MenubarGroup = (props) => /* @__PURE__ */ jsx47(MenubarPrimitive.Group, { ..
2157
2165
  var MenubarPortal = (props) => /* @__PURE__ */ jsx47(MenubarPrimitive.Portal, { ...props });
2158
2166
  var MenubarSub = (props) => /* @__PURE__ */ jsx47(MenubarPrimitive.Sub, { ...props });
2159
2167
  var MenubarRadioGroup = (props) => /* @__PURE__ */ jsx47(MenubarPrimitive.RadioGroup, { ...props });
2160
- var itemCls3 = "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 data-[disabled]:pointer-events-none data-[disabled]:opacity-50";
2168
+ var itemCls3 = "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";
2161
2169
  var contentCls3 = "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";
2162
2170
  var Menubar = React42.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx47(
2163
2171
  MenubarPrimitive.Root,
@@ -2173,7 +2181,7 @@ var MenubarTrigger = React42.forwardRef(({ className, ...props }, ref) => /* @__
2173
2181
  {
2174
2182
  ref,
2175
2183
  className: cn(
2176
- "flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
2184
+ "flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground focus:ring-1 focus:ring-inset focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:ring-1 data-[state=open]:ring-inset data-[state=open]:ring-ring",
2177
2185
  className
2178
2186
  ),
2179
2187
  ...props
@@ -3021,7 +3029,8 @@ var ListItem = React51.forwardRef(
3021
3029
  } : void 0,
3022
3030
  className: cn(
3023
3031
  "flex items-center gap-3 px-3 py-3 font-sans outline-none",
3024
- interactive && "cursor-pointer hover:bg-accent focus-visible:bg-accent",
3032
+ // ring keeps the highlighted row perceptible where the accent fill is <3:1 (dark)
3033
+ interactive && "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",
3025
3034
  disabled && "pointer-events-none opacity-50",
3026
3035
  className
3027
3036
  ),
@@ -3199,7 +3208,7 @@ var NavigationBar = React55.forwardRef(
3199
3208
  type: "button",
3200
3209
  onClick: onBack,
3201
3210
  "aria-label": "Back",
3202
- className: "flex size-9 items-center justify-center rounded-full text-foreground outline-none transition-colors hover:bg-accent focus-visible:bg-accent",
3211
+ className: "flex size-9 items-center justify-center rounded-full text-foreground 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",
3203
3212
  children: /* @__PURE__ */ jsx61(ChevronLeft3, { className: "size-5" })
3204
3213
  }
3205
3214
  )) }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinetixui/ui",
3
- "version": "0.1.0",
3
+ "version": "0.4.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": [
@@ -89,18 +89,26 @@
89
89
  "react-dom": ">=18"
90
90
  },
91
91
  "devDependencies": {
92
- "@kinetixui/tokens": "0.1.0",
92
+ "@kinetixui/tokens": "0.4.0",
93
+ "@testing-library/jest-dom": "^6.6.3",
94
+ "@testing-library/react": "^16.1.0",
95
+ "@testing-library/user-event": "^14.5.2",
93
96
  "@types/react": "^18.3.17",
94
97
  "@types/react-dom": "^18.3.5",
98
+ "@vitejs/plugin-react": "^4.3.4",
99
+ "jsdom": "^25.0.1",
95
100
  "react": "^18.3.1",
96
101
  "react-dom": "^18.3.1",
97
102
  "tailwindcss": "^3.4.17",
98
103
  "tsup": "^8.3.5",
99
- "typescript": "^5.7.2"
104
+ "typescript": "^5.7.2",
105
+ "vitest": "^2.1.8"
100
106
  },
101
107
  "scripts": {
102
108
  "build": "tsup src/index.ts --format esm --dts --external react --external react-dom",
103
109
  "lint": "eslint src",
104
- "typecheck": "tsc --noEmit"
110
+ "typecheck": "tsc --noEmit",
111
+ "test": "vitest run",
112
+ "test:watch": "vitest"
105
113
  }
106
114
  }
@@ -35,18 +35,25 @@ const buttonVariants = cva(
35
35
  variants: {
36
36
  variant: {
37
37
  // bg primary / text on-primary; hover+active step down the blue ramp; disabled -> border grey
38
+ // rest = --primary; hover/active dim it via opacity so it works in BOTH themes.
39
+ // (the old hover:bg-[--color-blue-600] was a hard-coded dark navy — on dark mode,
40
+ // where --primary is light, it inverted the button into the near-black background.)
38
41
  Primary:
39
- "bg-primary text-primary-foreground hover:bg-[--color-blue-600] active:bg-[--color-blue-700] disabled:bg-border disabled:text-muted-foreground",
40
- // rest = secondaryContainer; hover/active promote to full secondary with on-secondary text
42
+ "bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80 disabled:bg-border disabled:text-muted-foreground",
43
+ // rest = --secondary; hover/active promote to the --secondary-foreground fill with
44
+ // inverted text — theme-aware, unlike the previous hard-coded green primitives
41
45
  Secondary:
42
- "bg-secondary text-secondary-foreground hover:bg-[--color-green-500] hover:text-[--color-green-50] active:bg-[--color-green-600] active:text-[--color-green-50] focus-visible:bg-secondary disabled:bg-border disabled:text-muted-foreground",
43
- // 1px outline; hover fills with accent (LightBlue); active pins primary border+text
46
+ "bg-secondary text-secondary-foreground hover:bg-secondary-foreground hover:text-secondary active:bg-secondary-foreground/90 active:text-secondary focus-visible:bg-secondary disabled:bg-border disabled:text-muted-foreground",
47
+ // 1px outline; hover fills with accent + strengthens the edge to --ring so the
48
+ // state stays visible on the darkest surfaces (WCAG 2.2 SC 1.4.11); active pins primary
44
49
  Outline:
45
- "border border-input bg-transparent text-foreground hover:bg-accent active:border-primary active:bg-accent active:text-primary disabled:border-input disabled:text-muted-foreground disabled:bg-transparent",
50
+ "border border-input bg-transparent text-foreground hover:border-ring hover:bg-accent active:border-primary active:bg-accent active:text-primary disabled:border-input disabled:text-muted-foreground disabled:bg-transparent",
46
51
  Destructive:
47
52
  "bg-destructive text-destructive-foreground hover:brightness-95 active:brightness-90 disabled:bg-border disabled:text-muted-foreground",
53
+ // transparent rest; hover adds an inset --ring outline (the accent fill alone is
54
+ // <3:1 against the page on dark) plus the accent fill; active pins primary text
48
55
  Ghost:
49
- "bg-transparent text-foreground hover:bg-accent active:bg-accent active:text-primary disabled:text-muted-foreground disabled:bg-transparent",
56
+ "bg-transparent text-foreground hover:bg-accent hover:ring-1 hover:ring-inset hover:ring-ring active:bg-accent active:text-primary active:ring-0 disabled:text-muted-foreground disabled:bg-transparent",
50
57
  Link: "bg-transparent text-primary underline-offset-4 hover:underline hover:text-foreground rounded-none px-0 disabled:text-muted-foreground disabled:no-underline",
51
58
  },
52
59
  size: {
@@ -76,13 +83,13 @@ const buttonVariants = cva(
76
83
  },
77
84
  compoundVariants: [
78
85
  { variant: "Destructive", class: "focus-visible:shadow-focus-destructive" },
79
- { variant: "Primary", state: "Hover", class: "bg-[--color-blue-600]" },
80
- { variant: "Primary", state: "Active", class: "bg-[--color-blue-700]" },
81
- { variant: "Secondary", state: "Hover", class: "bg-[--color-green-500] text-[--color-green-50]" },
82
- { variant: "Secondary", state: "Active", class: "bg-[--color-green-600] text-[--color-green-50]" },
83
- { variant: "Outline", state: "Hover", class: "bg-accent" },
86
+ { variant: "Primary", state: "Hover", class: "bg-primary/90" },
87
+ { variant: "Primary", state: "Active", class: "bg-primary/80" },
88
+ { variant: "Secondary", state: "Hover", class: "bg-secondary-foreground text-secondary" },
89
+ { variant: "Secondary", state: "Active", class: "bg-secondary-foreground/90 text-secondary" },
90
+ { variant: "Outline", state: "Hover", class: "border-ring bg-accent" },
84
91
  { variant: "Outline", state: "Active", class: "border-primary bg-accent text-primary" },
85
- { variant: "Ghost", state: "Hover", class: "bg-accent" },
92
+ { variant: "Ghost", state: "Hover", class: "bg-accent ring-1 ring-inset ring-ring" },
86
93
  { variant: "Ghost", state: "Active", class: "bg-accent text-primary" },
87
94
  { variant: "Link", state: "Hover", class: "underline text-foreground" },
88
95
  { variant: "Link", size: ["sm", "md", "lg"], class: "py-2 px-0 h-auto" },
@@ -13,7 +13,7 @@ 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 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-50";
17
17
  const contentCls =
18
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";
19
19
 
@@ -15,7 +15,7 @@ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
15
15
  const contentCls =
16
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";
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 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-50";
19
19
 
20
20
  const DropdownMenuContent = React.forwardRef<
21
21
  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -84,7 +84,8 @@ const InputGroupButton = React.forwardRef<
84
84
  type={type}
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
- "outline-none transition-colors hover:bg-accent focus-visible:bg-accent disabled:cursor-not-allowed disabled:opacity-50",
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
89
  "[&_svg]:size-4 [&_svg]:shrink-0",
89
90
  className,
90
91
  )}
@@ -48,7 +48,9 @@ const ListItem = React.forwardRef<HTMLDivElement, ListItemProps>(
48
48
  }
49
49
  className={cn(
50
50
  "flex items-center gap-3 px-3 py-3 font-sans outline-none",
51
- interactive && "cursor-pointer hover:bg-accent focus-visible:bg-accent",
51
+ // ring keeps the highlighted row perceptible where the accent fill is <3:1 (dark)
52
+ interactive &&
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",
52
54
  disabled && "pointer-events-none opacity-50",
53
55
  className,
54
56
  )}
@@ -22,7 +22,7 @@ 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 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-50";
26
26
  const contentCls =
27
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";
28
28
 
@@ -45,7 +45,7 @@ const MenubarTrigger = React.forwardRef<
45
45
  <MenubarPrimitive.Trigger
46
46
  ref={ref}
47
47
  className={cn(
48
- "flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
48
+ "flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground focus:ring-1 focus:ring-inset focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:ring-1 data-[state=open]:ring-inset data-[state=open]:ring-ring",
49
49
  className,
50
50
  )}
51
51
  {...props}
@@ -30,7 +30,7 @@ const NavigationBar = React.forwardRef<HTMLElement, NavigationBarProps>(
30
30
  type="button"
31
31
  onClick={onBack}
32
32
  aria-label="Back"
33
- className="flex size-9 items-center justify-center rounded-full text-foreground outline-none transition-colors hover:bg-accent focus-visible:bg-accent"
33
+ className="flex size-9 items-center justify-center rounded-full text-foreground 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"
34
34
  >
35
35
  <ChevronLeft className="size-5" />
36
36
  </button>
@@ -89,7 +89,7 @@ const SelectItem = React.forwardRef<
89
89
  ref={ref}
90
90
  className={cn(
91
91
  "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none",
92
- "focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
92
+ "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",
93
93
  className,
94
94
  )}
95
95
  {...props}
@@ -6,7 +6,7 @@ import { cva, type VariantProps } from "class-variance-authority";
6
6
  import { cn } from "../lib/utils";
7
7
 
8
8
  const toggleVariants = cva(
9
- "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium font-sans transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0",
9
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium font-sans transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground data-[state=on]:ring-1 data-[state=on]:ring-inset data-[state=on]:ring-ring [&_svg]:size-4 [&_svg]:shrink-0",
10
10
  {
11
11
  variants: {
12
12
  variant: {
@@ -0,0 +1,74 @@
1
+ import * as React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { Badge } from "./components/badge";
6
+ import { Button } from "./components/button";
7
+ import { Input } from "./components/input";
8
+ import {
9
+ Accordion,
10
+ AccordionContent,
11
+ AccordionItem,
12
+ AccordionTrigger,
13
+ } from "./components/accordion";
14
+
15
+ describe("Button", () => {
16
+ it("renders the label and forwards clicks", async () => {
17
+ const onClick = vi.fn();
18
+ render(<Button onClick={onClick}>Save</Button>);
19
+ await userEvent.click(screen.getByRole("button", { name: "Save" }));
20
+ expect(onClick).toHaveBeenCalledOnce();
21
+ });
22
+
23
+ it("maps the design-source variant prop to a class", () => {
24
+ render(<Button variant="Destructive">x</Button>);
25
+ expect(screen.getByRole("button")).toHaveClass("bg-destructive");
26
+ });
27
+
28
+ it("renders as a child element with asChild", () => {
29
+ render(
30
+ <Button asChild>
31
+ <a href="/x">link</a>
32
+ </Button>,
33
+ );
34
+ expect(screen.getByRole("link", { name: "link" })).toHaveAttribute("href", "/x");
35
+ });
36
+ });
37
+
38
+ describe("Input", () => {
39
+ it("sets aria-invalid when state is Error", () => {
40
+ render(<Input state="Error" defaultValue="nope" />);
41
+ expect(screen.getByRole("textbox")).toHaveAttribute("aria-invalid", "true");
42
+ });
43
+
44
+ it("is not aria-invalid by default", () => {
45
+ render(<Input />);
46
+ expect(screen.getByRole("textbox")).not.toHaveAttribute("aria-invalid", "true");
47
+ });
48
+ });
49
+
50
+ describe("Badge", () => {
51
+ it("gives each variant a distinct class", () => {
52
+ const { rerender } = render(<Badge>tag</Badge>);
53
+ const def = screen.getByText("tag").className;
54
+ rerender(<Badge variant="secondary">tag</Badge>);
55
+ expect(screen.getByText("tag").className).not.toBe(def);
56
+ expect(screen.getByText("tag")).toHaveClass("text-secondary");
57
+ });
58
+ });
59
+
60
+ describe("Accordion", () => {
61
+ it("reveals content when a trigger is activated", async () => {
62
+ render(
63
+ <Accordion type="single" collapsible>
64
+ <AccordionItem value="a">
65
+ <AccordionTrigger>Question</AccordionTrigger>
66
+ <AccordionContent>Answer text</AccordionContent>
67
+ </AccordionItem>
68
+ </Accordion>,
69
+ );
70
+ expect(screen.queryByText("Answer text")).not.toBeInTheDocument();
71
+ await userEvent.click(screen.getByRole("button", { name: "Question" }));
72
+ expect(screen.getByText("Answer text")).toBeVisible();
73
+ });
74
+ });
@@ -0,0 +1,47 @@
1
+ import * as React from "react";
2
+ import { render, cleanup } from "@testing-library/react";
3
+ import { afterEach, describe, expect, it } from "vitest";
4
+
5
+ /**
6
+ * Every Storybook story is a real composition of a shipped component. Render
7
+ * each one in jsdom and assert it mounts without throwing — a cheap regression
8
+ * net across the whole library.
9
+ */
10
+ const stories = import.meta.glob("./stories/*.stories.tsx", { eager: true }) as Record<
11
+ string,
12
+ Record<string, unknown>
13
+ >;
14
+
15
+ // Stories that can't render meaningfully in jsdom (need real layout / canvas).
16
+ const SKIP = new Set<string>([]);
17
+
18
+ afterEach(cleanup);
19
+
20
+ describe("component smoke", () => {
21
+ const entries = Object.entries(stories).sort(([a], [b]) => a.localeCompare(b));
22
+ expect(entries.length).toBeGreaterThan(60);
23
+
24
+ for (const [path, mod] of entries) {
25
+ const name = path.split("/").pop()!.replace(".stories.tsx", "");
26
+ const meta = mod.default as { component?: React.ComponentType<unknown>; args?: Record<string, unknown> };
27
+
28
+ const storyKey = ["Default", "Playground", "Variants", "States"].find(
29
+ (k) => mod[k] && typeof mod[k] === "object",
30
+ );
31
+ const story = storyKey ? (mod[storyKey] as { render?: (...a: unknown[]) => React.ReactNode; args?: Record<string, unknown> }) : undefined;
32
+
33
+ (SKIP.has(name) ? it.skip : it)(`${name} mounts`, () => {
34
+ const args = { ...meta?.args, ...story?.args };
35
+ const node =
36
+ typeof story?.render === "function"
37
+ ? story.render(args, { args } as never)
38
+ : meta?.component
39
+ ? React.createElement(meta.component, args)
40
+ : null;
41
+
42
+ expect(node, `${name}: no renderable story export`).not.toBeNull();
43
+ const { container } = render(node as React.ReactElement);
44
+ expect(container).toBeInTheDocument();
45
+ });
46
+ }
47
+ });
@@ -0,0 +1,49 @@
1
+ /* AUTO-GENERATED by scripts/gen-stories.mjs — do not edit.
2
+ Source of truth: apps/web/src/registry/demos.tsx · controls: scripts/gen-stories.mjs CONTROLS */
3
+ import * as React from "react";
4
+ import type { Meta, StoryObj } from "@storybook/react";
5
+ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@kinetixui/ui";
6
+
7
+ const Demo = () => (
8
+ <Accordion type="single" collapsible className="w-full max-w-md">
9
+ <AccordionItem value="a">
10
+ <AccordionTrigger>Is it accessible?</AccordionTrigger>
11
+ <AccordionContent>Yes. It follows the WAI-ARIA design pattern.</AccordionContent>
12
+ </AccordionItem>
13
+ <AccordionItem value="b">
14
+ <AccordionTrigger>Is it themed?</AccordionTrigger>
15
+ <AccordionContent>Yes — entirely from the KinetixUI token contract.</AccordionContent>
16
+ </AccordionItem>
17
+ </Accordion>
18
+ );
19
+
20
+ const meta = {
21
+ title: "Data Display/Accordion",
22
+ component: Accordion,
23
+ args: { type: "single", collapsible: true },
24
+ argTypes: {
25
+ type: { control: "inline-radio", options: ["single", "multiple"] },
26
+ collapsible: { control: "boolean" },
27
+ },
28
+ parameters: {
29
+ layout: "centered",
30
+ docs: { source: { code: `<Accordion type="single" collapsible>\n <AccordionItem value="a">\n <AccordionTrigger>Is it accessible?</AccordionTrigger>\n <AccordionContent>Yes. It follows the WAI-ARIA design pattern.</AccordionContent>\n </AccordionItem>\n</Accordion>`, language: "tsx" } },
31
+ },
32
+ } satisfies Meta;
33
+
34
+ export default meta;
35
+
36
+ export const Playground: StoryObj<typeof meta> = { render: (args) => (
37
+ <Accordion {...args} className="w-full max-w-md">
38
+ <AccordionItem value="a">
39
+ <AccordionTrigger>Is it accessible?</AccordionTrigger>
40
+ <AccordionContent>Yes. It follows the WAI-ARIA design pattern.</AccordionContent>
41
+ </AccordionItem>
42
+ <AccordionItem value="b">
43
+ <AccordionTrigger>Is it themed?</AccordionTrigger>
44
+ <AccordionContent>Yes — entirely from the KinetixUI token contract.</AccordionContent>
45
+ </AccordionItem>
46
+ </Accordion>
47
+ ) };
48
+
49
+ export const Default: StoryObj<typeof meta> = { render: () => <Demo /> };
@@ -0,0 +1,38 @@
1
+ /* AUTO-GENERATED by scripts/gen-stories.mjs — do not edit.
2
+ Source of truth: apps/web/src/registry/demos.tsx · controls: scripts/gen-stories.mjs CONTROLS */
3
+ import * as React from "react";
4
+ import type { Meta, StoryObj } from "@storybook/react";
5
+ import { Alert, AlertDescription, AlertTitle } from "@kinetixui/ui";
6
+ import { Terminal } from "lucide-react";
7
+
8
+ const Demo = () => (
9
+ <Alert className="max-w-md">
10
+ <Terminal className="size-4" />
11
+ <AlertTitle>Heads up!</AlertTitle>
12
+ <AlertDescription>You can add components to your app using the CLI.</AlertDescription>
13
+ </Alert>
14
+ );
15
+
16
+ const meta = {
17
+ title: "Feedback/Alert",
18
+ component: Alert,
19
+ args: { variant: "default" },
20
+ argTypes: {
21
+ variant: { control: "inline-radio", options: ["default", "destructive", "success", "warning", "info"] },
22
+ },
23
+ parameters: {
24
+ layout: "centered",
25
+ docs: { source: { code: `<Alert>\n <Terminal className="size-4" />\n <AlertTitle>Heads up!</AlertTitle>\n <AlertDescription>You can add components to your app using the CLI.</AlertDescription>\n</Alert>`, language: "tsx" } },
26
+ },
27
+ } satisfies Meta;
28
+
29
+ export default meta;
30
+
31
+ export const Playground: StoryObj<typeof meta> = { render: (args) => (
32
+ <Alert {...args} className="max-w-md">
33
+ <AlertTitle>Heads up!</AlertTitle>
34
+ <AlertDescription>You can add components to your app using the CLI.</AlertDescription>
35
+ </Alert>
36
+ ) };
37
+
38
+ export const Default: StoryObj<typeof meta> = { render: () => <Demo /> };
@@ -0,0 +1,35 @@
1
+ /* AUTO-GENERATED by scripts/gen-stories.mjs — do not edit.
2
+ Source of truth: apps/web/src/registry/demos.tsx */
3
+ import * as React from "react";
4
+ import type { Meta, StoryObj } from "@storybook/react";
5
+ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, Button } from "@kinetixui/ui";
6
+
7
+ const Demo = () => (
8
+ <AlertDialog>
9
+ <AlertDialogTrigger asChild>
10
+ <Button variant="Outline">Delete account</Button>
11
+ </AlertDialogTrigger>
12
+ <AlertDialogContent>
13
+ <AlertDialogHeader>
14
+ <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
15
+ <AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
16
+ </AlertDialogHeader>
17
+ <AlertDialogFooter>
18
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
19
+ <AlertDialogAction>Continue</AlertDialogAction>
20
+ </AlertDialogFooter>
21
+ </AlertDialogContent>
22
+ </AlertDialog>
23
+ );
24
+
25
+ const meta = {
26
+ title: "Overlays/AlertDialog",
27
+ parameters: {
28
+ layout: "centered",
29
+ docs: { source: { code: `<AlertDialog>\n <AlertDialogTrigger asChild><Button variant="Outline">Delete account</Button></AlertDialogTrigger>\n <AlertDialogContent>…</AlertDialogContent>\n</AlertDialog>`, language: "tsx" } },
30
+ },
31
+ } satisfies Meta;
32
+
33
+ export default meta;
34
+
35
+ export const Default: StoryObj<typeof meta> = { render: () => <Demo /> };
@@ -0,0 +1,32 @@
1
+ /* AUTO-GENERATED by scripts/gen-stories.mjs — do not edit.
2
+ Source of truth: apps/web/src/registry/demos.tsx · controls: scripts/gen-stories.mjs CONTROLS */
3
+ import * as React from "react";
4
+ import type { Meta, StoryObj } from "@storybook/react";
5
+ import { AspectRatio } from "@kinetixui/ui";
6
+
7
+ const Demo = () => (
8
+ <div className="w-[280px]">
9
+ <AspectRatio ratio={16 / 9} className="rounded-md bg-muted" />
10
+ </div>
11
+ );
12
+
13
+ const meta = {
14
+ title: "Foundations/AspectRatio",
15
+ component: AspectRatio,
16
+ args: { ratio: 1.7778 },
17
+ argTypes: { ratio: { control: { type: "number", step: 0.05 } } },
18
+ parameters: {
19
+ layout: "centered",
20
+ docs: { source: { code: `<AspectRatio ratio={16 / 9} className="bg-muted rounded-md" />`, language: "tsx" } },
21
+ },
22
+ } satisfies Meta;
23
+
24
+ export default meta;
25
+
26
+ export const Playground: StoryObj<typeof meta> = { render: (args) => (
27
+ <div className="w-64">
28
+ <AspectRatio {...args} className="rounded-md bg-muted" />
29
+ </div>
30
+ ) };
31
+
32
+ export const Default: StoryObj<typeof meta> = { render: () => <Demo /> };