@vielzeug/refine 1.5.4 → 1.7.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 (329) hide show
  1. package/dist/accordion.cjs +1 -1
  2. package/dist/accordion.cjs.map +1 -1
  3. package/dist/accordion.js +30 -28
  4. package/dist/accordion.js.map +1 -1
  5. package/dist/button-group.cjs.map +1 -1
  6. package/dist/button-group.js.map +1 -1
  7. package/dist/button.cjs +1 -1
  8. package/dist/button.cjs.map +1 -1
  9. package/dist/button.js +1 -0
  10. package/dist/button.js.map +1 -1
  11. package/dist/checkbox-group.cjs +3 -3
  12. package/dist/checkbox-group.cjs.map +1 -1
  13. package/dist/checkbox-group.js +51 -58
  14. package/dist/checkbox-group.js.map +1 -1
  15. package/dist/combobox.cjs +29 -28
  16. package/dist/combobox.cjs.map +1 -1
  17. package/dist/combobox.js +201 -195
  18. package/dist/combobox.js.map +1 -1
  19. package/dist/content/card/card.css.cjs +1 -1
  20. package/dist/content/card/card.css.cjs.map +1 -1
  21. package/dist/content/card/card.css.js +1 -1
  22. package/dist/content/card/card.css.js.map +1 -1
  23. package/dist/content/chat-message/chat-message.css.cjs +1 -1
  24. package/dist/content/chat-message/chat-message.css.cjs.map +1 -1
  25. package/dist/content/chat-message/chat-message.css.js +1 -1
  26. package/dist/content/chat-message/chat-message.css.js.map +1 -1
  27. package/dist/content/icon/icon.d.ts.map +1 -1
  28. package/dist/content/index.d.ts +4 -0
  29. package/dist/content/index.d.ts.map +1 -1
  30. package/dist/content/list/_list-item-reveal.cjs +2 -0
  31. package/dist/content/list/_list-item-reveal.cjs.map +1 -0
  32. package/dist/content/list/_list-item-reveal.d.ts +25 -0
  33. package/dist/content/list/_list-item-reveal.d.ts.map +1 -0
  34. package/dist/content/list/_list-item-reveal.js +33 -0
  35. package/dist/content/list/_list-item-reveal.js.map +1 -0
  36. package/dist/content/list/list-item.css.cjs +2 -0
  37. package/dist/content/list/list-item.css.cjs.map +1 -0
  38. package/dist/content/list/list-item.css.js +6 -0
  39. package/dist/content/list/list-item.css.js.map +1 -0
  40. package/dist/content/list/list-item.d.ts +111 -0
  41. package/dist/content/list/list-item.d.ts.map +1 -0
  42. package/dist/content/list/list.css.cjs +2 -0
  43. package/dist/content/list/list.css.cjs.map +1 -0
  44. package/dist/content/list/list.css.js +6 -0
  45. package/dist/content/list/list.css.js.map +1 -0
  46. package/dist/content/list/list.d.ts +93 -0
  47. package/dist/content/list/list.d.ts.map +1 -0
  48. package/dist/custom-elements.json +6052 -5449
  49. package/dist/date-picker.cjs +2 -1
  50. package/dist/date-picker.cjs.map +1 -1
  51. package/dist/date-picker.js +2 -0
  52. package/dist/date-picker.js.map +1 -1
  53. package/dist/disclosure/tab-item/tab-item.d.ts.map +1 -1
  54. package/dist/disclosure/tab-panel/tab-panel.d.ts.map +1 -1
  55. package/dist/disclosure/tabs/tabs.d.ts.map +1 -1
  56. package/dist/drawer.cjs +1 -1
  57. package/dist/drawer.cjs.map +1 -1
  58. package/dist/drawer.js +47 -48
  59. package/dist/drawer.js.map +1 -1
  60. package/dist/file-input.cjs +111 -45
  61. package/dist/file-input.cjs.map +1 -1
  62. package/dist/file-input.js +216 -156
  63. package/dist/file-input.js.map +1 -1
  64. package/dist/headless/aria-reflection.cjs +2 -0
  65. package/dist/headless/aria-reflection.cjs.map +1 -0
  66. package/dist/headless/aria-reflection.d.ts +27 -0
  67. package/dist/headless/aria-reflection.d.ts.map +1 -0
  68. package/dist/headless/aria-reflection.js +18 -0
  69. package/dist/headless/aria-reflection.js.map +1 -0
  70. package/dist/headless/checkable.cjs +1 -1
  71. package/dist/headless/checkable.cjs.map +1 -1
  72. package/dist/headless/checkable.d.ts +2 -10
  73. package/dist/headless/checkable.d.ts.map +1 -1
  74. package/dist/headless/checkable.js +29 -29
  75. package/dist/headless/checkable.js.map +1 -1
  76. package/dist/headless/choice-field.cjs +1 -1
  77. package/dist/headless/choice-field.cjs.map +1 -1
  78. package/dist/headless/choice-field.d.ts +2 -7
  79. package/dist/headless/choice-field.d.ts.map +1 -1
  80. package/dist/headless/choice-field.js +37 -37
  81. package/dist/headless/choice-field.js.map +1 -1
  82. package/dist/headless/field-base.cjs +1 -1
  83. package/dist/headless/field-base.cjs.map +1 -1
  84. package/dist/headless/field-base.d.ts +17 -0
  85. package/dist/headless/field-base.d.ts.map +1 -1
  86. package/dist/headless/field-base.js +21 -6
  87. package/dist/headless/field-base.js.map +1 -1
  88. package/dist/headless/focus.cjs +1 -1
  89. package/dist/headless/focus.cjs.map +1 -1
  90. package/dist/headless/focus.d.ts +7 -0
  91. package/dist/headless/focus.d.ts.map +1 -1
  92. package/dist/headless/focus.js +6 -6
  93. package/dist/headless/focus.js.map +1 -1
  94. package/dist/headless/index.d.ts +4 -2
  95. package/dist/headless/index.d.ts.map +1 -1
  96. package/dist/headless/nav.cjs +1 -1
  97. package/dist/headless/nav.cjs.map +1 -1
  98. package/dist/headless/nav.d.ts +12 -0
  99. package/dist/headless/nav.d.ts.map +1 -1
  100. package/dist/headless/nav.js +68 -50
  101. package/dist/headless/nav.js.map +1 -1
  102. package/dist/headless/overlay.cjs +1 -1
  103. package/dist/headless/overlay.cjs.map +1 -1
  104. package/dist/headless/overlay.d.ts +2 -0
  105. package/dist/headless/overlay.d.ts.map +1 -1
  106. package/dist/headless/overlay.js +8 -5
  107. package/dist/headless/overlay.js.map +1 -1
  108. package/dist/headless/ref-callback.cjs +2 -0
  109. package/dist/headless/ref-callback.cjs.map +1 -0
  110. package/dist/headless/ref-callback.d.ts +26 -0
  111. package/dist/headless/ref-callback.d.ts.map +1 -0
  112. package/dist/headless/ref-callback.js +13 -0
  113. package/dist/headless/ref-callback.js.map +1 -0
  114. package/dist/headless/swipe.cjs +1 -1
  115. package/dist/headless/swipe.cjs.map +1 -1
  116. package/dist/headless/swipe.d.ts +2 -0
  117. package/dist/headless/swipe.d.ts.map +1 -1
  118. package/dist/headless/swipe.js +26 -21
  119. package/dist/headless/swipe.js.map +1 -1
  120. package/dist/headless.cjs +1 -1
  121. package/dist/headless.js +28 -26
  122. package/dist/icon.cjs +1 -1
  123. package/dist/icon.cjs.map +1 -1
  124. package/dist/icon.js +36 -38
  125. package/dist/icon.js.map +1 -1
  126. package/dist/index.cjs +1 -1
  127. package/dist/index.js +63 -61
  128. package/dist/input.cjs +29 -26
  129. package/dist/input.cjs.map +1 -1
  130. package/dist/input.js +91 -79
  131. package/dist/input.js.map +1 -1
  132. package/dist/inputs/button/button.css.cjs +1 -1
  133. package/dist/inputs/button/button.css.cjs.map +1 -1
  134. package/dist/inputs/button/button.css.js +1 -1
  135. package/dist/inputs/button/button.css.js.map +1 -1
  136. package/dist/inputs/button/button.d.ts +8 -0
  137. package/dist/inputs/button/button.d.ts.map +1 -1
  138. package/dist/inputs/checkbox/checkbox.css.cjs +1 -1
  139. package/dist/inputs/checkbox/checkbox.css.cjs.map +1 -1
  140. package/dist/inputs/checkbox/checkbox.css.js +1 -1
  141. package/dist/inputs/checkbox/checkbox.css.js.map +1 -1
  142. package/dist/inputs/combobox/combobox.d.ts +1 -0
  143. package/dist/inputs/combobox/combobox.d.ts.map +1 -1
  144. package/dist/inputs/date-picker/date-picker.css.cjs +1 -1
  145. package/dist/inputs/date-picker/date-picker.css.cjs.map +1 -1
  146. package/dist/inputs/date-picker/date-picker.css.js +1 -1
  147. package/dist/inputs/date-picker/date-picker.css.js.map +1 -1
  148. package/dist/inputs/date-picker/date-picker.d.ts +6 -0
  149. package/dist/inputs/date-picker/date-picker.d.ts.map +1 -1
  150. package/dist/inputs/file-input/file-input-upload.cjs +2 -0
  151. package/dist/inputs/file-input/file-input-upload.cjs.map +1 -0
  152. package/dist/inputs/file-input/file-input-upload.d.ts +76 -0
  153. package/dist/inputs/file-input/file-input-upload.d.ts.map +1 -0
  154. package/dist/inputs/file-input/file-input-upload.js +145 -0
  155. package/dist/inputs/file-input/file-input-upload.js.map +1 -0
  156. package/dist/inputs/file-input/file-input.css.cjs +1 -1
  157. package/dist/inputs/file-input/file-input.css.cjs.map +1 -1
  158. package/dist/inputs/file-input/file-input.css.js +1 -1
  159. package/dist/inputs/file-input/file-input.css.js.map +1 -1
  160. package/dist/inputs/file-input/file-input.d.ts +54 -1
  161. package/dist/inputs/file-input/file-input.d.ts.map +1 -1
  162. package/dist/inputs/input/input.css.cjs +1 -1
  163. package/dist/inputs/input/input.css.cjs.map +1 -1
  164. package/dist/inputs/input/input.css.js +1 -1
  165. package/dist/inputs/input/input.css.js.map +1 -1
  166. package/dist/inputs/input/input.d.ts +13 -0
  167. package/dist/inputs/input/input.d.ts.map +1 -1
  168. package/dist/inputs/message-composer/message-composer.css.cjs +1 -1
  169. package/dist/inputs/message-composer/message-composer.css.cjs.map +1 -1
  170. package/dist/inputs/message-composer/message-composer.css.js +1 -1
  171. package/dist/inputs/message-composer/message-composer.css.js.map +1 -1
  172. package/dist/inputs/message-composer/message-composer.d.ts +7 -0
  173. package/dist/inputs/message-composer/message-composer.d.ts.map +1 -1
  174. package/dist/inputs/number-input/number-input.css.cjs +1 -1
  175. package/dist/inputs/number-input/number-input.css.cjs.map +1 -1
  176. package/dist/inputs/number-input/number-input.css.js +1 -1
  177. package/dist/inputs/number-input/number-input.css.js.map +1 -1
  178. package/dist/inputs/number-input/number-input.d.ts +15 -0
  179. package/dist/inputs/number-input/number-input.d.ts.map +1 -1
  180. package/dist/inputs/otp-input/otp-input.css.cjs +1 -1
  181. package/dist/inputs/otp-input/otp-input.css.cjs.map +1 -1
  182. package/dist/inputs/otp-input/otp-input.css.js +1 -1
  183. package/dist/inputs/otp-input/otp-input.css.js.map +1 -1
  184. package/dist/inputs/radio/radio.d.ts.map +1 -1
  185. package/dist/inputs/select/select.d.ts +1 -0
  186. package/dist/inputs/select/select.d.ts.map +1 -1
  187. package/dist/inputs/shared/index.d.ts +1 -1
  188. package/dist/inputs/shared/index.d.ts.map +1 -1
  189. package/dist/inputs/shared/templates.cjs +26 -3
  190. package/dist/inputs/shared/templates.cjs.map +1 -1
  191. package/dist/inputs/shared/templates.d.ts +20 -0
  192. package/dist/inputs/shared/templates.d.ts.map +1 -1
  193. package/dist/inputs/shared/templates.js +29 -5
  194. package/dist/inputs/shared/templates.js.map +1 -1
  195. package/dist/inputs/switch/switch.css.cjs +1 -1
  196. package/dist/inputs/switch/switch.css.cjs.map +1 -1
  197. package/dist/inputs/switch/switch.css.js +1 -1
  198. package/dist/inputs/switch/switch.css.js.map +1 -1
  199. package/dist/inputs/switch/switch.d.ts.map +1 -1
  200. package/dist/inputs/textarea/textarea.css.cjs +1 -1
  201. package/dist/inputs/textarea/textarea.css.cjs.map +1 -1
  202. package/dist/inputs/textarea/textarea.css.js +1 -1
  203. package/dist/inputs/textarea/textarea.css.js.map +1 -1
  204. package/dist/inputs/textarea/textarea.d.ts +14 -0
  205. package/dist/inputs/textarea/textarea.d.ts.map +1 -1
  206. package/dist/inputs/time-picker/time-picker.css.cjs +1 -1
  207. package/dist/inputs/time-picker/time-picker.css.cjs.map +1 -1
  208. package/dist/inputs/time-picker/time-picker.css.js +1 -1
  209. package/dist/inputs/time-picker/time-picker.css.js.map +1 -1
  210. package/dist/inputs/time-picker/time-picker.d.ts +6 -0
  211. package/dist/inputs/time-picker/time-picker.d.ts.map +1 -1
  212. package/dist/list-item.cjs +22 -0
  213. package/dist/list-item.cjs.map +1 -0
  214. package/dist/list-item.js +91 -0
  215. package/dist/list-item.js.map +1 -0
  216. package/dist/list.cjs +2 -0
  217. package/dist/list.cjs.map +1 -0
  218. package/dist/list.js +64 -0
  219. package/dist/list.js.map +1 -0
  220. package/dist/menu.cjs +4 -4
  221. package/dist/menu.cjs.map +1 -1
  222. package/dist/menu.js +60 -59
  223. package/dist/menu.js.map +1 -1
  224. package/dist/message-composer.cjs +14 -20
  225. package/dist/message-composer.cjs.map +1 -1
  226. package/dist/message-composer.js +63 -64
  227. package/dist/message-composer.js.map +1 -1
  228. package/dist/number-input.cjs +5 -3
  229. package/dist/number-input.cjs.map +1 -1
  230. package/dist/number-input.js +8 -4
  231. package/dist/number-input.js.map +1 -1
  232. package/dist/otp-input.cjs +9 -9
  233. package/dist/otp-input.cjs.map +1 -1
  234. package/dist/otp-input.js +70 -62
  235. package/dist/otp-input.js.map +1 -1
  236. package/dist/overlay/menu/menu-item.css.cjs +1 -1
  237. package/dist/overlay/menu/menu-item.css.cjs.map +1 -1
  238. package/dist/overlay/menu/menu-item.css.js +1 -1
  239. package/dist/overlay/menu/menu-item.css.js.map +1 -1
  240. package/dist/overlay/menu/menu-separator.css.cjs +1 -1
  241. package/dist/overlay/menu/menu-separator.css.cjs.map +1 -1
  242. package/dist/overlay/menu/menu-separator.css.js +1 -1
  243. package/dist/overlay/menu/menu-separator.css.js.map +1 -1
  244. package/dist/overlay/menu/menu.css.cjs +1 -1
  245. package/dist/overlay/menu/menu.css.cjs.map +1 -1
  246. package/dist/overlay/menu/menu.css.js +1 -1
  247. package/dist/overlay/menu/menu.css.js.map +1 -1
  248. package/dist/overlay/shared/use-dialog.cjs +1 -1
  249. package/dist/overlay/shared/use-dialog.cjs.map +1 -1
  250. package/dist/overlay/shared/use-dialog.d.ts.map +1 -1
  251. package/dist/overlay/shared/use-dialog.js +7 -6
  252. package/dist/overlay/shared/use-dialog.js.map +1 -1
  253. package/dist/radio-group.cjs +3 -3
  254. package/dist/radio-group.cjs.map +1 -1
  255. package/dist/radio-group.js +64 -70
  256. package/dist/radio-group.js.map +1 -1
  257. package/dist/radio.cjs +4 -4
  258. package/dist/radio.cjs.map +1 -1
  259. package/dist/radio.js +58 -58
  260. package/dist/radio.js.map +1 -1
  261. package/dist/rating.cjs.map +1 -1
  262. package/dist/rating.js.map +1 -1
  263. package/dist/refine.cjs +426 -233
  264. package/dist/refine.cjs.map +1 -1
  265. package/dist/refine.iife.js +431 -238
  266. package/dist/refine.iife.js.map +1 -1
  267. package/dist/refine.js +452 -259
  268. package/dist/refine.js.map +1 -1
  269. package/dist/select.cjs +21 -21
  270. package/dist/select.cjs.map +1 -1
  271. package/dist/select.js +135 -127
  272. package/dist/select.js.map +1 -1
  273. package/dist/shared/size-presets.cjs +1 -1
  274. package/dist/shared/size-presets.cjs.map +1 -1
  275. package/dist/shared/size-presets.d.ts +74 -0
  276. package/dist/shared/size-presets.d.ts.map +1 -1
  277. package/dist/shared/size-presets.js +48 -1
  278. package/dist/shared/size-presets.js.map +1 -1
  279. package/dist/shared/types.d.ts +5 -0
  280. package/dist/shared/types.d.ts.map +1 -1
  281. package/dist/styles/index.cjs +1 -1
  282. package/dist/styles/index.cjs.map +1 -1
  283. package/dist/styles/index.d.ts +1 -0
  284. package/dist/styles/index.d.ts.map +1 -1
  285. package/dist/styles/index.js +5 -4
  286. package/dist/styles/index.js.map +1 -1
  287. package/dist/styles/mixins/field-variants.css.cjs +85 -0
  288. package/dist/styles/mixins/field-variants.css.cjs.map +1 -0
  289. package/dist/styles/mixins/field-variants.css.d.ts +66 -0
  290. package/dist/styles/mixins/field-variants.css.d.ts.map +1 -0
  291. package/dist/styles/mixins/field-variants.css.js +90 -0
  292. package/dist/styles/mixins/field-variants.css.js.map +1 -0
  293. package/dist/switch.cjs +3 -3
  294. package/dist/switch.cjs.map +1 -1
  295. package/dist/switch.js +38 -36
  296. package/dist/switch.js.map +1 -1
  297. package/dist/tab-item.cjs +8 -8
  298. package/dist/tab-item.cjs.map +1 -1
  299. package/dist/tab-item.js +38 -28
  300. package/dist/tab-item.js.map +1 -1
  301. package/dist/tab-panel.cjs +7 -7
  302. package/dist/tab-panel.cjs.map +1 -1
  303. package/dist/tab-panel.js +34 -26
  304. package/dist/tab-panel.js.map +1 -1
  305. package/dist/tabs.cjs +4 -4
  306. package/dist/tabs.cjs.map +1 -1
  307. package/dist/tabs.js +64 -60
  308. package/dist/tabs.js.map +1 -1
  309. package/dist/textarea.cjs +15 -18
  310. package/dist/textarea.cjs.map +1 -1
  311. package/dist/textarea.js +83 -74
  312. package/dist/textarea.js.map +1 -1
  313. package/dist/time-picker.cjs +34 -31
  314. package/dist/time-picker.cjs.map +1 -1
  315. package/dist/time-picker.js +121 -109
  316. package/dist/time-picker.js.map +1 -1
  317. package/package.json +21 -9
  318. package/dist/headless/dialog-focus.cjs +0 -2
  319. package/dist/headless/dialog-focus.cjs.map +0 -1
  320. package/dist/headless/dialog-focus.d.ts +0 -40
  321. package/dist/headless/dialog-focus.d.ts.map +0 -1
  322. package/dist/headless/dialog-focus.js +0 -22
  323. package/dist/headless/dialog-focus.js.map +0 -1
  324. package/dist/headless/focus-trap.cjs +0 -2
  325. package/dist/headless/focus-trap.cjs.map +0 -1
  326. package/dist/headless/focus-trap.d.ts +0 -45
  327. package/dist/headless/focus-trap.d.ts.map +0 -1
  328. package/dist/headless/focus-trap.js +0 -51
  329. package/dist/headless/focus-trap.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"choice-field.js","names":[],"sources":["../../src/headless/choice-field.ts"],"sourcesContent":["import { computed, type Readable, signal, watch } from '@vielzeug/ripple';\n\nimport { createField, type FieldHandle, type FieldOptions } from './field-base';\n\n// ── Choice change event payload ──────────────────────────────────────────────────────────────\n\nexport type ChoiceChangeDetail = {\n labels: string[];\n originalEvent?: Event;\n values: string[];\n};\n\n// ── Choice field ──────────────────────────────────────────────────────────────────────\n\nexport type ChoiceFieldOptions = FieldOptions & {\n multiple?: Readable<boolean | undefined>;\n /** Marks an empty selection invalid — feeds `validity`/`validationMessage` (see below). */\n required?: Readable<boolean | undefined>;\n /** Message for the empty-selection+required case. Defaults to `'Please make a selection.'`. */\n requiredMessage?: Readable<string | undefined>;\n /** `AbortSignal` from the component lifecycle. All internal subscriptions are disposed on abort. */\n signal: AbortSignal;\n /**\n * Current selection value(s). Accepts a comma-separated string\n * (legacy/single-select form serialisation) or a `string[]` array.\n * The field normalises both formats internally via `parseChoiceValues`.\n */\n value: Readable<string | string[] | undefined>;\n};\n\nexport type ChoiceFieldHandle = FieldHandle & {\n clear: () => void;\n formValue: Readable<string>;\n removeValue: (value: string) => void;\n /**\n * Restores the selection to its native \"default\": whatever `value` currently holds, if the\n * user has never changed the selection yet, or the value snapshotted at creation, if they\n * have. Two states, not one — see `createCheckable`'s `reset()` for the identical situation\n * with `checked`, and why: `ore-radio-group`/`ore-checkbox-group` reflect the current\n * selection back onto the host's `value`/`values` attribute for `:host([value])` styling\n * (there's no native form control here to carry it), so `options.value` changes on every\n * selection and can't double as \"the default to revert to\" post-interaction.\n */\n reset: () => void;\n selectedValue: Readable<string | undefined>;\n selectedValues: Readable<string[]>;\n selectValue: (value: string) => void;\n setValues: (values: string[]) => void;\n /**\n * Toggles `value` in the selection.\n * - **Multiple mode**: adds if absent, removes if present.\n * - **Single mode**: always selects `value` (acts like `selectValue` — cannot\n * deselect; use `clear()` to reset a single-select field).\n */\n toggleValue: (value: string) => void;\n /** Reactive validation message paired with `validity`. Empty string when valid. */\n validationMessage: Readable<string>;\n /**\n * Reactive `ValidityStateFlags` — `{ valueMissing: true }` while `required` and no option is\n * selected, `null` (valid) otherwise. Pass straight to `useField({ validity: choice.validity })`.\n */\n validity: Readable<ValidityStateFlags | null>;\n};\n\nconst parseChoiceValues = (raw: string | string[] | undefined): string[] => {\n if (!raw) return [];\n\n if (Array.isArray(raw)) return raw.filter(Boolean);\n\n return raw\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean);\n};\n\nexport const createChoiceField = (options: ChoiceFieldOptions): ChoiceFieldHandle => {\n const selectedValues = signal<string[]>([]);\n const isMultiple = computed(() => Boolean(options.multiple?.value));\n const formValue = computed(() =>\n isMultiple.value ? selectedValues.value.join(',') : (selectedValues.value[0] ?? ''),\n );\n\n const normalizeValues = (values: string[]): string[] => {\n const normalized = isMultiple.value ? values : values.slice(0, 1);\n\n return [...new Set(normalized.filter(Boolean))];\n };\n\n // Set the moment the user first changes the selection — see `reset()`'s doc comment for why\n // this matters: before that point, `options.value` is still a reliable \"current default\" to\n // resync from (e.g. an async-loaded value arriving after mount); after it, `options.value` is\n // contaminated by the selection-driven attribute reflection (on the components where that\n // applies) and the frozen snapshot is the only value that still represents \"the default\".\n let dirty = false;\n\n // The actual state setter, shared by every mutation path. Deliberately *not* where `dirty`\n // gets set — `syncFromProp` (below) calls this directly to stay exempt from it, since mirroring\n // an external prop change is never \"the user changed the selection\".\n const applyValues = (values: string[]): void => {\n selectedValues.value = normalizeValues(values);\n };\n\n const setValues = (values: string[]): void => {\n dirty = true;\n applyValues(values);\n };\n const clear = (): void => setValues([]);\n const removeValue = (value: string): void => {\n dirty = true;\n selectedValues.value = selectedValues.value.filter((v) => v !== value);\n };\n\n const selectValue = (value: string): void => {\n if (isMultiple.value) {\n if (!selectedValues.value.includes(value)) setValues([...selectedValues.value, value]);\n\n return;\n }\n\n setValues([value]);\n };\n\n const toggleValue = (value: string): void => {\n if (isMultiple.value) {\n if (selectedValues.value.includes(value)) removeValue(value);\n else selectValue(value);\n } else {\n selectValue(value);\n }\n };\n\n const syncFromProp = (nextValue: string | string[] | undefined): void => {\n applyValues(parseChoiceValues(nextValue));\n };\n\n const field = createField(options);\n\n const validity = computed<ValidityStateFlags | null>(() =>\n options.required?.value && selectedValues.value.length === 0 ? { valueMissing: true } : null,\n );\n const validationMessage = computed(() =>\n validity.value ? options.requiredMessage?.value || 'Please make a selection.' : '',\n );\n\n const valueSub = watch(options.value, syncFromProp, { immediate: true });\n\n options.signal.addEventListener('abort', () => valueSub.dispose(), { once: true });\n\n if (options.multiple) {\n const multipleSub = watch(options.multiple, () => syncFromProp(options.value.value));\n\n options.signal.addEventListener('abort', () => multipleSub.dispose(), { once: true });\n }\n\n const initialValues = selectedValues.value;\n const reset = (): void => {\n applyValues(dirty ? initialValues : parseChoiceValues(options.value.value));\n dirty = false;\n field.triggerValidation('change');\n };\n\n return {\n ...field,\n clear,\n formValue,\n removeValue,\n reset,\n selectedValue: computed(() => selectedValues.value[0]),\n selectedValues,\n selectValue,\n setValues,\n toggleValue,\n validationMessage,\n validity,\n };\n};\n"],"mappings":";;;AAgEA,IAAM,KAAqB,MACpB,IAED,MAAM,QAAQ,CAAG,IAAU,EAAI,OAAO,OAAO,IAE1C,EACJ,MAAM,GAAG,CAAC,CACV,KAAK,MAAU,EAAM,KAAK,CAAC,CAAC,CAC5B,OAAO,OAAO,IAPA,CAAC,GAUP,KAAqB,MAAmD;CACnF,IAAM,IAAiB,EAAiB,CAAC,CAAC,GACpC,IAAa,QAAe,EAAQ,EAAQ,UAAU,KAAM,GAC5D,IAAY,QAChB,EAAW,QAAQ,EAAe,MAAM,KAAK,GAAG,IAAK,EAAe,MAAM,MAAM,EAClF,GAEM,KAAmB,MAA+B;EACtD,IAAM,IAAa,EAAW,QAAQ,IAAS,EAAO,MAAM,GAAG,CAAC;EAEhE,OAAO,CAAC,GAAG,IAAI,IAAI,EAAW,OAAO,OAAO,CAAC,CAAC;CAChD,GAOI,IAAQ,IAKN,KAAe,MAA2B;EAC9C,EAAe,QAAQ,EAAgB,CAAM;CAC/C,GAEM,KAAa,MAA2B;EAE5C,AADA,IAAQ,IACR,EAAY,CAAM;CACpB,GACM,UAAoB,EAAU,CAAC,CAAC,GAChC,KAAe,MAAwB;EAE3C,AADA,IAAQ,IACR,EAAe,QAAQ,EAAe,MAAM,QAAQ,MAAM,MAAM,CAAK;CACvE,GAEM,KAAe,MAAwB;EAC3C,IAAI,EAAW,OAAO;GACpB,AAAK,EAAe,MAAM,SAAS,CAAK,KAAG,EAAU,CAAC,GAAG,EAAe,OAAO,CAAK,CAAC;GAErF;EACF;EAEA,EAAU,CAAC,CAAK,CAAC;CACnB,GAEM,KAAe,MAAwB;EAC3C,AAAI,EAAW,SACT,EAAe,MAAM,SAAS,CAAK,IAAG,EAAY,CAAK,IACtD,EAAY,CAAK;CAI1B,GAEM,KAAgB,MAAmD;EACvE,EAAY,EAAkB,CAAS,CAAC;CAC1C,GAEM,IAAQ,EAAY,CAAO,GAE3B,IAAW,QACf,EAAQ,UAAU,SAAS,EAAe,MAAM,WAAW,IAAI,EAAE,cAAc,GAAK,IAAI,IAC1F,GACM,IAAoB,QACxB,EAAS,QAAQ,EAAQ,iBAAiB,SAAS,6BAA6B,EAClF,GAEM,IAAW,EAAM,EAAQ,OAAO,GAAc,EAAE,WAAW,GAAK,CAAC;CAIvE,IAFA,EAAQ,OAAO,iBAAiB,eAAe,EAAS,QAAQ,GAAG,EAAE,MAAM,GAAK,CAAC,GAE7E,EAAQ,UAAU;EACpB,IAAM,IAAc,EAAM,EAAQ,gBAAgB,EAAa,EAAQ,MAAM,KAAK,CAAC;EAEnF,EAAQ,OAAO,iBAAiB,eAAe,EAAY,QAAQ,GAAG,EAAE,MAAM,GAAK,CAAC;CACtF;CAEA,IAAM,IAAgB,EAAe,OAC/B,UAAoB;EAGxB,AAFA,EAAY,IAAQ,IAAgB,EAAkB,EAAQ,MAAM,KAAK,CAAC,GAC1E,IAAQ,IACR,EAAM,kBAAkB,QAAQ;CAClC;CAEA,OAAO;EACL,GAAG;EACH;EACA;EACA;EACA;EACA,eAAe,QAAe,EAAe,MAAM,EAAE;EACrD;EACA;EACA;EACA;EACA;EACA;CACF;AACF"}
