@krizaka/ui 2.0.0-beta.6 → 2.0.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 (67) hide show
  1. package/README.md +7 -6
  2. package/dist/{button-D5aSsmxX.d.ts → button-Cgfkg_3y.d.ts} +5 -1
  3. package/dist/button.d.ts +5 -1
  4. package/dist/button.js +4 -2
  5. package/dist/button.js.map +1 -1
  6. package/dist/card.d.ts +7 -1
  7. package/dist/card.js +2 -2
  8. package/dist/card.js.map +1 -1
  9. package/dist/{chunk-IUGBKMJC.js → chunk-FHYZSO2S.js} +2 -2
  10. package/dist/{chunk-6DJ6SQY3.js → chunk-PESA2R4U.js} +5 -3
  11. package/dist/chunk-PESA2R4U.js.map +1 -0
  12. package/dist/command.d.ts +11 -2
  13. package/dist/command.js +17 -6
  14. package/dist/command.js.map +1 -1
  15. package/dist/confirm-button.d.ts +1 -1
  16. package/dist/confirm-button.js +1 -1
  17. package/dist/countdown.d.ts +7 -6
  18. package/dist/countdown.js +12 -8
  19. package/dist/countdown.js.map +1 -1
  20. package/dist/dialog.js +2 -2
  21. package/dist/dropdown-menu.js +3 -3
  22. package/dist/dropdown-menu.js.map +1 -1
  23. package/dist/field.js +2 -2
  24. package/dist/field.js.map +1 -1
  25. package/dist/native.d.ts +20 -8
  26. package/dist/native.js +129 -85
  27. package/dist/native.js.map +1 -1
  28. package/dist/theme.d.ts +1 -1
  29. package/dist/theme.js +1 -1
  30. package/package.json +3 -3
  31. package/registry/avatar.json +20 -5
  32. package/registry/button.json +36 -15
  33. package/registry/card.json +17 -2
  34. package/registry/command.json +30 -8
  35. package/registry/confirm-button.json +4 -4
  36. package/registry/countdown.json +53 -12
  37. package/registry/dropdown-menu.json +1 -1
  38. package/registry/examples/button/gradient.d.ts +4 -0
  39. package/registry/examples/button/gradient.js +11 -0
  40. package/registry/examples/button/gradient.tsx +5 -0
  41. package/registry/examples/button/native/gradient.d.ts +4 -0
  42. package/registry/examples/button/native/gradient.dark.png +0 -0
  43. package/registry/examples/button/native/gradient.js +12 -0
  44. package/registry/examples/button/native/gradient.light.png +0 -0
  45. package/registry/examples/button/native/gradient.tsx +5 -0
  46. package/registry/examples/command/dialog.js +1 -2
  47. package/registry/examples/command/dialog.tsx +1 -2
  48. package/registry/examples/command/inline.js +1 -2
  49. package/registry/examples/command/inline.tsx +1 -2
  50. package/registry/examples/command/no-results.d.ts +4 -0
  51. package/registry/examples/command/no-results.js +19 -0
  52. package/registry/examples/command/no-results.tsx +20 -0
  53. package/registry/examples/countdown/native/urgent.dark.png +0 -0
  54. package/registry/examples/countdown/native/urgent.light.png +0 -0
  55. package/registry/examples/countdown/native/with-label.d.ts +4 -0
  56. package/registry/examples/countdown/native/with-label.dark.png +0 -0
  57. package/registry/examples/countdown/native/with-label.js +13 -0
  58. package/registry/examples/countdown/native/with-label.light.png +0 -0
  59. package/registry/examples/countdown/native/with-label.tsx +7 -0
  60. package/registry/examples/countdown/with-label.d.ts +4 -0
  61. package/registry/examples/countdown/with-label.js +13 -0
  62. package/registry/examples/countdown/with-label.tsx +10 -0
  63. package/registry/field.json +1 -1
  64. package/registry/index.json +8 -8
  65. package/registry/theme.json +2 -2
  66. package/dist/chunk-6DJ6SQY3.js.map +0 -1
  67. /package/dist/{chunk-IUGBKMJC.js.map → chunk-FHYZSO2S.js.map} +0 -0
package/dist/theme.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { a as ButtonVariants } from './button-D5aSsmxX.js';
2
+ import { a as ButtonVariants } from './button-Cgfkg_3y.js';
3
3
  import 'tailwind-variants';
4
4
 
5
5
  type Mode = "dark" | "light" | "system";
package/dist/theme.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  buttonVariants
4
- } from "./chunk-6DJ6SQY3.js";
4
+ } from "./chunk-PESA2R4U.js";
5
5
 
6
6
  // src/theme/theme.tsx
7
7
  import * as React from "react";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krizaka/ui",
3
- "version": "2.0.0-beta.6",
3
+ "version": "2.0.0",
4
4
  "description": "The Krizaka front-end platform's components: the primitives (button, card, dialog, toast, menu, field, tabs, chip, switch, slider, command palette, theme, countdown…) on the semantic --kz-* tokens, the animated marks (Krizaka, Orazaka, Orochia) and the motion signature — web and /native.",
5
5
  "keywords": [
6
6
  "krizaka",
@@ -194,7 +194,7 @@
194
194
  "*.css"
195
195
  ],
196
196
  "peerDependencies": {
197
- "@krizaka/tailwind": ">=2.0.0-beta.6",
197
+ "@krizaka/tailwind": ">=2.0.0",
198
198
  "react": ">=18",
199
199
  "react-dom": ">=18",
200
200
  "react-native": ">=0.76",
@@ -238,7 +238,7 @@
238
238
  "typescript": "^5.9.0",
239
239
  "vitest": "^4.1.11",
240
240
  "@krizaka/config": "0.1.0",
241
- "@krizaka/tokens": "2.0.0-beta.6"
241
+ "@krizaka/tokens": "2.0.0"
242
242
  },
243
243
  "engines": {
244
244
  "node": ">=20"
@@ -171,12 +171,13 @@
171
171
  "differences": [
172
172
  "The fallback is the initial of `alt` when `fallback` is not given.",
173
173
  "`size` also takes a number of points.",
174
- "`src` is a URI string (an `Image` source), not a URL object."
174
+ "`src` is a URI string (an `Image` source), not a URL object.",
175
+ "An SVG `src` (`….svg`, `data:image/svg+xml,…`, or any URI with `svg`) is drawn by react-native-svg — `Image` cannot."
175
176
  ],
176
177
  "props": [
177
178
  {
178
179
  "component": "AvatarRoot",
179
- "description": "A round picture; the initial (or `fallback`) when there is none or it fails to load.",
180
+ "description": "",
180
181
  "props": [
181
182
  {
182
183
  "name": "alt",
@@ -203,7 +204,14 @@
203
204
  "name": "src",
204
205
  "type": "string | null",
205
206
  "default": null,
206
- "description": "The image URI; absent, `null` or failing: the fallback.",
207
+ "description": "The image URI; absent, `null` or failing: the fallback. An SVG (`….svg`, `data:image/svg+xml,…` — generated\navatars) is drawn by react-native-svg, which React Native's `Image` cannot do; `svg` forces it for a URI without\nthe extension.",
208
+ "required": false
209
+ },
210
+ {
211
+ "name": "svg",
212
+ "type": "boolean",
213
+ "default": null,
214
+ "description": "Draw `src` as an SVG even when its URI does not say so (an avatar service that answers `image/svg+xml`).",
207
215
  "required": false
208
216
  }
209
217
  ]
@@ -257,7 +265,14 @@
257
265
  "name": "src",
258
266
  "type": "string | null",
259
267
  "default": null,
260
- "description": "The image URI; absent, `null` or failing: the fallback.",
268
+ "description": "The image URI; absent, `null` or failing: the fallback. An SVG (`….svg`, `data:image/svg+xml,…` — generated\navatars) is drawn by react-native-svg, which React Native's `Image` cannot do; `svg` forces it for a URI without\nthe extension.",
269
+ "required": false
270
+ },
271
+ {
272
+ "name": "svg",
273
+ "type": "boolean",
274
+ "default": null,
275
+ "description": "Draw `src` as an SVG even when its URI does not say so (an avatar service that answers `image/svg+xml`).",
261
276
  "required": false
262
277
  }
263
278
  ]
@@ -301,7 +316,7 @@
301
316
  },
302
317
  {
303
318
  "path": "avatar/meta.ts",
304
- "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Avatar\",\n summary: \"A person or an account: their picture, or initials while it loads, when it fails or when there is none.\",\n status: \"stable\",\n category: \"data-display\",\n platforms: \"both\",\n whenToUse: [\n \"Next to a name: comments, creators, members, the signed-in account in a header.\",\n \"`Avatar.Group` to show who takes part (followers, collaborators) in little space.\",\n ],\n whenNotToUse: [\n { when: \"For a product, a video or a place: use an image in a card.\", use: \"card\" },\n { when: \"As the only way to say who someone is: show their name too, or give the avatar an `alt`.\" },\n ],\n bestPractices: [\n \"Always pass a `fallback` (initials) on the web: images fail, and the layout should not jump.\",\n \"Use one size per list: `sm` in dense lists, `md` by default, `lg`/`xl` on a profile.\",\n \"In a group, set `max` so a long list ends with `+n` instead of overflowing.\",\n ],\n accessibility: {\n keyboard: [],\n notes: [\n \"With `alt`, the picture is an image named by it; without `alt`, it is decorative — put the name in the text next to it.\",\n \"The fallback initials are shown, not announced, when an `alt` names the person.\",\n ],\n },\n related: [\"card\", \"badge\", \"skeleton\"],\n web: {\n imports: [\"Avatar\"],\n examples: [\n { name: \"with-image\", title: \"With an image\", description: \"A picture named by `alt`, with initials as `fallback`.\" },\n { name: \"fallback\", title: \"Fallback\", description: \"No image: the initials.\" },\n { name: \"sizes\", title: \"Sizes\", description: \"`xs`, `sm`, `md` (default), `lg`, `xl`.\" },\n { name: \"group\", title: \"Group\", description: \"`Avatar.Group` with `max`: the others become `+n`.\" },\n ],\n },\n native: {\n imports: [\"Avatar\"],\n differences: [\n \"The fallback is the initial of `alt` when `fallback` is not given.\",\n \"`size` also takes a number of points.\",\n \"`src` is a URI string (an `Image` source), not a URL object.\",\n ],\n examples: [\n { name: \"sizes\", title: \"Sizes\", description: \"Without an image: the initial of `alt`, from `xs` to `xl`.\" },\n { name: \"group\", title: \"Group\", description: \"`Avatar.Group max={3}`: the rest becomes `+n`.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
319
+ "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Avatar\",\n summary: \"A person or an account: their picture, or initials while it loads, when it fails or when there is none.\",\n status: \"stable\",\n category: \"data-display\",\n platforms: \"both\",\n whenToUse: [\n \"Next to a name: comments, creators, members, the signed-in account in a header.\",\n \"`Avatar.Group` to show who takes part (followers, collaborators) in little space.\",\n ],\n whenNotToUse: [\n { when: \"For a product, a video or a place: use an image in a card.\", use: \"card\" },\n { when: \"As the only way to say who someone is: show their name too, or give the avatar an `alt`.\" },\n ],\n bestPractices: [\n \"Always pass a `fallback` (initials) on the web: images fail, and the layout should not jump.\",\n \"Use one size per list: `sm` in dense lists, `md` by default, `lg`/`xl` on a profile.\",\n \"In a group, set `max` so a long list ends with `+n` instead of overflowing.\",\n ],\n accessibility: {\n keyboard: [],\n notes: [\n \"With `alt`, the picture is an image named by it; without `alt`, it is decorative — put the name in the text next to it.\",\n \"The fallback initials are shown, not announced, when an `alt` names the person.\",\n ],\n },\n related: [\"card\", \"badge\", \"skeleton\"],\n web: {\n imports: [\"Avatar\"],\n examples: [\n { name: \"with-image\", title: \"With an image\", description: \"A picture named by `alt`, with initials as `fallback`.\" },\n { name: \"fallback\", title: \"Fallback\", description: \"No image: the initials.\" },\n { name: \"sizes\", title: \"Sizes\", description: \"`xs`, `sm`, `md` (default), `lg`, `xl`.\" },\n { name: \"group\", title: \"Group\", description: \"`Avatar.Group` with `max`: the others become `+n`.\" },\n ],\n },\n native: {\n imports: [\"Avatar\"],\n differences: [\n \"The fallback is the initial of `alt` when `fallback` is not given.\",\n \"`size` also takes a number of points.\",\n \"`src` is a URI string (an `Image` source), not a URL object.\",\n \"An SVG `src` (`….svg`, `data:image/svg+xml,…`, or any URI with `svg`) is drawn by react-native-svg — `Image` cannot.\",\n ],\n examples: [\n { name: \"sizes\", title: \"Sizes\", description: \"Without an image: the initial of `alt`, from `xs` to `xl`.\" },\n { name: \"group\", title: \"Group\", description: \"`Avatar.Group max={3}`: the rest becomes `+n`.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
305
320
  },
