@krizaka/ui 3.2.0 → 3.3.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 (162) hide show
  1. package/dist/{chunk-I5RVM5CW.js → chunk-2FRWNSRO.js} +2 -6
  2. package/dist/chunk-2FRWNSRO.js.map +1 -0
  3. package/dist/chunk-BTMFZ66D.js +135 -0
  4. package/dist/chunk-BTMFZ66D.js.map +1 -0
  5. package/dist/chunk-C2BRIFJG.js +13 -0
  6. package/dist/chunk-C2BRIFJG.js.map +1 -0
  7. package/dist/chunk-KDXRYUD3.js +39 -0
  8. package/dist/chunk-KDXRYUD3.js.map +1 -0
  9. package/dist/{chunk-2PUDI7JS.js → chunk-ZKVQGXLD.js} +5 -3
  10. package/dist/{chunk-2PUDI7JS.js.map → chunk-ZKVQGXLD.js.map} +1 -1
  11. package/dist/command.js +1 -1
  12. package/dist/confirm-button.js +1 -1
  13. package/dist/data-table.js +3 -2
  14. package/dist/data-table.js.map +1 -1
  15. package/dist/date-picker.d.ts +3 -1
  16. package/dist/date-picker.js +13 -13
  17. package/dist/date-picker.js.map +1 -1
  18. package/dist/dropdown-menu.js +1 -1
  19. package/dist/filter-bar.js +3 -2
  20. package/dist/form.d.ts +23 -12
  21. package/dist/form.js +11 -47
  22. package/dist/form.js.map +1 -1
  23. package/dist/glow-card.js +1 -1
  24. package/dist/image-cropper.js +3 -1
  25. package/dist/image-cropper.js.map +1 -1
  26. package/dist/marquee.js +1 -1
  27. package/dist/native/form.d.ts +60 -15
  28. package/dist/native/form.js +103 -42
  29. package/dist/native/form.js.map +1 -1
  30. package/dist/native.d.ts +206 -2
  31. package/dist/native.js +1172 -541
  32. package/dist/native.js.map +1 -1
  33. package/dist/nav-bar.js +1 -1
  34. package/dist/popover.js +1 -1
  35. package/dist/radio-group.d.ts +66 -32
  36. package/dist/radio-group.js +54 -10
  37. package/dist/radio-group.js.map +1 -1
  38. package/dist/slider.d.ts +96 -35
  39. package/dist/slider.js +67 -13
  40. package/dist/slider.js.map +1 -1
  41. package/dist/terminal-replay.js +34 -60
  42. package/dist/terminal-replay.js.map +1 -1
  43. package/dist/toast.js +1 -1
  44. package/dist/tooltip.js +1 -1
  45. package/dist/video-player.js +3 -1
  46. package/dist/video-player.js.map +1 -1
  47. package/package.json +6 -6
  48. package/registry/combobox.json +225 -3
  49. package/registry/date-picker.json +16 -2
  50. package/registry/dialog.json +74 -3
  51. package/registry/examples/combobox/native/multiple.d.ts +4 -0
  52. package/registry/examples/combobox/native/multiple.dark.png +0 -0
  53. package/registry/examples/combobox/native/multiple.js +15 -0
  54. package/registry/examples/combobox/native/multiple.light.png +0 -0
  55. package/registry/examples/combobox/native/multiple.tsx +15 -0
  56. package/registry/examples/combobox/native/search.d.ts +4 -0
  57. package/registry/examples/combobox/native/search.dark.png +0 -0
  58. package/registry/examples/combobox/native/search.js +15 -0
  59. package/registry/examples/combobox/native/search.light.png +0 -0
  60. package/registry/examples/combobox/native/search.tsx +15 -0
  61. package/registry/examples/dialog/native/sheet.d.ts +4 -0
  62. package/registry/examples/dialog/native/sheet.dark.png +0 -0
  63. package/registry/examples/dialog/native/sheet.js +21 -0
  64. package/registry/examples/dialog/native/sheet.light.png +0 -0
  65. package/registry/examples/dialog/native/sheet.tsx +18 -0
  66. package/registry/examples/field/native/select.d.ts +4 -0
  67. package/registry/examples/field/native/select.dark.png +0 -0
  68. package/registry/examples/field/native/select.js +29 -0
  69. package/registry/examples/field/native/select.light.png +0 -0
  70. package/registry/examples/field/native/select.tsx +25 -0
  71. package/registry/examples/form/native/audience.dark.png +0 -0
  72. package/registry/examples/form/native/audience.light.png +0 -0
  73. package/registry/examples/form/native/profile.dark.png +0 -0
  74. package/registry/examples/form/native/profile.light.png +0 -0
  75. package/registry/examples/form/native/settings.dark.png +0 -0
  76. package/registry/examples/form/native/settings.light.png +0 -0
  77. package/registry/examples/form/native/sign-up.dark.png +0 -0
  78. package/registry/examples/form/native/sign-up.light.png +0 -0
  79. package/registry/examples/form/native/upload.dark.png +0 -0
  80. package/registry/examples/form/native/upload.light.png +0 -0
  81. package/registry/examples/radio-group/buttons.d.ts +4 -0
  82. package/registry/examples/radio-group/buttons.js +17 -0
  83. package/registry/examples/radio-group/buttons.tsx +21 -0
  84. package/registry/examples/radio-group/cards.js +1 -1
  85. package/registry/examples/radio-group/cards.tsx +2 -2
  86. package/registry/examples/radio-group/native/buttons.d.ts +4 -0
  87. package/registry/examples/radio-group/native/buttons.dark.png +0 -0
  88. package/registry/examples/radio-group/native/buttons.js +25 -0
  89. package/registry/examples/radio-group/native/buttons.light.png +0 -0
  90. package/registry/examples/radio-group/native/buttons.tsx +21 -0
  91. package/registry/examples/radio-group/native/cards.d.ts +4 -0
  92. package/registry/examples/radio-group/native/cards.dark.png +0 -0
  93. package/registry/examples/radio-group/native/cards.js +23 -0
  94. package/registry/examples/radio-group/native/cards.light.png +0 -0
  95. package/registry/examples/radio-group/native/cards.tsx +19 -0
  96. package/registry/examples/radio-group/native/list.d.ts +4 -0
  97. package/registry/examples/radio-group/native/list.dark.png +0 -0
  98. package/registry/examples/radio-group/native/list.js +24 -0
  99. package/registry/examples/radio-group/native/list.light.png +0 -0
  100. package/registry/examples/radio-group/native/list.tsx +20 -0
  101. package/registry/examples/radio-group/native/segmented.d.ts +4 -0
  102. package/registry/examples/radio-group/native/segmented.dark.png +0 -0
  103. package/registry/examples/radio-group/native/segmented.js +24 -0
  104. package/registry/examples/radio-group/native/segmented.light.png +0 -0
  105. package/registry/examples/radio-group/native/segmented.tsx +20 -0
  106. package/registry/examples/radio-group/segmented.d.ts +4 -0
  107. package/registry/examples/radio-group/segmented.js +15 -0
  108. package/registry/examples/radio-group/segmented.tsx +12 -0
  109. package/registry/examples/slider/floating.d.ts +4 -0
  110. package/registry/examples/slider/floating.js +11 -0
  111. package/registry/examples/slider/floating.tsx +12 -0
  112. package/registry/examples/slider/native/default.d.ts +4 -0
  113. package/registry/examples/slider/native/default.dark.png +0 -0
  114. package/registry/examples/slider/native/default.js +14 -0
  115. package/registry/examples/slider/native/default.light.png +0 -0
  116. package/registry/examples/slider/native/default.tsx +13 -0
  117. package/registry/examples/slider/native/range.d.ts +4 -0
  118. package/registry/examples/slider/native/range.dark.png +0 -0
  119. package/registry/examples/slider/native/range.js +12 -0
  120. package/registry/examples/slider/native/range.light.png +0 -0
  121. package/registry/examples/slider/native/range.tsx +11 -0
  122. package/registry/examples/slider/native/stepped.d.ts +4 -0
  123. package/registry/examples/slider/native/stepped.dark.png +0 -0
  124. package/registry/examples/slider/native/stepped.js +28 -0
  125. package/registry/examples/slider/native/stepped.light.png +0 -0
  126. package/registry/examples/slider/native/stepped.tsx +24 -0
  127. package/registry/examples/slider/native/vertical.d.ts +4 -0
  128. package/registry/examples/slider/native/vertical.dark.png +0 -0
  129. package/registry/examples/slider/native/vertical.js +16 -0
  130. package/registry/examples/slider/native/vertical.light.png +0 -0
  131. package/registry/examples/slider/native/vertical.tsx +13 -0
  132. package/registry/examples/slider/price-range.d.ts +4 -0
  133. package/registry/examples/slider/price-range.js +29 -0
  134. package/registry/examples/slider/price-range.tsx +27 -0
  135. package/registry/examples/slider/stepped.d.ts +4 -0
  136. package/registry/examples/slider/stepped.js +28 -0
  137. package/registry/examples/slider/stepped.tsx +24 -0
  138. package/registry/examples/slider/vertical.d.ts +4 -0
  139. package/registry/examples/slider/vertical.js +15 -0
  140. package/registry/examples/slider/vertical.tsx +14 -0
  141. package/registry/examples/switch/native/default.d.ts +4 -0
  142. package/registry/examples/switch/native/default.dark.png +0 -0
  143. package/registry/examples/switch/native/default.js +16 -0
  144. package/registry/examples/switch/native/default.light.png +0 -0
  145. package/registry/examples/switch/native/default.tsx +13 -0
  146. package/registry/examples/switch/native/settings.d.ts +4 -0
  147. package/registry/examples/switch/native/settings.dark.png +0 -0
  148. package/registry/examples/switch/native/settings.js +20 -0
  149. package/registry/examples/switch/native/settings.light.png +0 -0
  150. package/registry/examples/switch/native/settings.tsx +17 -0
  151. package/registry/field.json +15 -3
  152. package/registry/form.json +250 -42
  153. package/registry/index.json +24 -20
  154. package/registry/radio-group.json +227 -15
  155. package/registry/slider.json +314 -13
  156. package/registry/switch.json +115 -3
  157. package/registry/tabs.json +2 -2
  158. package/skills/choice-controls/SKILL.md +39 -0
  159. package/skills/form/SKILL.md +3 -0
  160. package/dist/chunk-I5RVM5CW.js.map +0 -1
  161. package/dist/chunk-RUPRYJ45.js +0 -82
  162. package/dist/chunk-RUPRYJ45.js.map +0 -1