1
+ {"version":3,"file":"choice-field.js","names":[],"sources":["../../src/headless/choice-field.ts"],"sourcesContent":["import { computed, type Readable, signal, watch } from '@vielzeug/ripple';\n\nimport { createDirtyTracker, createField, type FieldHandle, type FieldOptions } from './field-base';\n\n// ── Choice change event payload ──────────────────────────────────────────────────────────────\n\nexport type ChoiceChangeDetail = {\n labels: string[];\n originalEvent?: Event;\n values: string[];\n};\n\n// ── Choice field ──────────────────────────────────────────────────────────────────────\n\nexport type ChoiceFieldOptions = FieldOptions & {\n multiple?: Readable<boolean | undefined>;\n /** Marks an empty selection invalid — feeds `validity`/`validationMessage` (see below). */\n required?: Readable<boolean | undefined>;\n /** Message for the empty-selection+required case. Defaults to `'Please make a selection.'`. */\n requiredMessage?: Readable<string | undefined>;\n /** `AbortSignal` from the component lifecycle. All internal subscriptions are disposed on abort. */\n signal: AbortSignal;\n /**\n * Current selection value(s). Accepts a comma-separated string\n * (legacy/single-select form serialisation) or a `string[]` array.\n * The field normalises both formats internally via `parseChoiceValues`.\n */\n value: Readable<string | string[] | undefined>;\n};\n\nexport type ChoiceFieldHandle = FieldHandle & {\n clear: () => void;\n formValue: Readable<string>;\n removeValue: (value: string) => void;\n /**\n * Restores the selection to its native \"default\". See `DirtyTracker` for the two-state\n * rationale.\n */\n reset: () => void;\n selectedValue: Readable<string | undefined>;\n selectedValues: Readable<string[]>;\n selectValue: (value: string) => void;\n setValues: (values: string[]) => void;\n /**\n * Toggles `value` in the selection.\n * - **Multiple mode**: adds if absent, removes if present.\n * - **Single mode**: always selects `value` (acts like `selectValue` — cannot\n * deselect; use `clear()` to reset a single-select field).\n */\n toggleValue: (value: string) => void;\n /** Reactive validation message paired with `validity`. Empty string when valid. */\n validationMessage: Readable<string>;\n /**\n * Reactive `ValidityStateFlags` — `{ valueMissing: true }` while `required` and no option is\n * selected, `null` (valid) otherwise. Pass straight to `useField({ validity: choice.validity })`.\n */\n validity: Readable<ValidityStateFlags | null>;\n};\n\nconst parseChoiceValues = (raw: string | string[] | undefined): string[] => {\n if (!raw) return [];\n\n if (Array.isArray(raw)) return raw.filter(Boolean);\n\n return raw\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean);\n};\n\nexport const createChoiceField = (options: ChoiceFieldOptions): ChoiceFieldHandle => {\n const selectedValues = signal<string[]>([]);\n const isMultiple = computed(() => Boolean(options.multiple?.value));\n const formValue = computed(() =>\n isMultiple.value ? selectedValues.value.join(',') : (selectedValues.value[0] ?? ''),\n );\n\n const normalizeValues = (values: string[]): string[] => {\n const normalized = isMultiple.value ? values : values.slice(0, 1);\n\n return [...new Set(normalized.filter(Boolean))];\n };\n\n const dirtyTracker = createDirtyTracker();\n\n // The actual state setter, shared by every mutation path. Deliberately *not* where the dirty\n // tracker gets marked — `syncFromProp` (below) calls this directly to stay exempt from it,\n // since mirroring an external prop change is never \"the user changed the selection\".\n const applyValues = (values: string[]): void => {\n selectedValues.value = normalizeValues(values);\n };\n\n const setValues = (values: string[]): void => {\n dirtyTracker.markDirty();\n applyValues(values);\n };\n const clear = (): void => setValues([]);\n const removeValue = (value: string): void => {\n dirtyTracker.markDirty();\n selectedValues.value = selectedValues.value.filter((v) => v !== value);\n };\n\n const selectValue = (value: string): void => {\n if (isMultiple.value) {\n if (!selectedValues.value.includes(value)) setValues([...selectedValues.value, value]);\n\n return;\n }\n\n setValues([value]);\n };\n\n const toggleValue = (value: string): void => {\n if (isMultiple.value) {\n if (selectedValues.value.includes(value)) removeValue(value);\n else selectValue(value);\n } else {\n selectValue(value);\n }\n };\n\n const syncFromProp = (nextValue: string | string[] | undefined): void => {\n applyValues(parseChoiceValues(nextValue));\n };\n\n const field = createField(options);\n\n const validity = computed<ValidityStateFlags | null>(() =>\n options.required?.value && selectedValues.value.length === 0 ? { valueMissing: true } : null,\n );\n const validationMessage = computed(() =>\n validity.value ? options.requiredMessage?.value || 'Please make a selection.' : '',\n );\n\n const valueSub = watch(options.value, syncFromProp, { immediate: true });\n\n options.signal.addEventListener('abort', () => valueSub.dispose(), { once: true });\n\n if (options.multiple) {\n const multipleSub = watch(options.multiple, () => syncFromProp(options.value.value));\n\n options.signal.addEventListener('abort', () => multipleSub.dispose(), { once: true });\n }\n\n const initialValues = selectedValues.value;\n const reset = (): void => {\n applyValues(dirtyTracker.isDirty ? initialValues : parseChoiceValues(options.value.value));\n dirtyTracker.clear();\n field.triggerValidation('change');\n };\n\n return {\n ...field,\n clear,\n formValue,\n removeValue,\n reset,\n selectedValue: computed(() => selectedValues.value[0]),\n selectedValues,\n selectValue,\n setValues,\n toggleValue,\n validationMessage,\n validity,\n };\n};\n"],"mappings":";;;AA2DA,IAAM,KAAqB,MACpB,IAED,MAAM,QAAQ,CAAG,IAAU,EAAI,OAAO,OAAO,IAE1C,EACJ,MAAM,GAAG,CAAC,CACV,KAAK,MAAU,EAAM,KAAK,CAAC,CAAC,CAC5B,OAAO,OAAO,IAPA,CAAC,GAUP,KAAqB,MAAmD;CACnF,IAAM,IAAiB,EAAiB,CAAC,CAAC,GACpC,IAAa,QAAe,EAAQ,EAAQ,UAAU,KAAM,GAC5D,IAAY,QAChB,EAAW,QAAQ,EAAe,MAAM,KAAK,GAAG,IAAK,EAAe,MAAM,MAAM,EAClF,GAEM,KAAmB,MAA+B;EACtD,IAAM,IAAa,EAAW,QAAQ,IAAS,EAAO,MAAM,GAAG,CAAC;EAEhE,OAAO,CAAC,GAAG,IAAI,IAAI,EAAW,OAAO,OAAO,CAAC,CAAC;CAChD,GAEM,IAAe,EAAmB,GAKlC,KAAe,MAA2B;EAC9C,EAAe,QAAQ,EAAgB,CAAM;CAC/C,GAEM,KAAa,MAA2B;EAE5C,AADA,EAAa,UAAU,GACvB,EAAY,CAAM;CACpB,GACM,UAAoB,EAAU,CAAC,CAAC,GAChC,KAAe,MAAwB;EAE3C,AADA,EAAa,UAAU,GACvB,EAAe,QAAQ,EAAe,MAAM,QAAQ,MAAM,MAAM,CAAK;CACvE,GAEM,KAAe,MAAwB;EAC3C,IAAI,EAAW,OAAO;GACpB,AAAK,EAAe,MAAM,SAAS,CAAK,KAAG,EAAU,CAAC,GAAG,EAAe,OAAO,CAAK,CAAC;GAErF;EACF;EAEA,EAAU,CAAC,CAAK,CAAC;CACnB,GAEM,KAAe,MAAwB;EAC3C,AAAI,EAAW,SACT,EAAe,MAAM,SAAS,CAAK,IAAG,EAAY,CAAK,IACtD,EAAY,CAAK;CAI1B,GAEM,KAAgB,MAAmD;EACvE,EAAY,EAAkB,CAAS,CAAC;CAC1C,GAEM,IAAQ,EAAY,CAAO,GAE3B,IAAW,QACf,EAAQ,UAAU,SAAS,EAAe,MAAM,WAAW,IAAI,EAAE,cAAc,GAAK,IAAI,IAC1F,GACM,IAAoB,QACxB,EAAS,QAAQ,EAAQ,iBAAiB,SAAS,6BAA6B,EAClF,GAEM,IAAW,EAAM,EAAQ,OAAO,GAAc,EAAE,WAAW,GAAK,CAAC;CAIvE,IAFA,EAAQ,OAAO,iBAAiB,eAAe,EAAS,QAAQ,GAAG,EAAE,MAAM,GAAK,CAAC,GAE7E,EAAQ,UAAU;EACpB,IAAM,IAAc,EAAM,EAAQ,gBAAgB,EAAa,EAAQ,MAAM,KAAK,CAAC;EAEnF,EAAQ,OAAO,iBAAiB,eAAe,EAAY,QAAQ,GAAG,EAAE,MAAM,GAAK,CAAC;CACtF;CAEA,IAAM,IAAgB,EAAe,OAC/B,UAAoB;EAGxB,AAFA,EAAY,EAAa,UAAU,IAAgB,EAAkB,EAAQ,MAAM,KAAK,CAAC,GACzF,EAAa,MAAM,GACnB,EAAM,kBAAkB,QAAQ;CAClC;CAEA,OAAO;EACL,GAAG;EACH;EACA;EACA;EACA;EACA,eAAe,QAAe,EAAe,MAAM,EAAE;EACrD;EACA;EACA;EACA;EACA;EACA;CACF;AACF"}
@@ -1,2 +1,2 @@
1
- const e=require("../_dev.cjs");let t=require("@vielzeug/ore"),n=require("@vielzeug/ripple");var r=e=>(0,n.computed)(()=>({errorText:e.error?.value??``,helperText:e.helper?.value??``})),i=e=>(0,n.computed)(()=>{let t=e.value?.value??``,n=e.maxLength?.value,r=Number(n),i=Number.isFinite(r)&&r>0?r:null,a=i!==null,o=a?`${t.length} / ${i}`:``,s=a?t.length/i:0;return{counterAtLimit:a?s>=1:!1,counterNearLimit:a?s>=.9&&s<1:!1,counterText:o}}),a=(e,t=`counter`)=>e?e.counterAtLimit?`${t} at-limit`:e.counterNearLimit?`${t} near-limit`:t:t,o=i=>{let a=(0,n.computed)(()=>!!i.disabled?.value),o=(0,t.createStableId)(`helper`),s=(0,t.createStableId)(`error`),c=r({error:i.error,helper:i.helper}),l=(0,n.computed)(()=>c.value.errorText||c.value.helperText?o:null),u=(0,n.computed)(()=>c.value.errorText?s:null),d=(0,n.computed)(()=>c.value.errorText?`true`:null),f=null;return{ariaDescribedBy:l,ariaErrorMessage:u,ariaInvalid:d,assistiveId:o,attachFormField:e=>{f=e},disabled:a,errorId:s,errorText:(0,n.computed)(()=>c.value.errorText),helperText:(0,n.computed)(()=>c.value.helperText),triggerValidation:t=>{if(i.validateOn?.value===t){if(!f){e.warn(`triggerValidation() called before attachFormField() — validation will not run. See createTextField()/createChoiceField()/createCheckable()'s attachFormField doc comment.`);return}f.reportValidity()}}}},s=e=>{let r=e.id??(0,t.createStableId)(e.prefix??`field`),i=(0,t.createStableId)(`label`),a=e.label??(0,n.computed)(()=>void 0),o=e.hasLabel??(0,n.computed)(()=>!!a.value);return{ariaLabelledBy:(0,n.computed)(()=>o.value?i:null),fieldId:r,labelId:i,labelVisible:o}},c=e=>({...o(e),...s(e)});exports.counterClassName=a,exports.createAssistiveState=o,exports.createCounterState=i,exports.createErrorHelperState=r,exports.createField=c,exports.createLabelState=s;
1
+ const e=require("../_dev.cjs");let t=require("@vielzeug/ore"),n=require("@vielzeug/ripple");var r=e=>(0,n.computed)(()=>({errorText:e.error?.value??``,helperText:e.helper?.value??``})),i=e=>(0,n.computed)(()=>{let t=e.value?.value??``,n=e.maxLength?.value,r=Number(n),i=Number.isFinite(r)&&r>0?r:null,a=i!==null,o=a?`${t.length} / ${i}`:``,s=a?t.length/i:0;return{counterAtLimit:a?s>=1:!1,counterNearLimit:a?s>=.9&&s<1:!1,counterText:o}}),a=(e,t=`counter`)=>e?e.counterAtLimit?`${t} at-limit`:e.counterNearLimit?`${t} near-limit`:t:t;function o(){let e=!1;return{clear:()=>{e=!1},get isDirty(){return e},markDirty:()=>{e=!0}}}var s=i=>{let a=(0,n.computed)(()=>!!i.disabled?.value),o=(0,t.createStableId)(`helper`),s=(0,t.createStableId)(`error`),c=r({error:i.error,helper:i.helper}),l=(0,n.computed)(()=>c.value.errorText||c.value.helperText?o:null),u=(0,n.computed)(()=>c.value.errorText?s:null),d=(0,n.computed)(()=>c.value.errorText?`true`:null),f=null;return{ariaDescribedBy:l,ariaErrorMessage:u,ariaInvalid:d,assistiveId:o,attachFormField:e=>{f=e},disabled:a,errorId:s,errorText:(0,n.computed)(()=>c.value.errorText),helperText:(0,n.computed)(()=>c.value.helperText),triggerValidation:t=>{if(i.validateOn?.value===t){if(!f){e.warn(`triggerValidation() called before attachFormField() — validation will not run. See createTextField()/createChoiceField()/createCheckable()'s attachFormField doc comment.`);return}f.reportValidity()}}}},c=e=>{let r=e.id??(0,t.createStableId)(e.prefix??`field`),i=(0,t.createStableId)(`label`),a=e.label??(0,n.computed)(()=>void 0),o=e.hasLabel??(0,n.computed)(()=>!!a.value);return{ariaLabelledBy:(0,n.computed)(()=>o.value?i:null),fieldId:r,labelId:i,labelVisible:o}},l=e=>({...s(e),...c(e)});exports.counterClassName=a,exports.createAssistiveState=s,exports.createCounterState=i,exports.createDirtyTracker=o,exports.createErrorHelperState=r,exports.createField=l,exports.createLabelState=c;
2
2
  //# sourceMappingURL=field-base.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"field-base.cjs","names":[],"sources":["../../src/headless/field-base.ts"],"sourcesContent":["import { createStableId } from '@vielzeug/ore';\nimport { computed, type Readable } from '@vielzeug/ripple';\n\nimport { warn } from '../_dev';\n\n// ── Validation / context types ────────────────────────────────────────────────\n\nexport type ValidationTrigger = 'blur' | 'change' | 'input' | 'submit';\nexport type ControlValidationMode = ValidationTrigger | undefined;\n\n// ── Error/helper assistive state ──────────────────────────────────────────────\n\nexport type ErrorHelperState = {\n errorText: string;\n helperText: string;\n};\n\nexport type ErrorHelperOptions = {\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n};\n\nexport const createErrorHelperState = (options: ErrorHelperOptions): Readable<ErrorHelperState> =>\n computed(() => ({\n errorText: options.error?.value ?? '',\n helperText: options.helper?.value ?? '',\n }));\n\n// ── Counter state (opt-in) ─────────────────────────────────────────────────────\n\n/** Counter state for text fields with `maxLength`. */\nexport type CounterState = {\n counterAtLimit: boolean;\n counterNearLimit: boolean;\n counterText: string;\n};\n\nexport type CounterOptions = {\n maxLength?: Readable<number | undefined>;\n value: Readable<string | undefined>;\n};\n\n/**\n * Creates a reactive counter state signal for text fields with `maxLength`.\n * Only call this when `maxLength` may be set — the state is genuinely opt-in.\n *\n * @example\n * ```ts\n * const counter = createCounterState({ value: tf.value, maxLength: props.maxlength });\n * ```\n */\nexport const createCounterState = (options: CounterOptions): Readable<CounterState> =>\n computed<CounterState>(() => {\n const value = options.value?.value ?? '';\n const maxLength = options.maxLength?.value;\n const parsedMaxLength = Number(maxLength);\n const validMaxLength = Number.isFinite(parsedMaxLength) && parsedMaxLength > 0 ? parsedMaxLength : null;\n const hasCounter = validMaxLength !== null;\n const counterText = hasCounter ? `${value.length} / ${validMaxLength}` : '';\n const ratio = hasCounter ? value.length / validMaxLength : 0;\n\n return {\n counterAtLimit: hasCounter ? ratio >= 1 : false,\n counterNearLimit: hasCounter ? ratio >= 0.9 && ratio < 1 : false,\n counterText,\n };\n });\n\n/**\n * The single implementation of \"which modifier class does this counter state map to\" —\n * every field with a character counter (`ore-input`, `ore-textarea`, `ore-message-composer`)\n * renders the identical near-limit/at-limit styling, so this is the one place that decides it.\n *\n * @example\n * ```ts\n * html`<span class=\"${() => counterClassName(counter?.value)}\">...</span>`\n * ```\n */\nexport const counterClassName = (counter: CounterState | undefined, base = 'counter'): string => {\n if (!counter) return base;\n\n if (counter.counterAtLimit) return `${base} at-limit`;\n\n if (counter.counterNearLimit) return `${base} near-limit`;\n\n return base;\n};\n\n// ── Label placement ───────────────────────────────────────────────────────────\n\nexport type LabelPlacement = 'inset' | 'outside' | undefined;\n\n// ── Assistive state (error/helper text, describedby/errormessage/invalid) ─────\n\n/**\n * A field's error/helper/disabled/validation-trigger state — everything an input\n * needs *except* a visible `<label>`. Split out from label state (below) because\n * not every field has one: `ore-message-composer` names itself via `aria-label`\n * (a chat composer's accessible name conventionally comes from context/placeholder,\n * not a floating `<label>`), so it composes this directly instead of pulling in\n * label plumbing it can't use. `createField()` composes both for fields that do\n * render a `<label>`.\n */\nexport type AssistiveStateHandle = {\n /** `aria-describedby` value. Non-null when helper or error text is present. */\n ariaDescribedBy: Readable<string | null>;\n /** `aria-errormessage` value. Non-null when error text is present. */\n ariaErrorMessage: Readable<string | null>;\n /** `aria-invalid` value. `'true'` when error text is present, otherwise `null`. */\n ariaInvalid: Readable<'true' | null>;\n /**\n * The stable `id` used for `aria-describedby` on the input. Points at the\n * assistive-text region (covers both helper text and error text per WAI-ARIA).\n */\n assistiveId: string;\n /**\n * Registers the real form field handle (the return value of `useField()`) so that\n * `triggerValidation()` can call its `reportValidity()`. See `TextFieldHandle.attachFormField`\n * for why this is a post-hoc call instead of a constructor option — the same forward\n * reference applies here for choice fields.\n */\n attachFormField: (formField: { reportValidity(): void }) => void;\n disabled: Readable<boolean>;\n /** Stable `id` for the inline error message element (`aria-errormessage`). */\n errorId: string;\n /** Reactive error text. Empty string when no error is set. */\n errorText: Readable<string>;\n /** Reactive helper text. Empty string when no helper is set. */\n helperText: Readable<string>;\n triggerValidation: (on: Extract<ValidationTrigger, 'blur' | 'change'>) => void;\n};\n\nexport type AssistiveStateOptions = {\n disabled?: Readable<boolean | undefined>;\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n validateOn?: Readable<ControlValidationMode>;\n};\n\nexport const createAssistiveState = (options: AssistiveStateOptions): AssistiveStateHandle => {\n const disabled = computed(() => Boolean(options.disabled?.value));\n const assistiveId = createStableId('helper');\n const errorId = createStableId('error');\n const resolvedAssistive = createErrorHelperState({ error: options.error, helper: options.helper });\n\n const ariaDescribedBy = computed(() =>\n resolvedAssistive.value.errorText || resolvedAssistive.value.helperText ? assistiveId : null,\n );\n const ariaErrorMessage = computed(() => (resolvedAssistive.value.errorText ? errorId : null));\n const ariaInvalid = computed<'true' | null>(() => (resolvedAssistive.value.errorText ? 'true' : null));\n\n let formField: { reportValidity(): void } | null = null;\n const attachFormField = (nextFormField: { reportValidity(): void }): void => {\n formField = nextFormField;\n };\n\n const triggerValidation = (on: Extract<ValidationTrigger, 'blur' | 'change'>): void => {\n if (options.validateOn?.value !== on) return;\n\n if (!formField) {\n // Not user-facing — this only fires when a component author wired `validateOn` but\n // forgot the matching `attachFormField(useField(...))` call, so validation silently\n // never runs instead of erroring where the mistake actually is.\n warn(\n \"triggerValidation() called before attachFormField() — validation will not run. See createTextField()/createChoiceField()/createCheckable()'s attachFormField doc comment.\",\n );\n\n return;\n }\n\n formField.reportValidity();\n };\n\n return {\n ariaDescribedBy,\n ariaErrorMessage,\n ariaInvalid,\n assistiveId,\n attachFormField,\n disabled,\n errorId,\n errorText: computed(() => resolvedAssistive.value.errorText),\n helperText: computed(() => resolvedAssistive.value.helperText),\n triggerValidation,\n };\n};\n\n// ── Label state (visible `<label>` + `aria-labelledby`) ────────────────────────\n\nexport type LabelStateHandle = {\n /** `aria-labelledby` value. Non-null when a label is visible. */\n ariaLabelledBy: Readable<string | null>;\n /** Stable `id` for the underlying form control — the `<label for=\"...\">` target. */\n fieldId: string;\n /** Stable `id` for the label element. Stamp this on your `<label id=\"...\">`. */\n labelId: string;\n /** Whether the label should be visible. Use to toggle `hidden` on the `<label>`. */\n labelVisible: Readable<boolean>;\n};\n\nexport type LabelStateOptions = {\n /**\n * Override for label visibility that takes precedence over deriving it from\n * the `label` text signal. Pass a computed signal that checks both prop and\n * slot presence to enable slot-first composition.\n *\n * @example\n * ```ts\n * const hasLabel = computed(() => !!props.label.value || slots.has('label').value);\n * const label = createLabelState({ ...options, hasLabel });\n * ```\n */\n hasLabel?: Readable<boolean>;\n /**\n * When provided, used directly as `fieldId` instead of generating one via\n * `createStableId`. Useful in tests for deterministic ID assertions.\n */\n id?: string;\n /**\n * Label text signal. When provided, `labelVisible` and `ariaLabelledBy` are\n * computed reactively from this value (see `hasLabel` for the slot-first override).\n */\n label?: Readable<string | undefined>;\n /**\n * Label placement signal. Defaults to `'inset'`.\n *\n * Not consumed by `createLabelState()` itself — visibility is driven solely by\n * `hasLabel`/`label`. Threaded through so component authors can apply\n * placement-specific styling (`label-placement` attribute) without a second\n * option; if placement-dependent visibility is ever needed, wire it into\n * `labelVisible` here instead of adding a new option.\n */\n labelPlacement?: Readable<LabelPlacement>;\n prefix?: string;\n};\n\nexport const createLabelState = (options: LabelStateOptions): LabelStateHandle => {\n const fieldId = options.id ?? createStableId(options.prefix ?? 'field');\n const labelId = createStableId('label');\n const label$ = options.label ?? computed(() => undefined);\n\n // Slot-first composition: if the caller provides `hasLabel`, use it instead\n // of deriving visibility purely from the label text signal. This allows\n // components with a `<slot name=\"label\">` to stay visible even when the\n // `label` prop is empty but a slotted label element is present.\n const labelVisible = options.hasLabel ?? computed(() => Boolean(label$.value));\n const ariaLabelledBy = computed(() => (labelVisible.value ? labelId : null));\n\n return { ariaLabelledBy, fieldId, labelId, labelVisible };\n};\n\n// ── Field handle (assistive + label, composed) ──────────────────────────────\n\n/**\n * Common handle returned by `createField` — the base for both `TextFieldHandle`\n * and `ChoiceFieldHandle`. Composes `createAssistiveState` + `createLabelState`;\n * call those directly instead if a field has no visible `<label>` (see\n * `createAssistiveState`'s doc comment).\n *\n * ARIA signals and label state are flat on the handle — no nested `aria` or\n * `label` sub-objects. `labelId`/`labelVisible`/`ariaLabelledBy` are always\n * present but only meaningful when `label`/`hasLabel` was passed — omit both\n * and they resolve to an always-hidden, unreferenced label.\n *\n * @example\n * ```html\n * <label id=\"${labelId}\" ?hidden=\"${() => !labelVisible.value}\">...</label>\n * <input\n * :aria-labelledby=\"${ariaLabelledBy}\"\n * :aria-describedby=\"${ariaDescribedBy}\"\n * :aria-invalid=\"${ariaInvalid}\" />\n * ```\n */\nexport type FieldHandle = AssistiveStateHandle & LabelStateHandle;\n\n/** Options shared by both `createTextField` and `createChoiceField`. */\nexport type FieldOptions = AssistiveStateOptions & LabelStateOptions;\n\nexport const createField = (options: FieldOptions): FieldHandle => ({\n ...createAssistiveState(options),\n ...createLabelState(options),\n});\n"],"mappings":"4FAsBA,IAAa,EAA0B,IAAA,EAAA,EAAA,SAAA,MACrB,CACd,UAAW,EAAQ,OAAO,OAAS,GACnC,WAAY,EAAQ,QAAQ,OAAS,EACvC,EAAE,EAyBS,EAAsB,IAAA,EAAA,EAAA,SAAA,KACJ,CAC3B,IAAM,EAAQ,EAAQ,OAAO,OAAS,GAChC,EAAY,EAAQ,WAAW,MAC/B,EAAkB,OAAO,CAAS,EAClC,EAAiB,OAAO,SAAS,CAAe,GAAK,EAAkB,EAAI,EAAkB,KAC7F,EAAa,IAAmB,KAChC,EAAc,EAAa,GAAG,EAAM,OAAO,KAAK,IAAmB,GACnE,EAAQ,EAAa,EAAM,OAAS,EAAiB,EAE3D,MAAO,CACL,eAAgB,EAAa,GAAS,EAAI,GAC1C,iBAAkB,EAAa,GAAS,IAAO,EAAQ,EAAI,GAC3D,aACF,CACF,CAAC,EAYU,GAAoB,EAAmC,EAAO,YACpE,EAED,EAAQ,eAAuB,GAAG,EAAK,WAEvC,EAAQ,iBAAyB,GAAG,EAAK,aAEtC,EANc,EA4DV,EAAwB,GAAyD,CAC5F,IAAM,GAAA,EAAA,EAAA,SAAA,KAA0B,EAAQ,EAAQ,UAAU,KAAM,EAC1D,GAAA,EAAA,EAAA,eAAA,CAA6B,QAAQ,EACrC,GAAA,EAAA,EAAA,eAAA,CAAyB,OAAO,EAChC,EAAoB,EAAuB,CAAE,MAAO,EAAQ,MAAO,OAAQ,EAAQ,MAAO,CAAC,EAE3F,GAAA,EAAA,EAAA,SAAA,KACJ,EAAkB,MAAM,WAAa,EAAkB,MAAM,WAAa,EAAc,IAC1F,EACM,GAAA,EAAA,EAAA,SAAA,KAAmC,EAAkB,MAAM,UAAY,EAAU,IAAK,EACtF,GAAA,EAAA,EAAA,SAAA,KAA6C,EAAkB,MAAM,UAAY,OAAS,IAAK,EAEjG,EAA+C,KAsBnD,MAAO,CACL,kBACA,mBACA,cACA,cACA,gBA1BuB,GAAoD,CAC3E,EAAY,CACd,EAyBE,WACA,UACA,WAAA,EAAA,EAAA,SAAA,KAA0B,EAAkB,MAAM,SAAS,EAC3D,YAAA,EAAA,EAAA,SAAA,KAA2B,EAAkB,MAAM,UAAU,EAC7D,kBA3ByB,GAA4D,CACjF,KAAQ,YAAY,QAAU,EAElC,IAAI,CAAC,EAAW,CAId,EAAA,KACE,2KACF,EAEA,MACF,CAEA,EAAU,eAAe,CAFzB,CAGF,CAaA,CACF,EAmDa,EAAoB,GAAiD,CAChF,IAAM,EAAU,EAAQ,KAAA,EAAA,EAAA,eAAA,CAAqB,EAAQ,QAAU,OAAO,EAChE,GAAA,EAAA,EAAA,eAAA,CAAyB,OAAO,EAChC,EAAS,EAAQ,QAAA,EAAA,EAAA,SAAA,KAAwB,IAAA,EAAS,EAMlD,EAAe,EAAQ,WAAA,EAAA,EAAA,SAAA,KAA2B,EAAQ,EAAO,KAAM,EAG7E,MAAO,CAAE,gBAAA,EAAA,EAAA,SAAA,KAF8B,EAAa,MAAQ,EAAU,IAE7D,EAAgB,UAAS,UAAS,cAAa,CAC1D,EA6Ba,EAAe,IAAwC,CAClE,GAAG,EAAqB,CAAO,EAC/B,GAAG,EAAiB,CAAO,CAC7B"}