306
321
  {
307
322
  "path": "meta.ts",
@@ -2,8 +2,8 @@
2
2
  "name": "button",
3
3
  "type": "primitive",
4
4
  "title": "Button",
5
- "summary": "The action: a button in five looks (primary, secondary, outline, ghost, danger), three sizes and two shapes.",
6
- "description": "The action: a button in five looks (primary, secondary, outline, ghost, danger), three sizes and two shapes.",
5
+ "summary": "The action: a button in six looks (primary, gradient, secondary, outline, ghost, danger), three sizes and two shapes.",
6
+ "description": "The action: a button in six looks (primary, gradient, secondary, outline, ghost, danger), three sizes and two shapes.",
7
7
  "status": "stable",
8
8
  "category": "actions",
9
9
  "platforms": "both",
@@ -96,9 +96,9 @@
96
96
  },
97
97
  {
98
98
  "name": "variant",
99
- "type": "\"danger\" | \"primary\" | \"secondary\" | \"outline\" | \"ghost\"",
99
+ "type": "\"danger\" | \"primary\" | \"gradient\" | \"secondary\" | \"outline\" | \"ghost\"",
100
100
  "default": "secondary",
101
- "description": "The look: `primary` (the accent), `secondary` (a raised surface), `outline`, `ghost`, `danger`.",
101
+ "description": "The look: `primary` (the accent), `gradient` (accent → accent-2), `secondary` (a raised surface), `outline`,\n`ghost`, `danger`.",
102
102
  "required": false
103
103
  }
104
104
  ]
@@ -144,9 +144,9 @@
144
144
  },
145
145
  {
146
146
  "name": "variant",
147
- "type": "\"danger\" | \"primary\" | \"secondary\" | \"outline\" | \"ghost\"",
147
+ "type": "\"danger\" | \"primary\" | \"gradient\" | \"secondary\" | \"outline\" | \"ghost\"",
148
148
  "default": "secondary",
149
- "description": "The look: `primary` (the accent), `secondary` (a raised surface), `outline`, `ghost`, `danger`.",
149
+ "description": "The look: `primary` (the accent), `gradient` (accent → accent-2), `secondary` (a raised surface), `outline`,\n`ghost`, `danger`.",
150
150
  "required": false
151
151
  }
152
152
  ]
@@ -161,6 +161,14 @@
161
161
  "module": "@krizaka/ui/registry/examples/button/primary",
162
162
  "code": "import { Button } from \"@krizaka/ui/button\";\n\nexport default function ButtonPrimary() {\n return <Button variant=\"primary\">Continue</Button>;\n}\n"
163
163
  },
164
+ {
165
+ "name": "gradient",
166
+ "title": "Gradient",
167
+ "description": "The brand's signature, accent → accent-2: a hero or a closing call to action, once per page.",
168
+ "path": "examples/button/gradient.tsx",
169
+ "module": "@krizaka/ui/registry/examples/button/gradient",
170
+ "code": "import { Button } from \"@krizaka/ui/button\";\n\nexport default function ButtonGradient() {\n return <Button variant=\"gradient\">Start free</Button>;\n}\n"
171
+ },
164
172
  {
165
173
  "name": "secondary",
166
174
  "title": "Secondary",
@@ -250,7 +258,8 @@
250
258
  "The text is the `label` prop (a string), not children: it is also the accessible name.",
251
259
  "`onPress` instead of `onClick` (a `Pressable`); haptics, if any, belong to your `onPress`.",
252
260
  "`icon` places an icon before the label; there is no `asChild`.",
253
- "`loading` shows the platform's `ActivityIndicator` in place of the icon."
261
+ "`loading` shows the platform's `ActivityIndicator` in place of the icon.",
262
+ "`variant=\"gradient\"` draws its fill with react-native-svg (React Native has no CSS gradient)."
254
263
  ],
255
264
  "props": [
256
265
  {
@@ -303,7 +312,7 @@
303
312
  "name": "variant",
304
313
  "type": "ButtonVariant",
305
314
  "default": null,
306
- "description": "The look: `primary`, `secondary` (default), `outline`, `ghost`, `danger` — as on the web.",
315
+ "description": "The look: `primary`, `gradient` (accent → accent-2), `secondary` (default), `outline`, `ghost`, `danger` — as on the web.",
307
316
  "required": false
308
317
  }
309
318
  ]
@@ -358,7 +367,7 @@
358
367
  "name": "variant",
359
368
  "type": "ButtonVariant",
360
369
  "default": null,
361
- "description": "The look: `primary`, `secondary` (default), `outline`, `ghost`, `danger` — as on the web.",
370
+ "description": "The look: `primary`, `gradient` (accent → accent-2), `secondary` (default), `outline`, `ghost`, `danger` — as on the web.",
362
371
  "required": false
363
372
  }
364
373
  ]
@@ -377,6 +386,18 @@
377
386
  "light": "examples/button/native/primary.light.png"
378
387
  }
379
388
  },