@@ -6,7 +6,7 @@
6
6
  "description": "A text field that suggests and completes — one value or several as chips, from a static list or a server, with values created from what was typed.",
7
7
  "status": "beta",
8
8
  "category": "forms",
9
- "platforms": "web",
9
+ "platforms": "both",
10
10
  "whenToUse": [
11
11
  "To choose among many values the person knows by name: a creator, a country, a model, a playlist.",
12
12
  "With `onSearch` when the values live on a server (debounced, the previous request aborted).",
@@ -269,7 +269,229 @@
269
269
  }
270
270
  ]
271
271
  },
272
- "native": null,
272
+ "native": {
273
+ "entry": "@krizaka/ui/native",
274
+ "import": "import { Combobox } from \"@krizaka/ui/native\";",
275
+ "differences": [
276
+ "A field that opens a `Sheet` with a search box and the matching options: radios for one value, checkboxes and a Done button with `multiple`.",
277
+ "`onSearch(query, signal)` is debounced (250 ms) and aborted by a newer query, as on the web; no `creatable` yet."
278
+ ],
279
+ "props": [
280
+ {
281
+ "component": "Select",
282
+ "description": "One choice among many — the web `<select>`: a field that opens a sheet of options; picking one closes it.",
283
+ "props": [
284
+ {
285
+ "name": "label",
286
+ "type": "string",
287
+ "default": null,
288
+ "description": "The visible label, passed translated (the field's name).",
289
+ "required": true
290
+ },
291
+ {
292
+ "name": "options",
293
+ "type": "readonly SelectOption[]",
294
+ "default": null,
295
+ "description": "The choices.",
296
+ "required": true
297
+ },
298
+ {
299
+ "name": "defaultValue",
300
+ "type": "string | null",
301
+ "default": "null",
302
+ "description": "The chosen value at first, uncontrolled.",
303
+ "required": false
304
+ },
305
+ {
306
+ "name": "disabled",
307
+ "type": "boolean",
308
+ "default": null,
309
+ "description": "Not available.",
310
+ "required": false
311
+ },
312
+ {
313
+ "name": "error",
314
+ "type": "string",
315
+ "default": null,
316
+ "description": "An error under the field: a danger edge, read and announced. Replaces the hint.",
317
+ "required": false
318
+ },
319
+ {
320
+ "name": "hint",
321
+ "type": "string",
322
+ "default": null,
323
+ "description": "A help text under the field.",
324
+ "required": false
325
+ },
326
+ {
327
+ "name": "onBlur",
328
+ "type": "(() => void)",
329
+ "default": null,
330
+ "description": "Called when the sheet closes (the field is left).",
331
+ "required": false
332
+ },
333
+ {
334
+ "name": "onValueChange",
335
+ "type": "((value: string) => void)",
336
+ "default": null,
337
+ "description": "Called with the value picked.",
338
+ "required": false
339
+ },
340
+ {
341
+ "name": "placeholder",
342
+ "type": "string",
343
+ "default": null,
344
+ "description": "Shown while nothing is chosen (\"Choose a category\"), passed translated.",
345
+ "required": false
346
+ },
347
+ {
348
+ "name": "sheetTitle",
349
+ "type": "string",
350
+ "default": null,
351
+ "description": "The sheet's title. Default: the label.",
352
+ "required": false
353
+ },
354
+ {
355
+ "name": "style",
356
+ "type": "StyleProp<ViewStyle>",
357
+ "default": null,
358
+ "description": "Styles merged last, on the field.",
359
+ "required": false
360
+ },
361
+ {
362
+ "name": "value",
363
+ "type": "string | null",
364
+ "default": null,
365
+ "description": "The chosen value (controlled); `null` or `\"\"` for none.",
366
+ "required": false
367
+ }
368
+ ]
369
+ },
370
+ {
371
+ "component": "Combobox",
372
+ "description": "A choice among many, searched: a field that opens a sheet with a search box and the matching options.",
373
+ "props": [
374
+ {
375
+ "name": "label",
376
+ "type": "string",
377
+ "default": null,
378
+ "description": "The visible label, passed translated (the field's name).",
379
+ "required": true
380
+ },
381
+ {
382
+ "name": "disabled",
383
+ "type": "boolean",
384
+ "default": null,
385
+ "description": "Not available.",
386
+ "required": false
387
+ },
388
+ {
389
+ "name": "error",
390
+ "type": "string",
391
+ "default": null,
392
+ "description": "An error under the field: a danger edge, read and announced. Replaces the hint.",
393
+ "required": false
394
+ },
395
+ {
396
+ "name": "hint",
397
+ "type": "string",
398
+ "default": null,
399
+ "description": "A help text under the field.",
400
+ "required": false
401
+ },
402
+ {
403
+ "name": "labels",
404
+ "type": "Partial<ComboboxLabels>",
405
+ "default": null,
406
+ "description": "The words, passed translated; English by default.",
407
+ "required": false
408
+ },
409
+ {
410
+ "name": "multiple",
411
+ "type": "boolean",
412
+ "default": null,
413
+ "description": "Several values (`string[]`, checkboxes, a Done button); one by default.",
414
+ "required": false
415
+ },
416
+ {
417
+ "name": "onBlur",
418
+ "type": "(() => void)",
419
+ "default": null,
420
+ "description": "Called when the sheet closes (the field is left).",
421
+ "required": false
422
+ },
423
+ {
424
+ "name": "onSearch",
425
+ "type": "((query: string, signal: AbortSignal) => Promise<readonly SelectOption[]>)",
426
+ "default": null,
427
+ "description": "Fetches the options for a query (debounced 250 ms, aborted by a newer one).",
428
+ "required": false
429
+ },
430
+ {
431
+ "name": "onValueChange",
432
+ "type": "((value: string | string[]) => void)",
433
+ "default": null,
434
+ "description": "Called with the value — or the values — chosen.",
435
+ "required": false
436
+ },
437
+ {
438
+ "name": "options",
439
+ "type": "readonly SelectOption[]",
440
+ "default": "[]",
441
+ "description": "The options, filtered by what is typed.",
442
+ "required": false
443
+ },
444
+ {
445
+ "name": "placeholder",
446
+ "type": "string",
447
+ "default": null,
448
+ "description": "Shown while nothing is chosen (\"Choose a category\"), passed translated.",
449
+ "required": false
450
+ },
451
+ {
452
+ "name": "style",
453
+ "type": "StyleProp<ViewStyle>",
454
+ "default": null,
455
+ "description": "Styles merged last, on the field.",
456
+ "required": false
457
+ },
458
+ {
459
+ "name": "value",
460
+ "type": "string | readonly string[] | null",
461
+ "default": null,
462
+ "description": "The chosen value(s) (controlled): a string, or strings with `multiple`.",
463
+ "required": false
464
+ }
465
+ ]
466
+ }
467
+ ],
468
+ "examples": [
469
+ {
470
+ "name": "search",
471
+ "title": "Search",
472
+ "description": "Cities filtered as you type.",
473
+ "path": "examples/combobox/native/search.tsx",
474
+ "module": "@krizaka/ui/registry/examples/combobox/native/search",
475
+ "code": "import { Combobox } from \"@krizaka/ui/native\";\nimport * as React from \"react\";\nimport { View } from \"react-native\";\n\nconst CITIES = [\"Montréal\", \"Québec\", \"Laval\", \"Gatineau\", \"Sherbrooke\", \"Trois-Rivières\"].map((c) => ({ value: c.toLowerCase(), label: c }));\n\n// A choice among many, searched in a sheet: what is typed filters the options.\nexport default function NativeComboboxSearch() {\n const [city, setCity] = React.useState<string>(\"montréal\");\n return (\n <View style={{ width: 300 }}>\n <Combobox label=\"City\" placeholder=\"Choose a city\" options={CITIES} value={city} onValueChange={(v) => setCity(String(v))} />\n </View>\n );\n}\n",
476
+ "screenshots": {
477
+ "dark": "examples/combobox/native/search.dark.png",
478
+ "light": "examples/combobox/native/search.light.png"
479
+ }
480
+ },
481
+ {
482
+ "name": "multiple",
483
+ "title": "Several values",
484
+ "description": "Interests as checkboxes; the field lists them.",
485
+ "path": "examples/combobox/native/multiple.tsx",
486
+ "module": "@krizaka/ui/registry/examples/combobox/native/multiple",
487
+ "code": "import { Combobox } from \"@krizaka/ui/native\";\nimport * as React from \"react\";\nimport { View } from \"react-native\";\n\nconst TOPICS = [\"Music\", \"Sport\", \"Art\", \"Cooking\", \"Gaming\", \"Travel\"].map((t) => ({ value: t.toLowerCase(), label: t }));\n\n// Several values: checkboxes in the sheet, Done closes it; the field lists the choices.\nexport default function NativeComboboxMultiple() {\n const [topics, setTopics] = React.useState<string[]>([\"music\", \"travel\"]);\n return (\n <View style={{ width: 300 }}>\n <Combobox label=\"Interests\" multiple options={TOPICS} value={topics} onValueChange={(v) => setTopics(Array.isArray(v) ? v : [v])} />\n </View>\n );\n}\n",
488
+ "screenshots": {
489
+ "dark": "examples/combobox/native/multiple.dark.png",
490
+ "light": "examples/combobox/native/multiple.light.png"
491
+ }
492
+ }
493
+ ]
494
+ },
273
495
  "files": [
274
496
  {
275
497
  "path": "combobox/combobox.tsx",
@@ -281,7 +503,7 @@
281
503
  },
282
504
  {
283
505
  "path": "combobox/meta.ts",
284
- "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Combobox\",\n summary: \"A text field that suggests and completes — one value or several as chips, from a static list or a server, with values created from what was typed.\",\n status: \"beta\",\n category: \"forms\",\n platforms: \"web\",\n whenToUse: [\n \"To choose among many values the person knows by name: a creator, a country, a model, a playlist.\",\n \"With `onSearch` when the values live on a server (debounced, the previous request aborted).\",\n \"With `multiple` for several values (collaborators, categories), and `creatable` when a new one can be added.\",\n ],\n whenNotToUse: [\n { when: \"For fewer than about seven options known in advance: radios or a select.\", use: \"radio-group\" },\n { when: \"For free words without a closed list (tags, keywords): a tag input.\", use: \"tag-input\" },\n { when: \"To run a search and show results: a search field.\", use: \"search-field\" },\n ],\n bestPractices: [\n \"Write `label` as the question (“Collaborators”), the `placeholder` as an example (“Type a name”).\",\n \"Return at most 20 options from `onSearch`; put the best match first.\",\n \"Give options a `description` when names collide (two creators named Eric: their handles).\",\n \"Set `maxSelected` when the product has a limit, and say it in `hint`.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"Arrow Down / Arrow Up\", action: \"Open the list and move the active option.\" },\n { keys: \"Home / End\", action: \"First or last option, when the list is open.\" },\n { keys: \"Enter\", action: \"Choose the active option (or create the typed value).\" },\n { keys: \"Esc\", action: \"Close the list; for one value, restore its label.\" },\n { keys: \"Backspace\", action: \"In an empty input, remove the last chip.\" },\n ],\n notes: [\n \"The ARIA 1.2 combobox pattern: the focus stays in the input, `aria-activedescendant` points at the active option of the `listbox`.\",\n \"The number of results is announced in a polite live region (`labels.results`).\",\n \"Chosen options carry `aria-selected`; each chip's remove button is named (“Remove Eric”).\",\n \"The label is a real `<label>`; `hint` and `error` are wired with `aria-describedby`, `error` sets `aria-invalid`.\",\n ],\n },\n related: [\"tag-input\", \"search-field\", \"field\", \"chip\", \"command\"],\n web: {\n imports: [\"Combobox\"],\n examples: [\n { name: \"async\", title: \"From a server\", description: \"`onSearch`: creators fetched as you type, debounced, the stale request aborted.\" },\n { name: \"multiple\", title: \"Several values\", description: \"Collaborators as chips; Backspace removes the last one, at most three.\" },\n { name: \"creatable\", title: \"Create a value\", description: \"Pick a playlist or create one from what was typed.\" },\n { name: \"invalid\", title: \"With an error\", description: \"A required choice left empty: the error is read with the field.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
506
+ "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Combobox\",\n summary: \"A text field that suggests and completes — one value or several as chips, from a static list or a server, with values created from what was typed.\",\n status: \"beta\",\n category: \"forms\",\n platforms: \"both\",\n whenToUse: [\n \"To choose among many values the person knows by name: a creator, a country, a model, a playlist.\",\n \"With `onSearch` when the values live on a server (debounced, the previous request aborted).\",\n \"With `multiple` for several values (collaborators, categories), and `creatable` when a new one can be added.\",\n ],\n whenNotToUse: [\n { when: \"For fewer than about seven options known in advance: radios or a select.\", use: \"radio-group\" },\n { when: \"For free words without a closed list (tags, keywords): a tag input.\", use: \"tag-input\" },\n { when: \"To run a search and show results: a search field.\", use: \"search-field\" },\n ],\n bestPractices: [\n \"Write `label` as the question (“Collaborators”), the `placeholder` as an example (“Type a name”).\",\n \"Return at most 20 options from `onSearch`; put the best match first.\",\n \"Give options a `description` when names collide (two creators named Eric: their handles).\",\n \"Set `maxSelected` when the product has a limit, and say it in `hint`.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"Arrow Down / Arrow Up\", action: \"Open the list and move the active option.\" },\n { keys: \"Home / End\", action: \"First or last option, when the list is open.\" },\n { keys: \"Enter\", action: \"Choose the active option (or create the typed value).\" },\n { keys: \"Esc\", action: \"Close the list; for one value, restore its label.\" },\n { keys: \"Backspace\", action: \"In an empty input, remove the last chip.\" },\n ],\n notes: [\n \"The ARIA 1.2 combobox pattern: the focus stays in the input, `aria-activedescendant` points at the active option of the `listbox`.\",\n \"The number of results is announced in a polite live region (`labels.results`).\",\n \"Chosen options carry `aria-selected`; each chip's remove button is named (“Remove Eric”).\",\n \"The label is a real `<label>`; `hint` and `error` are wired with `aria-describedby`, `error` sets `aria-invalid`.\",\n ],\n },\n related: [\"tag-input\", \"search-field\", \"field\", \"chip\", \"command\"],\n web: {\n imports: [\"Combobox\"],\n examples: [\n { name: \"async\", title: \"From a server\", description: \"`onSearch`: creators fetched as you type, debounced, the stale request aborted.\" },\n { name: \"multiple\", title: \"Several values\", description: \"Collaborators as chips; Backspace removes the last one, at most three.\" },\n { name: \"creatable\", title: \"Create a value\", description: \"Pick a playlist or create one from what was typed.\" },\n { name: \"invalid\", title: \"With an error\", description: \"A required choice left empty: the error is read with the field.\" },\n ],\n },\n native: {\n imports: [\"Combobox\"],\n differences: [\n \"A field that opens a `Sheet` with a search box and the matching options: radios for one value, checkboxes and a Done button with `multiple`.\",\n \"`onSearch(query, signal)` is debounced (250 ms) and aborted by a newer query, as on the web; no `creatable` yet.\",\n ],\n examples: [\n { name: \"search\", title: \"Search\", description: \"Cities filtered as you type.\" },\n { name: \"multiple\", title: \"Several values\", description: \"Interests as checkboxes; the field lists them.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
285
507
  },
286
508
  {
287
509
  "path": "glyphs.tsx",
@@ -1115,6 +1115,13 @@
1115
1115
  "description": "Marks the group invalid (a danger edge): pair it with an error read through `aria-describedby`.",
1116
1116
  "required": false
1117
1117
  },
1118
+ {
1119
+ "name": "unset",
1120
+ "type": "boolean",
1121
+ "default": null,
1122
+ "description": "Nothing is chosen yet: the values under the band stay quiet until a wheel is turned.",
1123
+ "required": false
1124
+ },
1118
1125
  {
1119
1126
  "name": "visible",
1120
1127
  "type": "3 | 5 | 7",
@@ -1440,7 +1447,7 @@
1440
1447
  },
1441
1448
  {
1442
1449
  "path": "date-picker/date-field.tsx",
1443
- "content": "// Client. Two ways to give a memorable date — a birth date, an expiry — that a calendar would make slow to reach:\n// `DateField` (day · month · year typed in the locale's order, the next box focused once one is full; the web form's\n// `entry=\"parts\"`) and `DateWheel` (three wheels: the touch idiom, bounded by `min` / `max` — an age gate is\n// `max={latestBirthDate(18)}`). The value is `YYYY-MM-DD` either way, like the native `DateField` and `DateWheel`.\nimport * as React from \"react\";\n\nimport { useControllable } from \"../media\";\nimport { tv } from \"../tv\";\nimport { composeDate, DATE_PART_LENGTH, type DatePart, datePartsOrder, daysInMonth, isoOf, isoParts, monthNames, splitDate, todayIso, yearRange } from \"./parts\";\nimport { Wheel, WheelPicker } from \"./wheel\";\n\nexport const dateField = tv({\n slots: {\n root: \"flex flex-col gap-1.5 border-0 p-0\",\n legend: \"mb-1.5 p-0 text-xs font-semibold text-fg-secondary\",\n row: \"flex gap-2\",\n part: \"flex flex-col gap-1 text-xs text-fg-secondary\",\n input:\n \"h-11 rounded-lg border border-border-default bg-surface-2 px-2 text-center text-base tabular-nums text-fg inset-shadow-well transition-[border-color,box-shadow] \" +\n \"placeholder:text-fg-muted focus-visible:border-accent focus-visible:outline-hidden focus-visible:focus-ring-inset aria-invalid:border-danger disabled:cursor-not-allowed disabled:opacity-50\",\n hint: \"text-xs text-fg-secondary\",\n error: \"flex items-center gap-1.5 text-xs font-medium text-fg-danger before:h-1.5 before:w-1.5 before:shrink-0 before:rounded-full before:bg-danger\",\n },\n});\n\nexport type DateFieldLabels = {\n /** The name of the day (\"Day\"). */\n day: string;\n /** The name of the month (\"Month\"). */\n month: string;\n /** The name of the year (\"Year\"). */\n year: string;\n};\n\nexport const dateFieldLabels: DateFieldLabels = { day: \"Day\", month: \"Month\", year: \"Year\" };\n\ntype Common = {\n /** The visible label, passed translated (the group's name). */\n label: string;\n /** The BCP 47 locale: the order of the parts, the names of the months. */\n locale: string;\n /** A help text under the field (\"For example, 27 3 1990\"). */\n hint?: string;\n /** An error under the field: marks it invalid and is read with it. Replaces the hint. */\n error?: string;\n /** Not available. */\n disabled?: boolean;\n /** The name of a hidden input holding the value, for a plain `<form>` post. */\n name?: string;\n /** The names of the parts, passed translated; English by default. */\n labels?: Partial<DateFieldLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\nexport type DateFieldProps = Common & {\n /** The date, `YYYY-MM-DD` (controlled); `\"\"` for none. What is typed comes back as it is, real date or not: the schema decides. */\n value?: string;\n /** The date at first, uncontrolled. */\n defaultValue?: string;\n /** Called on every key with the date typed so far (`\"\"` when the boxes are empty). */\n onValueChange?: (value: string) => void;\n /** Called when the focus leaves the field. */\n onBlur?: () => void;\n /** `bday` for a date of birth: the browser offers to fill it. */\n autoComplete?: \"bday\" | \"off\";\n};\n\nfunction Caption({ id, hint, error }: { id: string; hint?: string; error?: string }) {\n const s = dateField();\n if (error)\n return (\n <p id={`${id}-error`} className={s.error()}>\n {error}\n </p>\n );\n return hint ? (\n <p id={`${id}-hint`} className={s.hint()}>\n {hint}\n </p>\n ) : null;\n}\n\n/** Day · month · year typed in the locale's order: numeric keyboards, the next box focused when one is full. */\nexport function DateField({ label, locale, hint, error, disabled, name, labels: given, className, value: valueProp, defaultValue = \"\", onValueChange, onBlur, autoComplete }: DateFieldProps) {\n const labels = { ...dateFieldLabels, ...given };\n const s = dateField();\n const id = React.useId();\n const [value, setValue] = useControllable<string>(valueProp, defaultValue, onValueChange);\n const order = React.useMemo(() => datePartsOrder(locale), [locale]);\n const [parts, setParts] = React.useState(() => splitDate(value));\n // A value set from outside (a reset, a suggestion) replaces what is in the boxes.\n if ((value ?? \"\") !== composeDate(parts) && composeDate(splitDate(value)) === (value ?? \"\")) setParts(splitDate(value));\n const refs = { day: React.useRef<HTMLInputElement>(null), month: React.useRef<HTMLInputElement>(null), year: React.useRef<HTMLInputElement>(null) };\n const describedBy = error ? `${id}-error` : hint ? `${id}-hint` : undefined;\n return (\n <fieldset className={s.root({ className })} aria-describedby={describedBy} onBlur={(e) => !e.currentTarget.contains(e.relatedTarget as Node | null) && onBlur?.()}>\n <legend className={s.legend()}>{label}</legend>\n <div className={s.row()}>\n {order.map((part: DatePart) => (\n <label key={part} className={s.part()}>\n {labels[part]}\n <input\n ref={refs[part]}\n inputMode=\"numeric\"\n autoComplete={autoComplete === \"bday\" ? `bday-${part}` : \"off\"}\n maxLength={DATE_PART_LENGTH[part]}\n value={parts[part]}\n disabled={disabled}\n aria-invalid={error ? true : undefined}\n className={s.input({ className: part === \"year\" ? \"w-20\" : \"w-14\" })}\n onChange={(e) => {\n const digits = e.target.value.replace(/\\D/g, \"\").slice(0, DATE_PART_LENGTH[part]);\n const next = { ...parts, [part]: digits };\n setParts(next);\n setValue(composeDate(next));\n const following = order[order.indexOf(part) + 1];\n if (digits.length === DATE_PART_LENGTH[part] && following) refs[following].current?.focus();\n }}\n />\n </label>\n ))}\n </div>\n {name ? <input type=\"hidden\" name={name} value={value ?? \"\"} /> : null}\n <Caption id={id} hint={hint} error={error} />\n </fieldset>\n );\n}\n\nexport type DateWheelProps = Common & {\n /** The date, `YYYY-MM-DD` (controlled); `\"\"` or `null` for none (the wheels then rest on `defaultView`). */\n value?: string | null;\n /** The date at first, uncontrolled. */\n defaultValue?: string | null;\n /** Called with the date under the band, always a real date within `min` / `max`. */\n onValueChange?: (value: string) => void;\n /** The earliest date (ISO): the years start there, the days before it are refused. Default: 120 years ago. */\n min?: string;\n /** The latest date (ISO): `latestBirthDate(18)` for an age gate. Default: today. */\n max?: string;\n /** Where the wheels rest while there is no value. Default: `max` (a birth date: the youngest allowed). */\n defaultView?: string;\n};\n\n/** Day, month and year as three wheels in the locale's order, bounded by `min` and `max`: always a real date. */\nexport function DateWheel({ label, locale, hint, error, disabled, name, labels: given, className, value: valueProp, defaultValue = null, onValueChange, min, max, defaultView }: DateWheelProps) {\n const labels = { ...dateFieldLabels, ...given };\n const id = React.useId();\n const [value, setValue] = useControllable<string | null>(valueProp, defaultValue, (v) => v && onValueChange?.(v));\n const today = todayIso();\n const hi = max ?? today;\n const lo = min ?? isoOf(Number(today.slice(0, 4)) - 120, 1, 1);\n const shown = value || defaultView || hi;\n const { year, month, day } = isoParts(shown);\n const order = React.useMemo(() => datePartsOrder(locale), [locale]);\n const months = React.useMemo(() => monthNames(locale, \"long\"), [locale]);\n const number = React.useMemo(() => new Intl.NumberFormat(locale, { useGrouping: false }), [locale]);\n const inBounds = (iso: string) => iso >= lo && iso <= hi;\n const set = (y: number, m: number, d: number) => {\n let next = isoOf(y, m, d);\n if (next > hi) next = hi;\n if (next < lo) next = lo;\n setValue(next);\n };\n const lastDay = daysInMonth(year, month);\n const columns: Record<DatePart, React.ReactNode> = {\n day: (\n <Wheel\n key=\"day\"\n label={labels.day}\n disabled={disabled}\n options={Array.from({ length: lastDay }, (_, i) => ({ value: String(i + 1), label: number.format(i + 1), disabled: !inBounds(isoOf(year, month, i + 1)) }))}\n value={String(Math.min(day, lastDay))}\n onValueChange={(v) => set(year, month, Number(v))}\n className=\"min-w-14\"\n />\n ),\n month: (\n <Wheel\n key=\"month\"\n label={labels.month}\n disabled={disabled}\n options={months.map((n, i) => ({ value: String(i + 1), label: n, disabled: isoOf(year, i + 1, daysInMonth(year, i + 1)) < lo || isoOf(year, i + 1, 1) > hi }))}\n value={String(month)}\n onValueChange={(v) => set(year, Number(v), day)}\n className=\"min-w-32 capitalize\"\n />\n ),\n year: (\n <Wheel\n key=\"year\"\n label={labels.year}\n disabled={disabled}\n options={yearRange(Number(hi.slice(0, 4)), Number(lo.slice(0, 4))).map((y) => ({ value: String(y), label: number.format(y) }))}\n value={String(year)}\n onValueChange={(v) => set(Number(v), month, day)}\n className=\"min-w-20\"\n />\n ),\n };\n return (\n <div className={dateField().root({ className })}>\n <span aria-hidden className={dateField().legend()}>\n {label}\n </span>\n <WheelPicker label={label} aria-describedby={error ? `${id}-error` : hint ? `${id}-hint` : undefined} invalid={Boolean(error)}>{order.map((part) => columns[part])}</WheelPicker>\n {name ? <input type=\"hidden\" name={name} value={value ?? \"\"} /> : null}\n <Caption id={id} hint={hint} error={error} />\n </div>\n );\n}\n"
1450
+ "content": "// Client. Two ways to give a memorable date — a birth date, an expiry — that a calendar would make slow to reach:\n// `DateField` (day · month · year typed in the locale's order, the next box focused once one is full; the web form's\n// `entry=\"parts\"`) and `DateWheel` (three wheels: the touch idiom, bounded by `min` / `max` — an age gate is\n// `max={latestBirthDate(18)}`). The value is `YYYY-MM-DD` either way, like the native `DateField` and `DateWheel`.\nimport * as React from \"react\";\n\nimport { useControllable } from \"../media\";\nimport { tv } from \"../tv\";\nimport { composeDate, DATE_PART_LENGTH, type DatePart, datePartsOrder, daysInMonth, isoOf, isoParts, monthNames, splitDate, todayIso, yearRange } from \"./parts\";\nimport { Wheel, WheelPicker } from \"./wheel\";\n\nexport const dateField = tv({\n slots: {\n root: \"flex flex-col gap-1.5 border-0 p-0\",\n legend: \"mb-1.5 p-0 text-xs font-semibold text-fg-secondary\",\n row: \"flex gap-2\",\n part: \"flex flex-col gap-1 text-xs text-fg-secondary\",\n input:\n \"h-11 rounded-lg border border-border-default bg-surface-2 px-2 text-center text-base tabular-nums text-fg inset-shadow-well transition-[border-color,box-shadow] \" +\n \"placeholder:text-fg-muted focus-visible:border-accent focus-visible:outline-hidden focus-visible:focus-ring-inset aria-invalid:border-danger disabled:cursor-not-allowed disabled:opacity-50\",\n hint: \"text-xs text-fg-secondary\",\n error: \"flex items-center gap-1.5 text-xs font-medium text-fg-danger before:h-1.5 before:w-1.5 before:shrink-0 before:rounded-full before:bg-danger\",\n },\n});\n\nexport type DateFieldLabels = {\n /** The name of the day (\"Day\"). */\n day: string;\n /** The name of the month (\"Month\"). */\n month: string;\n /** The name of the year (\"Year\"). */\n year: string;\n};\n\nexport const dateFieldLabels: DateFieldLabels = { day: \"Day\", month: \"Month\", year: \"Year\" };\n\ntype Common = {\n /** The visible label, passed translated (the group's name). */\n label: string;\n /** The BCP 47 locale: the order of the parts, the names of the months. */\n locale: string;\n /** A help text under the field (\"For example, 27 3 1990\"). */\n hint?: string;\n /** An error under the field: marks it invalid and is read with it. Replaces the hint. */\n error?: string;\n /** Not available. */\n disabled?: boolean;\n /** The name of a hidden input holding the value, for a plain `<form>` post. */\n name?: string;\n /** The names of the parts, passed translated; English by default. */\n labels?: Partial<DateFieldLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\nexport type DateFieldProps = Common & {\n /** The date, `YYYY-MM-DD` (controlled); `\"\"` for none. What is typed comes back as it is, real date or not: the schema decides. */\n value?: string;\n /** The date at first, uncontrolled. */\n defaultValue?: string;\n /** Called on every key with the date typed so far (`\"\"` when the boxes are empty). */\n onValueChange?: (value: string) => void;\n /** Called when the focus leaves the field. */\n onBlur?: () => void;\n /** `bday` for a date of birth: the browser offers to fill it. */\n autoComplete?: \"bday\" | \"off\";\n};\n\nfunction Caption({ id, hint, error }: { id: string; hint?: string; error?: string }) {\n const s = dateField();\n if (error)\n return (\n <p id={`${id}-error`} className={s.error()}>\n {error}\n </p>\n );\n return hint ? (\n <p id={`${id}-hint`} className={s.hint()}>\n {hint}\n </p>\n ) : null;\n}\n\n/** Day · month · year typed in the locale's order: numeric keyboards, the next box focused when one is full. */\nexport function DateField({ label, locale, hint, error, disabled, name, labels: given, className, value: valueProp, defaultValue = \"\", onValueChange, onBlur, autoComplete }: DateFieldProps) {\n const labels = { ...dateFieldLabels, ...given };\n const s = dateField();\n const id = React.useId();\n const [value, setValue] = useControllable<string>(valueProp, defaultValue, onValueChange);\n const order = React.useMemo(() => datePartsOrder(locale), [locale]);\n const [parts, setParts] = React.useState(() => splitDate(value));\n // A value set from outside (a reset, a suggestion) replaces what is in the boxes.\n if ((value ?? \"\") !== composeDate(parts) && composeDate(splitDate(value)) === (value ?? \"\")) setParts(splitDate(value));\n const refs = { day: React.useRef<HTMLInputElement>(null), month: React.useRef<HTMLInputElement>(null), year: React.useRef<HTMLInputElement>(null) };\n const describedBy = error ? `${id}-error` : hint ? `${id}-hint` : undefined;\n return (\n <fieldset className={s.root({ className })} aria-describedby={describedBy} onBlur={(e) => !e.currentTarget.contains(e.relatedTarget as Node | null) && onBlur?.()}>\n <legend className={s.legend()}>{label}</legend>\n <div className={s.row()}>\n {order.map((part: DatePart) => (\n <label key={part} className={s.part()}>\n {labels[part]}\n <input\n ref={refs[part]}\n inputMode=\"numeric\"\n autoComplete={autoComplete === \"bday\" ? `bday-${part}` : \"off\"}\n maxLength={DATE_PART_LENGTH[part]}\n value={parts[part]}\n disabled={disabled}\n aria-invalid={error ? true : undefined}\n className={s.input({ className: part === \"year\" ? \"w-20\" : \"w-14\" })}\n onChange={(e) => {\n const digits = e.target.value.replace(/\\D/g, \"\").slice(0, DATE_PART_LENGTH[part]);\n const next = { ...parts, [part]: digits };\n setParts(next);\n setValue(composeDate(next));\n const following = order[order.indexOf(part) + 1];\n if (digits.length === DATE_PART_LENGTH[part] && following) refs[following].current?.focus();\n }}\n />\n </label>\n ))}\n </div>\n {name ? <input type=\"hidden\" name={name} value={value ?? \"\"} /> : null}\n <Caption id={id} hint={hint} error={error} />\n </fieldset>\n );\n}\n\nexport type DateWheelProps = Common & {\n /** The date, `YYYY-MM-DD` (controlled); `\"\"` or `null` for none (the wheels then rest on `defaultView`). */\n value?: string | null;\n /** The date at first, uncontrolled. */\n defaultValue?: string | null;\n /** Called with the date under the band, always a real date within `min` / `max`. */\n onValueChange?: (value: string) => void;\n /** The earliest date (ISO): the years start there, the days before it are refused. Default: 120 years ago. */\n min?: string;\n /** The latest date (ISO): `latestBirthDate(18)` for an age gate. Default: today. */\n max?: string;\n /** Where the wheels rest while there is no value. Default: `max` (a birth date: the youngest allowed). */\n defaultView?: string;\n};\n\n/** Day, month and year as three wheels in the locale's order, bounded by `min` and `max`: always a real date. */\nexport function DateWheel({ label, locale, hint, error, disabled, name, labels: given, className, value: valueProp, defaultValue = null, onValueChange, min, max, defaultView }: DateWheelProps) {\n const labels = { ...dateFieldLabels, ...given };\n const id = React.useId();\n const [value, setValue] = useControllable<string | null>(valueProp, defaultValue, (v) => v && onValueChange?.(v));\n const today = todayIso();\n const hi = max ?? today;\n const lo = min ?? isoOf(Number(today.slice(0, 4)) - 120, 1, 1);\n const shown = value || defaultView || hi;\n const { year, month, day } = isoParts(shown);\n const order = React.useMemo(() => datePartsOrder(locale), [locale]);\n const months = React.useMemo(() => monthNames(locale, \"long\"), [locale]);\n const number = React.useMemo(() => new Intl.NumberFormat(locale, { useGrouping: false }), [locale]);\n const inBounds = (iso: string) => iso >= lo && iso <= hi;\n const set = (y: number, m: number, d: number) => {\n let next = isoOf(y, m, d);\n if (next > hi) next = hi;\n if (next < lo) next = lo;\n setValue(next);\n };\n const lastDay = daysInMonth(year, month);\n const columns: Record<DatePart, React.ReactNode> = {\n day: (\n <Wheel\n key=\"day\"\n label={labels.day}\n disabled={disabled}\n options={Array.from({ length: lastDay }, (_, i) => ({ value: String(i + 1), label: number.format(i + 1), disabled: !inBounds(isoOf(year, month, i + 1)) }))}\n value={String(Math.min(day, lastDay))}\n onValueChange={(v) => set(year, month, Number(v))}\n className=\"min-w-14\"\n />\n ),\n month: (\n <Wheel\n key=\"month\"\n label={labels.month}\n disabled={disabled}\n options={months.map((n, i) => ({ value: String(i + 1), label: n, disabled: isoOf(year, i + 1, daysInMonth(year, i + 1)) < lo || isoOf(year, i + 1, 1) > hi }))}\n value={String(month)}\n onValueChange={(v) => set(year, Number(v), day)}\n className=\"min-w-32 capitalize\"\n />\n ),\n year: (\n <Wheel\n key=\"year\"\n label={labels.year}\n disabled={disabled}\n options={yearRange(Number(hi.slice(0, 4)), Number(lo.slice(0, 4))).map((y) => ({ value: String(y), label: number.format(y) }))}\n value={String(year)}\n onValueChange={(v) => set(Number(v), month, day)}\n className=\"min-w-20\"\n />\n ),\n };\n return (\n <div className={dateField().root({ className })}>\n <span aria-hidden className={dateField().legend()}>\n {label}\n </span>\n <WheelPicker label={label} aria-describedby={error ? `${id}-error` : hint ? `${id}-hint` : undefined} invalid={Boolean(error)} unset={!value}>{order.map((part) => columns[part])}</WheelPicker>\n {name ? <input type=\"hidden\" name={name} value={value ?? \"\"} /> : null}\n <Caption id={id} hint={hint} error={error} />\n </div>\n );\n}\n"
1444
1451
  },
1445
1452
  {
1446
1453
  "path": "date-picker/date-picker.tsx",
@@ -1472,7 +1479,7 @@
1472
1479
  },
1473
1480
  {
1474
1481
  "path": "date-picker/wheel.tsx",
1475
- "content": "// Client. A wheel: a column of values that turns under a lit band — the touch idiom of a time, a date of birth, a\n// quantity. Each column is a `spinbutton` for assistive technologies and the keyboard (Up / Down, Page Up / Page Down by\n// five, Home / End), its value read as words (`aria-valuetext`); the hand scrolls it (scroll snap) or clicks a value.\n// The curve is a scroll-driven animation (CSS `animation-timeline: view()`): no script per frame, flat where unsupported.\nimport * as React from \"react\";\n\nimport { usePrefersReducedMotion } from \"../media\";\nimport { tv } from \"../tv\";\n\n/** A value of a wheel: what is stored, and its words (passed translated or formatted for the locale). */\nexport type WheelOption = { value: string; label: string; disabled?: boolean };\n\nexport const wheel = tv({\n slots: {\n root: \"relative inline-flex select-none items-stretch gap-1 rounded-2xl border border-border-default data-[invalid]:border-danger bg-surface-1 bg-sheen px-2 shadow-raised inset-shadow-edge\",\n band: \"pointer-events-none absolute inset-x-1.5 top-1/2 -translate-y-1/2 rounded-xl border border-border-subtle bg-surface-3 inset-shadow-well\",\n column:\n \"relative z-[1] min-w-14 flex-1 rounded-xl outline-hidden focus-visible:focus-ring-inset data-[disabled]:pointer-events-none data-[disabled]:opacity-40\",\n scroller:\n \"kz-wheel h-full snap-y snap-mandatory overflow-y-auto overscroll-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden \" +\n \"[mask-image:linear-gradient(to_bottom,transparent,#000_30%,#000_70%,transparent)]\",\n item:\n \"kz-wheel-item flex snap-center items-center justify-center px-2 text-base tabular-nums text-fg-secondary transition-colors \" +\n \"data-[selected]:font-semibold data-[selected]:text-fg data-[disabled]:text-fg-muted data-[disabled]:line-through\",\n divider: \"z-[1] self-center text-base font-semibold text-fg-secondary\",\n },\n});\n\nconst ITEM = 40;\n\nexport type WheelProps = {\n /** The column's accessible name (\"Hour\", \"Month\"), passed translated. */\n label: string;\n /** The values, in order. */\n options: readonly WheelOption[];\n /** The chosen value (controlled). */\n value?: string | null;\n /** The chosen value at first, uncontrolled. Default: the first one. */\n defaultValue?: string;\n /** Called with the value that comes to rest under the band. */\n onValueChange?: (value: string) => void;\n /** Not available. */\n disabled?: boolean;\n /** Classes merged on the column (a width: `w-20`). */\n className?: string;\n};\n\nexport type WheelPickerProps = {\n /** The group's accessible name (\"Departure time\"), passed translated. */\n label: string;\n /** The wheels (`Wheel`), and dividers (`WheelDivider`) between them. */\n children: React.ReactNode;\n /** How many values are visible: 3, 5 (default) or 7. */\n visible?: 3 | 5 | 7;\n /** The help text or the error that describes the group (their ids). */\n \"aria-describedby\"?: string;\n /** Marks the group invalid (a danger edge): pair it with an error read through `aria-describedby`. */\n invalid?: boolean;\n /** Classes merged on the root. */\n className?: string;\n};\n\nconst VisibleContext = React.createContext(5);\n\n/** The frame of one or several wheels: a raised surface, the lit band across them, a group named by `label`. */\nexport function WheelPicker({ label, children, visible = 5, \"aria-describedby\": describedBy, invalid, className }: WheelPickerProps) {\n const s = wheel();\n return (\n <VisibleContext.Provider value={visible}>\n <div role=\"group\" aria-label={label} aria-describedby={describedBy} data-invalid={invalid ? \"\" : undefined} className={s.root({ className })} style={{ height: ITEM * visible }}>\n <span aria-hidden className={s.band()} style={{ height: ITEM }} />\n {children}\n </div>\n </VisibleContext.Provider>\n );\n}\n\n/** A divider between two wheels (`:` between the hours and the minutes). */\nexport function WheelDivider({ children }: { children: React.ReactNode }) {\n return (\n <span aria-hidden className={wheel().divider()}>\n {children}\n </span>\n );\n}\n\nconst nearestEnabled = (options: readonly WheelOption[], index: number) => {\n for (let d = 0; d < options.length; d++) {\n if (options[index + d] && !options[index + d]!.disabled) return index + d;\n if (options[index - d] && !options[index - d]!.disabled) return index - d;\n }\n return index;\n};\n\n/** One column of a `WheelPicker`: a spinbutton over a scroll-snapped list of values. */\nexport function Wheel({ label, options, value: valueProp, defaultValue, onValueChange, disabled, className }: WheelProps) {\n const s = wheel();\n const visible = React.useContext(VisibleContext);\n const [own, setOwn] = React.useState(defaultValue ?? options[0]?.value ?? \"\");\n const value = valueProp ?? own;\n const index = Math.max(0, options.findIndex((o) => o.value === value));\n const scroller = React.useRef<HTMLDivElement>(null);\n const settle = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n const programmatic = React.useRef(false);\n const reduce = usePrefersReducedMotion();\n const mounted = React.useRef(false);\n\n const commit = React.useCallback(\n (i: number) => {\n const target = nearestEnabled(options, Math.min(options.length - 1, Math.max(0, i)));\n const option = options[target];\n if (!option) return;\n if (valueProp === undefined) setOwn(option.value);\n if (option.value !== value) onValueChange?.(option.value);\n },\n [options, valueProp, value, onValueChange],\n );\n\n // The list follows the value (a key, a click, a change from outside): at once the first time, smoothly after.\n React.useEffect(() => {\n const el = scroller.current;\n if (!el) return;\n const top = index * ITEM;\n if (Math.abs(el.scrollTop - top) < 1) return;\n programmatic.current = true;\n if (typeof el.scrollTo === \"function\") el.scrollTo({ top, behavior: mounted.current && !reduce ? \"smooth\" : \"auto\" });\n else el.scrollTop = top;\n mounted.current = true;\n const t = setTimeout(() => (programmatic.current = false), 400);\n return () => clearTimeout(t);\n }, [index, reduce]);\n\n const onScroll = () => {\n if (programmatic.current) return;\n if (settle.current) clearTimeout(settle.current);\n settle.current = setTimeout(() => {\n const el = scroller.current;\n if (el) commit(Math.round(el.scrollTop / ITEM));\n }, 90);\n };\n React.useEffect(() => () => void (settle.current && clearTimeout(settle.current)), []);\n\n const onKeyDown = (event: React.KeyboardEvent) => {\n const step = (by: number) => {\n let i = index + by;\n while (options[i]?.disabled) i += Math.sign(by);\n return options[i] ? i : index;\n };\n const moves: Record<string, () => number> = {\n ArrowUp: () => step(-1),\n ArrowDown: () => step(1),\n PageUp: () => Math.max(0, nearestEnabled(options, index - 5)),\n PageDown: () => Math.min(options.length - 1, nearestEnabled(options, index + 5)),\n Home: () => nearestEnabled(options, 0),\n End: () => nearestEnabled(options, options.length - 1),\n };\n const move = moves[event.key];\n if (!move) return;\n event.preventDefault();\n commit(move());\n };\n\n const pad = ((visible - 1) / 2) * ITEM;\n const current = options[index];\n return (\n <div\n role=\"spinbutton\"\n tabIndex={disabled ? -1 : 0}\n aria-label={label}\n aria-valuenow={index}\n aria-valuemin={0}\n aria-valuemax={Math.max(0, options.length - 1)}\n aria-valuetext={current?.label}\n aria-disabled={disabled || undefined}\n data-disabled={disabled ? \"\" : undefined}\n className={s.column({ className })}\n onKeyDown={onKeyDown}\n >\n <div ref={scroller} aria-hidden className={s.scroller()} style={{ paddingBlock: pad, scrollPaddingBlock: pad }} onScroll={onScroll}>\n {options.map((o, i) => (\n <div\n key={o.value}\n data-value={o.value}\n data-selected={i === index ? \"\" : undefined}\n data-disabled={o.disabled ? \"\" : undefined}\n className={s.item()}\n style={{ height: ITEM }}\n onClick={() => !o.disabled && commit(i)}\n >\n {o.label}\n </div>\n ))}\n </div>\n </div>\n );\n}\n"
1482
+ "content": "// Client. A wheel: a column of values that turns under a lit band — the touch idiom of a time, a date of birth, a\n// quantity. Each column is a `spinbutton` for assistive technologies and the keyboard (Up / Down, Page Up / Page Down by\n// five, Home / End), its value read as words (`aria-valuetext`); the hand scrolls it (scroll snap) or clicks a value.\n// The curve is a scroll-driven animation (CSS `animation-timeline: view()`): no script per frame, flat where unsupported.\nimport * as React from \"react\";\n\nimport { usePrefersReducedMotion } from \"../media\";\nimport { tv } from \"../tv\";\n\n/** A value of a wheel: what is stored, and its words (passed translated or formatted for the locale). */\nexport type WheelOption = { value: string; label: string; disabled?: boolean };\n\nexport const wheel = tv({\n slots: {\n root: \"group/wheel relative inline-flex select-none items-stretch gap-1 rounded-2xl border border-border-default data-[invalid]:border-danger bg-surface-1 bg-sheen px-2 shadow-raised inset-shadow-edge\",\n band: \"pointer-events-none absolute inset-x-1.5 top-1/2 -translate-y-1/2 rounded-xl border border-border-subtle bg-surface-3 inset-shadow-well\",\n column:\n \"relative z-[1] min-w-14 flex-1 rounded-xl outline-hidden focus-visible:focus-ring-inset data-[disabled]:pointer-events-none data-[disabled]:opacity-40\",\n scroller:\n \"kz-wheel h-full snap-y snap-mandatory overflow-y-auto overscroll-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden \" +\n \"[mask-image:linear-gradient(to_bottom,transparent,#000_30%,#000_70%,transparent)]\",\n item:\n \"kz-wheel-item flex snap-center items-center justify-center px-2 text-base tabular-nums text-fg-secondary transition-colors \" +\n \"data-[selected]:font-semibold data-[selected]:text-fg data-[disabled]:text-fg-muted data-[disabled]:line-through \" +\n \"group-data-[unset]/wheel:data-[selected]:font-normal group-data-[unset]/wheel:data-[selected]:text-fg-secondary\",\n divider: \"z-[1] self-center text-base font-semibold text-fg-secondary\",\n },\n});\n\nconst ITEM = 40;\n\nexport type WheelProps = {\n /** The column's accessible name (\"Hour\", \"Month\"), passed translated. */\n label: string;\n /** The values, in order. */\n options: readonly WheelOption[];\n /** The chosen value (controlled). */\n value?: string | null;\n /** The chosen value at first, uncontrolled. Default: the first one. */\n defaultValue?: string;\n /** Called with the value that comes to rest under the band. */\n onValueChange?: (value: string) => void;\n /** Not available. */\n disabled?: boolean;\n /** Classes merged on the column (a width: `w-20`). */\n className?: string;\n};\n\nexport type WheelPickerProps = {\n /** The group's accessible name (\"Departure time\"), passed translated. */\n label: string;\n /** The wheels (`Wheel`), and dividers (`WheelDivider`) between them. */\n children: React.ReactNode;\n /** How many values are visible: 3, 5 (default) or 7. */\n visible?: 3 | 5 | 7;\n /** The help text or the error that describes the group (their ids). */\n \"aria-describedby\"?: string;\n /** Marks the group invalid (a danger edge): pair it with an error read through `aria-describedby`. */\n invalid?: boolean;\n /** Nothing is chosen yet: the values under the band stay quiet until a wheel is turned. */\n unset?: boolean;\n /** Classes merged on the root. */\n className?: string;\n};\n\nconst VisibleContext = React.createContext(5);\n\n/** The frame of one or several wheels: a raised surface, the lit band across them, a group named by `label`. */\nexport function WheelPicker({ label, children, visible = 5, \"aria-describedby\": describedBy, invalid, unset, className }: WheelPickerProps) {\n const s = wheel();\n return (\n <VisibleContext.Provider value={visible}>\n <div role=\"group\" aria-label={label} aria-describedby={describedBy} data-invalid={invalid ? \"\" : undefined} data-unset={unset ? \"\" : undefined} className={s.root({ className })} style={{ height: ITEM * visible }}>\n <span aria-hidden className={s.band()} style={{ height: ITEM }} />\n {children}\n </div>\n </VisibleContext.Provider>\n );\n}\n\n/** A divider between two wheels (`:` between the hours and the minutes). */\nexport function WheelDivider({ children }: { children: React.ReactNode }) {\n return (\n <span aria-hidden className={wheel().divider()}>\n {children}\n </span>\n );\n}\n\nconst nearestEnabled = (options: readonly WheelOption[], index: number) => {\n for (let d = 0; d < options.length; d++) {\n if (options[index + d] && !options[index + d]!.disabled) return index + d;\n if (options[index - d] && !options[index - d]!.disabled) return index - d;\n }\n return index;\n};\n\n/** One column of a `WheelPicker`: a spinbutton over a scroll-snapped list of values. */\nexport function Wheel({ label, options, value: valueProp, defaultValue, onValueChange, disabled, className }: WheelProps) {\n const s = wheel();\n const visible = React.useContext(VisibleContext);\n const [own, setOwn] = React.useState(defaultValue ?? options[0]?.value ?? \"\");\n const value = valueProp ?? own;\n const index = Math.max(0, options.findIndex((o) => o.value === value));\n const scroller = React.useRef<HTMLDivElement>(null);\n const settle = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n const programmatic = React.useRef(false);\n const reduce = usePrefersReducedMotion();\n const mounted = React.useRef(false);\n\n const commit = React.useCallback(\n (i: number) => {\n const target = nearestEnabled(options, Math.min(options.length - 1, Math.max(0, i)));\n const option = options[target];\n if (!option) return;\n if (valueProp === undefined) setOwn(option.value);\n if (option.value !== value) onValueChange?.(option.value);\n },\n [options, valueProp, value, onValueChange],\n );\n\n // The list follows the value (a key, a click, a change from outside): at once the first time, smoothly after.\n React.useEffect(() => {\n const el = scroller.current;\n if (!el) return;\n const top = index * ITEM;\n if (Math.abs(el.scrollTop - top) < 1) return;\n programmatic.current = true;\n if (typeof el.scrollTo === \"function\") el.scrollTo({ top, behavior: mounted.current && !reduce ? \"smooth\" : \"auto\" });\n else el.scrollTop = top;\n mounted.current = true;\n const t = setTimeout(() => (programmatic.current = false), 400);\n return () => clearTimeout(t);\n }, [index, reduce]);\n\n const onScroll = () => {\n if (programmatic.current) return;\n if (settle.current) clearTimeout(settle.current);\n settle.current = setTimeout(() => {\n const el = scroller.current;\n if (el) commit(Math.round(el.scrollTop / ITEM));\n }, 90);\n };\n React.useEffect(() => () => void (settle.current && clearTimeout(settle.current)), []);\n\n const onKeyDown = (event: React.KeyboardEvent) => {\n const step = (by: number) => {\n let i = index + by;\n while (options[i]?.disabled) i += Math.sign(by);\n return options[i] ? i : index;\n };\n const moves: Record<string, () => number> = {\n ArrowUp: () => step(-1),\n ArrowDown: () => step(1),\n PageUp: () => Math.max(0, nearestEnabled(options, index - 5)),\n PageDown: () => Math.min(options.length - 1, nearestEnabled(options, index + 5)),\n Home: () => nearestEnabled(options, 0),\n End: () => nearestEnabled(options, options.length - 1),\n };\n const move = moves[event.key];\n if (!move) return;\n event.preventDefault();\n commit(move());\n };\n\n const pad = ((visible - 1) / 2) * ITEM;\n const current = options[index];\n return (\n <div\n role=\"spinbutton\"\n tabIndex={disabled ? -1 : 0}\n aria-label={label}\n aria-valuenow={index}\n aria-valuemin={0}\n aria-valuemax={Math.max(0, options.length - 1)}\n aria-valuetext={current?.label}\n aria-disabled={disabled || undefined}\n data-disabled={disabled ? \"\" : undefined}\n className={s.column({ className })}\n onKeyDown={onKeyDown}\n >\n <div ref={scroller} aria-hidden className={s.scroller()} style={{ paddingBlock: pad, scrollPaddingBlock: pad }} onScroll={onScroll}>\n {options.map((o, i) => (\n <div\n key={o.value}\n data-value={o.value}\n data-selected={i === index ? \"\" : undefined}\n data-disabled={o.disabled ? \"\" : undefined}\n className={s.item()}\n style={{ height: ITEM }}\n onClick={() => !o.disabled && commit(i)}\n >\n {o.label}\n </div>\n ))}\n </div>\n </div>\n );\n}\n"
1476
1483
  },
1477
1484
  {
1478
1485
  "path": "glyphs.tsx",
@@ -2540,6 +2547,13 @@
2540
2547
  "description": "Marks the group invalid (a danger edge): pair it with an error read through `aria-describedby`.",
2541
2548
  "required": false
2542
2549
  },
2550
+ {
2551
+ "name": "unset",
2552
+ "type": "boolean",
2553
+ "default": null,
2554
+ "description": "Nothing is chosen yet: the values under the band stay quiet until a wheel is turned.",
2555
+ "required": false
2556
+ },
2543
2557
  {
2544
2558
  "name": "visible",
2545
2559
  "type": "3 | 5 | 7",
@@ -6,7 +6,7 @@
6
6
  "description": "A window over the page — centred, a bottom sheet or a side panel — and `AlertDialog` for a confirmation.",
7
7
  "status": "stable",
8
8
  "category": "overlays",
9
- "platforms": "web",
9
+ "platforms": "both",
10
10
  "whenToUse": [
11
11
  "For a short task that must be finished or cancelled before going back: invite someone, edit a title.",
12
12
  "`AlertDialog` to confirm a destructive or irreversible action, with an async `onConfirm`.",
@@ -308,7 +308,78 @@
308
308
  }
309
309
  ]
310
310
  },
