@krizaka/ui 2.0.0-beta.2 → 2.0.0-beta.3

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 (184) hide show
  1. package/README.md +30 -0
  2. package/dist/alert.d.ts +2 -0
  3. package/dist/alert.js.map +1 -1
  4. package/dist/avatar.d.ts +5 -0
  5. package/dist/avatar.js.map +1 -1
  6. package/dist/badge.d.ts +12 -0
  7. package/dist/badge.js.map +1 -1
  8. package/dist/{button-G5qY4qSY.d.ts → button-8La7l0V4.d.ts} +15 -0
  9. package/dist/button.d.ts +19 -0
  10. package/dist/button.js.map +1 -1
  11. package/dist/card.d.ts +24 -1
  12. package/dist/card.js.map +1 -1
  13. package/dist/chip.d.ts +23 -0
  14. package/dist/chip.js.map +1 -1
  15. package/dist/{chunk-5FRYKFTO.js → chunk-3X5MVH54.js} +2 -2
  16. package/dist/chunk-3X5MVH54.js.map +1 -0
  17. package/dist/{chunk-QRG5RYEI.js → chunk-ZUD4U22H.js} +1 -1
  18. package/dist/chunk-ZUD4U22H.js.map +1 -0
  19. package/dist/command.d.ts +4 -1
  20. package/dist/command.js +2 -2
  21. package/dist/command.js.map +1 -1
  22. package/dist/confirm-button.d.ts +1 -1
  23. package/dist/confirm-button.js +1 -1
  24. package/dist/countdown.d.ts +3 -0
  25. package/dist/countdown.js.map +1 -1
  26. package/dist/{dialog-DGBY2grk.d.ts → dialog-GQ4Nvuyf.d.ts} +10 -0
  27. package/dist/dialog.d.ts +9 -1
  28. package/dist/dialog.js +2 -2
  29. package/dist/dialog.js.map +1 -1
  30. package/dist/dropdown-menu.d.ts +1 -0
  31. package/dist/dropdown-menu.js.map +1 -1
  32. package/dist/empty-state.d.ts +2 -0
  33. package/dist/empty-state.js.map +1 -1
  34. package/dist/kbd.d.ts +7 -1
  35. package/dist/kbd.js.map +1 -1
  36. package/dist/page-header.d.ts +2 -0
  37. package/dist/page-header.js.map +1 -1
  38. package/dist/progress.d.ts +5 -0
  39. package/dist/progress.js.map +1 -1
  40. package/dist/skeleton.d.ts +7 -1
  41. package/dist/skeleton.js.map +1 -1
  42. package/dist/slider.d.ts +2 -0
  43. package/dist/slider.js.map +1 -1
  44. package/dist/spinner.d.ts +5 -0
  45. package/dist/spinner.js.map +1 -1
  46. package/dist/stat.d.ts +2 -0
  47. package/dist/stat.js.map +1 -1
  48. package/dist/switch.d.ts +7 -3
  49. package/dist/switch.js.map +1 -1
  50. package/dist/tabs.d.ts +4 -1
  51. package/dist/tabs.js.map +1 -1
  52. package/dist/theme.d.ts +10 -2
  53. package/dist/theme.js +4 -2
  54. package/dist/theme.js.map +1 -1
  55. package/dist/tooltip.d.ts +3 -0
  56. package/dist/tooltip.js.map +1 -1
  57. package/package.json +14 -3
  58. package/registry/alert.json +59 -0
  59. package/registry/avatar.json +114 -0
  60. package/registry/badge.json +59 -0
  61. package/registry/button.json +115 -0
  62. package/registry/card.json +154 -0
  63. package/registry/checkbox.json +46 -0
  64. package/registry/chip.json +164 -0
  65. package/registry/cn.json +21 -0
  66. package/registry/command.json +162 -0
  67. package/registry/confirm-button.json +104 -0
  68. package/registry/countdown.json +84 -0
  69. package/registry/demos/alert.d.ts +5 -0
  70. package/registry/demos/alert.js +12 -0
  71. package/registry/demos/alert.tsx +15 -0
  72. package/registry/demos/avatar.d.ts +5 -0
  73. package/registry/demos/avatar.js +14 -0
  74. package/registry/demos/avatar.tsx +13 -0
  75. package/registry/demos/badge.d.ts +5 -0
  76. package/registry/demos/badge.js +11 -0
  77. package/registry/demos/badge.tsx +5 -0
  78. package/registry/demos/button.d.ts +5 -0
  79. package/registry/demos/button.js +11 -0
  80. package/registry/demos/button.tsx +5 -0
  81. package/registry/demos/card.d.ts +5 -0
  82. package/registry/demos/card.js +30 -0
  83. package/registry/demos/card.tsx +37 -0
  84. package/registry/demos/checkbox.d.ts +5 -0
  85. package/registry/demos/checkbox.js +11 -0
  86. package/registry/demos/checkbox.tsx +5 -0
  87. package/registry/demos/chip.d.ts +5 -0
  88. package/registry/demos/chip.js +15 -0
  89. package/registry/demos/chip.tsx +11 -0
  90. package/registry/demos/cn.d.ts +7 -0
  91. package/registry/demos/cn.js +17 -0
  92. package/registry/demos/cn.tsx +9 -0
  93. package/registry/demos/command.d.ts +5 -0
  94. package/registry/demos/command.js +32 -0
  95. package/registry/demos/command.tsx +28 -0
  96. package/registry/demos/confirm-button.d.ts +5 -0
  97. package/registry/demos/confirm-button.js +12 -0
  98. package/registry/demos/confirm-button.tsx +13 -0
  99. package/registry/demos/countdown.d.ts +5 -0
  100. package/registry/demos/countdown.js +13 -0
  101. package/registry/demos/countdown.tsx +10 -0
  102. package/registry/demos/dialog.d.ts +7 -0
  103. package/registry/demos/dialog.js +29 -0
  104. package/registry/demos/dialog.tsx +31 -0
  105. package/registry/demos/dropdown-menu.d.ts +7 -0
  106. package/registry/demos/dropdown-menu.js +36 -0
  107. package/registry/demos/dropdown-menu.tsx +27 -0
  108. package/registry/demos/empty-state.d.ts +5 -0
  109. package/registry/demos/empty-state.js +15 -0
  110. package/registry/demos/empty-state.tsx +12 -0
  111. package/registry/demos/field.d.ts +5 -0
  112. package/registry/demos/field.js +15 -0
  113. package/registry/demos/field.tsx +11 -0
  114. package/registry/demos/kbd.d.ts +5 -0
  115. package/registry/demos/kbd.js +11 -0
  116. package/registry/demos/kbd.tsx +5 -0
  117. package/registry/demos/page-header.d.ts +5 -0
  118. package/registry/demos/page-header.js +11 -0
  119. package/registry/demos/page-header.tsx +5 -0
  120. package/registry/demos/popover.d.ts +7 -0
  121. package/registry/demos/popover.js +25 -0
  122. package/registry/demos/popover.tsx +31 -0
  123. package/registry/demos/progress.d.ts +5 -0
  124. package/registry/demos/progress.js +11 -0
  125. package/registry/demos/progress.tsx +5 -0
  126. package/registry/demos/radio-group.d.ts +5 -0
  127. package/registry/demos/radio-group.js +16 -0
  128. package/registry/demos/radio-group.tsx +14 -0
  129. package/registry/demos/separator.d.ts +5 -0
  130. package/registry/demos/separator.js +15 -0
  131. package/registry/demos/separator.tsx +11 -0
  132. package/registry/demos/skeleton.d.ts +5 -0
  133. package/registry/demos/skeleton.js +20 -0
  134. package/registry/demos/skeleton.tsx +16 -0
  135. package/registry/demos/slider.d.ts +5 -0
  136. package/registry/demos/slider.js +11 -0
  137. package/registry/demos/slider.tsx +5 -0
  138. package/registry/demos/slot.d.ts +5 -0
  139. package/registry/demos/slot.js +14 -0
  140. package/registry/demos/slot.tsx +13 -0
  141. package/registry/demos/spinner.d.ts +5 -0
  142. package/registry/demos/spinner.js +11 -0
  143. package/registry/demos/spinner.tsx +5 -0
  144. package/registry/demos/stat.d.ts +5 -0
  145. package/registry/demos/stat.js +11 -0
  146. package/registry/demos/stat.tsx +5 -0
  147. package/registry/demos/switch.d.ts +5 -0
  148. package/registry/demos/switch.js +11 -0
  149. package/registry/demos/switch.tsx +5 -0
  150. package/registry/demos/tabs.d.ts +5 -0
  151. package/registry/demos/tabs.js +22 -0
  152. package/registry/demos/tabs.tsx +28 -0
  153. package/registry/demos/theme.d.ts +5 -0
  154. package/registry/demos/theme.js +11 -0
  155. package/registry/demos/theme.tsx +9 -0
  156. package/registry/demos/toast.d.ts +8 -0
  157. package/registry/demos/toast.js +29 -0
  158. package/registry/demos/toast.tsx +31 -0
  159. package/registry/demos/tooltip.d.ts +7 -0
  160. package/registry/demos/tooltip.js +15 -0
  161. package/registry/demos/tooltip.tsx +15 -0
  162. package/registry/dialog.json +218 -0
  163. package/registry/dropdown-menu.json +63 -0
  164. package/registry/empty-state.json +59 -0
  165. package/registry/field.json +66 -0
  166. package/registry/index.json +320 -0
  167. package/registry/kbd.json +38 -0
  168. package/registry/page-header.json +66 -0
  169. package/registry/popover.json +44 -0
  170. package/registry/progress.json +80 -0
  171. package/registry/radio-group.json +64 -0
  172. package/registry/separator.json +25 -0
  173. package/registry/skeleton.json +38 -0
  174. package/registry/slider.json +95 -0
  175. package/registry/slot.json +31 -0
  176. package/registry/spinner.json +45 -0
  177. package/registry/stat.json +66 -0
  178. package/registry/switch.json +53 -0
  179. package/registry/tabs.json +44 -0
  180. package/registry/theme.json +78 -0
  181. package/registry/toast.json +54 -0
  182. package/registry/tooltip.json +91 -0
  183. package/dist/chunk-5FRYKFTO.js.map +0 -1
  184. package/dist/chunk-QRG5RYEI.js.map +0 -1
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "radio-group",
3
+ "type": "primitive",
4
+ "description": "`RadioGroup.Root/Item/Card`, `radioGroup` (the slots). Client (\"use client\").",
5
+ "files": [
6
+ {
7
+ "path": "radio-group/index.ts",
8
+ "content": "/** @krizaka/ui/radio-group — `RadioGroup.Root/Item/Card`, `radioGroup` (the slots). Client (\"use client\"). */\nexport {\n RadioGroup,\n radioGroup,\n RadioGroupCard,\n RadioGroupItem,\n type RadioGroupItemProps,\n RadioGroupRoot,\n type RadioGroupRootProps,\n} from \"./radio-group\";\n"
9
+ },
10
+ {
11
+ "path": "radio-group/radio-group.tsx",
12
+ "content": "// Client (a context). Radix RadioGroup: role=\"radiogroup\" / \"radio\", one tab stop, the arrows move and select, a\n// hidden input inside a form. Two looks: `RadioGroup.Item` (a dot and its words) and `RadioGroup.Card` (a whole card\n// is the radio: an amount, a pack, a payment method, a plan).\nimport { RadioGroup as RadioGroupPrimitive } from \"radix-ui\";\nimport type * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nexport const radioGroup = tv({\n slots: {\n root: \"grid gap-2 data-[orientation=horizontal]:flex data-[orientation=horizontal]:flex-wrap\",\n item:\n \"peer inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-border-strong bg-surface-1 transition-colors \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 \" +\n \"hover:border-accent disabled:cursor-not-allowed disabled:opacity-40 data-[state=checked]:border-accent aria-invalid:border-danger\",\n dot: \"h-2.5 w-2.5 rounded-full bg-accent\",\n label: \"flex cursor-pointer items-start gap-3 text-sm text-fg has-disabled:cursor-not-allowed has-disabled:opacity-60\",\n card:\n \"relative flex min-w-0 flex-col items-start gap-1 rounded-xl border border-border-default bg-surface-1 p-3 text-left text-sm text-fg transition-colors \" +\n \"hover:border-border-strong focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring \" +\n \"disabled:cursor-not-allowed disabled:opacity-40 data-[state=checked]:border-accent data-[state=checked]:bg-accent-soft \" +\n \"data-[state=checked]:shadow-sm\",\n },\n});\nconst s = radioGroup();\n\nexport type RadioGroupRootProps = React.ComponentProps<typeof RadioGroupPrimitive.Root> & {\n /** The group's accessible name — passed translated (or `aria-labelledby` a visible heading). */\n label?: string;\n /** Marks every radio invalid: aria-invalid (pair it with a `Field.Error`). */\n invalid?: boolean;\n};\n\n/** `value`/`defaultValue` + `onValueChange`, `orientation`, `required`, `name` for a form. */\nexport function RadioGroupRoot({ label, invalid, className, ...props }: RadioGroupRootProps) {\n return (\n <RadioGroupPrimitive.Root\n aria-label={label}\n aria-invalid={invalid || undefined}\n data-invalid={invalid ? \"\" : undefined}\n className={s.root({ className })}\n {...props}\n />\n );\n}\n\nexport type RadioGroupItemProps = Omit<React.ComponentProps<typeof RadioGroupPrimitive.Item>, \"children\"> & {\n /** The words next to the dot, clickable. Otherwise name it with `Field.Label htmlFor` or `aria-label`. */\n children?: React.ReactNode;\n};\n\n/** A radio: a dot, and its words when given. */\nexport function RadioGroupItem({ className, children, ...props }: RadioGroupItemProps) {\n const item = (\n <RadioGroupPrimitive.Item className={s.item({ className: children ? \"mt-0.5\" : className })} {...props}>\n <RadioGroupPrimitive.Indicator className={s.dot()} />\n </RadioGroupPrimitive.Item>\n );\n if (!children) return item;\n return (\n <label className={s.label({ className })}>\n {item}\n <span className=\"min-w-0\">{children}</span>\n </label>\n );\n}\n\n/** A whole card as the radio: its content is the radio's name. `data-state=\"checked\"` styles the chosen one. */\nexport function RadioGroupCard({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {\n return <RadioGroupPrimitive.Item className={s.card({ className })} {...props} />;\n}\n\n/** `RadioGroup.Root/Item/Card`. */\nexport const RadioGroup = { Root: RadioGroupRoot, Item: RadioGroupItem, Card: RadioGroupCard };\n"
13
+ }
14
+ ],
15
+ "dependencies": [
16
+ "radix-ui@^1.7.0",
17
+ "tailwind-variants@^3.3.1"
18
+ ],
19
+ "registryDependencies": [],
20
+ "demo": {
21
+ "path": "demos/radio-group.tsx",
22
+ "content": "import { RadioGroup } from \"@krizaka/ui/radio-group\";\n\nexport default function RadioGroupDemo() {\n return (\n <RadioGroup.Root label=\"Notification frequency\" defaultValue=\"daily\">\n <RadioGroup.Item value=\"instant\">Instantly</RadioGroup.Item>\n <RadioGroup.Item value=\"daily\">Once a day</RadioGroup.Item>\n <RadioGroup.Item value=\"weekly\">Once a week</RadioGroup.Item>\n <RadioGroup.Item value=\"never\" disabled>\n Never\n </RadioGroup.Item>\n </RadioGroup.Root>\n );\n}\n"
23
+ },
24
+ "props": [
25
+ {
26
+ "component": "RadioGroupRoot",
27
+ "description": "`value`/`defaultValue` + `onValueChange`, `orientation`, `required`, `name` for a form.",
28
+ "props": [
29
+ {
30
+ "name": "invalid",
31
+ "type": "boolean",
32
+ "default": null,
33
+ "description": "Marks every radio invalid: aria-invalid (pair it with a `Field.Error`).",
34
+ "required": false
35
+ },
36
+ {
37
+ "name": "label",
38
+ "type": "string",
39
+ "default": null,
40
+ "description": "The group's accessible name — passed translated (or `aria-labelledby` a visible heading).",
41
+ "required": false
42
+ }
43
+ ]
44
+ },
45
+ {
46
+ "component": "RadioGroupItem",
47
+ "description": "A radio: a dot, and its words when given.",
48
+ "props": [
49
+ {
50
+ "name": "children",
51
+ "type": "ReactNode",
52
+ "default": null,
53
+ "description": "The words next to the dot, clickable. Otherwise name it with `Field.Label htmlFor` or `aria-label`.",
54
+ "required": false
55
+ }
56
+ ]
57
+ },
58
+ {
59
+ "component": "RadioGroupCard",
60
+ "description": "A whole card as the radio: its content is the radio's name. `data-state=\"checked\"` styles the chosen one.",
61
+ "props": []
62
+ }
63
+ ]
64
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "separator",
3
+ "type": "primitive",
4
+ "description": "`Separator`, `separatorVariants` (Radix). Server-safe.",
5
+ "files": [
6
+ {
7
+ "path": "separator/index.ts",
8
+ "content": "/** @krizaka/ui/separator — `Separator`, `separatorVariants` (Radix). Server-safe. */\nexport { Separator, type SeparatorProps, separatorVariants } from \"./separator\";\n"
9
+ },
10
+ {
11
+ "path": "separator/separator.tsx",
12
+ "content": "// Server-safe. A line between groups: decorative by default (hidden from assistive technology), or a real\n// `role=\"separator\"` with `decorative={false}`.\nimport { Separator as SeparatorPrimitive } from \"radix-ui\";\nimport type * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nexport const separatorVariants = tv({\n base: \"shrink-0 bg-border-default\",\n variants: { orientation: { horizontal: \"h-px w-full\", vertical: \"h-full w-px self-stretch\" } },\n defaultVariants: { orientation: \"horizontal\" },\n});\n\nexport type SeparatorProps = React.ComponentProps<typeof SeparatorPrimitive.Root>;\n\nexport function Separator({ orientation = \"horizontal\", decorative = true, className, ...props }: SeparatorProps) {\n return <SeparatorPrimitive.Root orientation={orientation} decorative={decorative} className={separatorVariants({ orientation, className })} {...props} />;\n}\n"
13
+ }
14
+ ],
15
+ "dependencies": [
16
+ "radix-ui@^1.7.0",
17
+ "tailwind-variants@^3.3.1"
18
+ ],
19
+ "registryDependencies": [],
20
+ "demo": {
21
+ "path": "demos/separator.tsx",
22
+ "content": "import { Separator } from \"@krizaka/ui/separator\";\n\nexport default function SeparatorDemo() {\n return (\n <div className=\"flex w-72 flex-col gap-3 text-sm text-fg\">\n <p>Account</p>\n <Separator />\n <p className=\"text-fg-secondary\">Notifications</p>\n </div>\n );\n}\n"
23
+ },
24
+ "props": []
25
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "skeleton",
3
+ "type": "primitive",
4
+ "description": "`Skeleton`, `skeletonVariants`. Server-safe.",
5
+ "files": [
6
+ {
7
+ "path": "skeleton/index.ts",
8
+ "content": "/** @krizaka/ui/skeleton — `Skeleton`, `skeletonVariants`. Server-safe. */\nexport { Skeleton, type SkeletonProps, skeletonVariants } from \"./skeleton\";\n"
9
+ },
10
+ {
11
+ "path": "skeleton/skeleton.tsx",
12
+ "content": "// Server-safe. A placeholder for content on its way: decorative, so hidden from assistive technology.\nimport type * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nexport const skeletonVariants = tv({\n base: \"block bg-surface-3 motion-safe:animate-pulse\",\n variants: {\n shape: { text: \"h-3 w-full rounded-sm\", circle: \"aspect-square h-10 rounded-full\", rect: \"h-24 w-full rounded-lg\" },\n },\n defaultVariants: { shape: \"text\" },\n});\n\nexport type SkeletonProps = React.ComponentProps<\"span\"> & {\n /**\n * text (a line) · circle (an avatar; give it a height) · rect (a media; give it a height).\n * @default \"text\"\n */\n shape?: VariantProps<typeof skeletonVariants>[\"shape\"];\n};\n\n/** A pulsing shape the size of what is loading. Announce the wait elsewhere (aria-busy on the region, a Spinner). */\nexport function Skeleton({ shape, className, ...props }: SkeletonProps) {\n return <span aria-hidden data-shape={shape ?? \"text\"} className={skeletonVariants({ shape, className })} {...props} />;\n}\n"
13
+ }
14
+ ],
15
+ "dependencies": [
16
+ "tailwind-variants@^3.3.1"
17
+ ],
18
+ "registryDependencies": [],
19
+ "demo": {
20
+ "path": "demos/skeleton.tsx",
21
+ "content": "import { Skeleton } from \"@krizaka/ui/skeleton\";\n\nexport default function SkeletonDemo() {\n return (\n <div className=\"flex flex-col gap-3 rounded-xl border border-border-default bg-surface-1 p-4\">\n <Skeleton shape=\"rect\" className=\"h-32\" />\n <div className=\"flex items-center gap-3\">\n <Skeleton shape=\"circle\" className=\"h-8\" />\n <div className=\"flex flex-1 flex-col gap-2\">\n <Skeleton />\n <Skeleton className=\"w-2/3\" />\n </div>\n </div>\n </div>\n );\n}\n"
22
+ },
23
+ "props": [
24
+ {
25
+ "component": "Skeleton",
26
+ "description": "A pulsing shape the size of what is loading. Announce the wait elsewhere (aria-busy on the region, a Spinner).",
27
+ "props": [
28
+ {
29
+ "name": "shape",
30
+ "type": "\"circle\" | \"rect\" | \"text\"",
31
+ "default": "text",
32
+ "description": "text (a line) · circle (an avatar; give it a height) · rect (a media; give it a height).",
33
+ "required": false
34
+ }
35
+ ]
36
+ }
37
+ ]
38
+ }
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "slider",
3
+ "type": "primitive",
4
+ "description": "`Slider` (one value or a range), `slider` (the slots). Client (\"use client\").",
5
+ "files": [
6
+ {
7
+ "path": "slider/index.ts",
8
+ "content": "/** @krizaka/ui/slider — `Slider` (one value or a range), `slider` (the slots). Client (\"use client\"). */\nexport { Slider, slider, type SliderProps, type SliderValue } from \"./slider\";\n"
9
+ },
10
+ {
11
+ "path": "slider/slider.tsx",
12
+ "content": "// Client. Radix Slider: role=\"slider\" on each thumb, arrows / Page Up / Page Down / Home / End, a range with two\n// thumbs. The words arrive as props: `label` names the thumb(s), `formatValue` gives the aria-valuetext and the value\n// shown next to a visible label.\nimport { Slider as SliderPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nexport const slider = tv({\n slots: {\n root: \"flex flex-col gap-2\",\n header: \"flex items-baseline justify-between gap-3 text-xs font-semibold text-fg-secondary\",\n value: \"font-mono text-[11px] tabular-nums text-fg-secondary\",\n control:\n \"relative flex h-6 w-full touch-none select-none items-center data-disabled:cursor-not-allowed data-disabled:opacity-40\",\n track: \"relative h-1.5 w-full grow overflow-hidden rounded-full bg-surface-3\",\n range: \"absolute h-full rounded-full bg-linear-to-r from-accent to-accent-2\",\n thumb:\n \"block h-5 w-5 cursor-grab rounded-full border border-border-strong bg-fg-on-media shadow-md transition-[box-shadow] \" +\n \"focus-visible:outline-hidden focus-visible:ring-4 focus-visible:ring-ring/50 active:cursor-grabbing\",\n },\n});\n\nexport type SliderValue = number | [number, number];\n\nexport type SliderProps<V extends SliderValue = number> = Omit<\n React.ComponentProps<typeof SliderPrimitive.Root>,\n \"value\" | \"defaultValue\" | \"onValueChange\" | \"onValueCommit\" | \"children\"\n> & {\n /** One number (a thumb) or two (a range). Controlled with `onValueChange`. */\n value?: V;\n /** The value at first, uncontrolled: one number, or two for a range. */\n defaultValue?: V;\n /** Called on every move, with one number or two. */\n onValueChange?: (value: V) => void;\n /** Called once the drag or the key press ends. */\n onValueCommit?: (value: V) => void;\n /** The accessible name — of the thumb, or of both thumbs of a range — passed translated. */\n label: string;\n /** For a range: each thumb's own name ([\"Minimum\", \"Maximum\"]), passed translated. */\n thumbLabels?: [string, string];\n /** The value as words (aria-valuetext) and as shown: \"1.5×\", \"−12 %\", \"3 min\". */\n formatValue?: (value: number) => string;\n /** Shows the label and the value above the track. Without it, `label` only names the thumb. */\n showLabel?: boolean;\n /** Fills from this value instead of the minimum (a ±range from 0: brightness, balance). One thumb only. */\n origin?: number;\n};\n\nconst toArray = (v: SliderValue | undefined) => (v === undefined ? undefined : typeof v === \"number\" ? [v] : v);\n\n/** A slider on one value or a range: `value`/`defaultValue` + `onValueChange`, `min`, `max`, `step`. */\nexport function Slider<V extends SliderValue = number>({\n value,\n defaultValue,\n onValueChange,\n onValueCommit,\n label,\n thumbLabels,\n formatValue = String,\n showLabel,\n origin,\n min = 0,\n max = 100,\n className,\n ...props\n}: SliderProps<V>) {\n const s = slider();\n const range = Array.isArray(value ?? defaultValue);\n const [inner, setInner] = React.useState(() => toArray(defaultValue) ?? [min]);\n const current = toArray(value) ?? inner;\n const out = (next: number[]) => (range ? next : next[0]) as V;\n const pct = (v: number) => ((v - min) / (max - min || 1)) * 100;\n const fill =\n origin !== undefined && !range\n ? { left: `${Math.min(pct(origin), pct(current[0]))}%`, right: `${100 - Math.max(pct(origin), pct(current[0]))}%` }\n : undefined;\n const id = React.useId();\n return (\n <div className={s.root({ className })} data-range={range ? \"\" : undefined}>\n {showLabel && (\n <div className={s.header()}>\n <span id={`${id}-label`}>{label}</span>\n <span className={s.value()} aria-hidden>\n {current.map(formatValue).join(\" – \")}\n </span>\n </div>\n )}\n <SliderPrimitive.Root\n min={min}\n max={max}\n value={current}\n onValueChange={(next) => {\n if (value === undefined) setInner(next);\n onValueChange?.(out(next));\n }}\n onValueCommit={onValueCommit && ((next) => onValueCommit(out(next)))}\n className={s.control()}\n {...props}\n >\n <SliderPrimitive.Track className={s.track()}>\n {fill ? <span data-origin=\"\" className={s.range()} style={fill} /> : <SliderPrimitive.Range className={s.range()} />}\n </SliderPrimitive.Track>\n {current.map((v, i) => (\n <SliderPrimitive.Thumb\n key={i}\n aria-label={range ? (thumbLabels?.[i] ?? label) : showLabel ? undefined : label}\n aria-labelledby={!range && showLabel ? `${id}-label` : undefined}\n aria-valuetext={formatValue(v)}\n className={s.thumb()}\n />\n ))}\n </SliderPrimitive.Root>\n </div>\n );\n}\n"
13
+ }
14
+ ],
15
+ "dependencies": [
16
+ "radix-ui@^1.7.0",
17
+ "tailwind-variants@^3.3.1"
18
+ ],
19
+ "registryDependencies": [],
20
+ "demo": {
21
+ "path": "demos/slider.tsx",
22
+ "content": "import { Slider } from \"@krizaka/ui/slider\";\n\nexport default function SliderDemo() {\n return <Slider label=\"Volume\" defaultValue={60} />;\n}\n"
23
+ },
24
+ "props": [
25
+ {
26
+ "component": "Slider",
27
+ "description": "A slider on one value or a range: `value`/`defaultValue` + `onValueChange`, `min`, `max`, `step`.",
28
+ "props": [
29
+ {
30
+ "name": "label",
31
+ "type": "string",
32
+ "default": null,
33
+ "description": "The accessible name — of the thumb, or of both thumbs of a range — passed translated.",
34
+ "required": true
35
+ },
36
+ {
37
+ "name": "defaultValue",
38
+ "type": "SliderValue",
39
+ "default": null,
40
+ "description": "The value at first, uncontrolled: one number, or two for a range.",
41
+ "required": false
42
+ },
43
+ {
44
+ "name": "formatValue",
45
+ "type": "((value: number) => string)",
46
+ "default": null,
47
+ "description": "The value as words (aria-valuetext) and as shown: \"1.5×\", \"−12 %\", \"3 min\".",
48
+ "required": false
49
+ },
50
+ {
51
+ "name": "onValueChange",
52
+ "type": "((value: V) => void)",
53
+ "default": null,
54
+ "description": "Called on every move, with one number or two.",
55
+ "required": false
56
+ },
57
+ {
58
+ "name": "onValueCommit",
59
+ "type": "((value: V) => void)",
60
+ "default": null,
61
+ "description": "Called once the drag or the key press ends.",
62
+ "required": false
63
+ },
64
+ {
65
+ "name": "origin",
66
+ "type": "number",
67
+ "default": null,
68
+ "description": "Fills from this value instead of the minimum (a ±range from 0: brightness, balance). One thumb only.",
69
+ "required": false
70
+ },
71
+ {
72
+ "name": "showLabel",
73
+ "type": "boolean",
74
+ "default": null,
75
+ "description": "Shows the label and the value above the track. Without it, `label` only names the thumb.",
76
+ "required": false
77
+ },
78
+ {
79
+ "name": "thumbLabels",
80
+ "type": "[string, string]",
81
+ "default": null,
82
+ "description": "For a range: each thumb's own name ([\"Minimum\", \"Maximum\"]), passed translated.",
83
+ "required": false
84
+ },
85
+ {
86
+ "name": "value",
87
+ "type": "SliderValue",
88
+ "default": null,
89
+ "description": "One number (a thumb) or two (a range). Controlled with `onValueChange`.",
90
+ "required": false
91
+ }
92
+ ]
93
+ }
94
+ ]
95
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "slot",
3
+ "type": "primitive",
4
+ "description": "`Slot` (what `asChild` renders) and `VisuallyHidden` (a name for assistive technology only), from Radix.",
5
+ "files": [
6
+ {
7
+ "path": "slot/index.ts",
8
+ "content": "/** @krizaka/ui/slot — `Slot` (what `asChild` renders) and `VisuallyHidden` (a name for assistive technology only), from Radix. */\nimport { Slot as SlotPrimitive, VisuallyHidden as VisuallyHiddenPrimitive } from \"radix-ui\";\n\n/** Merges its props, `className` and `ref` into its only child: the engine of every `asChild`. */\nexport const Slot = SlotPrimitive.Root;\n/** Hidden on screen, read by assistive technology. */\nexport const VisuallyHidden = VisuallyHiddenPrimitive.Root;\n"
9
+ }
10
+ ],
11
+ "dependencies": [
12
+ "radix-ui@^1.7.0"
13
+ ],
14
+ "registryDependencies": [],
15
+ "demo": {
16
+ "path": "demos/slot.tsx",
17
+ "content": "import { Slot, VisuallyHidden } from \"@krizaka/ui/slot\";\n\nexport default function SlotDemo() {\n // Slot merges its props and classes into its only child: the link is rendered, styled by the Slot.\n return (\n <Slot className=\"text-sm font-semibold text-fg underline underline-offset-4\">\n <a href=\"#docs\">\n Read the docs\n <VisuallyHidden> about Slot</VisuallyHidden>\n </a>\n </Slot>\n );\n}\n"
18
+ },
19
+ "props": [
20
+ {
21
+ "component": "Slot",
22
+ "description": "Merges its props, `className` and `ref` into its only child: the engine of every `asChild`.",
23
+ "props": []
24
+ },
25
+ {
26
+ "component": "VisuallyHidden",
27
+ "description": "Hidden on screen, read by assistive technology.",
28
+ "props": []
29
+ }
30
+ ]
31
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "spinner",
3
+ "type": "primitive",
4
+ "description": "`Spinner`, `spinnerVariants`. Server-safe.",
5
+ "files": [
6
+ {
7
+ "path": "spinner/index.ts",
8
+ "content": "/** @krizaka/ui/spinner — `Spinner`, `spinnerVariants`. Server-safe. */\nexport { Spinner, type SpinnerProps, spinnerVariants } from \"./spinner\";\n"
9
+ },
10
+ {
11
+ "path": "spinner/spinner.tsx",
12
+ "content": "// Server-safe. An indeterminate wait: an SVG ring, announced as a status with its `label`.\nimport type * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nexport const spinnerVariants = tv({\n base: \"shrink-0 text-accent motion-safe:animate-spin\",\n variants: { size: { sm: \"h-4 w-4\", md: \"h-6 w-6\", lg: \"h-10 w-10\" } },\n defaultVariants: { size: \"md\" },\n});\n\nexport type SpinnerProps = Omit<React.ComponentProps<\"svg\">, \"children\"> &\n VariantProps<typeof spinnerVariants> & {\n /**\n * sm · md · lg.\n * @default \"md\"\n */\n size?: VariantProps<typeof spinnerVariants>[\"size\"];\n /** The accessible name of the wait, e.g. \"Loading\" — passed translated. */\n label: string;\n };\n\n/** A turning ring with `role=\"status\"` (it stops turning under prefers-reduced-motion). */\nexport function Spinner({ label, size, className, ...props }: SpinnerProps) {\n return (\n <svg\n role=\"status\"\n aria-label={label}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n data-size={size ?? \"md\"}\n className={spinnerVariants({ size, className })}\n {...props}\n >\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" strokeOpacity=\"0.2\" strokeWidth=\"3\" />\n <path d=\"M21 12a9 9 0 0 0-9-9\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" />\n </svg>\n );\n}\n"
13
+ }
14
+ ],
15
+ "dependencies": [
16
+ "tailwind-variants@^3.3.1"
17
+ ],
18
+ "registryDependencies": [],
19
+ "demo": {
20
+ "path": "demos/spinner.tsx",
21
+ "content": "import { Spinner } from \"@krizaka/ui/spinner\";\n\nexport default function SpinnerDemo() {\n return <Spinner label=\"Loading\" />;\n}\n"
22
+ },
23
+ "props": [
24
+ {
25
+ "component": "Spinner",
26
+ "description": "A turning ring with `role=\"status\"` (it stops turning under prefers-reduced-motion).",
27
+ "props": [
28
+ {
29
+ "name": "label",
30
+ "type": "string",
31
+ "default": null,
32
+ "description": "The accessible name of the wait, e.g. \"Loading\" — passed translated.",
33
+ "required": true
34
+ },
35
+ {
36
+ "name": "size",
37
+ "type": "\"sm\" | \"md\" | \"lg\"",
38
+ "default": "md",
39
+ "description": "sm · md · lg.",
40
+ "required": false
41
+ }
42
+ ]
43
+ }
44
+ ]
45
+ }
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "stat",
3
+ "type": "primitive",
4
+ "description": "`Stat`, `stat` (the slots). Server-safe.",
5
+ "files": [
6
+ {
7
+ "path": "stat/index.ts",
8
+ "content": "/** @krizaka/ui/stat — `Stat`, `stat` (the slots). Server-safe. */\nexport { Stat, stat, type StatProps, type StatTrend } from \"./stat\";\n"
9
+ },
10
+ {
11
+ "path": "stat/stat.tsx",
12
+ "content": "// Server-safe. A key figure: a label, the value in tabular digits, a hint, a trend. Every word is a prop.\nimport type * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nexport const stat = tv({\n slots: {\n root: \"flex min-w-0 flex-col gap-1\",\n label: \"text-xs font-semibold uppercase tracking-wider text-fg-secondary\",\n value: \"font-display text-2xl font-black tabular-nums text-fg\",\n meta: \"flex flex-wrap items-center gap-x-2 gap-y-1 text-xs\",\n // The status tokens are too light for small text on a light surface: the words stay a text role, the arrow carries\n // the direction's colour.\n trend: \"inline-flex items-center gap-1 font-semibold text-fg [&>svg]:h-3.5 [&>svg]:w-3.5 [&>svg]:shrink-0\",\n hint: \"text-fg-secondary\",\n },\n variants: {\n trend: {\n up: { trend: \"[&>svg]:text-success\" },\n down: { trend: \"[&>svg]:text-danger\" },\n flat: { trend: \"[&>svg]:text-fg-secondary\" },\n },\n },\n});\n\nexport type StatTrend = \"up\" | \"down\" | \"flat\";\n\nconst ARROW: Record<StatTrend, string> = { up: \"M7 17 17 7M8 7h9v9\", down: \"M7 7l10 10M17 8v9H8\", flat: \"M5 12h14M13 6l6 6-6 6\" };\n\nexport type StatProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n /** What the figure is (\"Revenue\"), above it. */\n label: React.ReactNode;\n /** The figure, formatted by the app, in tabular digits. */\n value: React.ReactNode;\n /** A line under the value: the period, the comparison. */\n hint?: React.ReactNode;\n /** The direction of the change; its arrow is decorative. */\n trend?: StatTrend;\n /** The change, in words, as read and shown (e.g. \"+12 % vs last week\") — the arrow alone says nothing. */\n trendLabel?: React.ReactNode;\n};\n\nexport function Stat({ label, value, hint, trend, trendLabel, className, ...props }: StatProps) {\n const s = stat({ trend });\n return (\n <div data-trend={trend} className={s.root({ className })} {...props}>\n <p className={s.label()}>{label}</p>\n <p className={s.value()}>{value}</p>\n {(trendLabel || hint) && (\n <p className={s.meta()}>\n {trend && trendLabel && (\n <span className={s.trend()}>\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d={ARROW[trend]} />\n </svg>\n {trendLabel}\n </span>\n )}\n {hint && <span className={s.hint()}>{hint}</span>}\n </p>\n )}\n </div>\n );\n}\n"
13
+ }
14
+ ],
15
+ "dependencies": [
16
+ "tailwind-variants@^3.3.1"
17
+ ],
18
+ "registryDependencies": [],
19
+ "demo": {
20
+ "path": "demos/stat.tsx",
21
+ "content": "import { Stat } from \"@krizaka/ui/stat\";\n\nexport default function StatDemo() {\n return <Stat label=\"Revenue\" value=\"€4,812\" hint=\"vs last month\" trend=\"up\" trendLabel=\"+12 %\" />;\n}\n"
22
+ },
23
+ "props": [
24
+ {
25
+ "component": "Stat",
26
+ "description": "",
27
+ "props": [
28
+ {
29
+ "name": "label",
30
+ "type": "ReactNode",
31
+ "default": null,
32
+ "description": "What the figure is (\"Revenue\"), above it.",
33
+ "required": true
34
+ },
35
+ {
36
+ "name": "value",
37
+ "type": "ReactNode",
38
+ "default": null,
39
+ "description": "The figure, formatted by the app, in tabular digits.",
40
+ "required": true
41
+ },
42
+ {
43
+ "name": "hint",
44
+ "type": "ReactNode",
45
+ "default": null,
46
+ "description": "A line under the value: the period, the comparison.",
47
+ "required": false
48
+ },
49
+ {
50
+ "name": "trend",
51
+ "type": "StatTrend",
52
+ "default": null,
53
+ "description": "The direction of the change; its arrow is decorative.",
54
+ "required": false
55
+ },
56
+ {
57
+ "name": "trendLabel",
58
+ "type": "ReactNode",
59
+ "default": null,
60
+ "description": "The change, in words, as read and shown (e.g. \"+12 % vs last week\") — the arrow alone says nothing.",
61
+ "required": false
62
+ }
63
+ ]
64
+ }
65
+ ]
66
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "switch",
3
+ "type": "primitive",
4
+ "description": "`Switch`, `switchVariants`. Client (\"use client\").",
5
+ "files": [
6
+ {
7
+ "path": "switch/index.ts",
8
+ "content": "/** @krizaka/ui/switch — `Switch`, `switchVariants`. Client (\"use client\"). */\nexport { Switch, type SwitchProps, type SwitchVariants,switchVariants } from \"./switch\";\n"
9
+ },
10
+ {
11
+ "path": "switch/switch.tsx",
12
+ "content": "// Client. Radix Switch: role=\"switch\", aria-checked, Space and Enter toggle, a hidden input inside a form.\nimport { Switch as SwitchPrimitive } from \"radix-ui\";\nimport type * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nexport const switchVariants = tv({\n slots: {\n root:\n \"peer relative inline-flex shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-fg-muted transition-colors \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 \" +\n \"disabled:cursor-not-allowed disabled:opacity-40 data-[state=checked]:bg-accent aria-invalid:border-danger\",\n thumb:\n \"pointer-events-none block rounded-full bg-fg-on-media shadow-sm transition-transform data-[state=unchecked]:translate-x-0.5 motion-reduce:transition-none\",\n },\n variants: {\n size: {\n sm: { root: \"h-5 w-9\", thumb: \"h-4 w-4 data-[state=checked]:translate-x-[1.125rem]\" },\n md: { root: \"h-6 w-11\", thumb: \"h-5 w-5 data-[state=checked]:translate-x-[1.375rem]\" },\n },\n },\n defaultVariants: { size: \"md\" },\n});\n\nexport type SwitchVariants = VariantProps<typeof switchVariants>;\n\ntype Named =\n | {\n /** The accessible name — required when no visible label names it, passed translated. */\n label: string;\n }\n /** Named by a visible `<label htmlFor={id}>` (e.g. `Field.Label`). */\n | { label?: undefined; id: string }\n /** Named by another element. */\n | { label?: undefined; \"aria-labelledby\": string };\n\nexport type SwitchProps = Omit<React.ComponentProps<typeof SwitchPrimitive.Root>, \"children\"> &\n SwitchVariants &\n Named & {\n /**\n * sm · md.\n * @default \"md\"\n */\n size?: SwitchVariants[\"size\"];\n /** Marks it invalid: aria-invalid and data-invalid. */\n invalid?: boolean;\n };\n\n/** An on/off switch: `checked`/`defaultChecked` + `onCheckedChange`. */\nexport function Switch({ label, size, invalid, className, ...props }: SwitchProps) {\n const s = switchVariants({ size });\n return (\n <SwitchPrimitive.Root\n aria-label={label}\n aria-invalid={invalid || undefined}\n data-invalid={invalid ? \"\" : undefined}\n className={s.root({ className })}\n {...props}\n >\n <SwitchPrimitive.Thumb className={s.thumb()} />\n </SwitchPrimitive.Root>\n );\n}\n"
13
+ }
14
+ ],
15
+ "dependencies": [
16
+ "radix-ui@^1.7.0",
17
+ "tailwind-variants@^3.3.1"
18
+ ],
19
+ "registryDependencies": [],
20
+ "demo": {
21
+ "path": "demos/switch.tsx",
22
+ "content": "import { Switch } from \"@krizaka/ui/switch\";\n\nexport default function SwitchDemo() {\n return <Switch label=\"Autoplay\" />;\n}\n"
23
+ },
24
+ "props": [
25
+ {
26
+ "component": "Switch",
27
+ "description": "An on/off switch: `checked`/`defaultChecked` + `onCheckedChange`.",
28
+ "props": [
29
+ {
30
+ "name": "invalid",
31
+ "type": "boolean",
32
+ "default": null,
33
+ "description": "Marks it invalid: aria-invalid and data-invalid.",
34
+ "required": false
35
+ },
36
+ {
37
+ "name": "label",
38
+ "type": "string",
39
+ "default": null,
40
+ "description": "The accessible name — required when no visible label names it, passed translated.",
41
+ "required": false
42
+ },
43
+ {
44
+ "name": "size",
45
+ "type": "\"sm\" | \"md\"",
46
+ "default": "md",
47
+ "description": "sm · md.",
48
+ "required": false
49
+ }
50
+ ]
51
+ }
52
+ ]
53
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "tabs",
3
+ "type": "primitive",
4
+ "description": "`Tabs.Root/List/Trigger/Content` (variant underline · segmented · pills), `tabs` (the slots). Client (\"use client\").",
5
+ "files": [
6
+ {
7
+ "path": "tabs/index.ts",
8
+ "content": "/** @krizaka/ui/tabs — `Tabs.Root/List/Trigger/Content` (variant underline · segmented · pills), `tabs` (the slots). Client (\"use client\"). */\nexport { Tabs, tabs, TabsContent, TabsList, TabsRoot, type TabsRootProps, TabsTrigger, type TabsVariants } from \"./tabs\";\n"
9
+ },
10
+ {
11
+ "path": "tabs/tabs.tsx",
12
+ "content": "// Client (a context). Radix Tabs does the roles (tablist, tab, tabpanel), the aria-selected / aria-controls wiring,\n// the roving focus (arrows, Home, End) and automatic activation: this file only holds the three looks.\nimport { Tabs as TabsPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nexport const tabs = tv({\n slots: {\n root: \"flex gap-4 data-[orientation=horizontal]:flex-col data-[orientation=vertical]:flex-row\",\n list: \"flex shrink-0 data-[orientation=vertical]:flex-col\",\n trigger:\n \"inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap font-semibold transition-colors \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40\",\n content: \"kz-fade min-w-0 flex-1 rounded-lg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n },\n variants: {\n variant: {\n /* The section of a page: a line under the list, the active tab underlined in the accent. */\n underline: {\n list: \"gap-1 border-border-default data-[orientation=horizontal]:border-b data-[orientation=vertical]:border-r\",\n trigger:\n \"relative rounded-md px-3 py-2.5 text-sm text-fg-secondary hover:text-fg data-[state=active]:text-fg \" +\n \"after:absolute after:rounded-full after:bg-transparent data-[state=active]:after:bg-accent \" +\n \"data-[orientation=horizontal]:after:inset-x-2 data-[orientation=horizontal]:after:-bottom-px data-[orientation=horizontal]:after:h-0.5 \" +\n \"data-[orientation=vertical]:justify-start data-[orientation=vertical]:after:inset-y-1.5 data-[orientation=vertical]:after:-right-px data-[orientation=vertical]:after:w-0.5\",\n },\n /* A view switch: equal segments in a track, the active one filled. */\n segmented: {\n list: \"inline-grid auto-cols-fr grid-flow-col gap-1 self-start rounded-xl border border-border-default bg-surface-1 p-1 data-[orientation=vertical]:grid-flow-row\",\n trigger:\n \"min-h-8 rounded-lg px-3 py-1.5 text-xs text-fg-secondary hover:bg-surface-2 hover:text-fg \" +\n \"data-[state=active]:bg-accent data-[state=active]:text-on-accent data-[state=active]:shadow-sm data-[state=active]:hover:bg-accent\",\n },\n /* Pills that scroll sideways on a phone: a feed's sections. */\n pills: {\n list: \"gap-1.5 overflow-x-auto overscroll-x-contain [scrollbar-width:none] data-[orientation=horizontal]:pb-1\",\n trigger:\n \"h-9 rounded-full border border-border-default bg-surface-1 px-4 text-xs text-fg-secondary hover:border-border-strong hover:text-fg \" +\n \"data-[state=active]:border-accent data-[state=active]:bg-accent-soft data-[state=active]:text-fg\",\n },\n },\n },\n defaultVariants: { variant: \"underline\" },\n});\n\nexport type TabsVariants = VariantProps<typeof tabs>;\ntype Variant = NonNullable<TabsVariants[\"variant\"]>;\n\nconst VariantContext = React.createContext<Variant>(\"underline\");\n\nexport type TabsRootProps = React.ComponentProps<typeof TabsPrimitive.Root> & {\n /** underline (the sections of a page) · segmented (a view switch) · pills (a feed's sections). */\n variant?: TabsVariants[\"variant\"];\n};\n\n/** `value`/`defaultValue` + `onValueChange`, `orientation` (horizontal by default), `variant` for the list. */\nexport function TabsRoot({ variant = \"underline\", className, ...props }: TabsRootProps) {\n return (\n <VariantContext.Provider value={variant}>\n <TabsPrimitive.Root data-variant={variant} className={tabs({ variant }).root({ className })} {...props} />\n </VariantContext.Provider>\n );\n}\n\n/** The tabs. Name it (`aria-label`) when no heading says what it switches. */\nexport function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {\n const variant = React.useContext(VariantContext);\n return <TabsPrimitive.List data-variant={variant} className={tabs({ variant }).list({ className })} {...props} />;\n}\n\nexport function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n const variant = React.useContext(VariantContext);\n return <TabsPrimitive.Trigger className={tabs({ variant }).trigger({ className })} {...props} />;\n}\n\nexport function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {\n return <TabsPrimitive.Content className={tabs().content({ className })} {...props} />;\n}\n\n/** `Tabs.Root/List/Trigger/Content` — `variant` underline · segmented · pills on the root. */\nexport const Tabs = { Root: TabsRoot, List: TabsList, Trigger: TabsTrigger, Content: TabsContent };\n"
13
+ }
14
+ ],
15
+ "dependencies": [
16
+ "radix-ui@^1.7.0",
17
+ "tailwind-variants@^3.3.1"
18
+ ],
19
+ "registryDependencies": [],
20
+ "demo": {
21
+ "path": "demos/tabs.tsx",
22
+ "content": "import { Tabs } from \"@krizaka/ui/tabs\";\n\nexport default function TabsDemo() {\n return (\n <Tabs.Root defaultValue=\"videos\">\n <Tabs.List aria-label=\"Profile\">\n <Tabs.Trigger value=\"videos\">Videos</Tabs.Trigger>\n <Tabs.Trigger value=\"stories\">Stories</Tabs.Trigger>\n <Tabs.Trigger value=\"collections\">Collections</Tabs.Trigger>\n <Tabs.Trigger value=\"about\" disabled>\n About\n </Tabs.Trigger>\n </Tabs.List>\n <Tabs.Content value=\"videos\" className=\"text-sm text-fg-secondary\">\n Twelve videos, newest first.\n </Tabs.Content>\n <Tabs.Content value=\"stories\" className=\"text-sm text-fg-secondary\">\n Stories of the last 24 hours.\n </Tabs.Content>\n <Tabs.Content value=\"collections\" className=\"text-sm text-fg-secondary\">\n Three public collections.\n </Tabs.Content>\n <Tabs.Content value=\"about\" className=\"text-sm text-fg-secondary\">\n About this channel.\n </Tabs.Content>\n </Tabs.Root>\n );\n}\n"
23
+ },
24
+ "props": [
25
+ {
26
+ "component": "TabsRoot",
27
+ "description": "`value`/`defaultValue` + `onValueChange`, `orientation` (horizontal by default), `variant` for the list.",
28
+ "props": [
29
+ {
30
+ "name": "variant",
31
+ "type": "\"underline\" | \"segmented\" | \"pills\"",
32
+ "default": "underline",
33
+ "description": "underline (the sections of a page) · segmented (a view switch) · pills (a feed's sections).",
34
+ "required": false
35
+ }
36
+ ]
37
+ },
38
+ {
39
+ "component": "TabsList",
40
+ "description": "The tabs. Name it (`aria-label`) when no heading says what it switches.",
41
+ "props": []
42
+ }
43
+ ]
44
+ }
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "theme",
3
+ "type": "primitive",
4
+ "description": "`ThemeScript`, `ThemeProvider`, `useTheme`, `ThemeToggle`. Client (\"use client\").",
5
+ "files": [
6
+ {
7
+ "path": "theme/index.ts",
8
+ "content": "/** @krizaka/ui/theme — `ThemeScript`, `ThemeProvider`, `useTheme`, `ThemeToggle`. Client (\"use client\"). */\nexport {\n type Mode,\n type ThemeContextValue,\n ThemeProvider,\n type ThemeProviderProps,\n ThemeScript,\n ThemeToggle,\n type ThemeToggleProps,\n useTheme,\n} from \"./theme\";\n"
9
+ },
10
+ {
11
+ "path": "theme/theme.tsx",
12
+ "content": "// Client. ONE mechanism for the whole organisation: dark by default, `html.light` for light, `html.theme-<name>` for a\n// named theme, persisted in localStorage (`kz-theme` for the mode, `kz-theme-name` for the named theme).\nimport * as React from \"react\";\n\nimport { type ButtonVariants, buttonVariants } from \"../button/button\";\n\nexport type Mode = \"dark\" | \"light\" | \"system\";\n\nconst MODE_KEY = \"kz-theme\";\nconst NAME_KEY = \"kz-theme-name\";\nconst MODES: readonly Mode[] = [\"dark\", \"light\", \"system\"];\nconst DARK_QUERY = \"(prefers-color-scheme: dark)\";\n\nconst isMode = (value: unknown): value is Mode => MODES.includes(value as Mode);\n\n// Storage blocked (private mode, sandbox): the choice lives in memory for the page.\nconst memory = new Map<string, string>();\n\nfunction read(key: string): string | null {\n try {\n return localStorage.getItem(key);\n } catch {\n return memory.get(key) ?? null;\n }\n}\n\nfunction write(key: string, value: string | null) {\n if (value === null) memory.delete(key);\n else memory.set(key, value);\n try {\n if (value === null) localStorage.removeItem(key);\n else localStorage.setItem(key, value);\n } catch {\n // storage blocked: memory holds it\n }\n}\n\n// The persisted choice is an external store: every provider and tab reads the same value, the server reads none.\nconst listeners = new Set<() => void>();\nconst notify = () => listeners.forEach((listener) => listener());\nfunction subscribe(listener: () => void) {\n listeners.add(listener);\n window.addEventListener(\"storage\", listener);\n return () => {\n listeners.delete(listener);\n window.removeEventListener(\"storage\", listener);\n };\n}\n\nfunction applyMode(mode: Mode) {\n const dark = window.matchMedia(DARK_QUERY).matches;\n document.documentElement.classList.toggle(\"light\", mode === \"light\" || (mode === \"system\" && !dark));\n}\n\nfunction applyName(previous: string | null, next: string | null) {\n const classes = document.documentElement.classList;\n if (previous) classes.remove(`theme-${previous}`);\n if (next) classes.add(`theme-${next}`);\n}\n\n/**\n * In <head>, before anything renders: applies the persisted mode and named theme, so the page never flashes.\n * `nonce` for a strict Content-Security-Policy.\n */\nexport function ThemeScript({\n nonce,\n}: {\n /** The nonce of a strict Content-Security-Policy (`script-src 'nonce-…'`). */\n nonce?: string;\n}) {\n const js =\n `(function(){try{var m=localStorage.getItem(\"${MODE_KEY}\")||\"system\";var n=localStorage.getItem(\"${NAME_KEY}\");` +\n `var d=matchMedia(\"${DARK_QUERY}\").matches;var c=document.documentElement.classList;` +\n `c.toggle(\"light\",m===\"light\"||(m===\"system\"&&!d));if(n)c.add(\"theme-\"+n)}catch(e){}})()`;\n return <script nonce={nonce} dangerouslySetInnerHTML={{ __html: js }} />;\n}\n\nexport type ThemeContextValue = {\n /** The chosen mode: dark, light or follow the system. */\n mode: Mode;\n setMode: (mode: Mode) => void;\n /** The named theme (`html.theme-<name>`), or null. */\n theme: string | null;\n setTheme: (theme: string | null) => void;\n};\n\nconst ThemeContext = React.createContext<ThemeContextValue | null>(null);\n\nexport type ThemeProviderProps = {\n children: React.ReactNode;\n /** The mode before anything is persisted. */\n defaultMode?: Mode;\n};\n\nexport function ThemeProvider({ children, defaultMode = \"system\" }: ThemeProviderProps) {\n // Before hydration the server knows nothing persisted (ThemeScript has already applied it to <html>).\n const stored = React.useSyncExternalStore(subscribe, () => read(MODE_KEY), () => null);\n const mode: Mode = isMode(stored) ? stored : defaultMode;\n const theme = React.useSyncExternalStore(subscribe, () => read(NAME_KEY), () => null);\n\n // \"system\" follows the operating system while the page is open.\n React.useEffect(() => {\n if (mode !== \"system\") return;\n const query = window.matchMedia(DARK_QUERY);\n const onChange = () => applyMode(\"system\");\n query.addEventListener?.(\"change\", onChange);\n return () => query.removeEventListener?.(\"change\", onChange);\n }, [mode]);\n\n const setMode = React.useCallback((next: Mode) => {\n write(MODE_KEY, next);\n applyMode(next);\n notify();\n }, []);\n\n const setTheme = React.useCallback((next: string | null) => {\n applyName(read(NAME_KEY), next);\n write(NAME_KEY, next);\n notify();\n }, []);\n\n const value = React.useMemo(() => ({ mode, setMode, theme, setTheme }), [mode, setMode, theme, setTheme]);\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;\n}\n\nexport function useTheme(): ThemeContextValue {\n const context = React.useContext(ThemeContext);\n if (!context) throw new Error(\"useTheme: wrap the app in <ThemeProvider>\");\n return context;\n}\n\nconst ICONS: Record<Mode, React.ReactNode> = {\n dark: <path d=\"M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5Z\" />,\n light: (\n <>\n <circle cx=\"12\" cy=\"12\" r=\"4\" />\n <path d=\"M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4\" />\n </>\n ),\n system: (\n <>\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"12\" rx=\"2\" />\n <path d=\"M8 20h8M12 16v4\" />\n </>\n ),\n};\n\nexport type ThemeToggleProps = Omit<React.ComponentProps<\"button\">, \"onClick\" | \"children\"> &\n Pick<ButtonVariants, \"variant\" | \"shape\"> & {\n /** Any `Button` variant. */\n variant?: ButtonVariants[\"variant\"];\n /**\n * rounded · pill.\n * @default \"rounded\"\n */\n shape?: ButtonVariants[\"shape\"];\n /** The accessible name — a string, or one per current mode (e.g. `(mode) => t(\"theme.\" + mode)`). */\n label: string | ((mode: Mode) => string);\n };\n\n/**\n * An icon button (styled as `IconButton`, without its Slot) that cycles dark → light → system. Needs a\n * <ThemeProvider> above it. State: `data-mode`.\n */\nexport function ThemeToggle({ label, variant = \"ghost\", shape, className, ...props }: ThemeToggleProps) {\n const { mode, setMode } = useTheme();\n const next = MODES[(MODES.indexOf(mode) + 1) % MODES.length] as Mode;\n const name = typeof label === \"function\" ? label(mode) : label;\n return (\n <button\n type=\"button\"\n aria-label={name}\n title={name}\n data-mode={mode}\n onClick={() => setMode(next)}\n className={buttonVariants({ variant, shape, size: \"icon\", className })}\n {...props}\n >\n <svg viewBox=\"0 0 24 24\" className=\"h-5 w-5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n {ICONS[mode]}\n </svg>\n </button>\n );\n}\n"
13
+ }
14
+ ],
15
+ "dependencies": [],
16
+ "registryDependencies": [
17
+ "button"
18
+ ],
19
+ "demo": {
20
+ "path": "demos/theme.tsx",
21
+ "content": "import { ThemeProvider, ThemeToggle } from \"@krizaka/ui/theme\";\n\nexport default function ThemeDemo() {\n return (\n <ThemeProvider>\n <ThemeToggle label=\"Change the theme\" />\n </ThemeProvider>\n );\n}\n"
22
+ },
23
+ "props": [
24
+ {
25
+ "component": "ThemeProvider",
26
+ "description": "",
27
+ "props": [
28
+ {
29
+ "name": "defaultMode",
30
+ "type": "Mode",
31
+ "default": "system",
32
+ "description": "The mode before anything is persisted.",
33
+ "required": false
34
+ }
35
+ ]
36
+ },
37
+ {
38
+ "component": "ThemeScript",
39
+ "description": "In <head>, before anything renders: applies the persisted mode and named theme, so the page never flashes.\n`nonce` for a strict Content-Security-Policy.",
40
+ "props": [
41
+ {
42
+ "name": "nonce",
43
+ "type": "string",
44
+ "default": null,
45
+ "description": "The nonce of a strict Content-Security-Policy (`script-src 'nonce-…'`).",
46
+ "required": false
47
+ }
48
+ ]
49
+ },
50
+ {
51
+ "component": "ThemeToggle",
52
+ "description": "An icon button (styled as `IconButton`, without its Slot) that cycles dark → light → system. Needs a\n<ThemeProvider> above it. State: `data-mode`.",
53
+ "props": [
54
+ {
55
+ "name": "label",
56
+ "type": "string | ((mode: Mode) => string)",
57
+ "default": null,
58
+ "description": "The accessible name — a string, or one per current mode (e.g. `(mode) => t(\"theme.\" + mode)`).",
59
+ "required": true
60
+ },
61
+ {
62
+ "name": "shape",
63
+ "type": "\"rounded\" | \"pill\"",
64
+ "default": "rounded",
65
+ "description": "rounded · pill.",
66
+ "required": false
67
+ },
68
+ {
69
+ "name": "variant",
70
+ "type": "\"danger\" | \"primary\" | \"secondary\" | \"outline\" | \"ghost\"",
71
+ "default": "ghost",
72
+ "description": "Any `Button` variant.",
73
+ "required": false
74
+ }
75
+ ]
76
+ }
77
+ ]
78
+ }