389
+ {
390
+ "name": "gradient",
391
+ "title": "Gradient",
392
+ "description": "accent → accent-2, drawn with react-native-svg: the hero action of a screen.",
393
+ "path": "examples/button/native/gradient.tsx",
394
+ "module": "@krizaka/ui/registry/examples/button/native/gradient",
395
+ "code": "import { Button } from \"@krizaka/ui/native\";\n\nexport default function NativeButtonGradient() {\n return <Button variant=\"gradient\" label=\"Start free\" onPress={() => {}} />;\n}\n",
396
+ "screenshots": {
397
+ "dark": "examples/button/native/gradient.dark.png",
398
+ "light": "examples/button/native/gradient.light.png"
399
+ }
400
+ },
380
401
  {
381
402
  "name": "secondary",
382
403
  "title": "Secondary",
@@ -490,7 +511,7 @@
490
511
  "files": [
491
512
  {
492
513
  "path": "button/button.tsx",
493
- "content": "// Server-safe: no hook, no context. `buttonVariants` styles a <Link> or a Server Component without the component.\nimport { Slot } from \"radix-ui\";\nimport type * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nexport const buttonVariants = tv({\n base:\n \"inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap font-semibold transition-all \" +\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:pointer-events-none disabled:opacity-40 data-loading:cursor-progress active:scale-[0.98]\",\n variants: {\n variant: {\n primary: \"bg-accent text-on-accent shadow-sm hover:bg-accent-hover\",\n secondary: \"border border-border-default bg-surface-2 text-fg hover:border-border-strong hover:bg-surface-3\",\n outline: \"border border-border-default bg-transparent text-fg hover:border-accent hover:text-fg-accent\",\n ghost: \"text-fg-secondary hover:bg-surface-2 hover:text-fg\",\n // The danger token is invariant and too light for small text on a light surface (3.6:1): the label stays a\n // text role, the border and the tint carry the danger.\n danger: \"border border-danger/50 bg-danger/10 text-fg hover:border-danger hover:bg-danger/20\",\n },\n size: { sm: \"h-8 px-3 text-xs\", md: \"h-10 px-4 text-sm\", lg: \"h-12 px-6 text-sm\", icon: \"h-10 w-10\" },\n shape: { rounded: \"rounded-lg\", pill: \"rounded-full\" },\n },\n defaultVariants: { variant: \"secondary\", size: \"md\", shape: \"rounded\" },\n});\n\nexport type ButtonVariants = VariantProps<typeof buttonVariants>;\n\nexport type ButtonProps = React.ComponentProps<\"button\"> &\n ButtonVariants & {\n /**\n * The look: `primary` (the accent), `secondary` (a raised surface), `outline`, `ghost`, `danger`.\n * @default \"secondary\"\n */\n variant?: ButtonVariants[\"variant\"];\n /**\n * sm · md · lg, or `icon` (a square).\n * @default \"md\"\n */\n size?: ButtonVariants[\"size\"];\n /**\n * rounded · pill.\n * @default \"rounded\"\n */\n shape?: ButtonVariants[\"shape\"];\n /** Renders the child (e.g. a <Link>) instead of the <button>, with props, classes and ref merged. */\n asChild?: boolean;\n /** Disables and signals the wait (aria-busy, data-loading) — the product adds a spinner if it wants one. */\n loading?: boolean;\n };\n\nexport function Button({ asChild, loading, variant, size, shape, className, disabled, ...props }: ButtonProps) {\n const Comp = asChild ? Slot.Root : \"button\";\n return (\n <Comp\n type={asChild ? undefined : \"button\"}\n aria-busy={loading || undefined}\n data-loading={loading ? \"\" : undefined}\n data-variant={variant ?? \"secondary\"}\n disabled={asChild ? undefined : disabled || loading}\n aria-disabled={asChild && (disabled || loading) ? true : undefined}\n className={buttonVariants({ variant, size, shape, className })}\n {...props}\n />\n );\n}\n\nexport type IconButtonProps = Omit<ButtonProps, \"size\"> & {\n /** The accessible name (and tooltip) of the button — required, passed translated. */\n label: string;\n /**\n * sm · md · lg, or `icon` (a square).\n * @default \"icon\"\n */\n size?: ButtonProps[\"size\"];\n};\n\n/** An icon-only button: `label` is required, it is its accessible name. */\nexport function IconButton({ label, size = \"icon\", ...props }: IconButtonProps) {\n return <Button aria-label={label} title={label} size={size} {...props} />;\n}\n"
514
+ "content": "// Server-safe: no hook, no context. `buttonVariants` styles a <Link> or a Server Component without the component.\nimport { Slot } from \"radix-ui\";\nimport type * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nexport const buttonVariants = tv({\n base:\n \"inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap font-semibold transition-all \" +\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:pointer-events-none disabled:opacity-40 data-loading:cursor-progress active:scale-[0.98]\",\n variants: {\n variant: {\n primary: \"bg-accent text-on-accent shadow-sm hover:bg-accent-hover\",\n // The brand's signature: accent → accent-2 (on-accent reads on both stops, tested in @krizaka/tokens).\n gradient: \"bg-linear-to-r from-accent to-accent-2 text-on-accent shadow-sm hover:brightness-110\",\n secondary: \"border border-border-default bg-surface-2 text-fg hover:border-border-strong hover:bg-surface-3\",\n outline: \"border border-border-default bg-transparent text-fg hover:border-accent hover:text-fg-accent\",\n ghost: \"text-fg-secondary hover:bg-surface-2 hover:text-fg\",\n // The border and the tint carry the danger; the label stays the primary text role, legible on the tint at rest\n // and hovered over every surface (`text-fg-danger` is for text set directly on a surface).\n danger: \"border border-danger/50 bg-danger/10 text-fg hover:border-danger hover:bg-danger/20\",\n },\n size: { sm: \"h-8 px-3 text-xs\", md: \"h-10 px-4 text-sm\", lg: \"h-12 px-6 text-sm\", icon: \"h-10 w-10\" },\n shape: { rounded: \"rounded-lg\", pill: \"rounded-full\" },\n },\n defaultVariants: { variant: \"secondary\", size: \"md\", shape: \"rounded\" },\n});\n\nexport type ButtonVariants = VariantProps<typeof buttonVariants>;\n\nexport type ButtonProps = React.ComponentProps<\"button\"> &\n ButtonVariants & {\n /**\n * The look: `primary` (the accent), `gradient` (accent → accent-2), `secondary` (a raised surface), `outline`,\n * `ghost`, `danger`.\n * @default \"secondary\"\n */\n variant?: ButtonVariants[\"variant\"];\n /**\n * sm · md · lg, or `icon` (a square).\n * @default \"md\"\n */\n size?: ButtonVariants[\"size\"];\n /**\n * rounded · pill.\n * @default \"rounded\"\n */\n shape?: ButtonVariants[\"shape\"];\n /** Renders the child (e.g. a <Link>) instead of the <button>, with props, classes and ref merged. */\n asChild?: boolean;\n /** Disables and signals the wait (aria-busy, data-loading) — the product adds a spinner if it wants one. */\n loading?: boolean;\n };\n\nexport function Button({ asChild, loading, variant, size, shape, className, disabled, ...props }: ButtonProps) {\n const Comp = asChild ? Slot.Root : \"button\";\n return (\n <Comp\n type={asChild ? undefined : \"button\"}\n aria-busy={loading || undefined}\n data-loading={loading ? \"\" : undefined}\n data-variant={variant ?? \"secondary\"}\n disabled={asChild ? undefined : disabled || loading}\n aria-disabled={asChild && (disabled || loading) ? true : undefined}\n className={buttonVariants({ variant, size, shape, className })}\n {...props}\n />\n );\n}\n\nexport type IconButtonProps = Omit<ButtonProps, \"size\"> & {\n /** The accessible name (and tooltip) of the button — required, passed translated. */\n label: string;\n /**\n * sm · md · lg, or `icon` (a square).\n * @default \"icon\"\n */\n size?: ButtonProps[\"size\"];\n};\n\n/** An icon-only button: `label` is required, it is its accessible name. */\nexport function IconButton({ label, size = \"icon\", ...props }: IconButtonProps) {\n return <Button aria-label={label} title={label} size={size} {...props} />;\n}\n"
494
515
  },
495
516
  {
496
517
  "path": "button/index.ts",
@@ -498,7 +519,7 @@
498
519
  },
499
520
  {
500
521
  "path": "button/meta.ts",
501
- "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Button\",\n summary: \"The action: a button in five looks (primary, secondary, outline, ghost, danger), three sizes and two shapes.\",\n status: \"stable\",\n category: \"actions\",\n platforms: \"both\",\n whenToUse: [\n \"To run an action: submit a form, open a dialog, start an upload, save a draft.\",\n \"One `primary` button for the main action of a view; `secondary`, `outline` or `ghost` for the others.\",\n \"`IconButton` for a compact action shown by its icon alone (close, add, more), with a `label` for its name.\",\n ],\n whenNotToUse: [\n { when: \"To go to another page: use a link (`asChild` with your router's `<Link>` keeps the button look).\" },\n { when: \"To turn a setting on or off, with an immediate effect.\", use: \"switch\" },\n { when: \"For a destructive action that needs a second press to confirm, in place.\", use: \"confirm-button\" },\n { when: \"To choose one option among several, or filter a list.\", use: \"chip\" },\n ],\n bestPractices: [\n \"Name the action with a verb: “Save”, “Send the invitation” — not “OK” or “Yes”.\",\n \"One primary per view; the danger variant only for what destroys or cannot be undone.\",\n \"`loading` while the action runs: the button is disabled and says it is busy; keep its width by keeping its words short.\",\n \"Style a link without the component with `buttonVariants({ variant, size })`.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"Enter / Space\", action: \"Runs the action.\" },\n { keys: \"Tab\", action: \"Moves the focus to the next control; the focus ring uses `--kz-ring`.\" },\n ],\n notes: [\n \"A native `<button type=\\\"button\\\">`: the role, the focus and the keyboard come from the platform.\",\n \"`IconButton` requires `label`: it becomes the `aria-label` of a button that shows no text.\",\n \"`loading` sets `aria-busy` and disables the button; `disabled` keeps it out of the tab order.\",\n ],\n },\n related: [\"confirm-button\", \"dialog\", \"dropdown-menu\", \"tooltip\"],\n web: {\n imports: [\"Button\", \"IconButton\"],\n examples: [\n { name: \"primary\", title: \"Primary\", description: \"The main action of a view: the accent.\" },\n { name: \"secondary\", title: \"Secondary\", description: \"The default: a raised surface, for the other actions.\" },\n { name: \"outline\", title: \"Outline\", description: \"A lighter action next to a primary one.\" },\n { name: \"ghost\", title: \"Ghost\", description: \"In toolbars and dense lists: no frame until hovered.\" },\n { name: \"danger\", title: \"Danger\", description: \"A destructive action: the border and the tint carry the danger, the label stays legible in both themes.\" },\n { name: \"sizes\", title: \"Sizes\", description: \"`sm`, `md` (default) and `lg`.\" },\n { name: \"pill\", title: \"Pill\", description: \"`shape=\\\"pill\\\"`: fully rounded.\" },\n { name: \"disabled\", title: \"Disabled\", description: \"Not available yet: out of the tab order.\" },\n { name: \"loading\", title: \"Loading\", description: \"While the action runs: disabled, `aria-busy`, `data-loading`.\" },\n { name: \"as-child-link\", title: \"As a link\", description: \"`asChild` renders your `<a>` (or router link) with the button's look.\" },\n { name: \"icon\", title: \"Icon button\", description: \"`IconButton`: a square button holding an icon; `label` is its accessible name.\" },\n ],\n },\n native: {\n imports: [\"Button\", \"IconButton\"],\n differences: [\n \"The text is the `label` prop (a string), not children: it is also the accessible name.\",\n \"`onPress` instead of `onClick` (a `Pressable`); haptics, if any, belong to your `onPress`.\",\n \"`icon` places an icon before the label; there is no `asChild`.\",\n \"`loading` shows the platform's `ActivityIndicator` in place of the icon.\",\n ],\n examples: [\n { name: \"primary\", title: \"Primary\", description: \"The main action of a screen.\" },\n { name: \"secondary\", title: \"Secondary\", description: \"The default look.\" },\n { name: \"outline\", title: \"Outline\", description: \"A lighter action.\" },\n { name: \"ghost\", title: \"Ghost\", description: \"No frame: toolbars, headers.\" },\n { name: \"danger\", title: \"Danger\", description: \"A destructive action.\" },\n { name: \"sizes\", title: \"Sizes\", description: \"`sm`, `md`, `lg`, and `shape=\\\"pill\\\"`.\" },\n { name: \"icon\", title: \"With an icon\", description: \"`icon` before the label.\" },\n { name: \"disabled\", title: \"Disabled\", description: \"Not available yet.\" },\n { name: \"loading\", title: \"Loading\", description: \"Disabled and busy, a spinner in place of the icon.\" },\n { name: \"icon-only\", title: \"Icon button\", description: \"`IconButton`: `label` is required, it is the accessible name.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
522
+ "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Button\",\n summary: \"The action: a button in six looks (primary, gradient, secondary, outline, ghost, danger), three sizes and two shapes.\",\n status: \"stable\",\n category: \"actions\",\n platforms: \"both\",\n whenToUse: [\n \"To run an action: submit a form, open a dialog, start an upload, save a draft.\",\n \"One `primary` button for the main action of a view; `secondary`, `outline` or `ghost` for the others.\",\n \"`IconButton` for a compact action shown by its icon alone (close, add, more), with a `label` for its name.\",\n ],\n whenNotToUse: [\n { when: \"To go to another page: use a link (`asChild` with your router's `<Link>` keeps the button look).\" },\n { when: \"To turn a setting on or off, with an immediate effect.\", use: \"switch\" },\n { when: \"For a destructive action that needs a second press to confirm, in place.\", use: \"confirm-button\" },\n { when: \"To choose one option among several, or filter a list.\", use: \"chip\" },\n ],\n bestPractices: [\n \"Name the action with a verb: “Save”, “Send the invitation” — not “OK” or “Yes”.\",\n \"One primary per view; the danger variant only for what destroys or cannot be undone.\",\n \"`loading` while the action runs: the button is disabled and says it is busy; keep its width by keeping its words short.\",\n \"Style a link without the component with `buttonVariants({ variant, size })`.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"Enter / Space\", action: \"Runs the action.\" },\n { keys: \"Tab\", action: \"Moves the focus to the next control; the focus ring uses `--kz-ring`.\" },\n ],\n notes: [\n \"A native `<button type=\\\"button\\\">`: the role, the focus and the keyboard come from the platform.\",\n \"`IconButton` requires `label`: it becomes the `aria-label` of a button that shows no text.\",\n \"`loading` sets `aria-busy` and disables the button; `disabled` keeps it out of the tab order.\",\n ],\n },\n related: [\"confirm-button\", \"dialog\", \"dropdown-menu\", \"tooltip\"],\n web: {\n imports: [\"Button\", \"IconButton\"],\n examples: [\n { name: \"primary\", title: \"Primary\", description: \"The main action of a view: the accent.\" },\n { name: \"gradient\", title: \"Gradient\", description: \"The brand's signature, accent → accent-2: a hero or a closing call to action, once per page.\" },\n { name: \"secondary\", title: \"Secondary\", description: \"The default: a raised surface, for the other actions.\" },\n { name: \"outline\", title: \"Outline\", description: \"A lighter action next to a primary one.\" },\n { name: \"ghost\", title: \"Ghost\", description: \"In toolbars and dense lists: no frame until hovered.\" },\n { name: \"danger\", title: \"Danger\", description: \"A destructive action: the border and the tint carry the danger, the label stays legible in both themes.\" },\n { name: \"sizes\", title: \"Sizes\", description: \"`sm`, `md` (default) and `lg`.\" },\n { name: \"pill\", title: \"Pill\", description: \"`shape=\\\"pill\\\"`: fully rounded.\" },\n { name: \"disabled\", title: \"Disabled\", description: \"Not available yet: out of the tab order.\" },\n { name: \"loading\", title: \"Loading\", description: \"While the action runs: disabled, `aria-busy`, `data-loading`.\" },\n { name: \"as-child-link\", title: \"As a link\", description: \"`asChild` renders your `<a>` (or router link) with the button's look.\" },\n { name: \"icon\", title: \"Icon button\", description: \"`IconButton`: a square button holding an icon; `label` is its accessible name.\" },\n ],\n },\n native: {\n imports: [\"Button\", \"IconButton\"],\n differences: [\n \"The text is the `label` prop (a string), not children: it is also the accessible name.\",\n \"`onPress` instead of `onClick` (a `Pressable`); haptics, if any, belong to your `onPress`.\",\n \"`icon` places an icon before the label; there is no `asChild`.\",\n \"`loading` shows the platform's `ActivityIndicator` in place of the icon.\",\n \"`variant=\\\"gradient\\\"` draws its fill with react-native-svg (React Native has no CSS gradient).\",\n ],\n examples: [\n { name: \"primary\", title: \"Primary\", description: \"The main action of a screen.\" },\n { name: \"gradient\", title: \"Gradient\", description: \"accent → accent-2, drawn with react-native-svg: the hero action of a screen.\" },\n { name: \"secondary\", title: \"Secondary\", description: \"The default look.\" },\n { name: \"outline\", title: \"Outline\", description: \"A lighter action.\" },\n { name: \"ghost\", title: \"Ghost\", description: \"No frame: toolbars, headers.\" },\n { name: \"danger\", title: \"Danger\", description: \"A destructive action.\" },\n { name: \"sizes\", title: \"Sizes\", description: \"`sm`, `md`, `lg`, and `shape=\\\"pill\\\"`.\" },\n { name: \"icon\", title: \"With an icon\", description: \"`icon` before the label.\" },\n { name: \"disabled\", title: \"Disabled\", description: \"Not available yet.\" },\n { name: \"loading\", title: \"Loading\", description: \"Disabled and busy, a spinner in place of the icon.\" },\n { name: \"icon-only\", title: \"Icon button\", description: \"`IconButton`: `label` is required, it is the accessible name.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
502
523
  },
503
524
  {
504
525
  "path": "meta.ts",
@@ -549,9 +570,9 @@
549
570
  },
550
571
  {
551
572
  "name": "variant",
552
- "type": "\"danger\" | \"primary\" | \"secondary\" | \"outline\" | \"ghost\"",
573
+ "type": "\"danger\" | \"primary\" | \"gradient\" | \"secondary\" | \"outline\" | \"ghost\"",
553
574
  "default": "secondary",
554
- "description": "The look: `primary` (the accent), `secondary` (a raised surface), `outline`, `ghost`, `danger`.",
575
+ "description": "The look: `primary` (the accent), `gradient` (accent → accent-2), `secondary` (a raised surface), `outline`,\n`ghost`, `danger`.",
555
576
  "required": false
556
577
  }
557
578
  ]
@@ -597,9 +618,9 @@
597
618
  },