311
- "native": null,
311
+ "native": {
312
+ "entry": "@krizaka/ui/native",
313
+ "import": "import { Sheet } from \"@krizaka/ui/native\";",
314
+ "differences": [
315
+ "One component, `Sheet`: a bottom sheet on React Native's `Modal` (focus kept inside, the back button and the scrim close it), its grip on top, rising with the theme's ease.",
316
+ "It holds what a phone shows over a screen: a choice (`Select`, `Combobox` use it), the details of a pin on a map, a short form."
317
+ ],
318
+ "props": [
319
+ {
320
+ "component": "Sheet",
321
+ "description": "A bottom sheet: it rises over a dimmed screen, its grip on top, the scrim closes it. The web `Dialog` on a phone —\na choice (`Select`, `Combobox`), the details of a pin on a map, a short form. React Native's `Modal`: focus stays\ninside, the back button closes it. Rises with the theme's ease; still under reduced motion.",
322
+ "props": [
323
+ {
324
+ "name": "children",
325
+ "type": "ReactNode",
326
+ "default": null,
327
+ "description": "The content: a list, details, a form.",
328
+ "required": true
329
+ },
330
+ {
331
+ "name": "onOpenChange",
332
+ "type": "(open: boolean) => void",
333
+ "default": null,
334
+ "description": "Called to close it: the scrim pressed, the back button, a choice made.",
335
+ "required": true
336
+ },
337
+ {
338
+ "name": "open",
339
+ "type": "boolean",
340
+ "default": null,
341
+ "description": "Open (controlled).",
342
+ "required": true
343
+ },
344
+ {
345
+ "name": "closeLabel",
346
+ "type": "string",
347
+ "default": "Close",
348
+ "description": "The name of the close action read by screen readers (\"Close\"), passed translated.",
349
+ "required": false
350
+ },
351
+ {
352
+ "name": "style",
353
+ "type": "StyleProp<ViewStyle>",
354
+ "default": null,
355
+ "description": "Styles merged last, on the sheet.",
356
+ "required": false
357
+ },
358
+ {
359
+ "name": "title",
360
+ "type": "string",
361
+ "default": null,
362
+ "description": "The title at the top of the sheet (its accessible name), passed translated.",
363
+ "required": false
364
+ }
365
+ ]
366
+ }
367
+ ],
368
+ "examples": [
369
+ {
370
+ "name": "sheet",
371
+ "title": "Bottom sheet",
372
+ "description": "The details of a pick-up point over the dimmed screen.",
373
+ "path": "examples/dialog/native/sheet.tsx",
374
+ "module": "@krizaka/ui/registry/examples/dialog/native/sheet",
375
+ "code": "import { Button, Sheet, Txt } from \"@krizaka/ui/native\";\nimport * as React from \"react\";\nimport { View } from \"react-native\";\n\n// A bottom sheet: it rises over the dimmed screen, the scrim or the back button closes it.\nexport default function NativeDialogSheet({ defaultOpen }: { defaultOpen?: boolean }) {\n const [open, setOpen] = React.useState(Boolean(defaultOpen));\n return (\n <View>\n <Button label=\"Show the pick-up point\" onPress={() => setOpen(true)} />\n <Sheet open={open} onOpenChange={setOpen} title=\"Café Olimpico\">\n <Txt tone=\"secondary\">124 Rue Saint-Viateur O, Montréal · 350 m away</Txt>\n <Txt>Open until 10 pm. Ask for the order under your name at the counter.</Txt>\n <Button label=\"Get directions\" variant=\"primary\" onPress={() => setOpen(false)} />\n </Sheet>\n </View>\n );\n}\n",
376
+ "screenshots": {
377
+ "dark": "examples/dialog/native/sheet.dark.png",
378
+ "light": "examples/dialog/native/sheet.light.png"
379
+ }
380
+ }
381
+ ]
382
+ },
312
383
  "files": [
313
384
  {
314
385
  "path": "dialog/alert-dialog.tsx",
@@ -324,7 +395,7 @@
324
395
  },
325
396
  {
326
397
  "path": "dialog/meta.ts",
327
- "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Dialog\",\n summary: \"A window over the page — centred, a bottom sheet or a side panel — and `AlertDialog` for a confirmation.\",\n status: \"stable\",\n category: \"overlays\",\n platforms: \"web\",\n whenToUse: [\n \"For a short task that must be finished or cancelled before going back: invite someone, edit a title.\",\n \"`AlertDialog` to confirm a destructive or irreversible action, with an async `onConfirm`.\",\n \"`Sheet` (bottom) on phones, `placement=\\\"right\\\"` for a side panel of details.\",\n \"A gate the user must answer (age, terms): `hideClose` and `dismissible={false}`.\",\n ],\n whenNotToUse: [\n { when: \"For information next to an element, without blocking the page.\", use: \"popover\" },\n { when: \"For a message that needs no answer.\", use: \"toast\" },\n { when: \"For a long form or a flow of several steps: give it its own page.\" },\n { when: \"To confirm the deletion of one item in place.\", use: \"confirm-button\" },\n ],\n bestPractices: [\n \"Always a `Dialog.Title`; a `Dialog.Description` when the title is not enough.\",\n \"Name the buttons by what they do (“Send the invitation”), the cancel one by what it keeps (“Keep it”).\",\n \"Pass `closeLabel` translated: it names the close button.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"Esc\", action: \"Closes the dialog (unless `dismissible={false}`).\" },\n { keys: \"Tab / Shift+Tab\", action: \"Move within the dialog: the focus is trapped while it is open.\" },\n ],\n notes: [\n \"Radix Dialog: `role=\\\"dialog\\\"` (or `alertdialog`), `aria-modal`, named by its title and described by its description.\",\n \"The focus moves into the dialog when it opens and returns to the trigger when it closes; the page behind does not scroll.\",\n \"`AlertDialog` keeps the focus on the cancel button first.\",\n ],\n },\n related: [\"popover\", \"confirm-button\", \"command\", \"toast\"],\n web: {\n imports: [\"Dialog\", \"Sheet\", \"AlertDialog\"],\n examples: [\n { name: \"center\", title: \"Centred\", description: \"A short form, `size=\\\"md\\\"`.\" },\n { name: \"bottom\", title: \"Bottom sheet\", description: \"`placement=\\\"bottom\\\"`: a sheet on a phone, centred from `sm` up.\" },\n { name: \"right\", title: \"Side panel\", description: \"`placement=\\\"right\\\"`: full height, for details.\" },\n { name: \"left\", title: \"Navigation drawer\", description: \"`placement=\\\"left\\\"`: full height from the left, for an app's navigation on a phone.\" },\n { name: \"sheet-form\", title: \"Sheet with a form\", description: \"`Sheet`, `size=\\\"lg\\\"`.\" },\n { name: \"alert-destructive\", title: \"Alert dialog\", description: \"`AlertDialog tone=\\\"danger\\\"` with an async `onConfirm`.\" },\n { name: \"gate\", title: \"Gate\", description: \"No close button: it closes only through its actions.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
398
+ "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Dialog\",\n summary: \"A window over the page — centred, a bottom sheet or a side panel — and `AlertDialog` for a confirmation.\",\n status: \"stable\",\n category: \"overlays\",\n platforms: \"both\",\n whenToUse: [\n \"For a short task that must be finished or cancelled before going back: invite someone, edit a title.\",\n \"`AlertDialog` to confirm a destructive or irreversible action, with an async `onConfirm`.\",\n \"`Sheet` (bottom) on phones, `placement=\\\"right\\\"` for a side panel of details.\",\n \"A gate the user must answer (age, terms): `hideClose` and `dismissible={false}`.\",\n ],\n whenNotToUse: [\n { when: \"For information next to an element, without blocking the page.\", use: \"popover\" },\n { when: \"For a message that needs no answer.\", use: \"toast\" },\n { when: \"For a long form or a flow of several steps: give it its own page.\" },\n { when: \"To confirm the deletion of one item in place.\", use: \"confirm-button\" },\n ],\n bestPractices: [\n \"Always a `Dialog.Title`; a `Dialog.Description` when the title is not enough.\",\n \"Name the buttons by what they do (“Send the invitation”), the cancel one by what it keeps (“Keep it”).\",\n \"Pass `closeLabel` translated: it names the close button.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"Esc\", action: \"Closes the dialog (unless `dismissible={false}`).\" },\n { keys: \"Tab / Shift+Tab\", action: \"Move within the dialog: the focus is trapped while it is open.\" },\n ],\n notes: [\n \"Radix Dialog: `role=\\\"dialog\\\"` (or `alertdialog`), `aria-modal`, named by its title and described by its description.\",\n \"The focus moves into the dialog when it opens and returns to the trigger when it closes; the page behind does not scroll.\",\n \"`AlertDialog` keeps the focus on the cancel button first.\",\n ],\n },\n related: [\"popover\", \"confirm-button\", \"command\", \"toast\"],\n web: {\n imports: [\"Dialog\", \"Sheet\", \"AlertDialog\"],\n examples: [\n { name: \"center\", title: \"Centred\", description: \"A short form, `size=\\\"md\\\"`.\" },\n { name: \"bottom\", title: \"Bottom sheet\", description: \"`placement=\\\"bottom\\\"`: a sheet on a phone, centred from `sm` up.\" },\n { name: \"right\", title: \"Side panel\", description: \"`placement=\\\"right\\\"`: full height, for details.\" },\n { name: \"left\", title: \"Navigation drawer\", description: \"`placement=\\\"left\\\"`: full height from the left, for an app's navigation on a phone.\" },\n { name: \"sheet-form\", title: \"Sheet with a form\", description: \"`Sheet`, `size=\\\"lg\\\"`.\" },\n { name: \"alert-destructive\", title: \"Alert dialog\", description: \"`AlertDialog tone=\\\"danger\\\"` with an async `onConfirm`.\" },\n { name: \"gate\", title: \"Gate\", description: \"No close button: it closes only through its actions.\" },\n ],\n },\n native: {\n imports: [\"Sheet\"],\n differences: [\n \"One component, `Sheet`: a bottom sheet on React Native's `Modal` (focus kept inside, the back button and the scrim close it), its grip on top, rising with the theme's ease.\",\n \"It holds what a phone shows over a screen: a choice (`Select`, `Combobox` use it), the details of a pin on a map, a short form.\",\n ],\n examples: [{ name: \"sheet\", title: \"Bottom sheet\", description: \"The details of a pick-up point over the dimmed screen.\" }],\n },\n} satisfies ComponentMeta;\n"
328
399
  },
329
400
  {
330
401
  "path": "meta.ts",
@@ -0,0 +1,4 @@
1
+ import type { ComponentType } from "react";
2
+ /** A named example of the @krizaka/ui registry; `defaultOpen` opens what it opens (a dialog, a menu, toasts) at load. */
3
+ declare const Example: ComponentType<{ defaultOpen?: boolean }>;
4
+ export default Example;
@@ -0,0 +1,15 @@
1
+ "use client";
2
+
3
+ // registry/examples/combobox/native/multiple.tsx
4
+ import { Combobox } from "@krizaka/ui/native";
5
+ import * as React from "react";
6
+ import { View } from "react-native";
7
+ import { jsx } from "react/jsx-runtime";
8
+ var TOPICS = ["Music", "Sport", "Art", "Cooking", "Gaming", "Travel"].map((t) => ({ value: t.toLowerCase(), label: t }));
9
+ function NativeComboboxMultiple() {
10
+ const [topics, setTopics] = React.useState(["music", "travel"]);
11
+ return /* @__PURE__ */ jsx(View, { style: { width: 300 }, children: /* @__PURE__ */ jsx(Combobox, { label: "Interests", multiple: true, options: TOPICS, value: topics, onValueChange: (v) => setTopics(Array.isArray(v) ? v : [v]) }) });
12
+ }
13
+ export {
14
+ NativeComboboxMultiple as default
15
+ };
@@ -0,0 +1,15 @@
1
+ import { Combobox } from "@krizaka/ui/native";
2
+ import * as React from "react";
3
+ import { View } from "react-native";
4
+
5
+ const TOPICS = ["Music", "Sport", "Art", "Cooking", "Gaming", "Travel"].map((t) => ({ value: t.toLowerCase(), label: t }));
6
+
7
+ // Several values: checkboxes in the sheet, Done closes it; the field lists the choices.
8
+ export default function NativeComboboxMultiple() {
9
+ const [topics, setTopics] = React.useState<string[]>(["music", "travel"]);
10
+ return (
11
+ <View style={{ width: 300 }}>
12
+ <Combobox label="Interests" multiple options={TOPICS} value={topics} onValueChange={(v) => setTopics(Array.isArray(v) ? v : [v])} />
13
+ </View>
14
+ );
15
+ }
@@ -0,0 +1,4 @@
1
+ import type { ComponentType } from "react";
2
+ /** A named example of the @krizaka/ui registry; `defaultOpen` opens what it opens (a dialog, a menu, toasts) at load. */
3
+ declare const Example: ComponentType<{ defaultOpen?: boolean }>;
4
+ export default Example;
@@ -0,0 +1,15 @@
1
+ "use client";
2
+
3
+ // registry/examples/combobox/native/search.tsx
4
+ import { Combobox } from "@krizaka/ui/native";
5
+ import * as React from "react";
6
+ import { View } from "react-native";
7
+ import { jsx } from "react/jsx-runtime";
8
+ var CITIES = ["Montr\xE9al", "Qu\xE9bec", "Laval", "Gatineau", "Sherbrooke", "Trois-Rivi\xE8res"].map((c) => ({ value: c.toLowerCase(), label: c }));
9
+ function NativeComboboxSearch() {
10
+ const [city, setCity] = React.useState("montr\xE9al");
11
+ return /* @__PURE__ */ jsx(View, { style: { width: 300 }, children: /* @__PURE__ */ jsx(Combobox, { label: "City", placeholder: "Choose a city", options: CITIES, value: city, onValueChange: (v) => setCity(String(v)) }) });
12
+ }
13
+ export {
14
+ NativeComboboxSearch as default
15
+ };
@@ -0,0 +1,15 @@
1
+ import { Combobox } from "@krizaka/ui/native";
2
+ import * as React from "react";
3
+ import { View } from "react-native";
4
+
5
+ const CITIES = ["Montréal", "Québec", "Laval", "Gatineau", "Sherbrooke", "Trois-Rivières"].map((c) => ({ value: c.toLowerCase(), label: c }));
6
+
7
+ // A choice among many, searched in a sheet: what is typed filters the options.
8
+ export default function NativeComboboxSearch() {
9
+ const [city, setCity] = React.useState<string>("montréal");
10
+ return (
11
+ <View style={{ width: 300 }}>
12
+ <Combobox label="City" placeholder="Choose a city" options={CITIES} value={city} onValueChange={(v) => setCity(String(v))} />
13
+ </View>
14
+ );
15
+ }
@@ -0,0 +1,4 @@
1
+ import type { ComponentType } from "react";
2
+ /** A named example of the @krizaka/ui registry; `defaultOpen` opens what it opens (a dialog, a menu, toasts) at load. */
3
+ declare const Example: ComponentType<{ defaultOpen?: boolean }>;
4
+ export default Example;
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ // registry/examples/dialog/native/sheet.tsx
4
+ import { Button, Sheet, Txt } from "@krizaka/ui/native";
5
+ import * as React from "react";
6
+ import { View } from "react-native";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ function NativeDialogSheet({ defaultOpen }) {
9
+ const [open, setOpen] = React.useState(Boolean(defaultOpen));
10
+ return /* @__PURE__ */ jsxs(View, { children: [
11
+ /* @__PURE__ */ jsx(Button, { label: "Show the pick-up point", onPress: () => setOpen(true) }),
12
+ /* @__PURE__ */ jsxs(Sheet, { open, onOpenChange: setOpen, title: "Caf\xE9 Olimpico", children: [
13
+ /* @__PURE__ */ jsx(Txt, { tone: "secondary", children: "124 Rue Saint-Viateur O, Montr\xE9al \xB7 350 m away" }),
14
+ /* @__PURE__ */ jsx(Txt, { children: "Open until 10 pm. Ask for the order under your name at the counter." }),
15
+ /* @__PURE__ */ jsx(Button, { label: "Get directions", variant: "primary", onPress: () => setOpen(false) })
16
+ ] })
17
+ ] });
18
+ }
19
+ export {
20
+ NativeDialogSheet as default
21
+ };