1
+ {"version":3,"file":"field-base.cjs","names":[],"sources":["../../src/headless/field-base.ts"],"sourcesContent":["import { createStableId } from '@vielzeug/ore';\nimport { computed, type Readable } from '@vielzeug/ripple';\n\nimport { warn } from '../_dev';\n\n// ── Validation / context types ────────────────────────────────────────────────\n\nexport type ValidationTrigger = 'blur' | 'change' | 'input' | 'submit';\nexport type ControlValidationMode = ValidationTrigger | undefined;\n\n// ── Error/helper assistive state ──────────────────────────────────────────────\n\nexport type ErrorHelperState = {\n errorText: string;\n helperText: string;\n};\n\nexport type ErrorHelperOptions = {\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n};\n\nexport const createErrorHelperState = (options: ErrorHelperOptions): Readable<ErrorHelperState> =>\n computed(() => ({\n errorText: options.error?.value ?? '',\n helperText: options.helper?.value ?? '',\n }));\n\n// ── Counter state (opt-in) ─────────────────────────────────────────────────────\n\n/** Counter state for text fields with `maxLength`. */\nexport type CounterState = {\n counterAtLimit: boolean;\n counterNearLimit: boolean;\n counterText: string;\n};\n\nexport type CounterOptions = {\n maxLength?: Readable<number | undefined>;\n value: Readable<string | undefined>;\n};\n\n/**\n * Creates a reactive counter state signal for text fields with `maxLength`.\n * Only call this when `maxLength` may be set — the state is genuinely opt-in.\n *\n * @example\n * ```ts\n * const counter = createCounterState({ value: tf.value, maxLength: props.maxlength });\n * ```\n */\nexport const createCounterState = (options: CounterOptions): Readable<CounterState> =>\n computed<CounterState>(() => {\n const value = options.value?.value ?? '';\n const maxLength = options.maxLength?.value;\n const parsedMaxLength = Number(maxLength);\n const validMaxLength = Number.isFinite(parsedMaxLength) && parsedMaxLength > 0 ? parsedMaxLength : null;\n const hasCounter = validMaxLength !== null;\n const counterText = hasCounter ? `${value.length} / ${validMaxLength}` : '';\n const ratio = hasCounter ? value.length / validMaxLength : 0;\n\n return {\n counterAtLimit: hasCounter ? ratio >= 1 : false,\n counterNearLimit: hasCounter ? ratio >= 0.9 && ratio < 1 : false,\n counterText,\n };\n });\n\n/**\n * The single implementation of \"which modifier class does this counter state map to\" —\n * every field with a character counter (`ore-input`, `ore-textarea`, `ore-message-composer`)\n * renders the identical near-limit/at-limit styling, so this is the one place that decides it.\n *\n * @example\n * ```ts\n * html`<span class=\"${() => counterClassName(counter?.value)}\">...</span>`\n * ```\n */\nexport const counterClassName = (counter: CounterState | undefined, base = 'counter'): string => {\n if (!counter) return base;\n\n if (counter.counterAtLimit) return `${base} at-limit`;\n\n if (counter.counterNearLimit) return `${base} near-limit`;\n\n return base;\n};\n\n// ── Dirty tracking (two-state reset) ───────────────────────────────────────────\n\n/**\n * Tracks whether a control's value has ever been changed by user interaction.\n *\n * Backs the \"two-state reset()\" pattern used by `createCheckable` (`checked`/\n * `indeterminate`) and `createChoiceField` (`selectedValues`): before the first\n * interaction, the live prop is still a reliable \"current default\" to resync from\n * (e.g. an async-loaded value arriving after mount); after it, the prop is\n * contaminated by interaction-driven attribute reflection, so only a snapshot taken\n * at creation still represents \"the default\" in the native sense. `reset()` callers\n * check `isDirty` to decide which source to revert to, then call `clear()`.\n */\nexport type DirtyTracker = {\n clear: () => void;\n readonly isDirty: boolean;\n markDirty: () => void;\n};\n\nexport function createDirtyTracker(): DirtyTracker {\n let dirty = false;\n\n return {\n clear: () => {\n dirty = false;\n },\n get isDirty() {\n return dirty;\n },\n markDirty: () => {\n dirty = true;\n },\n };\n}\n\n// ── Label placement ───────────────────────────────────────────────────────────\n\nexport type LabelPlacement = 'inset' | 'outside' | undefined;\n\n// ── Assistive state (error/helper text, describedby/errormessage/invalid) ─────\n\n/**\n * A field's error/helper/disabled/validation-trigger state — everything an input\n * needs *except* a visible `<label>`. Split out from label state (below) because\n * not every field has one: `ore-message-composer` names itself via `aria-label`\n * (a chat composer's accessible name conventionally comes from context/placeholder,\n * not a floating `<label>`), so it composes this directly instead of pulling in\n * label plumbing it can't use. `createField()` composes both for fields that do\n * render a `<label>`.\n */\nexport type AssistiveStateHandle = {\n /** `aria-describedby` value. Non-null when helper or error text is present. */\n ariaDescribedBy: Readable<string | null>;\n /** `aria-errormessage` value. Non-null when error text is present. */\n ariaErrorMessage: Readable<string | null>;\n /** `aria-invalid` value. `'true'` when error text is present, otherwise `null`. */\n ariaInvalid: Readable<'true' | null>;\n /**\n * The stable `id` used for `aria-describedby` on the input. Points at the\n * assistive-text region (covers both helper text and error text per WAI-ARIA).\n */\n assistiveId: string;\n /**\n * Registers the real form field handle (the return value of `useField()`) so that\n * `triggerValidation()` can call its `reportValidity()`. See `TextFieldHandle.attachFormField`\n * for why this is a post-hoc call instead of a constructor option — the same forward\n * reference applies here for choice fields.\n */\n attachFormField: (formField: { reportValidity(): void }) => void;\n disabled: Readable<boolean>;\n /** Stable `id` for the inline error message element (`aria-errormessage`). */\n errorId: string;\n /** Reactive error text. Empty string when no error is set. */\n errorText: Readable<string>;\n /** Reactive helper text. Empty string when no helper is set. */\n helperText: Readable<string>;\n triggerValidation: (on: Extract<ValidationTrigger, 'blur' | 'change'>) => void;\n};\n\nexport type AssistiveStateOptions = {\n disabled?: Readable<boolean | undefined>;\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n validateOn?: Readable<ControlValidationMode>;\n};\n\nexport const createAssistiveState = (options: AssistiveStateOptions): AssistiveStateHandle => {\n const disabled = computed(() => Boolean(options.disabled?.value));\n const assistiveId = createStableId('helper');\n const errorId = createStableId('error');\n const resolvedAssistive = createErrorHelperState({ error: options.error, helper: options.helper });\n\n const ariaDescribedBy = computed(() =>\n resolvedAssistive.value.errorText || resolvedAssistive.value.helperText ? assistiveId : null,\n );\n const ariaErrorMessage = computed(() => (resolvedAssistive.value.errorText ? errorId : null));\n const ariaInvalid = computed<'true' | null>(() => (resolvedAssistive.value.errorText ? 'true' : null));\n\n let formField: { reportValidity(): void } | null = null;\n const attachFormField = (nextFormField: { reportValidity(): void }): void => {\n formField = nextFormField;\n };\n\n const triggerValidation = (on: Extract<ValidationTrigger, 'blur' | 'change'>): void => {\n if (options.validateOn?.value !== on) return;\n\n if (!formField) {\n // Not user-facing — this only fires when a component author wired `validateOn` but\n // forgot the matching `attachFormField(useField(...))` call, so validation silently\n // never runs instead of erroring where the mistake actually is.\n warn(\n \"triggerValidation() called before attachFormField() — validation will not run. See createTextField()/createChoiceField()/createCheckable()'s attachFormField doc comment.\",\n );\n\n return;\n }\n\n formField.reportValidity();\n };\n\n return {\n ariaDescribedBy,\n ariaErrorMessage,\n ariaInvalid,\n assistiveId,\n attachFormField,\n disabled,\n errorId,\n errorText: computed(() => resolvedAssistive.value.errorText),\n helperText: computed(() => resolvedAssistive.value.helperText),\n triggerValidation,\n };\n};\n\n// ── Label state (visible `<label>` + `aria-labelledby`) ────────────────────────\n\nexport type LabelStateHandle = {\n /** `aria-labelledby` value. Non-null when a label is visible. */\n ariaLabelledBy: Readable<string | null>;\n /** Stable `id` for the underlying form control — the `<label for=\"...\">` target. */\n fieldId: string;\n /** Stable `id` for the label element. Stamp this on your `<label id=\"...\">`. */\n labelId: string;\n /** Whether the label should be visible. Use to toggle `hidden` on the `<label>`. */\n labelVisible: Readable<boolean>;\n};\n\nexport type LabelStateOptions = {\n /**\n * Override for label visibility that takes precedence over deriving it from\n * the `label` text signal. Pass a computed signal that checks both prop and\n * slot presence to enable slot-first composition.\n *\n * @example\n * ```ts\n * const hasLabel = computed(() => !!props.label.value || slots.has('label').value);\n * const label = createLabelState({ ...options, hasLabel });\n * ```\n */\n hasLabel?: Readable<boolean>;\n /**\n * When provided, used directly as `fieldId` instead of generating one via\n * `createStableId`. Useful in tests for deterministic ID assertions.\n */\n id?: string;\n /**\n * Label text signal. When provided, `labelVisible` and `ariaLabelledBy` are\n * computed reactively from this value (see `hasLabel` for the slot-first override).\n */\n label?: Readable<string | undefined>;\n /**\n * Label placement signal. Defaults to `'inset'`.\n *\n * Not consumed by `createLabelState()` itself — visibility is driven solely by\n * `hasLabel`/`label`. Threaded through so component authors can apply\n * placement-specific styling (`label-placement` attribute) without a second\n * option; if placement-dependent visibility is ever needed, wire it into\n * `labelVisible` here instead of adding a new option.\n */\n labelPlacement?: Readable<LabelPlacement>;\n prefix?: string;\n};\n\nexport const createLabelState = (options: LabelStateOptions): LabelStateHandle => {\n const fieldId = options.id ?? createStableId(options.prefix ?? 'field');\n const labelId = createStableId('label');\n const label$ = options.label ?? computed(() => undefined);\n\n // Slot-first composition: if the caller provides `hasLabel`, use it instead\n // of deriving visibility purely from the label text signal. This allows\n // components with a `<slot name=\"label\">` to stay visible even when the\n // `label` prop is empty but a slotted label element is present.\n const labelVisible = options.hasLabel ?? computed(() => Boolean(label$.value));\n const ariaLabelledBy = computed(() => (labelVisible.value ? labelId : null));\n\n return { ariaLabelledBy, fieldId, labelId, labelVisible };\n};\n\n// ── Field handle (assistive + label, composed) ──────────────────────────────\n\n/**\n * Common handle returned by `createField` — the base for both `TextFieldHandle`\n * and `ChoiceFieldHandle`. Composes `createAssistiveState` + `createLabelState`;\n * call those directly instead if a field has no visible `<label>` (see\n * `createAssistiveState`'s doc comment).\n *\n * ARIA signals and label state are flat on the handle — no nested `aria` or\n * `label` sub-objects. `labelId`/`labelVisible`/`ariaLabelledBy` are always\n * present but only meaningful when `label`/`hasLabel` was passed — omit both\n * and they resolve to an always-hidden, unreferenced label.\n *\n * @example\n * ```html\n * <label id=\"${labelId}\" ?hidden=\"${() => !labelVisible.value}\">...</label>\n * <input\n * :aria-labelledby=\"${ariaLabelledBy}\"\n * :aria-describedby=\"${ariaDescribedBy}\"\n * :aria-invalid=\"${ariaInvalid}\" />\n * ```\n */\nexport type FieldHandle = AssistiveStateHandle & LabelStateHandle;\n\n/** Options shared by both `createTextField` and `createChoiceField`. */\nexport type FieldOptions = AssistiveStateOptions & LabelStateOptions;\n\nexport const createField = (options: FieldOptions): FieldHandle => ({\n ...createAssistiveState(options),\n ...createLabelState(options),\n});\n"],"mappings":"4FAsBA,IAAa,EAA0B,IAAA,EAAA,EAAA,SAAA,MACrB,CACd,UAAW,EAAQ,OAAO,OAAS,GACnC,WAAY,EAAQ,QAAQ,OAAS,EACvC,EAAE,EAyBS,EAAsB,IAAA,EAAA,EAAA,SAAA,KACJ,CAC3B,IAAM,EAAQ,EAAQ,OAAO,OAAS,GAChC,EAAY,EAAQ,WAAW,MAC/B,EAAkB,OAAO,CAAS,EAClC,EAAiB,OAAO,SAAS,CAAe,GAAK,EAAkB,EAAI,EAAkB,KAC7F,EAAa,IAAmB,KAChC,EAAc,EAAa,GAAG,EAAM,OAAO,KAAK,IAAmB,GACnE,EAAQ,EAAa,EAAM,OAAS,EAAiB,EAE3D,MAAO,CACL,eAAgB,EAAa,GAAS,EAAI,GAC1C,iBAAkB,EAAa,GAAS,IAAO,EAAQ,EAAI,GAC3D,aACF,CACF,CAAC,EAYU,GAAoB,EAAmC,EAAO,YACpE,EAED,EAAQ,eAAuB,GAAG,EAAK,WAEvC,EAAQ,iBAAyB,GAAG,EAAK,aAEtC,EANc,EA4BvB,SAAgB,GAAmC,CACjD,IAAI,EAAQ,GAEZ,MAAO,CACL,UAAa,CACX,EAAQ,EACV,EACA,IAAI,SAAU,CACZ,OAAO,CACT,EACA,cAAiB,CACf,EAAQ,EACV,CACF,CACF,CAqDA,IAAa,EAAwB,GAAyD,CAC5F,IAAM,GAAA,EAAA,EAAA,SAAA,KAA0B,EAAQ,EAAQ,UAAU,KAAM,EAC1D,GAAA,EAAA,EAAA,eAAA,CAA6B,QAAQ,EACrC,GAAA,EAAA,EAAA,eAAA,CAAyB,OAAO,EAChC,EAAoB,EAAuB,CAAE,MAAO,EAAQ,MAAO,OAAQ,EAAQ,MAAO,CAAC,EAE3F,GAAA,EAAA,EAAA,SAAA,KACJ,EAAkB,MAAM,WAAa,EAAkB,MAAM,WAAa,EAAc,IAC1F,EACM,GAAA,EAAA,EAAA,SAAA,KAAmC,EAAkB,MAAM,UAAY,EAAU,IAAK,EACtF,GAAA,EAAA,EAAA,SAAA,KAA6C,EAAkB,MAAM,UAAY,OAAS,IAAK,EAEjG,EAA+C,KAsBnD,MAAO,CACL,kBACA,mBACA,cACA,cACA,gBA1BuB,GAAoD,CAC3E,EAAY,CACd,EAyBE,WACA,UACA,WAAA,EAAA,EAAA,SAAA,KAA0B,EAAkB,MAAM,SAAS,EAC3D,YAAA,EAAA,EAAA,SAAA,KAA2B,EAAkB,MAAM,UAAU,EAC7D,kBA3ByB,GAA4D,CACjF,KAAQ,YAAY,QAAU,EAElC,IAAI,CAAC,EAAW,CAId,EAAA,KACE,2KACF,EAEA,MACF,CAEA,EAAU,eAAe,CAFzB,CAGF,CAaA,CACF,EAmDa,EAAoB,GAAiD,CAChF,IAAM,EAAU,EAAQ,KAAA,EAAA,EAAA,eAAA,CAAqB,EAAQ,QAAU,OAAO,EAChE,GAAA,EAAA,EAAA,eAAA,CAAyB,OAAO,EAChC,EAAS,EAAQ,QAAA,EAAA,EAAA,SAAA,KAAwB,IAAA,EAAS,EAMlD,EAAe,EAAQ,WAAA,EAAA,EAAA,SAAA,KAA2B,EAAQ,EAAO,KAAM,EAG7E,MAAO,CAAE,gBAAA,EAAA,EAAA,SAAA,KAF8B,EAAa,MAAQ,EAAU,IAE7D,EAAgB,UAAS,UAAS,cAAa,CAC1D,EA6Ba,EAAe,IAAwC,CAClE,GAAG,EAAqB,CAAO,EAC/B,GAAG,EAAiB,CAAO,CAC7B"}
@@ -41,6 +41,23 @@ export declare const createCounterState: (options: CounterOptions) => Readable<C
41
41
  * ```
42
42
  */
43
43
  export declare const counterClassName: (counter: CounterState | undefined, base?: string) => string;
44
+ /**
45
+ * Tracks whether a control's value has ever been changed by user interaction.
46
+ *
47
+ * Backs the "two-state reset()" pattern used by `createCheckable` (`checked`/
48
+ * `indeterminate`) and `createChoiceField` (`selectedValues`): before the first
49
+ * interaction, the live prop is still a reliable "current default" to resync from
50
+ * (e.g. an async-loaded value arriving after mount); after it, the prop is
51
+ * contaminated by interaction-driven attribute reflection, so only a snapshot taken
52
+ * at creation still represents "the default" in the native sense. `reset()` callers
53
+ * check `isDirty` to decide which source to revert to, then call `clear()`.
54
+ */
55
+ export type DirtyTracker = {
56
+ clear: () => void;
57
+ readonly isDirty: boolean;
58
+ markDirty: () => void;
59
+ };
60
+ export declare function createDirtyTracker(): DirtyTracker;
44
61
  export type LabelPlacement = 'inset' | 'outside' | undefined;
45
62
  /**
46
63
  * A field's error/helper/disabled/validation-trigger state — everything an input
@@ -1 +1 @@
1
- {"version":3,"file":"field-base.d.ts","sourceRoot":"","sources":["../../src/headless/field-base.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"field-base.d.ts","sourceRoot":"","sources":["../../src/headless/field-base.ts"],"names":[],"mappings":"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"}
@@ -12,7 +12,22 @@ var r = (e) => n(() => ({
12
12
  counterNearLimit: a ? s >= .9 && s < 1 : !1,
13
13
  counterText: o
14
14
  };
15
- }), a = (e, t = "counter") => e ? e.counterAtLimit ? `${t} at-limit` : e.counterNearLimit ? `${t} near-limit` : t : t, o = (i) => {
15
+ }), a = (e, t = "counter") => e ? e.counterAtLimit ? `${t} at-limit` : e.counterNearLimit ? `${t} near-limit` : t : t;
16
+ function o() {
17
+ let e = !1;
18
+ return {
19
+ clear: () => {
20
+ e = !1;
21
+ },
22
+ get isDirty() {
23
+ return e;
24
+ },
25
+ markDirty: () => {
26
+ e = !0;
27
+ }
28
+ };
29
+ }
30
+ var s = (i) => {
16
31
  let a = n(() => !!i.disabled?.value), o = t("helper"), s = t("error"), c = r({
17
32
  error: i.error,
18
33
  helper: i.helper
@@ -39,7 +54,7 @@ var r = (e) => n(() => ({
39
54
  }
40
55
  }
41
56
  };
42
- }, s = (e) => {
57
+ }, c = (e) => {
43
58
  let r = e.id ?? t(e.prefix ?? "field"), i = t("label"), a = e.label ?? n(() => void 0), o = e.hasLabel ?? n(() => !!a.value);
44
59
  return {
45
60
  ariaLabelledBy: n(() => o.value ? i : null),
@@ -47,11 +62,11 @@ var r = (e) => n(() => ({
47
62
  labelId: i,
48
63
  labelVisible: o
49
64
  };
50
- }, c = (e) => ({
51
- ...o(e),
52
- ...s(e)
65
+ }, l = (e) => ({
66
+ ...s(e),
67
+ ...c(e)
53
68
  });
54
69
  //#endregion
55
- export { a as counterClassName, o as createAssistiveState, i as createCounterState, r as createErrorHelperState, c as createField, s as createLabelState };
70
+ export { a as counterClassName, s as createAssistiveState, i as createCounterState, o as createDirtyTracker, r as createErrorHelperState, l as createField, c as createLabelState };
56
71
 
57
72
  //# sourceMappingURL=field-base.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"field-base.js","names":[],"sources":["../../src/headless/field-base.ts"],"sourcesContent":["import { createStableId } from '@vielzeug/ore';\nimport { computed, type Readable } from '@vielzeug/ripple';\n\nimport { warn } from '../_dev';\n\n// ── Validation / context types ────────────────────────────────────────────────\n\nexport type ValidationTrigger = 'blur' | 'change' | 'input' | 'submit';\nexport type ControlValidationMode = ValidationTrigger | undefined;\n\n// ── Error/helper assistive state ──────────────────────────────────────────────\n\nexport type ErrorHelperState = {\n errorText: string;\n helperText: string;\n};\n\nexport type ErrorHelperOptions = {\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n};\n\nexport const createErrorHelperState = (options: ErrorHelperOptions): Readable<ErrorHelperState> =>\n computed(() => ({\n errorText: options.error?.value ?? '',\n helperText: options.helper?.value ?? '',\n }));\n\n// ── Counter state (opt-in) ─────────────────────────────────────────────────────\n\n/** Counter state for text fields with `maxLength`. */\nexport type CounterState = {\n counterAtLimit: boolean;\n counterNearLimit: boolean;\n counterText: string;\n};\n\nexport type CounterOptions = {\n maxLength?: Readable<number | undefined>;\n value: Readable<string | undefined>;\n};\n\n/**\n * Creates a reactive counter state signal for text fields with `maxLength`.\n * Only call this when `maxLength` may be set — the state is genuinely opt-in.\n *\n * @example\n * ```ts\n * const counter = createCounterState({ value: tf.value, maxLength: props.maxlength });\n * ```\n */\nexport const createCounterState = (options: CounterOptions): Readable<CounterState> =>\n computed<CounterState>(() => {\n const value = options.value?.value ?? '';\n const maxLength = options.maxLength?.value;\n const parsedMaxLength = Number(maxLength);\n const validMaxLength = Number.isFinite(parsedMaxLength) && parsedMaxLength > 0 ? parsedMaxLength : null;\n const hasCounter = validMaxLength !== null;\n const counterText = hasCounter ? `${value.length} / ${validMaxLength}` : '';\n const ratio = hasCounter ? value.length / validMaxLength : 0;\n\n return {\n counterAtLimit: hasCounter ? ratio >= 1 : false,\n counterNearLimit: hasCounter ? ratio >= 0.9 && ratio < 1 : false,\n counterText,\n };\n });\n\n/**\n * The single implementation of \"which modifier class does this counter state map to\" —\n * every field with a character counter (`ore-input`, `ore-textarea`, `ore-message-composer`)\n * renders the identical near-limit/at-limit styling, so this is the one place that decides it.\n *\n * @example\n * ```ts\n * html`<span class=\"${() => counterClassName(counter?.value)}\">...</span>`\n * ```\n */\nexport const counterClassName = (counter: CounterState | undefined, base = 'counter'): string => {\n if (!counter) return base;\n\n if (counter.counterAtLimit) return `${base} at-limit`;\n\n if (counter.counterNearLimit) return `${base} near-limit`;\n\n return base;\n};\n\n// ── Label placement ───────────────────────────────────────────────────────────\n\nexport type LabelPlacement = 'inset' | 'outside' | undefined;\n\n// ── Assistive state (error/helper text, describedby/errormessage/invalid) ─────\n\n/**\n * A field's error/helper/disabled/validation-trigger state — everything an input\n * needs *except* a visible `<label>`. Split out from label state (below) because\n * not every field has one: `ore-message-composer` names itself via `aria-label`\n * (a chat composer's accessible name conventionally comes from context/placeholder,\n * not a floating `<label>`), so it composes this directly instead of pulling in\n * label plumbing it can't use. `createField()` composes both for fields that do\n * render a `<label>`.\n */\nexport type AssistiveStateHandle = {\n /** `aria-describedby` value. Non-null when helper or error text is present. */\n ariaDescribedBy: Readable<string | null>;\n /** `aria-errormessage` value. Non-null when error text is present. */\n ariaErrorMessage: Readable<string | null>;\n /** `aria-invalid` value. `'true'` when error text is present, otherwise `null`. */\n ariaInvalid: Readable<'true' | null>;\n /**\n * The stable `id` used for `aria-describedby` on the input. Points at the\n * assistive-text region (covers both helper text and error text per WAI-ARIA).\n */\n assistiveId: string;\n /**\n * Registers the real form field handle (the return value of `useField()`) so that\n * `triggerValidation()` can call its `reportValidity()`. See `TextFieldHandle.attachFormField`\n * for why this is a post-hoc call instead of a constructor option — the same forward\n * reference applies here for choice fields.\n */\n attachFormField: (formField: { reportValidity(): void }) => void;\n disabled: Readable<boolean>;\n /** Stable `id` for the inline error message element (`aria-errormessage`). */\n errorId: string;\n /** Reactive error text. Empty string when no error is set. */\n errorText: Readable<string>;\n /** Reactive helper text. Empty string when no helper is set. */\n helperText: Readable<string>;\n triggerValidation: (on: Extract<ValidationTrigger, 'blur' | 'change'>) => void;\n};\n\nexport type AssistiveStateOptions = {\n disabled?: Readable<boolean | undefined>;\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n validateOn?: Readable<ControlValidationMode>;\n};\n\nexport const createAssistiveState = (options: AssistiveStateOptions): AssistiveStateHandle => {\n const disabled = computed(() => Boolean(options.disabled?.value));\n const assistiveId = createStableId('helper');\n const errorId = createStableId('error');\n const resolvedAssistive = createErrorHelperState({ error: options.error, helper: options.helper });\n\n const ariaDescribedBy = computed(() =>\n resolvedAssistive.value.errorText || resolvedAssistive.value.helperText ? assistiveId : null,\n );\n const ariaErrorMessage = computed(() => (resolvedAssistive.value.errorText ? errorId : null));\n const ariaInvalid = computed<'true' | null>(() => (resolvedAssistive.value.errorText ? 'true' : null));\n\n let formField: { reportValidity(): void } | null = null;\n const attachFormField = (nextFormField: { reportValidity(): void }): void => {\n formField = nextFormField;\n };\n\n const triggerValidation = (on: Extract<ValidationTrigger, 'blur' | 'change'>): void => {\n if (options.validateOn?.value !== on) return;\n\n if (!formField) {\n // Not user-facing — this only fires when a component author wired `validateOn` but\n // forgot the matching `attachFormField(useField(...))` call, so validation silently\n // never runs instead of erroring where the mistake actually is.\n warn(\n \"triggerValidation() called before attachFormField() — validation will not run. See createTextField()/createChoiceField()/createCheckable()'s attachFormField doc comment.\",\n );\n\n return;\n }\n\n formField.reportValidity();\n };\n\n return {\n ariaDescribedBy,\n ariaErrorMessage,\n ariaInvalid,\n assistiveId,\n attachFormField,\n disabled,\n errorId,\n errorText: computed(() => resolvedAssistive.value.errorText),\n helperText: computed(() => resolvedAssistive.value.helperText),\n triggerValidation,\n };\n};\n\n// ── Label state (visible `<label>` + `aria-labelledby`) ────────────────────────\n\nexport type LabelStateHandle = {\n /** `aria-labelledby` value. Non-null when a label is visible. */\n ariaLabelledBy: Readable<string | null>;\n /** Stable `id` for the underlying form control — the `<label for=\"...\">` target. */\n fieldId: string;\n /** Stable `id` for the label element. Stamp this on your `<label id=\"...\">`. */\n labelId: string;\n /** Whether the label should be visible. Use to toggle `hidden` on the `<label>`. */\n labelVisible: Readable<boolean>;\n};\n\nexport type LabelStateOptions = {\n /**\n * Override for label visibility that takes precedence over deriving it from\n * the `label` text signal. Pass a computed signal that checks both prop and\n * slot presence to enable slot-first composition.\n *\n * @example\n * ```ts\n * const hasLabel = computed(() => !!props.label.value || slots.has('label').value);\n * const label = createLabelState({ ...options, hasLabel });\n * ```\n */\n hasLabel?: Readable<boolean>;\n /**\n * When provided, used directly as `fieldId` instead of generating one via\n * `createStableId`. Useful in tests for deterministic ID assertions.\n */\n id?: string;\n /**\n * Label text signal. When provided, `labelVisible` and `ariaLabelledBy` are\n * computed reactively from this value (see `hasLabel` for the slot-first override).\n */\n label?: Readable<string | undefined>;\n /**\n * Label placement signal. Defaults to `'inset'`.\n *\n * Not consumed by `createLabelState()` itself — visibility is driven solely by\n * `hasLabel`/`label`. Threaded through so component authors can apply\n * placement-specific styling (`label-placement` attribute) without a second\n * option; if placement-dependent visibility is ever needed, wire it into\n * `labelVisible` here instead of adding a new option.\n */\n labelPlacement?: Readable<LabelPlacement>;\n prefix?: string;\n};\n\nexport const createLabelState = (options: LabelStateOptions): LabelStateHandle => {\n const fieldId = options.id ?? createStableId(options.prefix ?? 'field');\n const labelId = createStableId('label');\n const label$ = options.label ?? computed(() => undefined);\n\n // Slot-first composition: if the caller provides `hasLabel`, use it instead\n // of deriving visibility purely from the label text signal. This allows\n // components with a `<slot name=\"label\">` to stay visible even when the\n // `label` prop is empty but a slotted label element is present.\n const labelVisible = options.hasLabel ?? computed(() => Boolean(label$.value));\n const ariaLabelledBy = computed(() => (labelVisible.value ? labelId : null));\n\n return { ariaLabelledBy, fieldId, labelId, labelVisible };\n};\n\n// ── Field handle (assistive + label, composed) ──────────────────────────────\n\n/**\n * Common handle returned by `createField` — the base for both `TextFieldHandle`\n * and `ChoiceFieldHandle`. Composes `createAssistiveState` + `createLabelState`;\n * call those directly instead if a field has no visible `<label>` (see\n * `createAssistiveState`'s doc comment).\n *\n * ARIA signals and label state are flat on the handle — no nested `aria` or\n * `label` sub-objects. `labelId`/`labelVisible`/`ariaLabelledBy` are always\n * present but only meaningful when `label`/`hasLabel` was passed — omit both\n * and they resolve to an always-hidden, unreferenced label.\n *\n * @example\n * ```html\n * <label id=\"${labelId}\" ?hidden=\"${() => !labelVisible.value}\">...</label>\n * <input\n * :aria-labelledby=\"${ariaLabelledBy}\"\n * :aria-describedby=\"${ariaDescribedBy}\"\n * :aria-invalid=\"${ariaInvalid}\" />\n * ```\n */\nexport type FieldHandle = AssistiveStateHandle & LabelStateHandle;\n\n/** Options shared by both `createTextField` and `createChoiceField`. */\nexport type FieldOptions = AssistiveStateOptions & LabelStateOptions;\n\nexport const createField = (options: FieldOptions): FieldHandle => ({\n ...createAssistiveState(options),\n ...createLabelState(options),\n});\n"],"mappings":";;;;AAsBA,IAAa,KAA0B,MACrC,SAAgB;CACd,WAAW,EAAQ,OAAO,SAAS;CACnC,YAAY,EAAQ,QAAQ,SAAS;AACvC,EAAE,GAyBS,KAAsB,MACjC,QAA6B;CAC3B,IAAM,IAAQ,EAAQ,OAAO,SAAS,IAChC,IAAY,EAAQ,WAAW,OAC/B,IAAkB,OAAO,CAAS,GAClC,IAAiB,OAAO,SAAS,CAAe,KAAK,IAAkB,IAAI,IAAkB,MAC7F,IAAa,MAAmB,MAChC,IAAc,IAAa,GAAG,EAAM,OAAO,KAAK,MAAmB,IACnE,IAAQ,IAAa,EAAM,SAAS,IAAiB;CAE3D,OAAO;EACL,gBAAgB,IAAa,KAAS,IAAI;EAC1C,kBAAkB,IAAa,KAAS,MAAO,IAAQ,IAAI;EAC3D;CACF;AACF,CAAC,GAYU,KAAoB,GAAmC,IAAO,cACpE,IAED,EAAQ,iBAAuB,GAAG,EAAK,aAEvC,EAAQ,mBAAyB,GAAG,EAAK,eAEtC,IANc,GA4DV,KAAwB,MAAyD;CAC5F,IAAM,IAAW,QAAe,EAAQ,EAAQ,UAAU,KAAM,GAC1D,IAAc,EAAe,QAAQ,GACrC,IAAU,EAAe,OAAO,GAChC,IAAoB,EAAuB;EAAE,OAAO,EAAQ;EAAO,QAAQ,EAAQ;CAAO,CAAC,GAE3F,IAAkB,QACtB,EAAkB,MAAM,aAAa,EAAkB,MAAM,aAAa,IAAc,IAC1F,GACM,IAAmB,QAAgB,EAAkB,MAAM,YAAY,IAAU,IAAK,GACtF,IAAc,QAA+B,EAAkB,MAAM,YAAY,SAAS,IAAK,GAEjG,IAA+C;CAsBnD,OAAO;EACL;EACA;EACA;EACA;EACA,kBA1BuB,MAAoD;GAC3E,IAAY;EACd;EAyBE;EACA;EACA,WAAW,QAAe,EAAkB,MAAM,SAAS;EAC3D,YAAY,QAAe,EAAkB,MAAM,UAAU;EAC7D,oBA3ByB,MAA4D;GACjF,MAAQ,YAAY,UAAU,GAElC;QAAI,CAAC,GAAW;KAId,EACE,2KACF;KAEA;IACF;IAEA,EAAU,eAAe;GAFzB;EAGF;CAaA;AACF,GAmDa,KAAoB,MAAiD;CAChF,IAAM,IAAU,EAAQ,MAAM,EAAe,EAAQ,UAAU,OAAO,GAChE,IAAU,EAAe,OAAO,GAChC,IAAS,EAAQ,SAAS,QAAe,KAAA,CAAS,GAMlD,IAAe,EAAQ,YAAY,QAAe,EAAQ,EAAO,KAAM;CAG7E,OAAO;EAAE,gBAFc,QAAgB,EAAa,QAAQ,IAAU,IAE7D;EAAgB;EAAS;EAAS;CAAa;AAC1D,GA6Ba,KAAe,OAAwC;CAClE,GAAG,EAAqB,CAAO;CAC/B,GAAG,EAAiB,CAAO;AAC7B"}