598
619
  {
599
620
  "name": "variant",
600
- "type": "\"danger\" | \"primary\" | \"secondary\" | \"outline\" | \"ghost\"",
621
+ "type": "\"danger\" | \"primary\" | \"gradient\" | \"secondary\" | \"outline\" | \"ghost\"",
601
622
  "default": "secondary",
602
- "description": "The look: `primary` (the accent), `secondary` (a raised surface), `outline`, `ghost`, `danger`.",
623
+ "description": "The look: `primary` (the accent), `gradient` (accent → accent-2), `secondary` (a raised surface), `outline`,\n`ghost`, `danger`.",
603
624
  "required": false
604
625
  }
605
626
  ]
@@ -29,6 +29,7 @@
29
29
  "Build product cards from these parts and override any of them with `className` — never fork the card.",
30
30
  "Keep one primary target per card: either the whole card is a link, or it holds buttons — not both.",
31
31
  "Give `Card.Media` a fixed `aspect` so a grid stays aligned while images load.",
32
+ "`Card.Image` loads lazily; pass `loading=\"eager\"` (and `fetchPriority=\"high\"`) to the first cards of a page, above the fold.",
32
33
  "`reveal={index}` with one `MotionObserver` per page for the entrance; it is shown at once under reduced motion."
33
34
  ],
34
35
  "accessibility": {
@@ -119,6 +120,13 @@
119
120
  "description": "Shown when there is no image: an icon, an illustration (decorative).",
120
121
  "required": false
121
122
  },