1
+ {"version":3,"file":"field-base.js","names":[],"sources":["../../src/headless/field-base.ts"],"sourcesContent":["import { createStableId } from '@vielzeug/ore';\nimport { computed, type Readable } from '@vielzeug/ripple';\n\nimport { warn } from '../_dev';\n\n// ── Validation / context types ────────────────────────────────────────────────\n\nexport type ValidationTrigger = 'blur' | 'change' | 'input' | 'submit';\nexport type ControlValidationMode = ValidationTrigger | undefined;\n\n// ── Error/helper assistive state ──────────────────────────────────────────────\n\nexport type ErrorHelperState = {\n errorText: string;\n helperText: string;\n};\n\nexport type ErrorHelperOptions = {\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n};\n\nexport const createErrorHelperState = (options: ErrorHelperOptions): Readable<ErrorHelperState> =>\n computed(() => ({\n errorText: options.error?.value ?? '',\n helperText: options.helper?.value ?? '',\n }));\n\n// ── Counter state (opt-in) ─────────────────────────────────────────────────────\n\n/** Counter state for text fields with `maxLength`. */\nexport type CounterState = {\n counterAtLimit: boolean;\n counterNearLimit: boolean;\n counterText: string;\n};\n\nexport type CounterOptions = {\n maxLength?: Readable<number | undefined>;\n value: Readable<string | undefined>;\n};\n\n/**\n * Creates a reactive counter state signal for text fields with `maxLength`.\n * Only call this when `maxLength` may be set — the state is genuinely opt-in.\n *\n * @example\n * ```ts\n * const counter = createCounterState({ value: tf.value, maxLength: props.maxlength });\n * ```\n */\nexport const createCounterState = (options: CounterOptions): Readable<CounterState> =>\n computed<CounterState>(() => {\n const value = options.value?.value ?? '';\n const maxLength = options.maxLength?.value;\n const parsedMaxLength = Number(maxLength);\n const validMaxLength = Number.isFinite(parsedMaxLength) && parsedMaxLength > 0 ? parsedMaxLength : null;\n const hasCounter = validMaxLength !== null;\n const counterText = hasCounter ? `${value.length} / ${validMaxLength}` : '';\n const ratio = hasCounter ? value.length / validMaxLength : 0;\n\n return {\n counterAtLimit: hasCounter ? ratio >= 1 : false,\n counterNearLimit: hasCounter ? ratio >= 0.9 && ratio < 1 : false,\n counterText,\n };\n });\n\n/**\n * The single implementation of \"which modifier class does this counter state map to\" —\n * every field with a character counter (`ore-input`, `ore-textarea`, `ore-message-composer`)\n * renders the identical near-limit/at-limit styling, so this is the one place that decides it.\n *\n * @example\n * ```ts\n * html`<span class=\"${() => counterClassName(counter?.value)}\">...</span>`\n * ```\n */\nexport const counterClassName = (counter: CounterState | undefined, base = 'counter'): string => {\n if (!counter) return base;\n\n if (counter.counterAtLimit) return `${base} at-limit`;\n\n if (counter.counterNearLimit) return `${base} near-limit`;\n\n return base;\n};\n\n// ── Dirty tracking (two-state reset) ───────────────────────────────────────────\n\n/**\n * Tracks whether a control's value has ever been changed by user interaction.\n *\n * Backs the \"two-state reset()\" pattern used by `createCheckable` (`checked`/\n * `indeterminate`) and `createChoiceField` (`selectedValues`): before the first\n * interaction, the live prop is still a reliable \"current default\" to resync from\n * (e.g. an async-loaded value arriving after mount); after it, the prop is\n * contaminated by interaction-driven attribute reflection, so only a snapshot taken\n * at creation still represents \"the default\" in the native sense. `reset()` callers\n * check `isDirty` to decide which source to revert to, then call `clear()`.\n */\nexport type DirtyTracker = {\n clear: () => void;\n readonly isDirty: boolean;\n markDirty: () => void;\n};\n\nexport function createDirtyTracker(): DirtyTracker {\n let dirty = false;\n\n return {\n clear: () => {\n dirty = false;\n },\n get isDirty() {\n return dirty;\n },\n markDirty: () => {\n dirty = true;\n },\n };\n}\n\n// ── Label placement ───────────────────────────────────────────────────────────\n\nexport type LabelPlacement = 'inset' | 'outside' | undefined;\n\n// ── Assistive state (error/helper text, describedby/errormessage/invalid) ─────\n\n/**\n * A field's error/helper/disabled/validation-trigger state — everything an input\n * needs *except* a visible `<label>`. Split out from label state (below) because\n * not every field has one: `ore-message-composer` names itself via `aria-label`\n * (a chat composer's accessible name conventionally comes from context/placeholder,\n * not a floating `<label>`), so it composes this directly instead of pulling in\n * label plumbing it can't use. `createField()` composes both for fields that do\n * render a `<label>`.\n */\nexport type AssistiveStateHandle = {\n /** `aria-describedby` value. Non-null when helper or error text is present. */\n ariaDescribedBy: Readable<string | null>;\n /** `aria-errormessage` value. Non-null when error text is present. */\n ariaErrorMessage: Readable<string | null>;\n /** `aria-invalid` value. `'true'` when error text is present, otherwise `null`. */\n ariaInvalid: Readable<'true' | null>;\n /**\n * The stable `id` used for `aria-describedby` on the input. Points at the\n * assistive-text region (covers both helper text and error text per WAI-ARIA).\n */\n assistiveId: string;\n /**\n * Registers the real form field handle (the return value of `useField()`) so that\n * `triggerValidation()` can call its `reportValidity()`. See `TextFieldHandle.attachFormField`\n * for why this is a post-hoc call instead of a constructor option — the same forward\n * reference applies here for choice fields.\n */\n attachFormField: (formField: { reportValidity(): void }) => void;\n disabled: Readable<boolean>;\n /** Stable `id` for the inline error message element (`aria-errormessage`). */\n errorId: string;\n /** Reactive error text. Empty string when no error is set. */\n errorText: Readable<string>;\n /** Reactive helper text. Empty string when no helper is set. */\n helperText: Readable<string>;\n triggerValidation: (on: Extract<ValidationTrigger, 'blur' | 'change'>) => void;\n};\n\nexport type AssistiveStateOptions = {\n disabled?: Readable<boolean | undefined>;\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n validateOn?: Readable<ControlValidationMode>;\n};\n\nexport const createAssistiveState = (options: AssistiveStateOptions): AssistiveStateHandle => {\n const disabled = computed(() => Boolean(options.disabled?.value));\n const assistiveId = createStableId('helper');\n const errorId = createStableId('error');\n const resolvedAssistive = createErrorHelperState({ error: options.error, helper: options.helper });\n\n const ariaDescribedBy = computed(() =>\n resolvedAssistive.value.errorText || resolvedAssistive.value.helperText ? assistiveId : null,\n );\n const ariaErrorMessage = computed(() => (resolvedAssistive.value.errorText ? errorId : null));\n const ariaInvalid = computed<'true' | null>(() => (resolvedAssistive.value.errorText ? 'true' : null));\n\n let formField: { reportValidity(): void } | null = null;\n const attachFormField = (nextFormField: { reportValidity(): void }): void => {\n formField = nextFormField;\n };\n\n const triggerValidation = (on: Extract<ValidationTrigger, 'blur' | 'change'>): void => {\n if (options.validateOn?.value !== on) return;\n\n if (!formField) {\n // Not user-facing — this only fires when a component author wired `validateOn` but\n // forgot the matching `attachFormField(useField(...))` call, so validation silently\n // never runs instead of erroring where the mistake actually is.\n warn(\n \"triggerValidation() called before attachFormField() — validation will not run. See createTextField()/createChoiceField()/createCheckable()'s attachFormField doc comment.\",\n );\n\n return;\n }\n\n formField.reportValidity();\n };\n\n return {\n ariaDescribedBy,\n ariaErrorMessage,\n ariaInvalid,\n assistiveId,\n attachFormField,\n disabled,\n errorId,\n errorText: computed(() => resolvedAssistive.value.errorText),\n helperText: computed(() => resolvedAssistive.value.helperText),\n triggerValidation,\n };\n};\n\n// ── Label state (visible `<label>` + `aria-labelledby`) ────────────────────────\n\nexport type LabelStateHandle = {\n /** `aria-labelledby` value. Non-null when a label is visible. */\n ariaLabelledBy: Readable<string | null>;\n /** Stable `id` for the underlying form control — the `<label for=\"...\">` target. */\n fieldId: string;\n /** Stable `id` for the label element. Stamp this on your `<label id=\"...\">`. */\n labelId: string;\n /** Whether the label should be visible. Use to toggle `hidden` on the `<label>`. */\n labelVisible: Readable<boolean>;\n};\n\nexport type LabelStateOptions = {\n /**\n * Override for label visibility that takes precedence over deriving it from\n * the `label` text signal. Pass a computed signal that checks both prop and\n * slot presence to enable slot-first composition.\n *\n * @example\n * ```ts\n * const hasLabel = computed(() => !!props.label.value || slots.has('label').value);\n * const label = createLabelState({ ...options, hasLabel });\n * ```\n */\n hasLabel?: Readable<boolean>;\n /**\n * When provided, used directly as `fieldId` instead of generating one via\n * `createStableId`. Useful in tests for deterministic ID assertions.\n */\n id?: string;\n /**\n * Label text signal. When provided, `labelVisible` and `ariaLabelledBy` are\n * computed reactively from this value (see `hasLabel` for the slot-first override).\n */\n label?: Readable<string | undefined>;\n /**\n * Label placement signal. Defaults to `'inset'`.\n *\n * Not consumed by `createLabelState()` itself — visibility is driven solely by\n * `hasLabel`/`label`. Threaded through so component authors can apply\n * placement-specific styling (`label-placement` attribute) without a second\n * option; if placement-dependent visibility is ever needed, wire it into\n * `labelVisible` here instead of adding a new option.\n */\n labelPlacement?: Readable<LabelPlacement>;\n prefix?: string;\n};\n\nexport const createLabelState = (options: LabelStateOptions): LabelStateHandle => {\n const fieldId = options.id ?? createStableId(options.prefix ?? 'field');\n const labelId = createStableId('label');\n const label$ = options.label ?? computed(() => undefined);\n\n // Slot-first composition: if the caller provides `hasLabel`, use it instead\n // of deriving visibility purely from the label text signal. This allows\n // components with a `<slot name=\"label\">` to stay visible even when the\n // `label` prop is empty but a slotted label element is present.\n const labelVisible = options.hasLabel ?? computed(() => Boolean(label$.value));\n const ariaLabelledBy = computed(() => (labelVisible.value ? labelId : null));\n\n return { ariaLabelledBy, fieldId, labelId, labelVisible };\n};\n\n// ── Field handle (assistive + label, composed) ──────────────────────────────\n\n/**\n * Common handle returned by `createField` — the base for both `TextFieldHandle`\n * and `ChoiceFieldHandle`. Composes `createAssistiveState` + `createLabelState`;\n * call those directly instead if a field has no visible `<label>` (see\n * `createAssistiveState`'s doc comment).\n *\n * ARIA signals and label state are flat on the handle — no nested `aria` or\n * `label` sub-objects. `labelId`/`labelVisible`/`ariaLabelledBy` are always\n * present but only meaningful when `label`/`hasLabel` was passed — omit both\n * and they resolve to an always-hidden, unreferenced label.\n *\n * @example\n * ```html\n * <label id=\"${labelId}\" ?hidden=\"${() => !labelVisible.value}\">...</label>\n * <input\n * :aria-labelledby=\"${ariaLabelledBy}\"\n * :aria-describedby=\"${ariaDescribedBy}\"\n * :aria-invalid=\"${ariaInvalid}\" />\n * ```\n */\nexport type FieldHandle = AssistiveStateHandle & LabelStateHandle;\n\n/** Options shared by both `createTextField` and `createChoiceField`. */\nexport type FieldOptions = AssistiveStateOptions & LabelStateOptions;\n\nexport const createField = (options: FieldOptions): FieldHandle => ({\n ...createAssistiveState(options),\n ...createLabelState(options),\n});\n"],"mappings":";;;;AAsBA,IAAa,KAA0B,MACrC,SAAgB;CACd,WAAW,EAAQ,OAAO,SAAS;CACnC,YAAY,EAAQ,QAAQ,SAAS;AACvC,EAAE,GAyBS,KAAsB,MACjC,QAA6B;CAC3B,IAAM,IAAQ,EAAQ,OAAO,SAAS,IAChC,IAAY,EAAQ,WAAW,OAC/B,IAAkB,OAAO,CAAS,GAClC,IAAiB,OAAO,SAAS,CAAe,KAAK,IAAkB,IAAI,IAAkB,MAC7F,IAAa,MAAmB,MAChC,IAAc,IAAa,GAAG,EAAM,OAAO,KAAK,MAAmB,IACnE,IAAQ,IAAa,EAAM,SAAS,IAAiB;CAE3D,OAAO;EACL,gBAAgB,IAAa,KAAS,IAAI;EAC1C,kBAAkB,IAAa,KAAS,MAAO,IAAQ,IAAI;EAC3D;CACF;AACF,CAAC,GAYU,KAAoB,GAAmC,IAAO,cACpE,IAED,EAAQ,iBAAuB,GAAG,EAAK,aAEvC,EAAQ,mBAAyB,GAAG,EAAK,eAEtC,IANc;AA4BvB,SAAgB,IAAmC;CACjD,IAAI,IAAQ;CAEZ,OAAO;EACL,aAAa;GACX,IAAQ;EACV;EACA,IAAI,UAAU;GACZ,OAAO;EACT;EACA,iBAAiB;GACf,IAAQ;EACV;CACF;AACF;AAqDA,IAAa,KAAwB,MAAyD;CAC5F,IAAM,IAAW,QAAe,EAAQ,EAAQ,UAAU,KAAM,GAC1D,IAAc,EAAe,QAAQ,GACrC,IAAU,EAAe,OAAO,GAChC,IAAoB,EAAuB;EAAE,OAAO,EAAQ;EAAO,QAAQ,EAAQ;CAAO,CAAC,GAE3F,IAAkB,QACtB,EAAkB,MAAM,aAAa,EAAkB,MAAM,aAAa,IAAc,IAC1F,GACM,IAAmB,QAAgB,EAAkB,MAAM,YAAY,IAAU,IAAK,GACtF,IAAc,QAA+B,EAAkB,MAAM,YAAY,SAAS,IAAK,GAEjG,IAA+C;CAsBnD,OAAO;EACL;EACA;EACA;EACA;EACA,kBA1BuB,MAAoD;GAC3E,IAAY;EACd;EAyBE;EACA;EACA,WAAW,QAAe,EAAkB,MAAM,SAAS;EAC3D,YAAY,QAAe,EAAkB,MAAM,UAAU;EAC7D,oBA3ByB,MAA4D;GACjF,MAAQ,YAAY,UAAU,GAElC;QAAI,CAAC,GAAW;KAId,EACE,2KACF;KAEA;IACF;IAEA,EAAU,eAAe;GAFzB;EAGF;CAaA;AACF,GAmDa,KAAoB,MAAiD;CAChF,IAAM,IAAU,EAAQ,MAAM,EAAe,EAAQ,UAAU,OAAO,GAChE,IAAU,EAAe,OAAO,GAChC,IAAS,EAAQ,SAAS,QAAe,KAAA,CAAS,GAMlD,IAAe,EAAQ,YAAY,QAAe,EAAQ,EAAO,KAAM;CAG7E,OAAO;EAAE,gBAFc,QAAgB,EAAa,QAAQ,IAAU,IAE7D;EAAgB;EAAS;EAAS;CAAa;AAC1D,GA6Ba,KAAe,OAAwC;CAClE,GAAG,EAAqB,CAAO;CAC/B,GAAG,EAAiB,CAAO;AAC7B"}
@@ -1,2 +1,2 @@
1
- function e(e){let t=null,n=null;return{applyInitialFocus(){n!==null&&(cancelAnimationFrame(n),n=null);let t=e.getInitialFocusSelector();if(t){let r=e.host.shadowRoot??e.host,i=null;try{i=r.querySelector(t)}catch{return}i&&(n=requestAnimationFrame(()=>{n=null,i.focus()}))}},cancelInitialFocus(){n!==null&&(cancelAnimationFrame(n),n=null)},captureReturnFocus(){t=document.activeElement},restoreFocus(){e.getReturnFocus()!==!1&&t&&(t.focus(),t=null)}}}exports.createFocusManager=e;
1
+ function e(e){let t=null,n=null,r=()=>{n!==null&&(cancelAnimationFrame(n),n=null)};return e.signal?.addEventListener(`abort`,r,{once:!0}),{applyInitialFocus(){r();let t=e.getInitialFocusSelector();if(t){let r=e.host.shadowRoot??e.host,i=null;try{i=r.querySelector(t)}catch{return}i&&(n=requestAnimationFrame(()=>{n=null,i.focus()}))}},cancelInitialFocus:r,captureReturnFocus(){t=document.activeElement},restoreFocus(){e.getReturnFocus()!==!1&&t&&(t.focus(),t=null)}}}exports.createFocusManager=e;
2
2
  //# sourceMappingURL=focus.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"focus.cjs","names":[],"sources":["../../src/headless/focus.ts"],"sourcesContent":["// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport type FocusManagerOptions = {\n /**\n * Returns the CSS selector for the element that should receive focus on open.\n *\n * **Security note:** this value is passed directly to `querySelector`. It must\n * come from trusted developer-controlled configuration, never from raw user input.\n */\n getInitialFocusSelector: () => string | undefined;\n /**\n * Returns whether focus should be restored to the previously focused element\n * on close. `undefined` or `true` means restore; `false` means skip.\n */\n getReturnFocus: () => boolean | undefined;\n /** Host element used for `querySelector` when resolving the initial focus target. */\n host: HTMLElement;\n};\n\nexport type FocusManager = {\n /** Move focus to the element matching `getInitialFocusSelector` (deferred one frame). */\n applyInitialFocus: () => void;\n /** Cancel a pending `applyInitialFocus` rAF, if any. */\n cancelInitialFocus: () => void;\n /** Capture the currently focused element so it can be restored later. */\n captureReturnFocus: () => void;\n /** Restore focus to the element captured by `captureReturnFocus`. */\n restoreFocus: () => void;\n};\n\n// ── Factory ───────────────────────────────────────────────────────────────────\n\n/**\n * Encapsulates the three-step focus lifecycle used by dialogs and drawers:\n * 1. Capture the element that triggered the open.\n * 2. Move focus inside the overlay (optionally to a specific element).\n * 3. Restore focus when the overlay closes.\n */\nexport function createFocusManager(options: FocusManagerOptions): FocusManager {\n let returnFocusEl: HTMLElement | null = null;\n let rafHandle: ReturnType<typeof requestAnimationFrame> | null = null;\n\n return {\n applyInitialFocus() {\n if (rafHandle !== null) {\n cancelAnimationFrame(rafHandle);\n rafHandle = null;\n }\n\n const selector = options.getInitialFocusSelector();\n\n if (selector) {\n // Query the shadow root first (all refine components render into Shadow DOM),\n // falling back to the host's light-DOM tree for non-Shadow contexts.\n const root = options.host.shadowRoot ?? options.host;\n\n let target: HTMLElement | null = null;\n\n try {\n target = root.querySelector<HTMLElement>(selector);\n } catch {\n // Malformed selector (SyntaxError) — skip initial focus rather than throw.\n return;\n }\n\n if (target) {\n rafHandle = requestAnimationFrame(() => {\n rafHandle = null;\n target.focus();\n });\n }\n }\n },\n\n cancelInitialFocus() {\n if (rafHandle !== null) {\n cancelAnimationFrame(rafHandle);\n rafHandle = null;\n }\n },\n\n captureReturnFocus() {\n returnFocusEl = document.activeElement as HTMLElement;\n },\n\n restoreFocus() {\n if (options.getReturnFocus() !== false && returnFocusEl) {\n returnFocusEl.focus();\n returnFocusEl = null;\n }\n },\n };\n}\n"],"mappings":"AAsCA,SAAgB,EAAmB,EAA4C,CAC7E,IAAI,EAAoC,KACpC,EAA6D,KAEjE,MAAO,CACL,mBAAoB,CACd,IAAc,OAChB,qBAAqB,CAAS,EAC9B,EAAY,MAGd,IAAM,EAAW,EAAQ,wBAAwB,EAEjD,GAAI,EAAU,CAGZ,IAAM,EAAO,EAAQ,KAAK,YAAc,EAAQ,KAE5C,EAA6B,KAEjC,GAAI,CACF,EAAS,EAAK,cAA2B,CAAQ,CACnD,MAAQ,CAEN,MACF,CAEI,IACF,EAAY,0BAA4B,CACtC,EAAY,KACZ,EAAO,MAAM,CACf,CAAC,EAEL,CACF,EAEA,oBAAqB,CACf,IAAc,OAChB,qBAAqB,CAAS,EAC9B,EAAY,KAEhB,EAEA,oBAAqB,CACnB,EAAgB,SAAS,aAC3B,EAEA,cAAe,CACT,EAAQ,eAAe,IAAM,IAAS,IACxC,EAAc,MAAM,EACpB,EAAgB,KAEpB,CACF,CACF"}
1
+ {"version":3,"file":"focus.cjs","names":[],"sources":["../../src/headless/focus.ts"],"sourcesContent":["// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport type FocusManagerOptions = {\n /**\n * Returns the CSS selector for the element that should receive focus on open.\n *\n * **Security note:** this value is passed directly to `querySelector`. It must\n * come from trusted developer-controlled configuration, never from raw user input.\n */\n getInitialFocusSelector: () => string | undefined;\n /**\n * Returns whether focus should be restored to the previously focused element\n * on close. `undefined` or `true` means restore; `false` means skip.\n */\n getReturnFocus: () => boolean | undefined;\n /** Host element used for `querySelector` when resolving the initial focus target. */\n host: HTMLElement;\n /**\n * `AbortSignal` from the component lifecycle. Cancels a pending `applyInitialFocus()`\n * animation frame automatically on abort — prevents a stray `.focus()` call landing on an\n * element that's been disconnected in the meantime (e.g. the dialog closes and unmounts\n * before the deferred focus frame runs).\n */\n signal?: AbortSignal;\n};\n\nexport type FocusManager = {\n /** Move focus to the element matching `getInitialFocusSelector` (deferred one frame). */\n applyInitialFocus: () => void;\n /** Cancel a pending `applyInitialFocus` rAF, if any. */\n cancelInitialFocus: () => void;\n /** Capture the currently focused element so it can be restored later. */\n captureReturnFocus: () => void;\n /** Restore focus to the element captured by `captureReturnFocus`. */\n restoreFocus: () => void;\n};\n\n// ── Factory ───────────────────────────────────────────────────────────────────\n\n/**\n * Encapsulates the three-step focus lifecycle used by dialogs and drawers:\n * 1. Capture the element that triggered the open.\n * 2. Move focus inside the overlay (optionally to a specific element).\n * 3. Restore focus when the overlay closes.\n */\nexport function createFocusManager(options: FocusManagerOptions): FocusManager {\n let returnFocusEl: HTMLElement | null = null;\n let rafHandle: ReturnType<typeof requestAnimationFrame> | null = null;\n\n const cancelInitialFocus = (): void => {\n if (rafHandle !== null) {\n cancelAnimationFrame(rafHandle);\n rafHandle = null;\n }\n };\n\n options.signal?.addEventListener('abort', cancelInitialFocus, { once: true });\n\n return {\n applyInitialFocus() {\n cancelInitialFocus();\n\n const selector = options.getInitialFocusSelector();\n\n if (selector) {\n // Query the shadow root first (all refine components render into Shadow DOM),\n // falling back to the host's light-DOM tree for non-Shadow contexts.\n const root = options.host.shadowRoot ?? options.host;\n\n let target: HTMLElement | null = null;\n\n try {\n target = root.querySelector<HTMLElement>(selector);\n } catch {\n // Malformed selector (SyntaxError) — skip initial focus rather than throw.\n return;\n }\n\n if (target) {\n rafHandle = requestAnimationFrame(() => {\n rafHandle = null;\n target.focus();\n });\n }\n }\n },\n\n cancelInitialFocus,\n\n captureReturnFocus() {\n returnFocusEl = document.activeElement as HTMLElement;\n },\n\n restoreFocus() {\n if (options.getReturnFocus() !== false && returnFocusEl) {\n returnFocusEl.focus();\n returnFocusEl = null;\n }\n },\n };\n}\n"],"mappings":"AA6CA,SAAgB,EAAmB,EAA4C,CAC7E,IAAI,EAAoC,KACpC,EAA6D,KAE3D,MAAiC,CACjC,IAAc,OAChB,qBAAqB,CAAS,EAC9B,EAAY,KAEhB,EAIA,OAFA,EAAQ,QAAQ,iBAAiB,QAAS,EAAoB,CAAE,KAAM,EAAK,CAAC,EAErE,CACL,mBAAoB,CAClB,EAAmB,EAEnB,IAAM,EAAW,EAAQ,wBAAwB,EAEjD,GAAI,EAAU,CAGZ,IAAM,EAAO,EAAQ,KAAK,YAAc,EAAQ,KAE5C,EAA6B,KAEjC,GAAI,CACF,EAAS,EAAK,cAA2B,CAAQ,CACnD,MAAQ,CAEN,MACF,CAEI,IACF,EAAY,0BAA4B,CACtC,EAAY,KACZ,EAAO,MAAM,CACf,CAAC,EAEL,CACF,EAEA,qBAEA,oBAAqB,CACnB,EAAgB,SAAS,aAC3B,EAEA,cAAe,CACT,EAAQ,eAAe,IAAM,IAAS,IACxC,EAAc,MAAM,EACpB,EAAgB,KAEpB,CACF,CACF"}
@@ -13,6 +13,13 @@ export type FocusManagerOptions = {
13
13
  getReturnFocus: () => boolean | undefined;
14
14
  /** Host element used for `querySelector` when resolving the initial focus target. */
15
15
  host: HTMLElement;
16
+ /**
17
+ * `AbortSignal` from the component lifecycle. Cancels a pending `applyInitialFocus()`
18
+ * animation frame automatically on abort — prevents a stray `.focus()` call landing on an
19
+ * element that's been disconnected in the meantime (e.g. the dialog closes and unmounts
20
+ * before the deferred focus frame runs).
21
+ */
22
+ signal?: AbortSignal;
16
23
  };