123
+ {
124
+ "name": "loading",
125
+ "type": "\"eager\" | \"lazy\"",
126
+ "default": "lazy",
127
+ "description": "When the browser loads it: `lazy` (default, a card in a list or below the fold) or `eager` for the first cards\nof a page — the image that is the largest contentful paint must not wait (add `fetchPriority=\"high\"`).",
128
+ "required": false
129
+ },
122
130
  {
123
131
  "name": "src",
124
132
  "type": "string | null",
@@ -405,7 +413,7 @@
405
413
  "files": [
406
414
  {
407
415
  "path": "card/card.tsx",
408
- "content": "// Server-safe: no hook, no context. One `tv` definition with slots: each part takes its slot, and a product overrides\n// any part through `className` (merged last by tailwind-merge).\nimport { Slot } from \"radix-ui\";\nimport type * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nimport { cn } from \"../cn\";\n\nexport const card = tv({\n slots: {\n root:\n \"group relative flex flex-col overflow-hidden border border-border-default bg-surface-1 text-fg transition-colors \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n media: \"relative overflow-hidden bg-media\",\n image: \"h-full w-full object-cover transition-transform duration-500 motion-safe:group-hover:scale-105\",\n fallback: \"flex h-full w-full items-center justify-center bg-linear-to-br from-accent/15 via-media to-accent-2/15 text-accent/60\",\n overlay: \"absolute z-10 flex items-center gap-1.5\",\n body: \"relative flex flex-1 flex-col gap-3\",\n title: \"line-clamp-1 text-sm font-semibold text-fg transition-colors group-hover:text-fg-accent\",\n description: \"line-clamp-2 text-xs text-fg-secondary\",\n stat: \"min-w-0\",\n statLabel: \"text-[10px] font-semibold uppercase tracking-wider text-fg-secondary\",\n statValue: \"font-display text-xl font-black tabular-nums text-fg\",\n footer: \"relative mt-auto flex items-center gap-2 border-t border-border-subtle pt-3 text-xs text-fg-secondary\",\n },\n variants: {\n radius: { md: { root: \"rounded-md\" }, lg: { root: \"rounded-lg\" }, xl: { root: \"rounded-xl\" } },\n tone: { default: {}, elevated: { root: \"bg-surface-2 shadow-md\" }, glass: { root: \"bg-surface-1/70 backdrop-blur-md\" } },\n interactive: { true: { root: \"kz-spotlight kz-lift hover:border-accent/50\" } },\n padding: { none: { body: \"p-0\" }, sm: { body: \"p-3\" }, md: { body: \"p-4\" }, lg: { body: \"p-6\" } },\n },\n defaultVariants: { radius: \"xl\", tone: \"default\", padding: \"md\" },\n});\n\ntype CardVariants = VariantProps<typeof card>;\n/** The parts without a variant read the default slots: no context, so server-safe. */\nconst slots = card();\n\nexport type CardRootProps = React.ComponentProps<\"div\"> &\n Pick<CardVariants, \"radius\" | \"tone\" | \"interactive\"> & {\n /**\n * md · lg · xl.\n * @default \"xl\"\n */\n radius?: CardVariants[\"radius\"];\n /**\n * default (the base surface) · elevated (raised, a shadow) · glass (translucent, blurred).\n * @default \"default\"\n */\n tone?: CardVariants[\"tone\"];\n /** The spotlight that follows the pointer and the lift on hover: for a card that is a link. */\n interactive?: CardVariants[\"interactive\"];\n /** Renders the child (a link, an article) instead of the <div>, with props, classes and ref merged. */\n asChild?: boolean;\n /** Enters on scroll (MotionObserver): the index in the grid becomes the delay (50 ms a step, 8 steps at most). */\n reveal?: number;\n };\n\nexport function CardRoot({ asChild, reveal, radius, tone, interactive, className, style, ...props }: CardRootProps) {\n const Comp = asChild ? Slot.Root : \"div\";\n const revealStyle =\n reveal === undefined ? style : ({ ...style, \"--kz-delay\": `${Math.min(Math.max(reveal, 0), 8) * 50}ms` } as React.CSSProperties);\n return (\n <Comp\n data-reveal={reveal === undefined ? undefined : \"\"}\n data-interactive={interactive ? \"\" : undefined}\n style={revealStyle}\n className={card({ radius, tone, interactive }).root({ className })}\n {...props}\n />\n );\n}\n\nconst ASPECT = { video: \"aspect-video\", square: \"aspect-square\", portrait: \"aspect-[3/4]\", auto: \"\" } as const;\nexport type CardMediaProps = React.ComponentProps<\"div\"> & {\n /** The ratio of the media: video (16:9) · square · portrait (3:4) · auto (the content's). */\n aspect?: keyof typeof ASPECT;\n};\n\nexport function CardMedia({ aspect = \"video\", className, ...props }: CardMediaProps) {\n return <div data-aspect={aspect} className={slots.media({ className: cn(ASPECT[aspect], className) })} {...props} />;\n}\n\nexport type CardImageProps = Omit<React.ComponentProps<\"img\">, \"src\"> & {\n /** The image's URL; `null` or nothing shows the `fallback`. */\n src?: string | null;\n /** Shown when there is no image: an icon, an illustration (decorative). */\n fallback?: React.ReactNode;\n};\n\n/** The image of a media, or its `fallback` when there is none. `alt` is empty by default: the title names the card. */\nexport function CardImage({ src, alt = \"\", fallback, className, ...props }: CardImageProps) {\n if (!src) {\n return (\n <div data-fallback=\"\" aria-hidden className={slots.fallback({ className })}>\n {fallback}\n </div>\n );\n }\n return <img src={src} alt={alt} loading=\"lazy\" decoding=\"async\" className={slots.image({ className })} {...props} />;\n}\n\nconst CORNER = {\n \"top-left\": \"left-2.5 top-2.5\",\n \"top-right\": \"right-2.5 top-2.5\",\n \"bottom-left\": \"bottom-2.5 left-2.5\",\n \"bottom-right\": \"bottom-2.5 right-2.5\",\n} as const;\nexport type CardOverlayProps = React.ComponentProps<\"div\"> & {\n /** The corner of the media it sits in. */\n corner?: keyof typeof CORNER;\n};\n\n/** What sits on the media (badges, a caption). `scrim` and `fg-on-media` are invariant: nothing to do for light. */\nexport function CardOverlay({ corner = \"top-left\", className, ...props }: CardOverlayProps) {\n return <div data-corner={corner} className={slots.overlay({ className: cn(CORNER[corner], className) })} {...props} />;\n}\n\nexport type CardBodyProps = React.ComponentProps<\"div\"> & {\n /**\n * none · sm · md · lg.\n * @default \"md\"\n */\n padding?: CardVariants[\"padding\"];\n};\n\nexport function CardBody({ padding, className, ...props }: CardBodyProps) {\n return <div className={card({ padding }).body({ className })} {...props} />;\n}\n\nexport type CardTitleProps = React.ComponentProps<\"h3\"> & {\n /** The heading level, to fit the page's outline. */\n as?: \"h2\" | \"h3\" | \"h4\";\n};\n\nexport function CardTitle({ as: Tag = \"h3\", className, ...props }: CardTitleProps) {\n return <Tag className={slots.title({ className })} {...props} />;\n}\n\nexport function CardDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return <p className={slots.description({ className })} {...props} />;\n}\n\nexport type CardStatProps = React.ComponentProps<\"div\"> & {\n /** What the figure is, in small capitals above it (the figure is the children). */\n label: React.ReactNode;\n};\n\n/** A figure put forward: the label in capitals, the value in tabular digits. */\nexport function CardStat({ label, className, children, ...props }: CardStatProps) {\n return (\n <div className={slots.stat({ className })} {...props}>\n <p className={slots.statLabel()}>{label}</p>\n <p className={slots.statValue()}>{children}</p>\n </div>\n );\n}\n\nexport function CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div className={slots.footer({ className })} {...props} />;\n}\n\n/** `Card.Root/Media/Image/Overlay/Body/Title/Description/Stat/Footer`. */\nexport const Card = {\n Root: CardRoot,\n Media: CardMedia,\n Image: CardImage,\n Overlay: CardOverlay,\n Body: CardBody,\n Title: CardTitle,\n Description: CardDescription,\n Stat: CardStat,\n Footer: CardFooter,\n};\n"
416
+ "content": "// Server-safe: no hook, no context. One `tv` definition with slots: each part takes its slot, and a product overrides\n// any part through `className` (merged last by tailwind-merge).\nimport { Slot } from \"radix-ui\";\nimport type * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nimport { cn } from \"../cn\";\n\nexport const card = tv({\n slots: {\n root:\n \"group relative flex flex-col overflow-hidden border border-border-default bg-surface-1 text-fg transition-colors \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n media: \"relative overflow-hidden bg-media\",\n image: \"h-full w-full object-cover transition-transform duration-500 motion-safe:group-hover:scale-105\",\n fallback: \"flex h-full w-full items-center justify-center bg-linear-to-br from-accent/15 via-media to-accent-2/15 text-accent/60\",\n overlay: \"absolute z-10 flex items-center gap-1.5\",\n body: \"relative flex flex-1 flex-col gap-3\",\n title: \"line-clamp-1 text-sm font-semibold text-fg transition-colors group-hover:text-fg-accent\",\n description: \"line-clamp-2 text-xs text-fg-secondary\",\n stat: \"min-w-0\",\n statLabel: \"text-[10px] font-semibold uppercase tracking-wider text-fg-secondary\",\n statValue: \"font-display text-xl font-black tabular-nums text-fg\",\n footer: \"relative mt-auto flex items-center gap-2 border-t border-border-subtle pt-3 text-xs text-fg-secondary\",\n },\n variants: {\n radius: { md: { root: \"rounded-md\" }, lg: { root: \"rounded-lg\" }, xl: { root: \"rounded-xl\" } },\n tone: { default: {}, elevated: { root: \"bg-surface-2 shadow-md\" }, glass: { root: \"bg-surface-1/70 backdrop-blur-md\" } },\n interactive: { true: { root: \"kz-spotlight kz-lift hover:border-accent/50\" } },\n padding: { none: { body: \"p-0\" }, sm: { body: \"p-3\" }, md: { body: \"p-4\" }, lg: { body: \"p-6\" } },\n },\n defaultVariants: { radius: \"xl\", tone: \"default\", padding: \"md\" },\n});\n\ntype CardVariants = VariantProps<typeof card>;\n/** The parts without a variant read the default slots: no context, so server-safe. */\nconst slots = card();\n\nexport type CardRootProps = React.ComponentProps<\"div\"> &\n Pick<CardVariants, \"radius\" | \"tone\" | \"interactive\"> & {\n /**\n * md · lg · xl.\n * @default \"xl\"\n */\n radius?: CardVariants[\"radius\"];\n /**\n * default (the base surface) · elevated (raised, a shadow) · glass (translucent, blurred).\n * @default \"default\"\n */\n tone?: CardVariants[\"tone\"];\n /** The spotlight that follows the pointer and the lift on hover: for a card that is a link. */\n interactive?: CardVariants[\"interactive\"];\n /** Renders the child (a link, an article) instead of the <div>, with props, classes and ref merged. */\n asChild?: boolean;\n /** Enters on scroll (MotionObserver): the index in the grid becomes the delay (50 ms a step, 8 steps at most). */\n reveal?: number;\n };\n\nexport function CardRoot({ asChild, reveal, radius, tone, interactive, className, style, ...props }: CardRootProps) {\n const Comp = asChild ? Slot.Root : \"div\";\n const revealStyle =\n reveal === undefined ? style : ({ ...style, \"--kz-delay\": `${Math.min(Math.max(reveal, 0), 8) * 50}ms` } as React.CSSProperties);\n return (\n <Comp\n data-reveal={reveal === undefined ? undefined : \"\"}\n data-interactive={interactive ? \"\" : undefined}\n style={revealStyle}\n className={card({ radius, tone, interactive }).root({ className })}\n {...props}\n />\n );\n}\n\nconst ASPECT = { video: \"aspect-video\", square: \"aspect-square\", portrait: \"aspect-[3/4]\", auto: \"\" } as const;\nexport type CardMediaProps = React.ComponentProps<\"div\"> & {\n /** The ratio of the media: video (16:9) · square · portrait (3:4) · auto (the content's). */\n aspect?: keyof typeof ASPECT;\n};\n\nexport function CardMedia({ aspect = \"video\", className, ...props }: CardMediaProps) {\n return <div data-aspect={aspect} className={slots.media({ className: cn(ASPECT[aspect], className) })} {...props} />;\n}\n\nexport type CardImageProps = Omit<React.ComponentProps<\"img\">, \"src\"> & {\n /** The image's URL; `null` or nothing shows the `fallback`. */\n src?: string | null;\n /** Shown when there is no image: an icon, an illustration (decorative). */\n fallback?: React.ReactNode;\n /**\n * When the browser loads it: `lazy` (default, a card in a list or below the fold) or `eager` for the first cards\n * of a page — the image that is the largest contentful paint must not wait (add `fetchPriority=\"high\"`).\n * @default \"lazy\"\n */\n loading?: \"lazy\" | \"eager\";\n};\n\n/** The image of a media, or its `fallback` when there is none. `alt` is empty by default: the title names the card. */\nexport function CardImage({ src, alt = \"\", fallback, loading = \"lazy\", className, ...props }: CardImageProps) {\n if (!src) {\n return (\n <div data-fallback=\"\" aria-hidden className={slots.fallback({ className })}>\n {fallback}\n </div>\n );\n }\n return <img src={src} alt={alt} loading={loading} decoding=\"async\" className={slots.image({ className })} {...props} />;\n}\n\nconst CORNER = {\n \"top-left\": \"left-2.5 top-2.5\",\n \"top-right\": \"right-2.5 top-2.5\",\n \"bottom-left\": \"bottom-2.5 left-2.5\",\n \"bottom-right\": \"bottom-2.5 right-2.5\",\n} as const;\nexport type CardOverlayProps = React.ComponentProps<\"div\"> & {\n /** The corner of the media it sits in. */\n corner?: keyof typeof CORNER;\n};\n\n/** What sits on the media (badges, a caption). `scrim` and `fg-on-media` are invariant: nothing to do for light. */\nexport function CardOverlay({ corner = \"top-left\", className, ...props }: CardOverlayProps) {\n return <div data-corner={corner} className={slots.overlay({ className: cn(CORNER[corner], className) })} {...props} />;\n}\n\nexport type CardBodyProps = React.ComponentProps<\"div\"> & {\n /**\n * none · sm · md · lg.\n * @default \"md\"\n */\n padding?: CardVariants[\"padding\"];\n};\n\nexport function CardBody({ padding, className, ...props }: CardBodyProps) {\n return <div className={card({ padding }).body({ className })} {...props} />;\n}\n\nexport type CardTitleProps = React.ComponentProps<\"h3\"> & {\n /** The heading level, to fit the page's outline. */\n as?: \"h2\" | \"h3\" | \"h4\";\n};\n\nexport function CardTitle({ as: Tag = \"h3\", className, ...props }: CardTitleProps) {\n return <Tag className={slots.title({ className })} {...props} />;\n}\n\nexport function CardDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return <p className={slots.description({ className })} {...props} />;\n}\n\nexport type CardStatProps = React.ComponentProps<\"div\"> & {\n /** What the figure is, in small capitals above it (the figure is the children). */\n label: React.ReactNode;\n};\n\n/** A figure put forward: the label in capitals, the value in tabular digits. */\nexport function CardStat({ label, className, children, ...props }: CardStatProps) {\n return (\n <div className={slots.stat({ className })} {...props}>\n <p className={slots.statLabel()}>{label}</p>\n <p className={slots.statValue()}>{children}</p>\n </div>\n );\n}\n\nexport function CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div className={slots.footer({ className })} {...props} />;\n}\n\n/** `Card.Root/Media/Image/Overlay/Body/Title/Description/Stat/Footer`. */\nexport const Card = {\n Root: CardRoot,\n Media: CardMedia,\n Image: CardImage,\n Overlay: CardOverlay,\n Body: CardBody,\n Title: CardTitle,\n Description: CardDescription,\n Stat: CardStat,\n Footer: CardFooter,\n};\n"
409
417
  },
410
418
  {
411
419
  "path": "card/index.ts",
@@ -413,7 +421,7 @@
413
421
  },
414
422
  {
415
423
  "path": "card/meta.ts",
416
- "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Card\",\n summary: \"The composed chassis of every card: a media with overlays, a body with a title, a description, stats and a footer.\",\n status: \"stable\",\n category: \"layout\",\n platforms: \"both\",\n whenToUse: [\n \"To present one item of a collection: a video, a creator, a guide, a plan.\",\n \"As a link to the item (`asChild` + `interactive`): the whole card is one target.\",\n \"To group a few figures under a title (`Card.Stat`).\",\n ],\n whenNotToUse: [\n { when: \"To frame a whole page section: use the page layout, not a card around everything.\" },\n { when: \"For a single key figure without a frame.\", use: \"stat\" },\n { when: \"For a message about the page.\", use: \"alert\" },\n ],\n bestPractices: [\n \"Build product cards from these parts and override any of them with `className` — never fork the card.\",\n \"Keep one primary target per card: either the whole card is a link, or it holds buttons — not both.\",\n \"Give `Card.Media` a fixed `aspect` so a grid stays aligned while images load.\",\n \"`reveal={index}` with one `MotionObserver` per page for the entrance; it is shown at once under reduced motion.\",\n ],\n accessibility: {\n keyboard: [{ keys: \"Tab / Enter\", action: \"An `asChild` link card is one focusable link that Enter follows.\" }],\n notes: [\n \"`Card.Title` is a heading (`h3` by default, `as` to change the level) so cards are reachable by heading.\",\n \"A card that is a link is named by its content: keep the title first.\",\n \"The image fallback is decorative and hidden from assistive technology.\",\n ],\n },\n related: [\"badge\", \"stat\", \"avatar\", \"skeleton\"],\n web: {\n imports: [\"Card\"],\n examples: [\n { name: \"media-card\", title: \"Media card\", description: \"An image, badges on the media, a title, a description and a footer.\" },\n { name: \"media-fallback\", title: \"Media fallback\", description: \"No image: the `fallback` on the accent gradient.\" },\n { name: \"interactive-link\", title: \"Interactive link\", description: \"`asChild` + `<a>`: the whole card is a link, with the spotlight and the lift.\" },\n { name: \"stat-card\", title: \"Stat card\", description: \"Figures put forward with `Card.Stat`, `tone=\\\"elevated\\\"`.\" },\n { name: \"reveal-grid\", title: \"Reveal grid\", description: \"Cards entering in cascade with `reveal` and `MotionObserver`.\" },\n ],\n },\n native: {\n imports: [\"Card\"],\n differences: [\n \"`onPress` on `Card.Root` makes the whole card a button (no `asChild`, no `interactive`).\",\n \"Built on `View`, `Image` and `Text`: style with `style`, not `className`.\",\n \"No `Card.Stat` and no `reveal`.\",\n ],\n examples: [\n { name: \"default\", title: \"Default\", description: \"A media, a body and a footer.\" },\n { name: \"elevated\", title: \"Elevated\", description: \"`tone=\\\"elevated\\\"`: one step higher, with a shadow.\" },\n { name: \"pressable\", title: \"Pressable\", description: \"`onPress`: the whole card is a button.\" },\n { name: \"body\", title: \"Text and an action\", description: \"A body only, with a button.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
424
+ "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Card\",\n summary: \"The composed chassis of every card: a media with overlays, a body with a title, a description, stats and a footer.\",\n status: \"stable\",\n category: \"layout\",\n platforms: \"both\",\n whenToUse: [\n \"To present one item of a collection: a video, a creator, a guide, a plan.\",\n \"As a link to the item (`asChild` + `interactive`): the whole card is one target.\",\n \"To group a few figures under a title (`Card.Stat`).\",\n ],\n whenNotToUse: [\n { when: \"To frame a whole page section: use the page layout, not a card around everything.\" },\n { when: \"For a single key figure without a frame.\", use: \"stat\" },\n { when: \"For a message about the page.\", use: \"alert\" },\n ],\n bestPractices: [\n \"Build product cards from these parts and override any of them with `className` — never fork the card.\",\n \"Keep one primary target per card: either the whole card is a link, or it holds buttons — not both.\",\n \"Give `Card.Media` a fixed `aspect` so a grid stays aligned while images load.\",\n \"`Card.Image` loads lazily; pass `loading=\\\"eager\\\"` (and `fetchPriority=\\\"high\\\"`) to the first cards of a page, above the fold.\",\n \"`reveal={index}` with one `MotionObserver` per page for the entrance; it is shown at once under reduced motion.\",\n ],\n accessibility: {\n keyboard: [{ keys: \"Tab / Enter\", action: \"An `asChild` link card is one focusable link that Enter follows.\" }],\n notes: [\n \"`Card.Title` is a heading (`h3` by default, `as` to change the level) so cards are reachable by heading.\",\n \"A card that is a link is named by its content: keep the title first.\",\n \"The image fallback is decorative and hidden from assistive technology.\",\n ],\n },\n related: [\"badge\", \"stat\", \"avatar\", \"skeleton\"],\n web: {\n imports: [\"Card\"],\n examples: [\n { name: \"media-card\", title: \"Media card\", description: \"An image, badges on the media, a title, a description and a footer.\" },\n { name: \"media-fallback\", title: \"Media fallback\", description: \"No image: the `fallback` on the accent gradient.\" },\n { name: \"interactive-link\", title: \"Interactive link\", description: \"`asChild` + `<a>`: the whole card is a link, with the spotlight and the lift.\" },\n { name: \"stat-card\", title: \"Stat card\", description: \"Figures put forward with `Card.Stat`, `tone=\\\"elevated\\\"`.\" },\n { name: \"reveal-grid\", title: \"Reveal grid\", description: \"Cards entering in cascade with `reveal` and `MotionObserver`.\" },\n ],\n },\n native: {\n imports: [\"Card\"],\n differences: [\n \"`onPress` on `Card.Root` makes the whole card a button (no `asChild`, no `interactive`).\",\n \"Built on `View`, `Image` and `Text`: style with `style`, not `className`.\",\n \"No `Card.Stat` and no `reveal`.\",\n ],\n examples: [\n { name: \"default\", title: \"Default\", description: \"A media, a body and a footer.\" },\n { name: \"elevated\", title: \"Elevated\", description: \"`tone=\\\"elevated\\\"`: one step higher, with a shadow.\" },\n { name: \"pressable\", title: \"Pressable\", description: \"`onPress`: the whole card is a button.\" },\n { name: \"body\", title: \"Text and an action\", description: \"A body only, with a button.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
417
425
  },
418
426
  {
419
427
  "path": "meta.ts",
@@ -497,6 +505,13 @@
497
505
  "description": "Shown when there is no image: an icon, an illustration (decorative).",
498
506
  "required": false
499
507
  },
508
+ {
509
+ "name": "loading",
510
+ "type": "\"eager\" | \"lazy\"",
511
+ "default": "lazy",
512
+ "description": "When the browser loads it: `lazy` (default, a card in a list or below the fold) or `eager` for the first cards\nof a page — the image that is the largest contentful paint must not wait (add `fetchPriority=\"high\"`).",
513
+ "required": false
514
+ },
500
515
  {
501
516
  "name": "src",
502
517
  "type": "string | null",
@@ -28,7 +28,7 @@
28
28
  "bestPractices": [
29
29
  "Group the commands (`Command.Group heading`) and show their shortcuts with `Command.Shortcut`.",
30
30
  "Add `keywords` so a command is found by the words people type, not only by its label.",
31
- "Always say what an empty search means with `Command.Empty emptyLabel`."
31
+ "Always say what an empty search means with `emptyLabel` on `Command.List` (said beside the listbox, never inside it)."
32
32
  ],
33
33
  "accessibility": {
34
34
  "keyboard": [
@@ -97,6 +97,13 @@
97
97
  "component": "CommandList",
98
98
  "description": "",
99
99
  "props": [
100
+ {
101
+ "name": "emptyLabel",
102
+ "type": "ReactNode",
103
+ "default": null,
104
+ "description": "Shown when nothing matches — passed translated. Rendered after the listbox, never inside it: an empty listbox is\nvalid, a listbox holding text is not.",
105
+ "required": false
106
+ },
100
107
  {
101
108
  "name": "label",
102
109
  "type": "string",
@@ -108,7 +115,7 @@
108
115
  },
109
116
  {
110
117
  "component": "CommandEmpty",
111
- "description": "Shown by cmdk only when no item matches.",
118
+ "description": "Shown by cmdk only when no item matches. Place it beside `Command.List` (or pass `emptyLabel` to the list), never\ninside it: inside, it is rendered as before but warns in development — the listbox then holds text, which axe\nrefuses (`aria-required-children`).",
112
119
  "props": [
113
120
  {
114
121
  "name": "emptyLabel",
@@ -200,7 +207,15 @@
200
207
  "description": "In a card: the input, groups, shortcuts, a disabled item.",
201
208
  "path": "examples/command/inline.tsx",
202
209
  "module": "@krizaka/ui/registry/examples/command/inline",
203
- "code": "import { Command } from \"@krizaka/ui/command\";\n\nexport default function CommandInline() {\n return (\n <Command.Root label=\"Commands\" className=\"w-[28rem] max-w-full rounded-xl border border-border-default shadow-lg\">\n <Command.Input placeholder=\"Type a command or search\" />\n <Command.List label=\"Suggestions\">\n <Command.Empty emptyLabel=\"Nothing matches.\" />\n <Command.Group heading=\"Pages\">\n <Command.Item>\n Home\n <Command.Shortcut>G H</Command.Shortcut>\n </Command.Item>\n <Command.Item>\n Wallet\n <Command.Shortcut>G W</Command.Shortcut>\n </Command.Item>\n <Command.Item disabled>Studio</Command.Item>\n </Command.Group>\n <Command.Separator />\n <Command.Group heading=\"Actions\">\n <Command.Item keywords={[\"video\", \"new\"]}>Upload a video</Command.Item>\n <Command.Item>Switch the theme</Command.Item>\n </Command.Group>\n </Command.List>\n </Command.Root>\n );\n}\n"
210
+ "code": "import { Command } from \"@krizaka/ui/command\";\n\nexport default function CommandInline() {\n return (\n <Command.Root label=\"Commands\" className=\"w-[28rem] max-w-full rounded-xl border border-border-default shadow-lg\">\n <Command.Input placeholder=\"Type a command or search\" />\n <Command.List label=\"Suggestions\" emptyLabel=\"Nothing matches.\">\n <Command.Group heading=\"Pages\">\n <Command.Item>\n Home\n <Command.Shortcut>G H</Command.Shortcut>\n </Command.Item>\n <Command.Item>\n Wallet\n <Command.Shortcut>G W</Command.Shortcut>\n </Command.Item>\n <Command.Item disabled>Studio</Command.Item>\n </Command.Group>\n <Command.Separator />\n <Command.Group heading=\"Actions\">\n <Command.Item keywords={[\"video\", \"new\"]}>Upload a video</Command.Item>\n <Command.Item>Switch the theme</Command.Item>\n </Command.Group>\n </Command.List>\n </Command.Root>\n );\n}\n"
211
+ },
212
+ {
213
+ "name": "no-results",
214
+ "title": "No results",
215
+ "description": "`emptyLabel` on `Command.List`: said beside the listbox when nothing matches.",
216
+ "path": "examples/command/no-results.tsx",
217
+ "module": "@krizaka/ui/registry/examples/command/no-results",
218
+ "code": "\"use client\";\n\nimport { Command } from \"@krizaka/ui/command\";\nimport { useState } from \"react\";\n\n// Nothing matches: `emptyLabel` is said beside the results, the listbox stays empty (valid for screen readers).\nexport default function CommandNoResults() {\n const [query, setQuery] = useState(\"zzz\");\n return (\n <Command.Root label=\"Commands\" className=\"w-[28rem] max-w-full rounded-xl border border-border-default shadow-lg\">\n <Command.Input placeholder=\"Type a command or search\" value={query} onValueChange={setQuery} />\n <Command.List label=\"Suggestions\" emptyLabel={`Nothing matches “${query}”.`}>\n <Command.Group heading=\"Pages\">\n <Command.Item>Home</Command.Item>\n <Command.Item>Wallet</Command.Item>\n </Command.Group>\n </Command.List>\n </Command.Root>\n );\n}\n"
204
219
  },
205
220
  {
206
221
  "name": "dialog",
@@ -208,7 +223,7 @@
208
223
  "description": "`CommandDialog`: the palette of an app, opened from a button or a shortcut.",
209
224
  "path": "examples/command/dialog.tsx",
210
225
  "module": "@krizaka/ui/registry/examples/command/dialog",
211
- "code": "\"use client\";\n\nimport { Button } from \"@krizaka/ui/button\";\nimport { Command, CommandDialog } from \"@krizaka/ui/command\";\nimport { Kbd } from \"@krizaka/ui/kbd\";\nimport { useState } from \"react\";\n\n// The palette of an app, in the platform's Dialog: open it from a button or your own ⌘K shortcut.\nexport default function CommandDialogExample({ defaultOpen = false }: { defaultOpen?: boolean }) {\n const [open, setOpen] = useState(defaultOpen);\n return (\n <>\n <Button variant=\"outline\" onClick={() => setOpen(true)}>\n Search <Kbd size=\"sm\">⌘K</Kbd>\n </Button>\n <CommandDialog\n open={open}\n onOpenChange={setOpen}\n label=\"Search\"\n footer={<p className=\"border-t border-border-subtle bg-surface-2 px-4 py-2.5 text-xs text-fg-secondary\">↑↓ to move, ↵ to open</p>}\n >\n <Command.Input placeholder=\"Search creators, videos, tags\" />\n <Command.List label=\"Results\">\n <Command.Empty emptyLabel=\"Nothing matches.\" />\n <Command.Group heading=\"Pages\">\n <Command.Item onSelect={() => setOpen(false)}>Home</Command.Item>\n <Command.Item onSelect={() => setOpen(false)}>Wallet</Command.Item>\n </Command.Group>\n <Command.Group heading=\"Actions\">\n <Command.Item keywords={[\"video\", \"new\"]}>Upload a video</Command.Item>\n </Command.Group>\n </Command.List>\n </CommandDialog>\n </>\n );\n}\n"
226
+ "code": "\"use client\";\n\nimport { Button } from \"@krizaka/ui/button\";\nimport { Command, CommandDialog } from \"@krizaka/ui/command\";\nimport { Kbd } from \"@krizaka/ui/kbd\";\nimport { useState } from \"react\";\n\n// The palette of an app, in the platform's Dialog: open it from a button or your own ⌘K shortcut.\nexport default function CommandDialogExample({ defaultOpen = false }: { defaultOpen?: boolean }) {\n const [open, setOpen] = useState(defaultOpen);\n return (\n <>\n <Button variant=\"outline\" onClick={() => setOpen(true)}>\n Search <Kbd size=\"sm\">⌘K</Kbd>\n </Button>\n <CommandDialog\n open={open}\n onOpenChange={setOpen}\n label=\"Search\"\n footer={<p className=\"border-t border-border-subtle bg-surface-2 px-4 py-2.5 text-xs text-fg-secondary\">↑↓ to move, ↵ to open</p>}\n >\n <Command.Input placeholder=\"Search creators, videos, tags\" />\n <Command.List label=\"Results\" emptyLabel=\"Nothing matches.\">\n <Command.Group heading=\"Pages\">\n <Command.Item onSelect={() => setOpen(false)}>Home</Command.Item>\n <Command.Item onSelect={() => setOpen(false)}>Wallet</Command.Item>\n </Command.Group>\n <Command.Group heading=\"Actions\">\n <Command.Item keywords={[\"video\", \"new\"]}>Upload a video</Command.Item>\n </Command.Group>\n </Command.List>\n </CommandDialog>\n </>\n );\n}\n"
212
227
  }
213
228
  ]
214
229
  },
@@ -216,7 +231,7 @@
216
231
  "files": [
217
232
  {
218
233
  "path": "command/command.tsx",
219
- "content": "// Client. A command palette on cmdk: the combobox / listbox roles, the active option (aria-activedescendant), the\n// arrows, Home / End, Enter, and the filtering (turn it off with `shouldFilter={false}` when the server searches).\n// `CommandDialog` puts it in the platform's Dialog. Words arrive as props.\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\nimport type * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { cn } from \"../cn\";\nimport { DialogContent, type DialogContentProps } from \"../dialog/dialog\";\n\nexport const command = tv({\n slots: {\n root: \"flex h-full w-full flex-col overflow-hidden bg-surface-1 text-fg\",\n inputWrap: \"flex items-center gap-3 border-b border-border-default px-4\",\n icon: \"h-5 w-5 shrink-0 text-fg-secondary\",\n input:\n \"h-13 w-full min-w-0 flex-1 bg-transparent text-sm text-fg outline-hidden placeholder:text-fg-muted disabled:cursor-not-allowed disabled:opacity-50 sm:text-base\",\n list: \"max-h-[min(60dvh,26rem)] scroll-py-2 overflow-y-auto overscroll-contain p-2\",\n empty: \"px-3 py-8 text-center text-sm text-fg-secondary\",\n loading: \"px-3 py-6 text-center text-sm text-fg-secondary\",\n group:\n \"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:pb-1.5 [&_[cmdk-group-heading]]:pt-3 [&_[cmdk-group-heading]]:text-xs \" +\n \"[&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-wider [&_[cmdk-group-heading]]:text-fg-secondary\",\n item:\n \"relative flex cursor-pointer select-none items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-fg outline-hidden transition-colors \" +\n \"before:absolute before:inset-y-2 before:left-0 before:w-0.5 before:rounded-full before:bg-transparent \" +\n \"data-[selected=true]:bg-surface-2 data-[selected=true]:before:bg-accent data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-40\",\n separator: \"-mx-2 my-1 h-px bg-border-subtle\",\n shortcut: \"ml-auto text-xs tracking-widest text-fg-secondary\",\n },\n});\nconst s = command();\n\nfunction SearchIcon({ className }: { className?: string }) {\n return (\n <svg viewBox=\"0 0 24 24\" className={className} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"m20 20-3.5-3.5\" />\n </svg>\n );\n}\n\nexport type CommandRootProps = React.ComponentProps<typeof CommandPrimitive> & {\n /** The accessible name of the palette — passed translated (\"Search\", \"Commands\"). */\n label: string;\n};\n\n/** The palette: `value` / `onValueChange` (the active item), `shouldFilter`, `filter`, `loop`. */\nexport function CommandRoot({ className, ...props }: CommandRootProps) {\n return <CommandPrimitive className={s.root({ className })} {...props} />;\n}\n\nexport type CommandInputProps = Omit<React.ComponentProps<typeof CommandPrimitive.Input>, \"placeholder\"> & {\n /** What to type — passed translated. The input is named by the palette's `label`. */\n placeholder: string;\n /** After the input, inside the bar: a spinner, a clear button. */\n trailing?: React.ReactNode;\n};\n\n/** The search field (`value` / `onValueChange` for the query), with a magnifier. */\nexport function CommandInput({ className, placeholder, trailing, ...props }: CommandInputProps) {\n return (\n <div className={s.inputWrap()} cmdk-input-wrapper=\"\">\n <SearchIcon className={s.icon()} />\n <CommandPrimitive.Input placeholder={placeholder} className={s.input({ className })} {...props} />\n {trailing}\n </div>\n );\n}\n\nexport type CommandListProps = React.ComponentProps<typeof CommandPrimitive.List> & {\n /** The accessible name of the list of results — passed translated (\"Suggestions\", \"Results\"). */\n label: string;\n};\n\nexport function CommandList({ className, ...props }: CommandListProps) {\n return <CommandPrimitive.List className={s.list({ className })} {...props} />;\n}\n\nexport type CommandEmptyProps = Omit<React.ComponentProps<typeof CommandPrimitive.Empty>, \"children\"> & {\n /** Shown when nothing matches — passed translated. */\n emptyLabel: React.ReactNode;\n};\n\n/** Shown by cmdk only when no item matches. */\nexport function CommandEmpty({ className, emptyLabel, ...props }: CommandEmptyProps) {\n return (\n <CommandPrimitive.Empty className={s.empty({ className })} {...props}>\n {emptyLabel}\n </CommandPrimitive.Empty>\n );\n}\n\n/** While results load (`label` is announced). */\nexport function CommandLoading({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Loading>) {\n return <CommandPrimitive.Loading className={s.loading({ className })} {...props} />;\n}\n\n/** A group of items under a `heading`. */\nexport function CommandGroup({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Group>) {\n return <CommandPrimitive.Group className={s.group({ className })} {...props} />;\n}\n\n/** An option: `onSelect` on Enter or click, `value` and `keywords` for the filter, `disabled`. */\nexport function CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {\n return <CommandPrimitive.Item className={s.item({ className })} {...props} />;\n}\n\n/** A line between groups. Decorative (`role=\"none\"`): cmdk's own separator is a role a listbox may not hold (axe). */\nexport function CommandSeparator({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div role=\"none\" className={s.separator({ className })} {...props} />;\n}\n\n/** A keyboard hint at the end of an item. */\nexport function CommandShortcut({ className, ...props }: React.ComponentProps<\"span\">) {\n return <span className={s.shortcut({ className })} {...props} />;\n}\n\n/** `Command.Root/Input/List/Empty/Loading/Group/Item/Separator/Shortcut`. */\nexport const Command = {\n Root: CommandRoot,\n Input: CommandInput,\n List: CommandList,\n Empty: CommandEmpty,\n Loading: CommandLoading,\n Group: CommandGroup,\n Item: CommandItem,\n Separator: CommandSeparator,\n Shortcut: CommandShortcut,\n};\n\nexport type CommandDialogProps = Omit<CommandRootProps, \"title\"> & {\n /** Open or closed (controlled), with `onOpenChange`. */\n open?: boolean;\n /** Open at first, uncontrolled. */\n defaultOpen?: boolean;\n /** Called when it opens or closes (Escape, a click outside, a shortcut of the product). */\n onOpenChange?: (open: boolean) => void;\n /** Under the palette, inside the dialog: shortcuts, a \"see all results\" link. */\n footer?: React.ReactNode;\n /** Passed to `Dialog.Content`: `size`, `container`, `className` of the dialog. */\n contentProps?: Omit<Extract<DialogContentProps, { hideClose: true }>, \"hideClose\" | \"children\" | \"placement\">;\n};\n\n/**\n * The palette in a dialog (Radix: focus trap, Escape, focus return). The dialog is named by `label`; it has no close\n * button — Escape and a click outside close it — and the input takes the focus when it opens.\n */\nexport function CommandDialog({ open, defaultOpen, onOpenChange, footer, contentProps, label, className, children, ...props }: CommandDialogProps) {\n const { className: contentClassName, ...content } = contentProps ?? {};\n return (\n <DialogPrimitive.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>\n <DialogContent\n hideClose\n size=\"lg\"\n aria-describedby={undefined}\n className={cn(\"top-[12dvh] translate-y-0 sm:top-[14dvh]\", contentClassName)}\n {...content}\n >\n <DialogPrimitive.Title className=\"sr-only\">{label}</DialogPrimitive.Title>\n <CommandRoot label={label} className={className} {...props}>\n {children}\n </CommandRoot>\n {footer}\n </DialogContent>\n </DialogPrimitive.Root>\n );\n}\n"
234
+ "content": "// Client. A command palette on cmdk: the combobox / listbox roles, the active option (aria-activedescendant), the\n// arrows, Home / End, Enter, and the filtering (turn it off with `shouldFilter={false}` when the server searches).\n// `CommandDialog` puts it in the platform's Dialog. Words arrive as props.\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { cn } from \"../cn\";\nimport { DialogContent, type DialogContentProps } from \"../dialog/dialog\";\n\nexport const command = tv({\n slots: {\n root: \"flex h-full w-full flex-col overflow-hidden bg-surface-1 text-fg\",\n inputWrap: \"flex items-center gap-3 border-b border-border-default px-4\",\n icon: \"h-5 w-5 shrink-0 text-fg-secondary\",\n input:\n \"h-13 w-full min-w-0 flex-1 bg-transparent text-sm text-fg outline-hidden placeholder:text-fg-muted disabled:cursor-not-allowed disabled:opacity-50 sm:text-base\",\n list: \"max-h-[min(60dvh,26rem)] scroll-py-2 overflow-y-auto overscroll-contain p-2 [&:not(:has([cmdk-item]))]:p-0\",\n empty: \"px-3 py-8 text-center text-sm text-fg-secondary\",\n loading: \"px-3 py-6 text-center text-sm text-fg-secondary\",\n group:\n \"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:pb-1.5 [&_[cmdk-group-heading]]:pt-3 [&_[cmdk-group-heading]]:text-xs \" +\n \"[&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-wider [&_[cmdk-group-heading]]:text-fg-secondary\",\n item:\n \"relative flex cursor-pointer select-none items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-fg outline-hidden transition-colors \" +\n \"before:absolute before:inset-y-2 before:left-0 before:w-0.5 before:rounded-full before:bg-transparent \" +\n \"data-[selected=true]:bg-surface-2 data-[selected=true]:before:bg-accent data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-40\",\n separator: \"-mx-2 my-1 h-px bg-border-subtle\",\n shortcut: \"ml-auto text-xs tracking-widest text-fg-secondary\",\n },\n});\nconst s = command();\n\nfunction SearchIcon({ className }: { className?: string }) {\n return (\n <svg viewBox=\"0 0 24 24\" className={className} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"m20 20-3.5-3.5\" />\n </svg>\n );\n}\n\nexport type CommandRootProps = React.ComponentProps<typeof CommandPrimitive> & {\n /** The accessible name of the palette — passed translated (\"Search\", \"Commands\"). */\n label: string;\n};\n\n/** The palette: `value` / `onValueChange` (the active item), `shouldFilter`, `filter`, `loop`. */\nexport function CommandRoot({ className, ...props }: CommandRootProps) {\n return <CommandPrimitive className={s.root({ className })} {...props} />;\n}\n\nexport type CommandInputProps = Omit<React.ComponentProps<typeof CommandPrimitive.Input>, \"placeholder\"> & {\n /** What to type — passed translated. The input is named by the palette's `label`. */\n placeholder: string;\n /** After the input, inside the bar: a spinner, a clear button. */\n trailing?: React.ReactNode;\n};\n\n/** The search field (`value` / `onValueChange` for the query), with a magnifier. */\nexport function CommandInput({ className, placeholder, trailing, ...props }: CommandInputProps) {\n return (\n <div className={s.inputWrap()} cmdk-input-wrapper=\"\">\n <SearchIcon className={s.icon()} />\n <CommandPrimitive.Input placeholder={placeholder} className={s.input({ className })} {...props} />\n {trailing}\n </div>\n );\n}\n\n// Set inside `Command.List`: a listbox may only hold options and groups (axe `aria-required-children`), so the empty\n// message must live beside it, not in it.\nconst InsideList = React.createContext(false);\n\nexport type CommandListProps = React.ComponentProps<typeof CommandPrimitive.List> & {\n /** The accessible name of the list of results — passed translated (\"Suggestions\", \"Results\"). */\n label: string;\n /**\n * Shown when nothing matches — passed translated. Rendered after the listbox, never inside it: an empty listbox is\n * valid, a listbox holding text is not.\n */\n emptyLabel?: React.ReactNode;\n};\n\nexport function CommandList({ className, emptyLabel, children, ...props }: CommandListProps) {\n return (\n <>\n <CommandPrimitive.List className={s.list({ className })} {...props}>\n <InsideList.Provider value>{children}</InsideList.Provider>\n </CommandPrimitive.List>\n {emptyLabel === undefined ? null : <CommandEmpty emptyLabel={emptyLabel} />}\n </>\n );\n}\n\nexport type CommandEmptyProps = Omit<React.ComponentProps<typeof CommandPrimitive.Empty>, \"children\"> & {\n /** Shown when nothing matches — passed translated. */\n emptyLabel: React.ReactNode;\n};\n\n/**\n * Shown by cmdk only when no item matches. Place it beside `Command.List` (or pass `emptyLabel` to the list), never\n * inside it: inside, it is rendered as before but warns in development — the listbox then holds text, which axe\n * refuses (`aria-required-children`).\n */\nexport function CommandEmpty({ className, emptyLabel, ...props }: CommandEmptyProps) {\n const inside = React.useContext(InsideList);\n React.useEffect(() => {\n if (inside && typeof process !== \"undefined\" && process.env.NODE_ENV !== \"production\") {\n console.warn(\"@krizaka/ui/command: <Command.Empty> inside <Command.List> breaks the listbox (axe aria-required-children). Pass `emptyLabel` to <Command.List> instead.\");\n }\n }, [inside]);\n return (\n <CommandPrimitive.Empty className={s.empty({ className })} {...props}>\n {emptyLabel}\n </CommandPrimitive.Empty>\n );\n}\n\n/** While results load (`label` is announced). */\nexport function CommandLoading({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Loading>) {\n return <CommandPrimitive.Loading className={s.loading({ className })} {...props} />;\n}\n\n/** A group of items under a `heading`. */\nexport function CommandGroup({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Group>) {\n return <CommandPrimitive.Group className={s.group({ className })} {...props} />;\n}\n\n/** An option: `onSelect` on Enter or click, `value` and `keywords` for the filter, `disabled`. */\nexport function CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {\n return <CommandPrimitive.Item className={s.item({ className })} {...props} />;\n}\n\n/** A line between groups. Decorative (`role=\"none\"`): cmdk's own separator is a role a listbox may not hold (axe). */\nexport function CommandSeparator({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div role=\"none\" className={s.separator({ className })} {...props} />;\n}\n\n/** A keyboard hint at the end of an item. */\nexport function CommandShortcut({ className, ...props }: React.ComponentProps<\"span\">) {\n return <span className={s.shortcut({ className })} {...props} />;\n}\n\n/** `Command.Root/Input/List/Empty/Loading/Group/Item/Separator/Shortcut`. */\nexport const Command = {\n Root: CommandRoot,\n Input: CommandInput,\n List: CommandList,\n Empty: CommandEmpty,\n Loading: CommandLoading,\n Group: CommandGroup,\n Item: CommandItem,\n Separator: CommandSeparator,\n Shortcut: CommandShortcut,\n};\n\nexport type CommandDialogProps = Omit<CommandRootProps, \"title\"> & {\n /** Open or closed (controlled), with `onOpenChange`. */\n open?: boolean;\n /** Open at first, uncontrolled. */\n defaultOpen?: boolean;\n /** Called when it opens or closes (Escape, a click outside, a shortcut of the product). */\n onOpenChange?: (open: boolean) => void;\n /** Under the palette, inside the dialog: shortcuts, a \"see all results\" link. */\n footer?: React.ReactNode;\n /** Passed to `Dialog.Content`: `size`, `container`, `className` of the dialog. */\n contentProps?: Omit<Extract<DialogContentProps, { hideClose: true }>, \"hideClose\" | \"children\" | \"placement\">;\n};\n\n/**\n * The palette in a dialog (Radix: focus trap, Escape, focus return). The dialog is named by `label`; it has no close\n * button — Escape and a click outside close it — and the input takes the focus when it opens.\n */\nexport function CommandDialog({ open, defaultOpen, onOpenChange, footer, contentProps, label, className, children, ...props }: CommandDialogProps) {\n const { className: contentClassName, ...content } = contentProps ?? {};\n return (\n <DialogPrimitive.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>\n <DialogContent\n hideClose\n size=\"lg\"\n aria-describedby={undefined}\n className={cn(\"top-[12dvh] translate-y-0 sm:top-[14dvh]\", contentClassName)}\n {...content}\n >\n <DialogPrimitive.Title className=\"sr-only\">{label}</DialogPrimitive.Title>\n <CommandRoot label={label} className={className} {...props}>\n {children}\n </CommandRoot>\n {footer}\n </DialogContent>\n </DialogPrimitive.Root>\n );\n}\n"
220
235
  },
221
236
  {
222
237
  "path": "command/index.ts",
@@ -224,7 +239,7 @@
224
239
  },
225
240
  {
226
241
  "path": "command/meta.ts",
227
- "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Command\",\n summary: \"A command palette on cmdk: a search input over a filtered list of commands, inline or in a dialog.\",\n status: \"beta\",\n category: \"navigation\",\n platforms: \"web\",\n whenToUse: [\n \"To reach any page or action of an app from the keyboard (⌘K): `CommandDialog`.\",\n \"For a searchable list of choices inside a panel: `Command.Root` inline.\",\n \"When results come from the server: `shouldFilter={false}` and `Command.Loading`.\",\n ],\n whenNotToUse: [\n { when: \"For a short list of actions on one item.\", use: \"dropdown-menu\" },\n { when: \"For a choice among a few options in a form: use a select or radios.\", use: \"radio-group\" },\n { when: \"As the only way to navigate: keep the visible navigation.\" },\n ],\n bestPractices: [\n \"Group the commands (`Command.Group heading`) and show their shortcuts with `Command.Shortcut`.\",\n \"Add `keywords` so a command is found by the words people type, not only by its label.\",\n \"Always say what an empty search means with `Command.Empty emptyLabel`.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"Arrow keys\", action: \"Move through the results.\" },\n { keys: \"Enter\", action: \"Runs the highlighted command.\" },\n { keys: \"Esc\", action: \"Closes the `CommandDialog` and returns the focus.\" },\n ],\n notes: [\n \"A combobox and a listbox: the input owns `aria-activedescendant`, the results are options.\",\n \"`label` names the palette, `Command.List label` the results.\",\n \"`CommandDialog` traps the focus in the platform's Dialog and puts it in the input when it opens.\",\n ],\n },\n related: [\"dialog\", \"dropdown-menu\", \"kbd\"],\n web: {\n imports: [\"Command\", \"CommandDialog\"],\n examples: [\n { name: \"inline\", title: \"Inline\", description: \"In a card: the input, groups, shortcuts, a disabled item.\" },\n { name: \"dialog\", title: \"In a dialog\", description: \"`CommandDialog`: the palette of an app, opened from a button or a shortcut.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
242
+ "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Command\",\n summary: \"A command palette on cmdk: a search input over a filtered list of commands, inline or in a dialog.\",\n status: \"beta\",\n category: \"navigation\",\n platforms: \"web\",\n whenToUse: [\n \"To reach any page or action of an app from the keyboard (⌘K): `CommandDialog`.\",\n \"For a searchable list of choices inside a panel: `Command.Root` inline.\",\n \"When results come from the server: `shouldFilter={false}` and `Command.Loading`.\",\n ],\n whenNotToUse: [\n { when: \"For a short list of actions on one item.\", use: \"dropdown-menu\" },\n { when: \"For a choice among a few options in a form: use a select or radios.\", use: \"radio-group\" },\n { when: \"As the only way to navigate: keep the visible navigation.\" },\n ],\n bestPractices: [\n \"Group the commands (`Command.Group heading`) and show their shortcuts with `Command.Shortcut`.\",\n \"Add `keywords` so a command is found by the words people type, not only by its label.\",\n \"Always say what an empty search means with `emptyLabel` on `Command.List` (said beside the listbox, never inside it).\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"Arrow keys\", action: \"Move through the results.\" },\n { keys: \"Enter\", action: \"Runs the highlighted command.\" },\n { keys: \"Esc\", action: \"Closes the `CommandDialog` and returns the focus.\" },\n ],\n notes: [\n \"A combobox and a listbox: the input owns `aria-activedescendant`, the results are options.\",\n \"`label` names the palette, `Command.List label` the results.\",\n \"`CommandDialog` traps the focus in the platform's Dialog and puts it in the input when it opens.\",\n ],\n },\n related: [\"dialog\", \"dropdown-menu\", \"kbd\"],\n web: {\n imports: [\"Command\", \"CommandDialog\"],\n examples: [\n { name: \"inline\", title: \"Inline\", description: \"In a card: the input, groups, shortcuts, a disabled item.\" },\n { name: \"no-results\", title: \"No results\", description: \"`emptyLabel` on `Command.List`: said beside the listbox when nothing matches.\" },\n { name: \"dialog\", title: \"In a dialog\", description: \"`CommandDialog`: the palette of an app, opened from a button or a shortcut.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
228
243
  },
229
244
  {
230
245
  "path": "meta.ts",
@@ -242,7 +257,7 @@
242
257
  ],
243
258
  "demo": {
244
259
  "path": "examples/command/inline.tsx",
245
- "content": "import { Command } from \"@krizaka/ui/command\";\n\nexport default function CommandInline() {\n return (\n <Command.Root label=\"Commands\" className=\"w-[28rem] max-w-full rounded-xl border border-border-default shadow-lg\">\n <Command.Input placeholder=\"Type a command or search\" />\n <Command.List label=\"Suggestions\">\n <Command.Empty emptyLabel=\"Nothing matches.\" />\n <Command.Group heading=\"Pages\">\n <Command.Item>\n Home\n <Command.Shortcut>G H</Command.Shortcut>\n </Command.Item>\n <Command.Item>\n Wallet\n <Command.Shortcut>G W</Command.Shortcut>\n </Command.Item>\n <Command.Item disabled>Studio</Command.Item>\n </Command.Group>\n <Command.Separator />\n <Command.Group heading=\"Actions\">\n <Command.Item keywords={[\"video\", \"new\"]}>Upload a video</Command.Item>\n <Command.Item>Switch the theme</Command.Item>\n </Command.Group>\n </Command.List>\n </Command.Root>\n );\n}\n"
260
+ "content": "import { Command } from \"@krizaka/ui/command\";\n\nexport default function CommandInline() {\n return (\n <Command.Root label=\"Commands\" className=\"w-[28rem] max-w-full rounded-xl border border-border-default shadow-lg\">\n <Command.Input placeholder=\"Type a command or search\" />\n <Command.List label=\"Suggestions\" emptyLabel=\"Nothing matches.\">\n <Command.Group heading=\"Pages\">\n <Command.Item>\n Home\n <Command.Shortcut>G H</Command.Shortcut>\n </Command.Item>\n <Command.Item>\n Wallet\n <Command.Shortcut>G W</Command.Shortcut>\n </Command.Item>\n <Command.Item disabled>Studio</Command.Item>\n </Command.Group>\n <Command.Separator />\n <Command.Group heading=\"Actions\">\n <Command.Item keywords={[\"video\", \"new\"]}>Upload a video</Command.Item>\n <Command.Item>Switch the theme</Command.Item>\n </Command.Group>\n </Command.List>\n </Command.Root>\n );\n}\n"
246
261
  },
247
262
  "props": [
248
263
  {
@@ -282,6 +297,13 @@
282
297
  "component": "CommandList",
283
298
  "description": "",
284
299
  "props": [
300
+ {
301
+ "name": "emptyLabel",
302
+ "type": "ReactNode",
303
+ "default": null,
304
+ "description": "Shown when nothing matches — passed translated. Rendered after the listbox, never inside it: an empty listbox is\nvalid, a listbox holding text is not.",
305
+ "required": false
306
+ },
285
307
  {
286
308
  "name": "label",
287
309
  "type": "string",
@@ -293,7 +315,7 @@
293
315
  },
294
316
  {
295
317
  "component": "CommandEmpty",
296
- "description": "Shown by cmdk only when no item matches.",
318
+ "description": "Shown by cmdk only when no item matches. Place it beside `Command.List` (or pass `emptyLabel` to the list), never\ninside it: inside, it is rendered as before but warns in development — the listbox then holds text, which axe\nrefuses (`aria-required-children`).",
297
319
  "props": [
298
320
  {
299
321
  "name": "emptyLabel",
@@ -125,9 +125,9 @@
125
125
  },
126
126
  {
127
127
  "name": "variant",
128
- "type": "\"danger\" | \"primary\" | \"secondary\" | \"outline\" | \"ghost\"",
128
+ "type": "\"danger\" | \"primary\" | \"gradient\" | \"secondary\" | \"outline\" | \"ghost\"",
129
129
  "default": "ghost",
130
- "description": "The look: `primary` (the accent), `secondary` (a raised surface), `outline`, `ghost`, `danger`.",
130
+ "description": "The look: `primary` (the accent), `gradient` (accent → accent-2), `secondary` (a raised surface), `outline`,\n`ghost`, `danger`.",
131
131
  "required": false
132
132
  }
133
133
  ]
@@ -260,9 +260,9 @@
260
260
  },
261
261
  {
262
262
  "name": "variant",
263
- "type": "\"danger\" | \"primary\" | \"secondary\" | \"outline\" | \"ghost\"",
263
+ "type": "\"danger\" | \"primary\" | \"gradient\" | \"secondary\" | \"outline\" | \"ghost\"",
264
264
  "default": "ghost",
265
- "description": "The look: `primary` (the accent), `secondary` (a raised surface), `outline`, `ghost`, `danger`.",
265
+ "description": "The look: `primary` (the accent), `gradient` (accent → accent-2), `secondary` (a raised surface), `outline`,\n`ghost`, `danger`.",
266
266
  "required": false
267
267
  }
268
268
  ]