17
24
  export type FocusManager = {
18
25
  /** Move focus to the element matching `getInitialFocusSelector` (deferred one frame). */
@@ -1 +1 @@
1
- {"version":3,"file":"focus.d.ts","sourceRoot":"","sources":["../../src/headless/focus.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,mBAAmB,GAAG;IAChC;;;;;OAKG;IACH,uBAAuB,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAClD;;;OAGG;IACH,cAAc,EAAE,MAAM,OAAO,GAAG,SAAS,CAAC;IAC1C,qFAAqF;IACrF,IAAI,EAAE,WAAW,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,yFAAyF;IACzF,iBAAiB,EAAE,MAAM,IAAI,CAAC;IAC9B,wDAAwD;IACxD,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,yEAAyE;IACzE,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,qEAAqE;IACrE,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B,CAAC;AAIF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAsD7E"}
1
+ {"version":3,"file":"focus.d.ts","sourceRoot":"","sources":["../../src/headless/focus.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,mBAAmB,GAAG;IAChC;;;;;OAKG;IACH,uBAAuB,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAClD;;;OAGG;IACH,cAAc,EAAE,MAAM,OAAO,GAAG,SAAS,CAAC;IAC1C,qFAAqF;IACrF,IAAI,EAAE,WAAW,CAAC;IAClB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,yFAAyF;IACzF,iBAAiB,EAAE,MAAM,IAAI,CAAC;IAC9B,wDAAwD;IACxD,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,yEAAyE;IACzE,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,qEAAqE;IACrE,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B,CAAC;AAIF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAuD7E"}
@@ -1,9 +1,11 @@
1
1
  //#region src/headless/focus.ts
2
2
  function e(e) {
3
- let t = null, n = null;
4
- return {
3
+ let t = null, n = null, r = () => {
4
+ n !== null && (cancelAnimationFrame(n), n = null);
5
+ };
6
+ return e.signal?.addEventListener("abort", r, { once: !0 }), {
5
7
  applyInitialFocus() {
6
- n !== null && (cancelAnimationFrame(n), n = null);
8
+ r();
7
9
  let t = e.getInitialFocusSelector();
8
10
  if (t) {
9
11
  let r = e.host.shadowRoot ?? e.host, i = null;
@@ -17,9 +19,7 @@ function e(e) {
17
19
  }));
18
20
  }
19
21
  },
20
- cancelInitialFocus() {
21
- n !== null && (cancelAnimationFrame(n), n = null);
22
- },
22
+ cancelInitialFocus: r,
23
23
  captureReturnFocus() {
24
24
  t = document.activeElement;
25
25
  },
@@ -1 +1 @@
1
- {"version":3,"file":"focus.js","names":[],"sources":["../../src/headless/focus.ts"],"sourcesContent":["// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport type FocusManagerOptions = {\n /**\n * Returns the CSS selector for the element that should receive focus on open.\n *\n * **Security note:** this value is passed directly to `querySelector`. It must\n * come from trusted developer-controlled configuration, never from raw user input.\n */\n getInitialFocusSelector: () => string | undefined;\n /**\n * Returns whether focus should be restored to the previously focused element\n * on close. `undefined` or `true` means restore; `false` means skip.\n */\n getReturnFocus: () => boolean | undefined;\n /** Host element used for `querySelector` when resolving the initial focus target. */\n host: HTMLElement;\n};\n\nexport type FocusManager = {\n /** Move focus to the element matching `getInitialFocusSelector` (deferred one frame). */\n applyInitialFocus: () => void;\n /** Cancel a pending `applyInitialFocus` rAF, if any. */\n cancelInitialFocus: () => void;\n /** Capture the currently focused element so it can be restored later. */\n captureReturnFocus: () => void;\n /** Restore focus to the element captured by `captureReturnFocus`. */\n restoreFocus: () => void;\n};\n\n// ── Factory ───────────────────────────────────────────────────────────────────\n\n/**\n * Encapsulates the three-step focus lifecycle used by dialogs and drawers:\n * 1. Capture the element that triggered the open.\n * 2. Move focus inside the overlay (optionally to a specific element).\n * 3. Restore focus when the overlay closes.\n */\nexport function createFocusManager(options: FocusManagerOptions): FocusManager {\n let returnFocusEl: HTMLElement | null = null;\n let rafHandle: ReturnType<typeof requestAnimationFrame> | null = null;\n\n return {\n applyInitialFocus() {\n if (rafHandle !== null) {\n cancelAnimationFrame(rafHandle);\n rafHandle = null;\n }\n\n const selector = options.getInitialFocusSelector();\n\n if (selector) {\n // Query the shadow root first (all refine components render into Shadow DOM),\n // falling back to the host's light-DOM tree for non-Shadow contexts.\n const root = options.host.shadowRoot ?? options.host;\n\n let target: HTMLElement | null = null;\n\n try {\n target = root.querySelector<HTMLElement>(selector);\n } catch {\n // Malformed selector (SyntaxError) — skip initial focus rather than throw.\n return;\n }\n\n if (target) {\n rafHandle = requestAnimationFrame(() => {\n rafHandle = null;\n target.focus();\n });\n }\n }\n },\n\n cancelInitialFocus() {\n if (rafHandle !== null) {\n cancelAnimationFrame(rafHandle);\n rafHandle = null;\n }\n },\n\n captureReturnFocus() {\n returnFocusEl = document.activeElement as HTMLElement;\n },\n\n restoreFocus() {\n if (options.getReturnFocus() !== false && returnFocusEl) {\n returnFocusEl.focus();\n returnFocusEl = null;\n }\n },\n };\n}\n"],"mappings":";AAsCA,SAAgB,EAAmB,GAA4C;CAC7E,IAAI,IAAoC,MACpC,IAA6D;CAEjE,OAAO;EACL,oBAAoB;GAClB,AAAI,MAAc,SAChB,qBAAqB,CAAS,GAC9B,IAAY;GAGd,IAAM,IAAW,EAAQ,wBAAwB;GAEjD,IAAI,GAAU;IAGZ,IAAM,IAAO,EAAQ,KAAK,cAAc,EAAQ,MAE5C,IAA6B;IAEjC,IAAI;KACF,IAAS,EAAK,cAA2B,CAAQ;IACnD,QAAQ;KAEN;IACF;IAEA,AAAI,MACF,IAAY,4BAA4B;KAEtC,AADA,IAAY,MACZ,EAAO,MAAM;IACf,CAAC;GAEL;EACF;EAEA,qBAAqB;GACnB,AAAI,MAAc,SAChB,qBAAqB,CAAS,GAC9B,IAAY;EAEhB;EAEA,qBAAqB;GACnB,IAAgB,SAAS;EAC3B;EAEA,eAAe;GACb,AAAI,EAAQ,eAAe,MAAM,MAAS,MACxC,EAAc,MAAM,GACpB,IAAgB;EAEpB;CACF;AACF"}
1
+ {"version":3,"file":"focus.js","names":[],"sources":["../../src/headless/focus.ts"],"sourcesContent":["// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport type FocusManagerOptions = {\n /**\n * Returns the CSS selector for the element that should receive focus on open.\n *\n * **Security note:** this value is passed directly to `querySelector`. It must\n * come from trusted developer-controlled configuration, never from raw user input.\n */\n getInitialFocusSelector: () => string | undefined;\n /**\n * Returns whether focus should be restored to the previously focused element\n * on close. `undefined` or `true` means restore; `false` means skip.\n */\n getReturnFocus: () => boolean | undefined;\n /** Host element used for `querySelector` when resolving the initial focus target. */\n host: HTMLElement;\n /**\n * `AbortSignal` from the component lifecycle. Cancels a pending `applyInitialFocus()`\n * animation frame automatically on abort — prevents a stray `.focus()` call landing on an\n * element that's been disconnected in the meantime (e.g. the dialog closes and unmounts\n * before the deferred focus frame runs).\n */\n signal?: AbortSignal;\n};\n\nexport type FocusManager = {\n /** Move focus to the element matching `getInitialFocusSelector` (deferred one frame). */\n applyInitialFocus: () => void;\n /** Cancel a pending `applyInitialFocus` rAF, if any. */\n cancelInitialFocus: () => void;\n /** Capture the currently focused element so it can be restored later. */\n captureReturnFocus: () => void;\n /** Restore focus to the element captured by `captureReturnFocus`. */\n restoreFocus: () => void;\n};\n\n// ── Factory ───────────────────────────────────────────────────────────────────\n\n/**\n * Encapsulates the three-step focus lifecycle used by dialogs and drawers:\n * 1. Capture the element that triggered the open.\n * 2. Move focus inside the overlay (optionally to a specific element).\n * 3. Restore focus when the overlay closes.\n */\nexport function createFocusManager(options: FocusManagerOptions): FocusManager {\n let returnFocusEl: HTMLElement | null = null;\n let rafHandle: ReturnType<typeof requestAnimationFrame> | null = null;\n\n const cancelInitialFocus = (): void => {\n if (rafHandle !== null) {\n cancelAnimationFrame(rafHandle);\n rafHandle = null;\n }\n };\n\n options.signal?.addEventListener('abort', cancelInitialFocus, { once: true });\n\n return {\n applyInitialFocus() {\n cancelInitialFocus();\n\n const selector = options.getInitialFocusSelector();\n\n if (selector) {\n // Query the shadow root first (all refine components render into Shadow DOM),\n // falling back to the host's light-DOM tree for non-Shadow contexts.\n const root = options.host.shadowRoot ?? options.host;\n\n let target: HTMLElement | null = null;\n\n try {\n target = root.querySelector<HTMLElement>(selector);\n } catch {\n // Malformed selector (SyntaxError) — skip initial focus rather than throw.\n return;\n }\n\n if (target) {\n rafHandle = requestAnimationFrame(() => {\n rafHandle = null;\n target.focus();\n });\n }\n }\n },\n\n cancelInitialFocus,\n\n captureReturnFocus() {\n returnFocusEl = document.activeElement as HTMLElement;\n },\n\n restoreFocus() {\n if (options.getReturnFocus() !== false && returnFocusEl) {\n returnFocusEl.focus();\n returnFocusEl = null;\n }\n },\n };\n}\n"],"mappings":";AA6CA,SAAgB,EAAmB,GAA4C;CAC7E,IAAI,IAAoC,MACpC,IAA6D,MAE3D,UAAiC;EACrC,AAAI,MAAc,SAChB,qBAAqB,CAAS,GAC9B,IAAY;CAEhB;CAIA,OAFA,EAAQ,QAAQ,iBAAiB,SAAS,GAAoB,EAAE,MAAM,GAAK,CAAC,GAErE;EACL,oBAAoB;GAClB,EAAmB;GAEnB,IAAM,IAAW,EAAQ,wBAAwB;GAEjD,IAAI,GAAU;IAGZ,IAAM,IAAO,EAAQ,KAAK,cAAc,EAAQ,MAE5C,IAA6B;IAEjC,IAAI;KACF,IAAS,EAAK,cAA2B,CAAQ;IACnD,QAAQ;KAEN;IACF;IAEA,AAAI,MACF,IAAY,4BAA4B;KAEtC,AADA,IAAY,MACZ,EAAO,MAAM;IACf,CAAC;GAEL;EACF;EAEA;EAEA,qBAAqB;GACnB,IAAgB,SAAS;EAC3B;EAEA,eAAe;GACb,AAAI,EAAQ,eAAe,MAAM,MAAS,MACxC,EAAc,MAAM,GACpB,IAAgB;EAEpB;CACF;AACF"}
@@ -1,6 +1,8 @@
1
1
  export { lifecycleSignal } from './ore';
2
2
  export { createAutoResize, type AutoResizeControl, type AutoResizeOptions } from './auto-resize';
3
- export { counterClassName, createAssistiveState, createErrorHelperState, createField, createLabelState, type AssistiveStateHandle, type AssistiveStateOptions, type ControlValidationMode, type CounterOptions, type CounterState, type ErrorHelperOptions, type ErrorHelperState, type FieldHandle, type FieldOptions, type LabelPlacement, type LabelStateHandle, type LabelStateOptions, type ValidationTrigger, } from './field-base';
3
+ export { bindRefCallback, type RefCallback } from './ref-callback';
4
+ export { setAriaReflection, type AriaReflectionProperty } from './aria-reflection';
5
+ export { counterClassName, createAssistiveState, createDirtyTracker, createErrorHelperState, createField, createLabelState, type AssistiveStateHandle, type AssistiveStateOptions, type ControlValidationMode, type CounterOptions, type CounterState, type DirtyTracker, type ErrorHelperOptions, type ErrorHelperState, type FieldHandle, type FieldOptions, type LabelPlacement, type LabelStateHandle, type LabelStateOptions, type ValidationTrigger, } from './field-base';
4
6
  export { createTextField, type TextFieldDetach, type TextFieldHandle, type TextFieldOptions } from './text-field';
5
7
  export { createChoiceField, type ChoiceChangeDetail, type ChoiceFieldHandle, type ChoiceFieldOptions, } from './choice-field';
6
8
  export { createCheckable, type CheckableChangePayload, type CheckableHandle, type CheckableOptions } from './checkable';
@@ -14,7 +16,6 @@ export { createSliderControl, type SliderControl, type SliderControlOptions } fr
14
16
  export { createSwipeControl, type SwipeAxis, type SwipeControl, type SwipeControlDetail, type SwipeControlOptions, } from './swipe';
15
17
  export { announce, type AnnouncePoliteness, type AnnounceOptions } from './announcer';
16
18
  export { createDatePickerControl, formatDisplayDate, parseIso, toIsoString, type DateCell, type DatePickerControl, type DatePickerControlOptions, type DatePickerView, type MonthCell, type YearCell, } from './date-picker';
17
- export { createDialogFocusControl, type DialogFocusControl, type DialogFocusControlOptions } from './dialog-focus';
18
19
  export { getChoiceLabel, getLightChildrenByTag } from './light-dom';
19
20
  export { toFiniteNumber, toFiniteNumberOr, toPositiveStep } from './numbers';
20
21
  export { createDataGridControl, type DataGridColumn, type DataGridControl, type DataGridControlOptions, type SelectionMode, type SortDirection, type SortState, } from './datagrid';
@@ -25,6 +26,7 @@ export { createOverlayControl, type OverlayControlOptions } from './overlay';
25
26
  export { createSelectionControl, type SelectionControl, type SelectionControlOptions } from './selection-control';
26
27
  export { createSortControl, type SortControl, type SortControlOptions } from './sort-control';
27
28
  export { createTypeahead, type Typeahead, type TypeaheadOptions } from './typeahead';
29
+ export { elementDirection } from './direction';
28
30
  export { parseStringTriggers } from './parse';
29
31
  export { syncedSignal } from './signals';
30
32
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/headless/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/headless/index.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("./keyboard.cjs"),t=require("./typeahead.cjs");let n=require("@vielzeug/ripple");var r=(e,t,n)=>{for(let r=t;r<e.length;r++)if(n(e[r],r))return r;return-1},i=(e,t,n)=>{for(let r=t;r>=0;r--)if(n(e[r],r))return r;return-1},a={both:{first:[`Home`],last:[`End`],next:[`ArrowDown`,`ArrowRight`],prev:[`ArrowUp`,`ArrowLeft`]},horizontal:{first:[`Home`],last:[`End`],next:[`ArrowRight`],prev:[`ArrowLeft`]},vertical:{first:[`Home`],last:[`End`],next:[`ArrowDown`],prev:[`ArrowUp`]}},o=e=>typeof e==`object`&&!!e&&`disabled`in e,s=s=>{let c=(0,n.signal)(-1),l=(e,t)=>s.isItemDisabled?.(e,t)??(o(e)?e.disabled:!1),u=(e,t,n)=>(e>=0&&(c.value=e,s.onNavigate?.(t,e,n)),e),d=(e,t,n)=>n===`forward`?r(e,t,(e,t)=>!l(e,t)):i(e,t,(e,t)=>!l(e,t)),f=e=>{if(e<0)return g(),-1;let t=s.getItems();if(!t.length)return-1;let n=Math.min(e,t.length-1);return l(t[n],n)?-1:(c.value=n,n)},p=(e,t)=>{let n=s.getItems(),r=c.value;if(!n.length)return-1;let i=r<0?e===`forward`?0:n.length-1:e===`forward`?r+1:r-1,a=d(n,i,e),o=e===`forward`?`next`:`prev`;if(a>=0)return u(a,o,t);if(s.loop){let r=e===`forward`?0:n.length-1,i=d(n,r,e);if(i>=0)return u(i,o,t)}return r},m=e=>{let t=s.getItems();if(!t.length)return-1;if(e===`first`){let e=d(t,0,`forward`);return u(e,`first`)}if(e===`last`){let e=d(t,t.length-1,`backward`);return u(e,`last`)}return p(e===`next`?`forward`:`backward`)},h=()=>{let e=s.getItems(),t=c.value;return t>=0&&t<e.length?e[t]:void 0},g=()=>{c.value=-1},_=()=>!!s.disabled?.value,v=()=>typeof s.orientation==`function`?s.orientation():s.orientation??`vertical`,y=()=>{let e=s.keys,t=a[v()],n={first:e?.first??t.first,last:e?.last??t.last,next:e?.next??t.next,prev:e?.prev??t.prev},r={};for(let e of[`next`,`prev`,`first`,`last`])for(let t of n[e])r[t]=t=>{if(e===`first`||e===`last`){let n=s.getItems();if(!n.length)return;let r=e===`first`?d(n,0,`forward`):d(n,n.length-1,`backward`);u(r,e,t)}else p(e===`next`?`forward`:`backward`,t)};return r},b=s.getItemLabel?t.createTypeahead({getIndex:()=>c.value,getItemLabel:s.getItemLabel,getItems:s.getItems,isItemDisabled:(e,t)=>l(e,t),onNavigate:(e,t)=>{_()||(c.value=e,s.onNavigate?.(`typeahead`,e,t))}}):null,x=typeof s.orientation==`function`?null:y(),S=t=>{let n=x??y();return e.dispatchKeyboardAction(t,{disabled:s.disabled,keymap:n})?!0:b?.handleKeydown(t)??!1},C=()=>{b?.reset()};return s.signal&&s.signal.addEventListener(`abort`,C,{once:!0}),{focusedIndex:c,getActiveItem:h,handleKeydown:S,navigate:m,reset:g,set:f,[Symbol.dispose]:C}};exports.createListControl=s;
1
+ const e=require("./keyboard.cjs"),t=require("./typeahead.cjs");let n=require("@vielzeug/ripple");var r=(e,t,n)=>{for(let r=t;r<e.length;r++)if(n(e[r],r))return r;return-1},i=(e,t,n)=>{for(let r=t;r>=0;r--)if(n(e[r],r))return r;return-1},a={both:{first:[`Home`],last:[`End`],next:[`ArrowDown`,`ArrowRight`],prev:[`ArrowUp`,`ArrowLeft`]},horizontal:{first:[`Home`],last:[`End`],next:[`ArrowRight`],prev:[`ArrowLeft`]},vertical:{first:[`Home`],last:[`End`],next:[`ArrowDown`],prev:[`ArrowUp`]}},o={both:{first:[`Home`],last:[`End`],next:[`ArrowDown`,`ArrowLeft`],prev:[`ArrowUp`,`ArrowRight`]},horizontal:{first:[`Home`],last:[`End`],next:[`ArrowLeft`],prev:[`ArrowRight`]},vertical:a.vertical},s=e=>typeof e==`object`&&!!e&&`disabled`in e,c=c=>{let l=(0,n.signal)(-1),u=(e,t)=>c.isItemDisabled?.(e,t)??(s(e)?e.disabled:!1),d=(e,t,n)=>(e>=0&&(l.value=e,c.onNavigate?.(t,e,n)),e),f=(e,t,n)=>n===`forward`?r(e,t,(e,t)=>!u(e,t)):i(e,t,(e,t)=>!u(e,t)),p=e=>{if(e<0)return _(),-1;let t=c.getItems();if(!t.length)return-1;let n=Math.min(e,t.length-1);return u(t[n],n)?-1:(l.value=n,n)},m=(e,t)=>{let n=c.getItems(),r=l.value;if(!n.length)return-1;let i=r<0?e===`forward`?0:n.length-1:e===`forward`?r+1:r-1,a=f(n,i,e),o=e===`forward`?`next`:`prev`;if(a>=0)return d(a,o,t);if(c.loop){let r=e===`forward`?0:n.length-1,i=f(n,r,e);if(i>=0)return d(i,o,t)}return r},h=e=>{let t=c.getItems();if(!t.length)return-1;if(e===`first`){let e=f(t,0,`forward`);return d(e,`first`)}if(e===`last`){let e=f(t,t.length-1,`backward`);return d(e,`last`)}return m(e===`next`?`forward`:`backward`)},g=()=>{let e=c.getItems(),t=l.value;return t>=0&&t<e.length?e[t]:void 0},_=()=>{l.value=-1},v=()=>!!c.disabled?.value,y=()=>typeof c.orientation==`function`?c.orientation():c.orientation??`vertical`,b=()=>typeof c.direction==`function`?c.direction():c.direction??`ltr`,x=()=>{let e=c.keys,t=(b()===`rtl`?o:a)[y()],n={first:e?.first??t.first,last:e?.last??t.last,next:e?.next??t.next,prev:e?.prev??t.prev},r={};for(let e of[`next`,`prev`,`first`,`last`])for(let t of n[e])r[t]=t=>{if(e===`first`||e===`last`){let n=c.getItems();if(!n.length)return;let r=e===`first`?f(n,0,`forward`):f(n,n.length-1,`backward`);d(r,e,t)}else m(e===`next`?`forward`:`backward`,t)};return r},S=c.getItemLabel?t.createTypeahead({getIndex:()=>l.value,getItemLabel:c.getItemLabel,getItems:c.getItems,isItemDisabled:(e,t)=>u(e,t),onNavigate:(e,t)=>{v()||(l.value=e,c.onNavigate?.(`typeahead`,e,t))}}):null,C=typeof c.orientation==`function`||typeof c.direction==`function`?null:x(),w=t=>{let n=C??x();return e.dispatchKeyboardAction(t,{disabled:c.disabled,keymap:n})?!0:S?.handleKeydown(t)??!1},T=!1,E=()=>{T||(T=!0,S?.reset())};return c.signal&&c.signal.addEventListener(`abort`,E,{once:!0}),{dispose:E,get disposed(){return T},focusedIndex:l,getActiveItem:g,handleKeydown:w,navigate:h,reset:_,set:p,[Symbol.dispose]:E}};exports.createListControl=c;
2
2
  //# sourceMappingURL=nav.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"nav.cjs","names":[],"sources":["../../src/headless/nav.ts"],"sourcesContent":["import { type Readable, signal } from '@vielzeug/ripple';\n\nimport { dispatchKeyboardAction } from './keyboard';\nimport { createTypeahead } from './typeahead';\n\n// ── Private array search helpers ────────────────────────────────────────────\nconst findForward = <T>(items: T[], start: number, predicate: (item: T, index: number) => boolean): number => {\n for (let idx = start; idx < items.length; idx++) {\n if (predicate(items[idx], idx)) return idx;\n }\n\n return -1;\n};\n\nconst findBackward = <T>(items: T[], start: number, predicate: (item: T, index: number) => boolean): number => {\n for (let idx = start; idx >= 0; idx--) {\n if (predicate(items[idx], idx)) return idx;\n }\n\n return -1;\n};\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\n/**\n * Arrow-key and Home/End navigation actions. Separate from `ListKeyAction` so\n * keymap declarations never need `Exclude<ListKeyAction, 'typeahead'>`.\n */\nexport type ListNavigationAction = 'first' | 'last' | 'next' | 'prev';\n\n/**\n * All possible navigation action names, including `'typeahead'` (character-key\n * search). Used in `onNavigate` callbacks where both nav and typeahead are valid.\n */\nexport type ListKeyAction = ListNavigationAction | 'typeahead';\n\n/**\n * Orientation-aware default key bindings for list navigation.\n * - `'vertical'` (default): Up/Down arrows, Home/End\n * - `'horizontal'`: Left/Right arrows, Home/End\n * - `'both'`: all four arrow keys + Home/End\n */\nconst DEFAULT_KEYS: Record<'both' | 'horizontal' | 'vertical', Record<ListNavigationAction, string[]>> = {\n both: { first: ['Home'], last: ['End'], next: ['ArrowDown', 'ArrowRight'], prev: ['ArrowUp', 'ArrowLeft'] },\n horizontal: { first: ['Home'], last: ['End'], next: ['ArrowRight'], prev: ['ArrowLeft'] },\n vertical: { first: ['Home'], last: ['End'], next: ['ArrowDown'], prev: ['ArrowUp'] },\n};\n\nexport type ListNavigationOptions<T> = {\n disabled?: Readable<boolean | undefined>;\n /**\n * Returns a plain-text label for an item used by typeahead search.\n * When provided, pressing a printable character key jumps to the first enabled\n * item whose label starts with the accumulated type buffer (500 ms reset window).\n * Omit for contexts where the host element is a text input (e.g. combobox).\n */\n getItemLabel?: (item: T, index: number) => string;\n getItems: () => T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n /** Override default key bindings for navigation actions. */\n keys?: Partial<Record<ListNavigationAction, string[]>>;\n loop?: boolean;\n /**\n * Called when navigation lands on a new index.\n * `event` is the originating `KeyboardEvent`, or `undefined` for programmatic navigation.\n * Use to sync side-effects such as focusing a DOM element.\n */\n onNavigate?: (action: ListKeyAction, index: number, event?: KeyboardEvent) => void;\n /**\n * Axis the list navigates along. Controls which arrow keys are treated as\n * `next`/`prev` when no explicit `keys` override is provided.\n * Pass a getter function for dynamic/reactive orientation (e.g. driven by a prop signal).\n *\n * - `'vertical'` (default): ↑↓ arrows\n * - `'horizontal'`: ←→ arrows\n * - `'both'`: all four arrow keys\n */\n orientation?: 'both' | 'horizontal' | 'vertical' | (() => 'both' | 'horizontal' | 'vertical');\n /** `AbortSignal` from the component lifecycle. Disposes typeahead timer on abort. */\n signal?: AbortSignal;\n};\n\nexport type ListControl<T> = {\n [Symbol.dispose](): void;\n /** Current focused index. `-1` when nothing is focused. */\n readonly focusedIndex: Readable<number>;\n getActiveItem(): T | undefined;\n handleKeydown(event: KeyboardEvent): boolean;\n /**\n * Programmatically navigate to a named position.\n * Always fires `onNavigate` — consistent with keyboard navigation.\n * Returns the resolved index, or `-1` if no enabled item was found.\n */\n navigate(action: ListNavigationAction): number;\n /** Reset focused index to `-1`. */\n reset(): void;\n /**\n * Set focus to the item at `index`. Returns the resolved index.\n * Pass a negative index to clear focus (same as `reset()`).\n */\n set(index: number): number;\n};\n\nconst hasDisabledProp = (item: unknown): item is { disabled: boolean } =>\n typeof item === 'object' && item !== null && 'disabled' in item;\n\nexport const createListControl = <T>(options: ListNavigationOptions<T>): ListControl<T> => {\n const _index = signal(-1);\n\n const isItemDisabled = (item: T, index: number): boolean =>\n options.isItemDisabled?.(item, index) ?? (hasDisabledProp(item) ? item.disabled : false);\n\n const commitIndex = (idx: number, action: ListKeyAction, event?: KeyboardEvent): number => {\n if (idx >= 0) {\n _index.value = idx;\n options.onNavigate?.(action, idx, event);\n }\n\n return idx;\n };\n\n const findEnabledIndex = (items: T[], start: number, direction: 'forward' | 'backward'): number => {\n if (direction === 'forward') return findForward(items, start, (item, i) => !isItemDisabled(item, i));\n\n return findBackward(items, start, (item, i) => !isItemDisabled(item, i));\n };\n\n const set = (index: number): number => {\n if (index < 0) {\n reset();\n\n return -1;\n }\n\n const items = options.getItems();\n\n if (!items.length) return -1;\n\n const clamped = Math.min(index, items.length - 1);\n\n if (isItemDisabled(items[clamped], clamped)) return -1;\n\n _index.value = clamped;\n\n return clamped;\n };\n\n const move = (direction: 'forward' | 'backward', event?: KeyboardEvent): number => {\n const items = options.getItems();\n const current = _index.value;\n\n if (!items.length) return -1;\n\n const start: number =\n current < 0\n ? direction === 'forward'\n ? 0\n : items.length - 1\n : direction === 'forward'\n ? current + 1\n : current - 1;\n const idx = findEnabledIndex(items, start, direction);\n const action: ListNavigationAction = direction === 'forward' ? 'next' : 'prev';\n\n if (idx >= 0) return commitIndex(idx, action, event);\n\n if (options.loop) {\n const wrapStart = direction === 'forward' ? 0 : items.length - 1;\n const wrapped = findEnabledIndex(items, wrapStart, direction);\n\n if (wrapped >= 0) return commitIndex(wrapped, action, event);\n }\n\n return current;\n };\n\n const navigate = (action: ListNavigationAction): number => {\n const items = options.getItems();\n\n if (!items.length) return -1;\n\n if (action === 'first') {\n const idx = findEnabledIndex(items, 0, 'forward');\n\n return commitIndex(idx, 'first');\n }\n\n if (action === 'last') {\n const idx = findEnabledIndex(items, items.length - 1, 'backward');\n\n return commitIndex(idx, 'last');\n }\n\n return move(action === 'next' ? 'forward' : 'backward');\n };\n\n const getActiveItem = (): T | undefined => {\n const items = options.getItems();\n const index = _index.value;\n\n return index >= 0 && index < items.length ? items[index] : undefined;\n };\n\n const reset = (): void => {\n _index.value = -1;\n };\n\n // ── Keyboard handling ──────────────────────────────────────────────────────\n\n const isKeyDisabled = (): boolean => Boolean(options.disabled?.value);\n\n const resolveOrientation = (): 'both' | 'horizontal' | 'vertical' =>\n typeof options.orientation === 'function' ? options.orientation() : (options.orientation ?? 'vertical');\n\n const buildKeymap = (): Record<string, (keyboardEvent: KeyboardEvent) => void> => {\n const keys = options.keys;\n const orientationDefaults = DEFAULT_KEYS[resolveOrientation()];\n const resolved = {\n first: keys?.first ?? orientationDefaults.first,\n last: keys?.last ?? orientationDefaults.last,\n next: keys?.next ?? orientationDefaults.next,\n prev: keys?.prev ?? orientationDefaults.prev,\n };\n const km: Record<string, (keyboardEvent: KeyboardEvent) => void> = {};\n\n for (const action of ['next', 'prev', 'first', 'last'] as const) {\n for (const key of resolved[action]) {\n km[key] = (keyboardEvent: KeyboardEvent) => {\n if (action === 'first' || action === 'last') {\n const items = options.getItems();\n\n if (!items.length) return;\n\n const idx =\n action === 'first'\n ? findEnabledIndex(items, 0, 'forward')\n : findEnabledIndex(items, items.length - 1, 'backward');\n\n commitIndex(idx, action, keyboardEvent);\n } else {\n move(action === 'next' ? 'forward' : 'backward', keyboardEvent);\n }\n };\n }\n }\n\n return km;\n };\n\n // ── Typeahead ──────────────────────────────────────────────────────────────\n\n const typeahead = options.getItemLabel\n ? createTypeahead({\n getIndex: () => _index.value,\n getItemLabel: options.getItemLabel,\n getItems: options.getItems,\n isItemDisabled: (item, index) => isItemDisabled(item, index),\n onNavigate: (index, event) => {\n if (!isKeyDisabled()) {\n _index.value = index;\n options.onNavigate?.('typeahead', index, event);\n }\n },\n })\n : null;\n\n const _staticKeymap: Record<string, (e: KeyboardEvent) => void> | null =\n typeof options.orientation === 'function' ? null : buildKeymap();\n\n const handleKeydown = (event: KeyboardEvent): boolean => {\n const km = _staticKeymap ?? buildKeymap();\n\n if (dispatchKeyboardAction(event, { disabled: options.disabled, keymap: km })) return true;\n\n return typeahead?.handleKeydown(event) ?? false;\n };\n\n const dispose = (): void => {\n typeahead?.reset();\n };\n\n if (options.signal) {\n options.signal.addEventListener('abort', dispose, { once: true });\n }\n\n return {\n focusedIndex: _index,\n getActiveItem,\n handleKeydown,\n navigate,\n reset,\n set,\n [Symbol.dispose]: dispose,\n };\n};\n"],"mappings":"iGAMA,IAAM,GAAkB,EAAY,EAAe,IAA2D,CAC5G,IAAK,IAAI,EAAM,EAAO,EAAM,EAAM,OAAQ,IACxC,GAAI,EAAU,EAAM,GAAM,CAAG,EAAG,OAAO,EAGzC,MAAO,EACT,EAEM,GAAmB,EAAY,EAAe,IAA2D,CAC7G,IAAK,IAAI,EAAM,EAAO,GAAO,EAAG,IAC9B,GAAI,EAAU,EAAM,GAAM,CAAG,EAAG,OAAO,EAGzC,MAAO,EACT,EAsBM,EAAmG,CACvG,KAAM,CAAE,MAAO,CAAC,MAAM,EAAG,KAAM,CAAC,KAAK,EAAG,KAAM,CAAC,YAAa,YAAY,EAAG,KAAM,CAAC,UAAW,WAAW,CAAE,EAC1G,WAAY,CAAE,MAAO,CAAC,MAAM,EAAG,KAAM,CAAC,KAAK,EAAG,KAAM,CAAC,YAAY,EAAG,KAAM,CAAC,WAAW,CAAE,EACxF,SAAU,CAAE,MAAO,CAAC,MAAM,EAAG,KAAM,CAAC,KAAK,EAAG,KAAM,CAAC,WAAW,EAAG,KAAM,CAAC,SAAS,CAAE,CACrF,EAyDM,EAAmB,GACvB,OAAO,GAAS,YAAY,GAAiB,aAAc,EAEhD,EAAwB,GAAsD,CACzF,IAAM,GAAA,EAAA,EAAA,OAAA,CAAgB,EAAE,EAElB,GAAkB,EAAS,IAC/B,EAAQ,iBAAiB,EAAM,CAAK,IAAM,EAAgB,CAAI,EAAI,EAAK,SAAW,IAE9E,GAAe,EAAa,EAAuB,KACnD,GAAO,IACT,EAAO,MAAQ,EACf,EAAQ,aAAa,EAAQ,EAAK,CAAK,GAGlC,GAGH,GAAoB,EAAY,EAAe,IAC/C,IAAc,UAAkB,EAAY,EAAO,GAAQ,EAAM,IAAM,CAAC,EAAe,EAAM,CAAC,CAAC,EAE5F,EAAa,EAAO,GAAQ,EAAM,IAAM,CAAC,EAAe,EAAM,CAAC,CAAC,EAGnE,EAAO,GAA0B,CACrC,GAAI,EAAQ,EAGV,OAFA,EAAM,EAEC,GAGT,IAAM,EAAQ,EAAQ,SAAS,EAE/B,GAAI,CAAC,EAAM,OAAQ,MAAO,GAE1B,IAAM,EAAU,KAAK,IAAI,EAAO,EAAM,OAAS,CAAC,EAMhD,OAJI,EAAe,EAAM,GAAU,CAAO,EAAU,IAEpD,EAAO,MAAQ,EAER,EACT,EAEM,GAAQ,EAAmC,IAAkC,CACjF,IAAM,EAAQ,EAAQ,SAAS,EACzB,EAAU,EAAO,MAEvB,GAAI,CAAC,EAAM,OAAQ,MAAO,GAE1B,IAAM,EACJ,EAAU,EACN,IAAc,UACZ,EACA,EAAM,OAAS,EACjB,IAAc,UACZ,EAAU,EACV,EAAU,EACZ,EAAM,EAAiB,EAAO,EAAO,CAAS,EAC9C,EAA+B,IAAc,UAAY,OAAS,OAExE,GAAI,GAAO,EAAG,OAAO,EAAY,EAAK,EAAQ,CAAK,EAEnD,GAAI,EAAQ,KAAM,CAChB,IAAM,EAAY,IAAc,UAAY,EAAI,EAAM,OAAS,EACzD,EAAU,EAAiB,EAAO,EAAW,CAAS,EAE5D,GAAI,GAAW,EAAG,OAAO,EAAY,EAAS,EAAQ,CAAK,CAC7D,CAEA,OAAO,CACT,EAEM,EAAY,GAAyC,CACzD,IAAM,EAAQ,EAAQ,SAAS,EAE/B,GAAI,CAAC,EAAM,OAAQ,MAAO,GAE1B,GAAI,IAAW,QAAS,CACtB,IAAM,EAAM,EAAiB,EAAO,EAAG,SAAS,EAEhD,OAAO,EAAY,EAAK,OAAO,CACjC,CAEA,GAAI,IAAW,OAAQ,CACrB,IAAM,EAAM,EAAiB,EAAO,EAAM,OAAS,EAAG,UAAU,EAEhE,OAAO,EAAY,EAAK,MAAM,CAChC,CAEA,OAAO,EAAK,IAAW,OAAS,UAAY,UAAU,CACxD,EAEM,MAAqC,CACzC,IAAM,EAAQ,EAAQ,SAAS,EACzB,EAAQ,EAAO,MAErB,OAAO,GAAS,GAAK,EAAQ,EAAM,OAAS,EAAM,GAAS,IAAA,EAC7D,EAEM,MAAoB,CACxB,EAAO,MAAQ,EACjB,EAIM,MAA+B,EAAQ,EAAQ,UAAU,MAEzD,MACJ,OAAO,EAAQ,aAAgB,WAAa,EAAQ,YAAY,EAAK,EAAQ,aAAe,WAExF,MAA4E,CAChF,IAAM,EAAO,EAAQ,KACf,EAAsB,EAAa,EAAmB,GACtD,EAAW,CACf,MAAO,GAAM,OAAS,EAAoB,MAC1C,KAAM,GAAM,MAAQ,EAAoB,KACxC,KAAM,GAAM,MAAQ,EAAoB,KACxC,KAAM,GAAM,MAAQ,EAAoB,IAC1C,EACM,EAA6D,CAAC,EAEpE,IAAK,IAAM,IAAU,CAAC,OAAQ,OAAQ,QAAS,MAAM,EACnD,IAAK,IAAM,KAAO,EAAS,GACzB,EAAG,GAAQ,GAAiC,CAC1C,GAAI,IAAW,SAAW,IAAW,OAAQ,CAC3C,IAAM,EAAQ,EAAQ,SAAS,EAE/B,GAAI,CAAC,EAAM,OAAQ,OAEnB,IAAM,EACJ,IAAW,QACP,EAAiB,EAAO,EAAG,SAAS,EACpC,EAAiB,EAAO,EAAM,OAAS,EAAG,UAAU,EAE1D,EAAY,EAAK,EAAQ,CAAa,CACxC,MACE,EAAK,IAAW,OAAS,UAAY,WAAY,CAAa,CAElE,EAIJ,OAAO,CACT,EAIM,EAAY,EAAQ,aACtB,EAAA,gBAAgB,CACd,aAAgB,EAAO,MACvB,aAAc,EAAQ,aACtB,SAAU,EAAQ,SAClB,gBAAiB,EAAM,IAAU,EAAe,EAAM,CAAK,EAC3D,YAAa,EAAO,IAAU,CACvB,EAAc,IACjB,EAAO,MAAQ,EACf,EAAQ,aAAa,YAAa,EAAO,CAAK,EAElD,CACF,CAAC,EACD,KAEE,EACJ,OAAO,EAAQ,aAAgB,WAAa,KAAO,EAAY,EAE3D,EAAiB,GAAkC,CACvD,IAAM,EAAK,GAAiB,EAAY,EAIxC,OAFI,EAAA,uBAAuB,EAAO,CAAE,SAAU,EAAQ,SAAU,OAAQ,CAAG,CAAC,EAAU,GAE/E,GAAW,cAAc,CAAK,GAAK,EAC5C,EAEM,MAAsB,CAC1B,GAAW,MAAM,CACnB,EAMA,OAJI,EAAQ,QACV,EAAQ,OAAO,iBAAiB,QAAS,EAAS,CAAE,KAAM,EAAK,CAAC,EAG3D,CACL,aAAc,EACd,gBACA,gBACA,WACA,QACA,OACC,OAAO,SAAU,CACpB,CACF"}
1
+ {"version":3,"file":"nav.cjs","names":[],"sources":["../../src/headless/nav.ts"],"sourcesContent":["import { type Readable, signal } from '@vielzeug/ripple';\n\nimport { dispatchKeyboardAction } from './keyboard';\nimport { createTypeahead } from './typeahead';\n\n// ── Private array search helpers ────────────────────────────────────────────\nconst findForward = <T>(items: T[], start: number, predicate: (item: T, index: number) => boolean): number => {\n for (let idx = start; idx < items.length; idx++) {\n if (predicate(items[idx], idx)) return idx;\n }\n\n return -1;\n};\n\nconst findBackward = <T>(items: T[], start: number, predicate: (item: T, index: number) => boolean): number => {\n for (let idx = start; idx >= 0; idx--) {\n if (predicate(items[idx], idx)) return idx;\n }\n\n return -1;\n};\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\n/**\n * Arrow-key and Home/End navigation actions. Separate from `ListKeyAction` so\n * keymap declarations never need `Exclude<ListKeyAction, 'typeahead'>`.\n */\nexport type ListNavigationAction = 'first' | 'last' | 'next' | 'prev';\n\n/**\n * All possible navigation action names, including `'typeahead'` (character-key\n * search). Used in `onNavigate` callbacks where both nav and typeahead are valid.\n */\nexport type ListKeyAction = ListNavigationAction | 'typeahead';\n\n/**\n * Orientation-aware default key bindings for list navigation.\n * - `'vertical'` (default): Up/Down arrows, Home/End\n * - `'horizontal'`: Left/Right arrows, Home/End\n * - `'both'`: all four arrow keys + Home/End\n */\nconst DEFAULT_KEYS: Record<'both' | 'horizontal' | 'vertical', Record<ListNavigationAction, string[]>> = {\n both: { first: ['Home'], last: ['End'], next: ['ArrowDown', 'ArrowRight'], prev: ['ArrowUp', 'ArrowLeft'] },\n horizontal: { first: ['Home'], last: ['End'], next: ['ArrowRight'], prev: ['ArrowLeft'] },\n vertical: { first: ['Home'], last: ['End'], next: ['ArrowDown'], prev: ['ArrowUp'] },\n};\n\n/**\n * RTL mirror of `DEFAULT_KEYS`'s horizontal component (per WAI-ARIA APG — tabs/radiogroup\n * flip Left/Right in `dir=\"rtl\"` so \"next\" always advances in reading order, not screen-space\n * direction). Only the Left/Right bindings differ; Home/End and the vertical arrows are\n * direction-independent and stay identical to `DEFAULT_KEYS`.\n */\nconst DEFAULT_KEYS_RTL: Record<'both' | 'horizontal' | 'vertical', Record<ListNavigationAction, string[]>> = {\n both: { first: ['Home'], last: ['End'], next: ['ArrowDown', 'ArrowLeft'], prev: ['ArrowUp', 'ArrowRight'] },\n horizontal: { first: ['Home'], last: ['End'], next: ['ArrowLeft'], prev: ['ArrowRight'] },\n vertical: DEFAULT_KEYS.vertical,\n};\n\nexport type ListNavigationOptions<T> = {\n /**\n * Text direction, used to mirror the default Left/Right arrow-key bindings for\n * `'horizontal'`/`'both'` orientation (WAI-ARIA APG: `dir=\"rtl\"` flips which arrow key\n * means \"next\" vs \"prev\"). Pass a getter for dynamic/reactive direction (e.g. derived from\n * `elementDirection(getHost())`, since a `dir` ancestor attribute can change at runtime).\n * Has no effect when an explicit `keys` override is provided. Defaults to `'ltr'`.\n */\n direction?: 'ltr' | 'rtl' | (() => 'ltr' | 'rtl');\n disabled?: Readable<boolean | undefined>;\n /**\n * Returns a plain-text label for an item used by typeahead search.\n * When provided, pressing a printable character key jumps to the first enabled\n * item whose label starts with the accumulated type buffer (500 ms reset window).\n * Omit for contexts where the host element is a text input (e.g. combobox).\n */\n getItemLabel?: (item: T, index: number) => string;\n getItems: () => T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n /** Override default key bindings for navigation actions. */\n keys?: Partial<Record<ListNavigationAction, string[]>>;\n loop?: boolean;\n /**\n * Called when navigation lands on a new index.\n * `event` is the originating `KeyboardEvent`, or `undefined` for programmatic navigation.\n * Use to sync side-effects such as focusing a DOM element.\n */\n onNavigate?: (action: ListKeyAction, index: number, event?: KeyboardEvent) => void;\n /**\n * Axis the list navigates along. Controls which arrow keys are treated as\n * `next`/`prev` when no explicit `keys` override is provided.\n * Pass a getter function for dynamic/reactive orientation (e.g. driven by a prop signal).\n *\n * - `'vertical'` (default): ↑↓ arrows\n * - `'horizontal'`: ←→ arrows\n * - `'both'`: all four arrow keys\n */\n orientation?: 'both' | 'horizontal' | 'vertical' | (() => 'both' | 'horizontal' | 'vertical');\n /** `AbortSignal` from the component lifecycle. Disposes typeahead timer on abort. */\n signal?: AbortSignal;\n};\n\nexport type ListControl<T> = {\n [Symbol.dispose](): void;\n /** Disposes the internal typeahead timer. Alias for `dispose()`. Conforms to the monorepo disposal convention. */\n dispose(): void;\n /** `true` after `dispose()` has been called. */\n readonly disposed: boolean;\n /** Current focused index. `-1` when nothing is focused. */\n readonly focusedIndex: Readable<number>;\n getActiveItem(): T | undefined;\n handleKeydown(event: KeyboardEvent): boolean;\n /**\n * Programmatically navigate to a named position.\n * Always fires `onNavigate` — consistent with keyboard navigation.\n * Returns the resolved index, or `-1` if no enabled item was found.\n */\n navigate(action: ListNavigationAction): number;\n /** Reset focused index to `-1`. */\n reset(): void;\n /**\n * Set focus to the item at `index`. Returns the resolved index.\n * Pass a negative index to clear focus (same as `reset()`).\n */\n set(index: number): number;\n};\n\nconst hasDisabledProp = (item: unknown): item is { disabled: boolean } =>\n typeof item === 'object' && item !== null && 'disabled' in item;\n\nexport const createListControl = <T>(options: ListNavigationOptions<T>): ListControl<T> => {\n const _index = signal(-1);\n\n const isItemDisabled = (item: T, index: number): boolean =>\n options.isItemDisabled?.(item, index) ?? (hasDisabledProp(item) ? item.disabled : false);\n\n const commitIndex = (idx: number, action: ListKeyAction, event?: KeyboardEvent): number => {\n if (idx >= 0) {\n _index.value = idx;\n options.onNavigate?.(action, idx, event);\n }\n\n return idx;\n };\n\n const findEnabledIndex = (items: T[], start: number, direction: 'forward' | 'backward'): number => {\n if (direction === 'forward') return findForward(items, start, (item, i) => !isItemDisabled(item, i));\n\n return findBackward(items, start, (item, i) => !isItemDisabled(item, i));\n };\n\n const set = (index: number): number => {\n if (index < 0) {\n reset();\n\n return -1;\n }\n\n const items = options.getItems();\n\n if (!items.length) return -1;\n\n const clamped = Math.min(index, items.length - 1);\n\n if (isItemDisabled(items[clamped], clamped)) return -1;\n\n _index.value = clamped;\n\n return clamped;\n };\n\n const move = (direction: 'forward' | 'backward', event?: KeyboardEvent): number => {\n const items = options.getItems();\n const current = _index.value;\n\n if (!items.length) return -1;\n\n const start: number =\n current < 0\n ? direction === 'forward'\n ? 0\n : items.length - 1\n : direction === 'forward'\n ? current + 1\n : current - 1;\n const idx = findEnabledIndex(items, start, direction);\n const action: ListNavigationAction = direction === 'forward' ? 'next' : 'prev';\n\n if (idx >= 0) return commitIndex(idx, action, event);\n\n if (options.loop) {\n const wrapStart = direction === 'forward' ? 0 : items.length - 1;\n const wrapped = findEnabledIndex(items, wrapStart, direction);\n\n if (wrapped >= 0) return commitIndex(wrapped, action, event);\n }\n\n return current;\n };\n\n const navigate = (action: ListNavigationAction): number => {\n const items = options.getItems();\n\n if (!items.length) return -1;\n\n if (action === 'first') {\n const idx = findEnabledIndex(items, 0, 'forward');\n\n return commitIndex(idx, 'first');\n }\n\n if (action === 'last') {\n const idx = findEnabledIndex(items, items.length - 1, 'backward');\n\n return commitIndex(idx, 'last');\n }\n\n return move(action === 'next' ? 'forward' : 'backward');\n };\n\n const getActiveItem = (): T | undefined => {\n const items = options.getItems();\n const index = _index.value;\n\n return index >= 0 && index < items.length ? items[index] : undefined;\n };\n\n const reset = (): void => {\n _index.value = -1;\n };\n\n // ── Keyboard handling ──────────────────────────────────────────────────────\n\n const isKeyDisabled = (): boolean => Boolean(options.disabled?.value);\n\n const resolveOrientation = (): 'both' | 'horizontal' | 'vertical' =>\n typeof options.orientation === 'function' ? options.orientation() : (options.orientation ?? 'vertical');\n\n const resolveDirection = (): 'ltr' | 'rtl' =>\n typeof options.direction === 'function' ? options.direction() : (options.direction ?? 'ltr');\n\n const buildKeymap = (): Record<string, (keyboardEvent: KeyboardEvent) => void> => {\n const keys = options.keys;\n const keyTable = resolveDirection() === 'rtl' ? DEFAULT_KEYS_RTL : DEFAULT_KEYS;\n const orientationDefaults = keyTable[resolveOrientation()];\n const resolved = {\n first: keys?.first ?? orientationDefaults.first,\n last: keys?.last ?? orientationDefaults.last,\n next: keys?.next ?? orientationDefaults.next,\n prev: keys?.prev ?? orientationDefaults.prev,\n };\n const km: Record<string, (keyboardEvent: KeyboardEvent) => void> = {};\n\n for (const action of ['next', 'prev', 'first', 'last'] as const) {\n for (const key of resolved[action]) {\n km[key] = (keyboardEvent: KeyboardEvent) => {\n if (action === 'first' || action === 'last') {\n const items = options.getItems();\n\n if (!items.length) return;\n\n const idx =\n action === 'first'\n ? findEnabledIndex(items, 0, 'forward')\n : findEnabledIndex(items, items.length - 1, 'backward');\n\n commitIndex(idx, action, keyboardEvent);\n } else {\n move(action === 'next' ? 'forward' : 'backward', keyboardEvent);\n }\n };\n }\n }\n\n return km;\n };\n\n // ── Typeahead ──────────────────────────────────────────────────────────────\n\n const typeahead = options.getItemLabel\n ? createTypeahead({\n getIndex: () => _index.value,\n getItemLabel: options.getItemLabel,\n getItems: options.getItems,\n isItemDisabled: (item, index) => isItemDisabled(item, index),\n onNavigate: (index, event) => {\n if (!isKeyDisabled()) {\n _index.value = index;\n options.onNavigate?.('typeahead', index, event);\n }\n },\n })\n : null;\n\n // Keymap depends on both orientation and direction — if either is dynamic, rebuild on every\n // keydown instead of caching once.\n const isDynamicKeymap = typeof options.orientation === 'function' || typeof options.direction === 'function';\n const _staticKeymap: Record<string, (e: KeyboardEvent) => void> | null = isDynamicKeymap ? null : buildKeymap();\n\n const handleKeydown = (event: KeyboardEvent): boolean => {\n const km = _staticKeymap ?? buildKeymap();\n\n if (dispatchKeyboardAction(event, { disabled: options.disabled, keymap: km })) return true;\n\n return typeahead?.handleKeydown(event) ?? false;\n };\n\n let disposed = false;\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n typeahead?.reset();\n };\n\n if (options.signal) {\n options.signal.addEventListener('abort', dispose, { once: true });\n }\n\n return {\n dispose,\n get disposed() {\n return disposed;\n },\n focusedIndex: _index,\n getActiveItem,\n handleKeydown,\n navigate,\n reset,\n set,\n [Symbol.dispose]: dispose,\n };\n};\n"],"mappings":"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"}
@@ -10,6 +10,14 @@ export type ListNavigationAction = 'first' | 'last' | 'next' | 'prev';
10
10
  */
11
11
  export type ListKeyAction = ListNavigationAction | 'typeahead';
12
12
  export type ListNavigationOptions<T> = {
13
+ /**
14
+ * Text direction, used to mirror the default Left/Right arrow-key bindings for
15
+ * `'horizontal'`/`'both'` orientation (WAI-ARIA APG: `dir="rtl"` flips which arrow key
16
+ * means "next" vs "prev"). Pass a getter for dynamic/reactive direction (e.g. derived from
17
+ * `elementDirection(getHost())`, since a `dir` ancestor attribute can change at runtime).
18
+ * Has no effect when an explicit `keys` override is provided. Defaults to `'ltr'`.
19
+ */
20
+ direction?: 'ltr' | 'rtl' | (() => 'ltr' | 'rtl');
13
21
  disabled?: Readable<boolean | undefined>;
14
22
  /**
15
23
  * Returns a plain-text label for an item used by typeahead search.
@@ -44,6 +52,10 @@ export type ListNavigationOptions<T> = {
44
52
  };
45
53
  export type ListControl<T> = {
46
54
  [Symbol.dispose](): void;
55
+ /** Disposes the internal typeahead timer. Alias for `dispose()`. Conforms to the monorepo disposal convention. */
56
+ dispose(): void;
57
+ /** `true` after `dispose()` has been called. */
58
+ readonly disposed: boolean;
47
59
  /** Current focused index. `-1` when nothing is focused. */
48
60
  readonly focusedIndex: Readable<number>;
49
61
  getActiveItem(): T | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"nav.d.ts","sourceRoot":"","sources":["../../src/headless/nav.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAU,MAAM,kBAAkB,CAAC;AAwBzD;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEtE;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,oBAAoB,GAAG,WAAW,CAAC;AAc/D,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI;IACrC,QAAQ,CAAC,EAAE,QAAQ,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACzC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;IACpB,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,4DAA4D;IAC5D,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IACvD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;IACnF;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,UAAU,GAAG,CAAC,MAAM,MAAM,GAAG,YAAY,GAAG,UAAU,CAAC,CAAC;IAC9F,qFAAqF;IACrF,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC3B,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;IACzB,2DAA2D;IAC3D,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxC,aAAa,IAAI,CAAC,GAAG,SAAS,CAAC;IAC/B,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAAC;IAC/C,mCAAmC;IACnC,KAAK,IAAI,IAAI,CAAC;IACd;;;OAGG;IACH,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CAC5B,CAAC;AAKF,eAAO,MAAM,iBAAiB,GAAI,CAAC,EAAE,SAAS,qBAAqB,CAAC,CAAC,CAAC,KAAG,WAAW,CAAC,CAAC,CA4LrF,CAAC"}
1
+ {"version":3,"file":"nav.d.ts","sourceRoot":"","sources":["../../src/headless/nav.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAU,MAAM,kBAAkB,CAAC;AAwBzD;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEtE;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,oBAAoB,GAAG,WAAW,CAAC;AA0B/D,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI;IACrC;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,CAAC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC;IAClD,QAAQ,CAAC,EAAE,QAAQ,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACzC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;IACpB,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,4DAA4D;IAC5D,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IACvD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;IACnF;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,UAAU,GAAG,CAAC,MAAM,MAAM,GAAG,YAAY,GAAG,UAAU,CAAC,CAAC;IAC9F,qFAAqF;IACrF,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC3B,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;IACzB,kHAAkH;IAClH,OAAO,IAAI,IAAI,CAAC;IAChB,gDAAgD;IAChD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,2DAA2D;IAC3D,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxC,aAAa,IAAI,CAAC,GAAG,SAAS,CAAC;IAC/B,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAAC;IAC/C,mCAAmC;IACnC,KAAK,IAAI,IAAI,CAAC;IACd;;;OAGG;IACH,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CAC5B,CAAC;AAKF,eAAO,MAAM,iBAAiB,GAAI,CAAC,EAAE,SAAS,qBAAqB,CAAC,CAAC,CAAC,KAAG,WAAW,CAAC,CAAC,CA2MrF,CAAC"}