@eifi1/ui-kit 0.34.0 → 0.35.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 (241) hide show
  1. package/README.md +82 -38
  2. package/dist/billing/billing-banner.d.ts +5 -2
  3. package/dist/billing/billing-banner.js.map +1 -1
  4. package/dist/billing/plan-limit-notice.d.ts +5 -2
  5. package/dist/billing/plan-limit-notice.js.map +1 -1
  6. package/dist/chart.js.map +1 -1
  7. package/dist/components/amount-input.d.ts +0 -3
  8. package/dist/components/amount-input.js +2 -2
  9. package/dist/components/amount-input.js.map +1 -1
  10. package/dist/components/calculator.d.ts +1 -4
  11. package/dist/components/calculator.js +1 -3
  12. package/dist/components/calculator.js.map +1 -1
  13. package/dist/components/chart-zoom.d.ts +0 -37
  14. package/dist/components/chart-zoom.js +1 -28
  15. package/dist/components/chart-zoom.js.map +1 -1
  16. package/dist/components/chip-input.d.ts +0 -4
  17. package/dist/components/chip-input.js +1 -5
  18. package/dist/components/chip-input.js.map +1 -1
  19. package/dist/components/combobox.d.ts +1 -1
  20. package/dist/components/combobox.js.map +1 -1
  21. package/dist/components/copy-button.d.ts +5 -1
  22. package/dist/components/copy-button.js.map +1 -1
  23. package/dist/components/currency-option-list.d.ts +3 -8
  24. package/dist/components/currency-option-list.js +1 -29
  25. package/dist/components/currency-option-list.js.map +1 -1
  26. package/dist/components/danger-confirm.d.ts +3 -3
  27. package/dist/components/danger-confirm.js.map +1 -1
  28. package/dist/components/data-table-filter-popover.d.ts +1 -1
  29. package/dist/components/data-table-filter-popover.js +1 -1
  30. package/dist/components/data-table-filter-popover.js.map +1 -1
  31. package/dist/components/data-table-labels.d.ts +1 -42
  32. package/dist/components/data-table-labels.js +2 -31
  33. package/dist/components/data-table-labels.js.map +1 -1
  34. package/dist/components/data-table-shared.d.ts +17 -1
  35. package/dist/components/data-table-shared.js +22 -0
  36. package/dist/components/data-table-shared.js.map +1 -1
  37. package/dist/components/data-table.d.ts +12 -2
  38. package/dist/components/data-table.js +18 -14
  39. package/dist/components/data-table.js.map +1 -1
  40. package/dist/components/dialog-frame.d.ts +3 -1
  41. package/dist/components/dialog-frame.js.map +1 -1
  42. package/dist/components/dropdown.d.ts +18 -15
  43. package/dist/components/dropdown.js +11 -3
  44. package/dist/components/dropdown.js.map +1 -1
  45. package/dist/components/entity-combobox.d.ts +1 -1
  46. package/dist/components/entity-combobox.js.map +1 -1
  47. package/dist/components/field-base.d.ts +0 -5
  48. package/dist/components/field-base.js +0 -5
  49. package/dist/components/field-base.js.map +1 -1
  50. package/dist/components/field-parts.d.ts +3 -11
  51. package/dist/components/field-parts.js +1 -9
  52. package/dist/components/field-parts.js.map +1 -1
  53. package/dist/components/field-strip.d.ts +8 -23
  54. package/dist/components/field-strip.js +24 -22
  55. package/dist/components/field-strip.js.map +1 -1
  56. package/dist/components/field-sync.d.ts +0 -4
  57. package/dist/components/field-sync.js +0 -4
  58. package/dist/components/field-sync.js.map +1 -1
  59. package/dist/components/field.d.ts +4 -4
  60. package/dist/components/field.js.map +1 -1
  61. package/dist/components/full-bleed-dialog.js +2 -2
  62. package/dist/components/full-bleed-dialog.js.map +1 -1
  63. package/dist/components/hover-menu.d.ts +6 -13
  64. package/dist/components/hover-menu.js +3 -3
  65. package/dist/components/hover-menu.js.map +1 -1
  66. package/dist/components/inline-entity-combobox.d.ts +1 -1
  67. package/dist/components/inline-entity-combobox.js.map +1 -1
  68. package/dist/components/modal.d.ts +9 -1
  69. package/dist/components/modal.js +7 -4
  70. package/dist/components/modal.js.map +1 -1
  71. package/dist/components/multi-select.d.ts +5 -16
  72. package/dist/components/multi-select.js +5 -10
  73. package/dist/components/multi-select.js.map +1 -1
  74. package/dist/components/number-input.d.ts +0 -3
  75. package/dist/components/number-input.js +2 -4
  76. package/dist/components/number-input.js.map +1 -1
  77. package/dist/components/popover.js +2 -2
  78. package/dist/components/popover.js.map +1 -1
  79. package/dist/components/series-chart.js.map +1 -1
  80. package/dist/components/server-wake.d.ts +0 -6
  81. package/dist/components/server-wake.js.map +1 -1
  82. package/dist/components/settings-fields.d.ts +4 -2
  83. package/dist/components/settings-fields.js.map +1 -1
  84. package/dist/components/stat-tile.d.ts +8 -2
  85. package/dist/components/stat-tile.js.map +1 -1
  86. package/dist/components/swipeable-row.d.ts +2 -17
  87. package/dist/components/swipeable-row.js +11 -16
  88. package/dist/components/swipeable-row.js.map +1 -1
  89. package/dist/components/tooltip.d.ts +9 -1
  90. package/dist/components/tooltip.js +20 -5
  91. package/dist/components/tooltip.js.map +1 -1
  92. package/dist/components/translation-review.d.ts +10 -2
  93. package/dist/components/translation-review.js +11 -3
  94. package/dist/components/translation-review.js.map +1 -1
  95. package/dist/components/use-table-state.d.ts +31 -3
  96. package/dist/components/use-table-state.js +53 -8
  97. package/dist/components/use-table-state.js.map +1 -1
  98. package/dist/components/user-avatar.d.ts +2 -2
  99. package/dist/components/user-avatar.js.map +1 -1
  100. package/dist/components/wizard-stepper.d.ts +1 -4
  101. package/dist/components/wizard-stepper.js +2 -3
  102. package/dist/components/wizard-stepper.js.map +1 -1
  103. package/dist/currencies.d.ts +23 -0
  104. package/dist/currencies.js +38 -0
  105. package/dist/currencies.js.map +1 -0
  106. package/dist/demo/demo-banner.d.ts +7 -2
  107. package/dist/demo/demo-banner.js.map +1 -1
  108. package/dist/eslint/index.d.ts +83 -0
  109. package/dist/eslint/index.js +69 -0
  110. package/dist/eslint/index.js.map +1 -0
  111. package/dist/eslint/role-utilities.d.ts +48 -0
  112. package/dist/eslint/role-utilities.generated.d.ts +2 -0
  113. package/dist/eslint/role-utilities.generated.js +143 -0
  114. package/dist/eslint/role-utilities.generated.js.map +1 -0
  115. package/dist/eslint/role-utilities.js +43 -0
  116. package/dist/eslint/role-utilities.js.map +1 -0
  117. package/dist/feedback/feedback-menu.d.ts +2 -2
  118. package/dist/feedback/feedback-menu.js.map +1 -1
  119. package/dist/feedback/feedback-thread.d.ts +3 -8
  120. package/dist/feedback/feedback-thread.js +0 -2
  121. package/dist/feedback/feedback-thread.js.map +1 -1
  122. package/dist/hooks/use-dismiss.d.ts +16 -3
  123. package/dist/hooks/use-dismiss.js +14 -2
  124. package/dist/hooks/use-dismiss.js.map +1 -1
  125. package/dist/i18n/kit-labels.d.ts +14 -4
  126. package/dist/i18n/kit-labels.js.map +1 -1
  127. package/dist/i18n/locales/en.js +1 -1
  128. package/dist/i18n/locales/en.js.map +2 -2
  129. package/dist/i18n/locales/fr.js +3 -2
  130. package/dist/i18n/locales/fr.js.map +1 -1
  131. package/dist/i18n/locales/it.js +3 -2
  132. package/dist/i18n/locales/it.js.map +1 -1
  133. package/dist/index.d.ts +2 -1
  134. package/dist/index.js +2 -2
  135. package/dist/index.js.map +1 -1
  136. package/dist/lib/refs.d.ts +13 -0
  137. package/dist/lib/refs.js +15 -0
  138. package/dist/lib/refs.js.map +1 -0
  139. package/dist/rhf/fields.d.ts +10 -10
  140. package/dist/rhf/fields.js.map +1 -1
  141. package/dist/rhf/form-core.d.ts +5 -4
  142. package/dist/rhf/form-core.js.map +1 -1
  143. package/dist/rhf/form.d.ts +7 -36
  144. package/dist/rhf/form.js +1 -16
  145. package/dist/rhf/form.js.map +1 -1
  146. package/dist/rhf/line-items.d.ts +1 -1
  147. package/dist/rhf/line-items.js.map +1 -1
  148. package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
  149. package/dist/rhf.js.map +1 -1
  150. package/dist/shell/option-switcher-menu.d.ts +5 -17
  151. package/dist/shell/option-switcher-menu.js +1 -3
  152. package/dist/shell/option-switcher-menu.js.map +1 -1
  153. package/dist/shell/role-switcher.d.ts +1 -4
  154. package/dist/shell/role-switcher.js +2 -3
  155. package/dist/shell/role-switcher.js.map +1 -1
  156. package/dist/shell/topbar-action-menu.d.ts +7 -19
  157. package/dist/shell/topbar-action-menu.js +1 -3
  158. package/dist/shell/topbar-action-menu.js.map +1 -1
  159. package/dist/shell/topbar-controls.d.ts +3 -12
  160. package/dist/shell/topbar-controls.js +6 -9
  161. package/dist/shell/topbar-controls.js.map +1 -1
  162. package/dist/testing/index.d.ts +1 -1
  163. package/dist/testing/index.js +2 -0
  164. package/dist/testing/index.js.map +1 -1
  165. package/dist/testing/viewport.d.ts +25 -2
  166. package/dist/testing/viewport.js +61 -10
  167. package/dist/testing/viewport.js.map +1 -1
  168. package/dist/theme/palette-presets.d.ts +0 -7
  169. package/dist/theme/palette-presets.js +0 -279
  170. package/dist/theme/palette-presets.js.map +1 -1
  171. package/dist/wizard/types.d.ts +0 -5
  172. package/dist/wizard/types.js +1 -5
  173. package/dist/wizard/types.js.map +1 -1
  174. package/package.json +9 -1
  175. package/src/billing/billing-banner.tsx +5 -2
  176. package/src/billing/plan-limit-notice.tsx +5 -2
  177. package/src/chart.ts +5 -3
  178. package/src/components/amount-input.tsx +2 -6
  179. package/src/components/calculator.tsx +2 -7
  180. package/src/components/chart-zoom.tsx +4 -47
  181. package/src/components/chip-input.tsx +0 -7
  182. package/src/components/combobox.tsx +1 -1
  183. package/src/components/copy-button.tsx +5 -1
  184. package/src/components/currency-option-list.tsx +8 -42
  185. package/src/components/danger-confirm.tsx +3 -3
  186. package/src/components/data-table-filter-popover.tsx +2 -1
  187. package/src/components/data-table-labels.ts +2 -84
  188. package/src/components/data-table-shared.tsx +55 -2
  189. package/src/components/data-table.tsx +29 -17
  190. package/src/components/dialog-frame.tsx +3 -1
  191. package/src/components/dropdown.tsx +42 -18
  192. package/src/components/entity-combobox.tsx +1 -1
  193. package/src/components/field-base.tsx +0 -11
  194. package/src/components/field-parts.tsx +5 -19
  195. package/src/components/field-strip.tsx +9 -25
  196. package/src/components/field-sync.tsx +0 -7
  197. package/src/components/field.tsx +4 -4
  198. package/src/components/full-bleed-dialog.tsx +4 -3
  199. package/src/components/hover-menu.tsx +12 -16
  200. package/src/components/inline-entity-combobox.tsx +1 -1
  201. package/src/components/modal.tsx +20 -4
  202. package/src/components/multi-select.tsx +13 -30
  203. package/src/components/number-input.tsx +3 -8
  204. package/src/components/popover.tsx +5 -3
  205. package/src/components/series-chart.tsx +1 -1
  206. package/src/components/server-wake.tsx +0 -6
  207. package/src/components/settings-fields.tsx +4 -2
  208. package/src/components/stat-tile.tsx +8 -2
  209. package/src/components/swipeable-row.tsx +15 -37
  210. package/src/components/tooltip.tsx +37 -6
  211. package/src/components/translation-review.tsx +25 -7
  212. package/src/components/use-table-state.ts +91 -10
  213. package/src/components/user-avatar.tsx +2 -2
  214. package/src/components/wizard-stepper.tsx +2 -6
  215. package/src/currencies.ts +70 -0
  216. package/src/demo/demo-banner.tsx +7 -2
  217. package/src/eslint/index.ts +161 -0
  218. package/src/eslint/role-utilities.generated.ts +146 -0
  219. package/src/eslint/role-utilities.ts +99 -0
  220. package/src/feedback/feedback-menu.tsx +2 -2
  221. package/src/feedback/feedback-thread.tsx +3 -8
  222. package/src/hooks/use-dismiss.ts +39 -4
  223. package/src/i18n/kit-labels.tsx +15 -5
  224. package/src/i18n/locales/fr.ts +3 -2
  225. package/src/i18n/locales/it.ts +3 -2
  226. package/src/index.ts +3 -1
  227. package/src/lib/refs.ts +22 -0
  228. package/src/rhf/fields.tsx +10 -10
  229. package/src/rhf/form-core.tsx +5 -4
  230. package/src/rhf/form.tsx +7 -43
  231. package/src/rhf/line-items.tsx +1 -1
  232. package/src/rhf/use-rhf-wizard-step.ts +4 -4
  233. package/src/rhf.ts +3 -2
  234. package/src/shell/option-switcher-menu.tsx +5 -22
  235. package/src/shell/role-switcher.tsx +2 -6
  236. package/src/shell/topbar-action-menu.tsx +7 -24
  237. package/src/shell/topbar-controls.tsx +6 -18
  238. package/src/testing/index.ts +3 -1
  239. package/src/testing/viewport.ts +125 -19
  240. package/src/theme/palette-presets.ts +7 -293
  241. package/src/wizard/types.ts +0 -8
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/inline-entity-combobox.tsx"],"sourcesContent":["// The id-keyed sibling of Combobox. Moved out of combobox.tsx as it was (MOD-9,\n// refactor audit 2026-10); combobox still exports `InlineEntityCombobox` and its props.\nimport { Fragment, useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { ChevronDown, Plus, X } from \"lucide-react\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./field-base\";\nimport { cn } from \"../lib/cn\";\nimport { useDropdown } from \"./dropdown\";\nimport { takeEscape, useDismissLayer } from \"../hooks/use-dismiss\";\nimport { isImeEnter } from \"../lib/ime\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { PickerSheet, SHEET_ROW_CLASS } from \"./picker-sheet\";\nimport {\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n endHintRowProps,\n isOptionEnabled,\n offersCreate,\n stepEnabled,\n useActiveOptionScroll,\n type ComboClearValue,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { foldForSearch, rankMatch } from \"../lib/text-match\";\nimport {\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n mergeDescribedBy,\n StaticLabelRow,\n useFieldError,\n useFieldHint,\n useLockReason,\n type DisabledReasonDisplay,\n} from \"./field-parts\";\nimport { hasContent } from \"../lib/presence\";\nimport { useCommitReason, type CommitScope } from \"./write-lock\";\nimport { lockedKeyDown, rowClass, SuggestionList, usePrimaryPressOnly } from \"./combobox-suggestions\";\n\n/** {@link InlineEntityCombobox}'s create row over the shared row look: in the brand\n * colour {@link Combobox}'s own create row wears, with a leading \"+\" as the panel\n * pickers' has — an action at the foot of the list, not one more record. */\nconst CREATE_ROW_CLASS = \"flex items-center gap-2 font-medium text-brand\";\n\n/** `id` is the INPUT's and `onChange` is the field's — see {@link ComboboxProps}\n * for why the div's spellings of them are omitted. */\nexport interface InlineEntityComboboxProps<V extends string | number, C extends ComboClearValue = null>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"id\" | \"onChange\"> {\n /** Selected option id; `null` or `clearValue` when nothing is selected. */\n value: V | C | null;\n /** Required and unanswered — see {@link Combobox}'s `invalid`. */\n invalid?: boolean;\n /** What is wrong with the value — see {@link Combobox}'s `error`. */\n error?: ReactNode;\n /** Standing advice — see {@link Combobox}'s `hint` (keksdose K4). */\n hint?: ReactNode;\n /** A picked/typed option emits its id; emptying the field, or the \"×\", emits\n * `clearValue`. */\n onChange: (v: V | C) => void;\n /** What a clear emits — `null` by default, `\"\"` for a schema that wants an empty\n * string. See {@link EntityCombobox}'s `clearValue`. */\n clearValue?: C;\n options: ComboOption<V>[];\n /** The visible label — see {@link Combobox}'s `label` (a ReactNode since 0.34, API-9). */\n label?: ReactNode;\n id?: string;\n placeholder?: string;\n className?: string;\n disabled?: boolean;\n /** Focus on mount, the way `<input autoFocus>` does — and, through `onFocus`\n * below, open the list with it. A click-the-value editor needs it: the cell\n * the user clicked names the field the editor should land the caret in, and\n * without a passthrough this component's fixed prop list puts the `<input>`\n * out of a caller's reach entirely (Keksdose feedback live #218). */\n autoFocus?: boolean;\n /** Phone sheet only — the anchored list has the field itself to type in. Falls\n * back to `placeholder`, then to `combobox.search` from the {@link UiKitProvider}. */\n searchPlaceholder?: string;\n /** Phone sheet only: a full screen showing nothing has to say why. The anchored\n * list simply does not open. Default: `combobox.noResults` from the provider. */\n emptyLabel?: string;\n closeLabel?: string;\n /** The phone sheet's heading (and, as a string, its accessible name) for a field\n * whose label is drawn by someone else — a form's `FormLabel`;\n * `RhfInlineEntityCombobox` hands its label over here (0.24). Default: `label`.\n * See {@link Combobox}'s `sheetTitle`. */\n sheetTitle?: ReactNode;\n /** Offer a clear \"×\" in place of the chevron whenever something is selected.\n *\n * Emptying the text already clears (see {@link reconcile}), and on a desktop that\n * is the fast way. It is not a way at all on a PHONE: touching the field opens the\n * full-screen sheet, which covers the very input the text would have been deleted\n * from — so a field that had been answered could not be UNanswered by any gesture\n * the screen offered (Keksdose live #236, filed from a phone). The \"×\" is the one\n * affordance both shells share, and it clears without opening anything. */\n clearable?: boolean;\n clearLabel?: string;\n /**\n * Why the choice cannot be changed — {@link Button}'s `disabledReason`, for a picker\n * that SAVES on change. keksdose G2: the statement review's account picker IS the\n * write (picking re-runs the duplicate check on the server), and on the read-only demo\n * the app wrapped it in a hand-made Tooltip over a native `disabled` — out of the tab\n * order, so the reason never reached a keyboard.\n *\n * With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the\n * settled grey), opens no list or sheet, offers no clear \"×\", and no pick, typed\n * label or clear reaches `onChange`; the reason is in the kit {@link Tooltip} and on\n * the input's `aria-describedby`. Wins over `disabled`. Use it CONTROLLED, as the\n * component always is: the shown label is `value`'s.\n */\n disabledReason?: ReactNode;\n /**\n * This picker COMMITS — choosing saves. Under a locked {@link WriteLockProvider} it is\n * locked the `disabledReason` way with the lock's reason (which wins over its own).\n * A picker inside a form with its own Save stays editable under the lock — leave this\n * off there and put `commit` on the Save. No provider, or an unlocked one: no effect.\n */\n commit?: CommitScope;\n /** Where the reason shows — {@link Button}'s `disabledReasonDisplay`: a line under the\n * field at Large and on a touch screen by default, the tooltip otherwise (DUP-3). */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * A last row that makes a new record from what was typed — keksdose G9, whose \"Create\n * cash account\" had to become a separate button because this picker had no such row.\n *\n * Shown while the typed query is non-empty and names no option exactly\n * (case-insensitive — an exact hit is a record the list already shows), worded\n * `createLabel(query)`, default `combobox.create`: \"Create “{query}”\". The arrow keys\n * reach it like an option; Enter or a click calls `onCreate(query)` and closes the\n * list. Making the record, and then passing its id as `value`, is the caller's: the\n * field shows the old selection until it does.\n *\n * The same rule as {@link EntityCombobox}'s `onCreate`, so the family agrees. It is a\n * row of the LIST, not an option: it is never `aria-selected`, never matched by a\n * typed label on blur, and never sorted among the records the way a sentinel option\n * would be (the reason keksdose gave for the separate button).\n */\n onCreate?: (query: string) => void;\n /** The create row's words for a query. Default: `combobox.create` from the\n * {@link UiKitProvider}, else English `Create “{query}”`. */\n createLabel?: (query: string) => string;\n /**\n * Offer the create row with NOTHING typed too, worded as this — keksdose's \"Create cash\n * account\", which mints an account under a default name and so needs no query.\n * `onCreate` then receives `\"\"`, and the caller supplies the name. Left out, the row\n * needs a query.\n *\n * One string rather than a switch: a row with no query has nothing to quote, so the\n * kit cannot word it — \"Create “”\" says nothing — and naming it is what turns it on.\n * With both, one handler serves both rows:\n *\n * ```tsx\n * onCreate={(name) => createCash(name || t(\"cash_account_default_name\"))}\n * createEmptyLabel={t(\"create_cash_account\")}\n * ```\n */\n createEmptyLabel?: string;\n /**\n * The create row WRITES while picking does not — keksdose dev#496: on the read-only\n * demo choosing the cash account is draft state and stays live, minting one is a\n * write. Under a locked {@link WriteLockProvider} the row stays in the list, dimmed and\n * passed over by the arrows, with the lock's reason as its second line (a hover is the\n * one explanation a phone cannot show), and `onCreate` is not called. Not needed with\n * `commit`: a locked field opens no list at all.\n */\n createCommit?: CommitScope;\n /**\n * The `<input>` — the focusable control, the one a reader types in — as a React 19\n * ref prop (0.24, kastlan). Its account picker is this component inside an\n * `RhfField` render, and react-hook-form's focus-on-error calls `focus()` on whatever\n * `field.ref` is handed: with no ref there was nothing to hand it, so a line-item form\n * whose account was missing failed its submit and left the caret where it was. Pass\n * `field.ref` here, or use `RhfInlineEntityCombobox`, which does. The wrapper is not\n * reachable this way — focus and selection are the input's. See {@link Combobox}'s `ref`.\n */\n ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * The id-keyed sibling of {@link Combobox}: identical anatomy (a real text input\n * with the shared field styling, chevron and inline as-you-type filtering) but\n * the value is an option id, not free text. Built so an entity picker can sit\n * next to a free-text combobox without any visible difference — a button-trigger\n * panel picker ({@link EntityCombobox}) never matches an input field exactly.\n *\n * Text-vs-value reconciliation: the input's text is transient. Picking a row or\n * typing an exact (unique) label commits that option; emptying the text commits\n * a clear; anything else reverts to the selected option's label on blur/Escape.\n *\n * **On a phone the list opens as a full-screen sheet**, exactly like {@link Combobox}\n * beside it (Keksdose live #200, extended by dev#477 — the account picker moved onto\n * this component and must not lose its sheet). The sheet carries its OWN query state\n * rather than reusing `text`: `text` is what {@link reconcile} judges on blur, so a\n * sheet that emptied it to show the full list would read as \"the user cleared the\n * field\" the moment it closed.\n */\nexport function InlineEntityCombobox<V extends string | number, C extends ComboClearValue = null>(\n props: InlineEntityComboboxProps<V, C>,\n) {\n const {\n value: rawValue,\n onChange,\n clearValue = null as C,\n options,\n label,\n id,\n placeholder,\n className,\n disabled,\n autoFocus,\n searchPlaceholder,\n emptyLabel,\n closeLabel,\n sheetTitle,\n clearable,\n clearLabel,\n disabledReason,\n disabledReasonDisplay,\n commit,\n onCreate,\n createLabel,\n createEmptyLabel,\n createCommit,\n invalid,\n error,\n hint,\n ref,\n \"aria-label\": ariaLabel,\n // Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s\n // render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and\n // on the wrapper div the hint and required state described nothing.\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n } = props;\n // The clear value reads as \"nothing selected\", whichever one the caller picked —\n // so from here down `value` is the id or `null`, as it always was.\n const value: V | null = rawValue == null || rawValue === clearValue ? null : (rawValue as V);\n const hintParts = useFieldHint(hint, ariaDescribedBy);\n const field = useFieldError(error, invalid, hintParts.describedBy, ariaInvalid);\n const generated = useId();\n const fieldId = id ?? generated;\n // See the twin above on why these hang off the generated id.\n const listboxId = `${generated}-listbox`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const isPhone = usePhoneLayout();\n // See the twin above: the phone sheet owns its own Back entry (live #309), and the\n // Escape layer is this field's own.\n const { open, setOpen, wrapperRef, panelRef } = useDropdown({ backCloses: !isPhone, escapeLayer: false });\n const primaryOnly = usePrimaryPressOnly();\n const [active, setActive] = useState(-1);\n // null = not editing → the input shows the selected option's label.\n const [text, setText] = useState<string | null>(null);\n const sheetInputRef = useRef<HTMLInputElement | null>(null);\n // The box the portalled list hangs off — see {@link SuggestionList}.\n const fieldRef = useRef<HTMLDivElement>(null);\n // The sheet's search box. Starts empty on every open, so a field that already\n // holds a value still offers the whole list — the shape a native <select> has\n // on a phone, and what the anchored panel gets from `query` below.\n const [sheetQuery, setSheetQuery] = useState(\"\");\n // Props first, provider second — see `EntityCombobox`. `emptyLabel` used to render\n // nothing when omitted, which left a phone user staring at a blank full-screen\n // sheet; now an omitted one says \"No results\" in the provider's language.\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n search: searchPlaceholder,\n noResults: emptyLabel,\n clear: clearLabel,\n create: createLabel,\n });\n // The lock (see `lockedKeyDown` above). A lock arriving mid-edit closes the list and\n // drops the loose text, adjusted while rendering: nothing typed before it may be\n // judged by `reconcile` after it.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const inert = locked || Boolean(disabled);\n if (locked && (open || text !== null)) {\n setOpen(false);\n setText(null);\n }\n // The create row's own lock (`createCommit`): the list stays live, the row does not.\n const createReason = useCommitReason(createCommit, undefined);\n const createLocked = hasContent(createReason);\n\n const selected = useMemo(\n () => (value == null ? null : (options.find((o) => o.value === value) ?? null)),\n [options, value],\n );\n const shown = text ?? selected?.label ?? \"\";\n // Nothing selected has nothing to clear, and the chevron comes back — the field\n // keeps exactly one trailing control, so the \"×\" never crowds the value it sits on.\n const showClear = Boolean(clearable && value != null && !inert);\n\n // Focusing select-alls the current label; filtering only kicks in once the\n // text actually differs from it, so an already-filled field still opens on\n // the FULL list instead of a single-row \"filter\" of its own value.\n const typedQuery = text !== null && text !== (selected?.label ?? \"\") ? text.trim() : \"\";\n const query = isPhone ? sheetQuery.trim() : typedQuery;\n // Folded as every kit list folds (DUP-5): accents off and the locale's case, so\n // \"zurich\" finds Zürich and, in Turkish, \"ı\" finds \"IRMAK\".\n const locale = useKitLocale();\n const matches = useMemo(() => {\n let ranked = options;\n if (query) {\n const q = foldForSearch(query, locale);\n const hit = (s: string | undefined) => s !== undefined && foldForSearch(s, locale).includes(q);\n // The label's whole name or its start first (`rankMatch` 0 and 1), then a match\n // anywhere in the label, the sublabel or the group.\n const leads = (o: ComboOption<V>) => {\n const rank = rankMatch(o.label, query, { locale });\n return rank === 0 || rank === 1;\n };\n const starts = options.filter(leads);\n const rest = options.filter((o) => !leads(o) && (hit(o.label) || hit(o.sublabel) || hit(o.group)));\n ranked = [...starts, ...rest];\n }\n if (!ranked.some((o) => o.group)) return ranked;\n // Keep each group contiguous so the headings below appear once instead of\n // re-appearing every time the ranking interleaves two groups — while preserving\n // \"best match first\": a Map keeps insertion order, so groups come out ordered by\n // their best-ranked member and members keep their rank order inside the group.\n const blocks = new Map<string, ComboOption<V>[]>();\n for (const o of ranked) {\n const key = o.group ?? \"\";\n if (!blocks.has(key)) blocks.set(key, []);\n blocks.get(key)!.push(o);\n }\n return [...blocks.values()].flat();\n }, [options, query, locale]);\n\n // The create row (`onCreate`), on the query as typed — not lowercased: it becomes a\n // name. Compared against EVERY option, not `matches`: an exact hit anywhere is a\n // record that exists, disabled or not. Restated from `text` rather than derived from\n // `typedQuery`: this value is handed to the caller's `onCreate`, and the React\n // Compiler cannot then keep `matches` memoised on anything it was derived from.\n const createQuery = isPhone\n ? sheetQuery.trim()\n : text !== null && text !== (selected?.label ?? \"\")\n ? text.trim()\n : \"\";\n const showCreate = offersCreate(\n onCreate,\n createQuery,\n options.map((o) => o.label),\n createEmptyLabel,\n );\n const createText = createQuery ? labels.create(createQuery) : createEmptyLabel;\n // Every row the keyboard can land on: the options, then the create row — passed over\n // like a disabled option while a write lock holds it.\n const rows: readonly { disabled?: boolean }[] = showCreate\n ? [...matches, { disabled: createLocked }]\n : matches;\n const createIndex = matches.length;\n\n // Pointer-device only: the phone's rows live in a {@link PickerSheet} whose own\n // search box holds focus, so the field behind it must not claim to be pointing at\n // one of them.\n // A disabled row is never the keyboard's, even if the list changed under it.\n const activeId = !isPhone && isOptionEnabled(rows[active]) ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n setSheetQuery(\"\");\n };\n // See the twin above: a layer while the list SHOWS, and Escape puts the label back.\n const showing = !isPhone && open && !inert && (matches.length > 0 || showCreate);\n useDismissLayer(showing, () => {\n setText(null);\n close();\n });\n const take = (o: ComboOption<V>) => {\n // A `disabled` option is listed, never taken — by any path; nor is anything while\n // the field is locked.\n if (o.disabled || locked) return;\n if (o.value !== value) onChange(o.value);\n setText(null);\n close();\n };\n /** Turn loose text into a decision: empty clears, an exact label match that names\n * ONE entity commits, anything else reverts to the selected label.\n *\n * Unique by VALUE, not by row. An option may deliberately appear twice — Keksdose\n * repeats recently-used categories in a \"Recent\" group at the top (live #203) — and\n * counting rows made every such option uncommittable: two hits, so nothing fired and\n * the field reverted to whatever was selected before. Typing a category you had just\n * used, then tabbing away, silently discarded it, and the more categories you used\n * the more of them stopped working. Two rows naming the same id are not an ambiguity;\n * two ids sharing a label are. */\n const create = () => {\n if (!onCreate || locked || createLocked) return;\n onCreate(createQuery);\n setText(null);\n close();\n };\n const reconcile = () => {\n // Locked, loose text is dropped rather than judged: nothing reaches `onChange`.\n if (text !== null && !locked) {\n const q = text.trim();\n if (!q) {\n if (value != null) onChange(clearValue);\n } else {\n // A disabled option's label typed out in full is not a way round `disabled`:\n // it is not a hit, so the text reverts like any other non-answer.\n const hits = options.filter(\n (o) => !o.disabled && o.label.toLowerCase() === q.toLowerCase(),\n );\n const ids = new Set(hits.map((h) => h.value));\n if (ids.size === 1 && hits[0].value !== value) onChange(hits[0].value);\n }\n }\n setText(null);\n close();\n };\n\n // The create row's inside, one for both shells: a \"+\" (it is an action, not a record),\n // the words, and — while a write lock holds it — the lock's reason as a second line.\n // Both lines wrap from Large up (text size §10.17, round 0.34 T1): the words hold what\n // the reader typed, which an ellipsis would cut.\n const createRowContent = (\n <>\n <Plus aria-hidden className=\"size-4 shrink-0\" />\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate-until-large\">{createText}</span>\n {createLocked && (\n <span className=\"block truncate-until-large text-xs font-normal text-placeholder\">\n {createReason}\n </span>\n )}\n </span>\n </>\n );\n // While locked the field sits in the reason's Tooltip — see Combobox.\n const withLock = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} display={disabledReasonDisplay} className=\"block\">\n {box}\n </LockedReason>\n ) : (\n box\n );\n\n return (\n // `rest` dresses the outer box — a `data-tour` anchor, a test id. Not the NAME,\n // description, invalid or required state: those belong on the <input> below,\n // which is the combobox a reader meets. Spread FIRST, so the field's ARIA and the\n // handlers carrying live #309 and dev#549 cannot be replaced from outside.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {/* A real <label for> — see {@link Combobox}; with a \"?\", in a row after the field. */}\n {label !== undefined && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel htmlFor={fieldId} className={inert ? \"opacity-50\" : undefined}>\n {label}\n </ComboboxFieldLabel>\n )}\n <EndHintRow {...endHintRowProps(\"hint\" in props, label !== undefined, hintParts.labelHint)}>\n {withLock(\n /* Inner wrapper for chevron centering — same reasoning as Combobox above.\n It is also what the portalled list anchors to.\n Dimmed as a whole when disabled (or locked), the way EntityCombobox dims its\n trigger. FIELD_BASE's grey alone left a disabled picker looking like a\n filled-in one beside the pickers that do dim (the label is not the input's\n `peer`, so it is dimmed by hand above). */\n <div ref={fieldRef} className={cn(\"relative\", inert && \"cursor-not-allowed opacity-50\")}>\n <input\n ref={ref}\n id={fieldId}\n value={shown}\n placeholder={placeholder}\n // The caller's name, over the <label for> above. Just the label, never\n // \"label: value\" the way a trigger button has to compose it: an input\n // already exposes its value separately.\n aria-label={ariaLabel}\n role=\"combobox\"\n aria-expanded={open}\n // Required by the role, and the half that was missing: the field said it\n // was expanded and never said what it had expanded (ESLint's\n // `role-has-required-aria-props`, the audit's §a11y).\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-autocomplete=\"list\"\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n aria-required={ariaRequired}\n autoComplete=\"off\"\n // Locked: focusable, `readOnly`, `aria-disabled` — see `lockedKeyDown`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || undefined}\n aria-disabled={locked || undefined}\n // eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.\n autoFocus={autoFocus}\n // `inputMode=\"none\"` rather than readOnly, for the same reason Combobox\n // above gives: the sheet carries the keyboard, and a readOnly field would\n // take FIELD_BASE's settled look on a field that is perfectly editable.\n inputMode={isPhone ? \"none\" : undefined}\n onMouseDown={primaryOnly.onMouseDown}\n onFocus={(e) => {\n // See {@link usePrimaryPressOnly}: a back/forward button lands here on\n // its way to navigating, and neither the list nor the select-all is\n // anything it asked for (live #309 rework). A locked field takes focus to\n // say why, and opens nothing.\n if (primaryOnly.fromAuxButton() || locked) return;\n setText(shown);\n e.currentTarget.select();\n setOpen(true);\n // The sheet has its own input; the field behind it must not also pull up\n // the keyboard and scroll the page under the dialog.\n if (isPhone) sheetInputRef.current?.focus();\n }}\n onBlur={() => {\n // On a phone the blur is the SHEET taking focus, not the user leaving the\n // field — reconciling there would close the sheet the instant it opened.\n if (!isPhone) reconcile();\n }}\n onChange={(e) => {\n if (locked) return;\n setText(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n if (locked) return lockedKeyDown(e);\n // Disabled rows are passed over; Up from \"nothing highlighted\" lands on\n // the first takeable row, as it always landed on row 0. The create row is\n // the last row the arrows reach.\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setOpen(true);\n setActive((i) => stepEnabled(rows, i, 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => (i < 0 ? stepEnabled(rows, -1, 1) : stepEnabled(rows, i, -1)));\n } else if (e.key === \"Enter\") {\n // An IME's Enter takes nothing, and judges no loose text (BUG-9).\n if (isImeEnter(e)) return;\n if (open && isOptionEnabled(rows[active])) {\n e.preventDefault();\n if (active === createIndex && showCreate) create();\n else take(matches[active]);\n } else {\n reconcile();\n }\n } else if (e.key === \"Escape\") {\n // A showing list takes the key and stops it — see the twin in\n // {@link Combobox} (round 0.34 BUG-1).\n if (showing && takeEscape(e)) return;\n setText(null);\n close();\n } else if (e.key === \"Tab\") {\n // Focus is in THIS input and stays there, so the browser's own Tab is\n // left alone; all that is needed is that the list stop covering what\n // the user is tabbing to. `reconcile` rather than `close`, because\n // leaving the field is exactly when loose text has to be judged.\n reconcile();\n }\n // Home/End stay with the caret — see the note in {@link Combobox}: this\n // field is editable, and its text is what `reconcile` judges.\n }}\n className={cn(\n FIELD_BASE,\n label !== undefined && FIELD_FLOATING_PAD,\n \"pe-9\",\n field.isInvalid && FIELD_INVALID,\n )}\n />\n {showClear ? (\n <button\n type=\"button\"\n // Out of the tab order, like the clear on `EntityCombobox`: the keyboard\n // already clears this field by selecting its text and deleting, and a\n // second stop between every picker and the next field is a worse trade\n // than the one gesture it saves.\n tabIndex={-1}\n aria-label={labels.clear}\n // preventDefault, exactly as the chevron does: without it the press\n // focuses the input, which on a phone opens the sheet over the field the\n // press was clearing.\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => {\n setText(null);\n close();\n if (!locked) onChange(clearValue);\n }}\n className={cn(\n \"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-placeholder\",\n \"hover:text-secondary\",\n )}\n >\n <X aria-hidden className=\"size-4\" />\n </button>\n ) : (\n <ChevronDown\n aria-hidden\n onMouseDown={(e) => {\n // Toggle on the chevron without stealing focus from the input — and\n // not at all on a disabled or locked field, which the input's own\n // `disabled`/`readOnly` does not stop from here.\n e.preventDefault();\n if (!inert) setOpen((o) => !o);\n }}\n className={cn(\n \"absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-placeholder\",\n // The box above does the dimming; dimming here too would halve it again.\n !inert && \"cursor-pointer\",\n )}\n />\n )}\n </div>,\n )}\n </EndHintRow>\n {label !== undefined && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel htmlFor={fieldId} className={cn(LABEL_IN_ROW, inert && \"opacity-50\")}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {isPhone && (\n <PickerSheet\n open={open && !inert}\n // Closing without choosing keeps the value: `text` was never emptied, so\n // reconcile has nothing to undo — it just puts the label back.\n onClose={reconcile}\n title={sheetTitle ?? label}\n query={sheetQuery}\n onQueryChange={(v) => {\n setSheetQuery(v);\n setActive(-1);\n }}\n searchPlaceholder={searchPlaceholder ?? placeholder ?? labels.search}\n inputRef={sheetInputRef}\n closeLabel={closeLabel}\n >\n <ul id={listboxId} role=\"listbox\">\n {matches.map((o, i) => (\n // Keyed by group AND value, like combobox-core.tsx: an option may\n // deliberately appear twice (see `reconcile`), and a bare value key\n // would collide.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {o.group && o.group !== matches[i - 1]?.group && (\n <li\n role=\"presentation\"\n className=\"px-4 pb-0.5 pt-3 text-caption font-semibold uppercase tracking-wide text-muted\"\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={o.value === value}\n aria-disabled={o.disabled || undefined}\n onClick={() => take(o)}\n className={cn(\n SHEET_ROW_CLASS,\n o.value === value && \"font-medium\",\n o.disabled && DISABLED_ROW_CLASS,\n )}\n >\n {o.label}\n </button>\n </li>\n </Fragment>\n ))}\n {showCreate && (\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n // An action, never chosen — see `onCreate`.\n aria-selected={false}\n aria-disabled={createLocked || undefined}\n onClick={create}\n className={cn(\n SHEET_ROW_CLASS,\n CREATE_ROW_CLASS,\n createLocked && DISABLED_ROW_CLASS,\n )}\n >\n {createRowContent}\n </button>\n </li>\n )}\n {/* \"Nothing matched\" only when there is nothing to do either: with a create\n row the list is not empty, it is offering to fill itself. */}\n {matches.length === 0 && !showCreate && (\n <li className=\"px-4 py-3 text-sm text-muted\">{labels.noResults}</li>\n )}\n </ul>\n </PickerSheet>\n )}\n {showing && (\n <SuggestionList id={listboxId} anchorRef={fieldRef} panelRef={panelRef}>\n {matches.map((o, i) => (\n // A group heading is emitted at each group boundary rather than repeating\n // the group on every row (feedback #136). `matches` is group-contiguous,\n // so comparing with the previous row is enough. Fragment key sits here;\n // the heading and the option carry their own list semantics — keyed by\n // group AND value, because an option may deliberately appear twice (see\n // `reconcile`).\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {o.group && o.group !== matches[i - 1]?.group && (\n <li\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-caption font-semibold uppercase tracking-wide text-muted first:pt-1\"\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n // See {@link Combobox}: the role rides the button, `aria-selected`\n // is the CHOSEN row, and the keyboard's row is the field's\n // `aria-activedescendant`.\n role=\"option\"\n aria-selected={o.value === value}\n aria-disabled={o.disabled || undefined}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown (not click) so the input's blur can't close the\n // list before the selection registers. Prevented on a disabled\n // row too, so pressing one leaves the field focused and open.\n e.preventDefault();\n take(o);\n }}\n onMouseEnter={() => {\n if (!o.disabled) setActive(i);\n }}\n className={cn(\n rowClass(i === active && !o.disabled),\n o.disabled && DISABLED_ROW_CLASS,\n // Indented under its heading, so the hierarchy is readable at a\n // glance instead of inferred from grey trailing text.\n o.group && \"ps-6\",\n o.value === value && \"font-medium\",\n )}\n >\n {o.label}\n {o.sublabel && (\n <span className=\"ms-2 text-xs text-placeholder\">\n {o.sublabel}\n </span>\n )}\n </button>\n </li>\n </Fragment>\n ))}\n {showCreate && (\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(createIndex)}\n role=\"option\"\n // An action, never chosen; whether the keyboard is ON it is the field's\n // `aria-activedescendant`'s to say.\n aria-selected={false}\n aria-disabled={createLocked || undefined}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown, like the rows above: the input's blur would otherwise\n // reconcile and close the list before the click landed.\n e.preventDefault();\n create();\n }}\n onMouseEnter={() => {\n if (!createLocked) setActive(createIndex);\n }}\n className={cn(\n rowClass(active === createIndex && !createLocked),\n CREATE_ROW_CLASS,\n createLocked && DISABLED_ROW_CLASS,\n )}\n >\n {createRowContent}\n </button>\n </li>\n )}\n </SuggestionList>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["Fragment","rest"]}
1
+ {"version":3,"sources":["../../src/components/inline-entity-combobox.tsx"],"sourcesContent":["// The id-keyed sibling of Combobox. Moved out of combobox.tsx as it was (MOD-9,\n// refactor audit 2026-10); combobox still exports `InlineEntityCombobox` and its props.\nimport { Fragment, useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { ChevronDown, Plus, X } from \"lucide-react\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./field-base\";\nimport { cn } from \"../lib/cn\";\nimport { useDropdown } from \"./dropdown\";\nimport { takeEscape, useDismissLayer } from \"../hooks/use-dismiss\";\nimport { isImeEnter } from \"../lib/ime\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { PickerSheet, SHEET_ROW_CLASS } from \"./picker-sheet\";\nimport {\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n endHintRowProps,\n isOptionEnabled,\n offersCreate,\n stepEnabled,\n useActiveOptionScroll,\n type ComboClearValue,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { foldForSearch, rankMatch } from \"../lib/text-match\";\nimport {\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n mergeDescribedBy,\n StaticLabelRow,\n useFieldError,\n useFieldHint,\n useLockReason,\n type DisabledReasonDisplay,\n} from \"./field-parts\";\nimport { hasContent } from \"../lib/presence\";\nimport { useCommitReason, type CommitScope } from \"./write-lock\";\nimport { lockedKeyDown, rowClass, SuggestionList, usePrimaryPressOnly } from \"./combobox-suggestions\";\n\n/** {@link InlineEntityCombobox}'s create row over the shared row look: in the brand\n * colour {@link Combobox}'s own create row wears, with a leading \"+\" as the panel\n * pickers' has — an action at the foot of the list, not one more record. */\nconst CREATE_ROW_CLASS = \"flex items-center gap-2 font-medium text-brand\";\n\n/** `id` is the INPUT's and `onChange` is the field's — see {@link ComboboxProps}\n * for why the div's spellings of them are omitted. */\nexport interface InlineEntityComboboxProps<V extends string | number, C extends ComboClearValue = null>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"id\" | \"onChange\"> {\n /** Selected option id; `null` or `clearValue` when nothing is selected. */\n value: V | C | null;\n /** Required and unanswered — see {@link Combobox}'s `invalid`. */\n invalid?: boolean;\n /** What is wrong with the value — see {@link Combobox}'s `error`. */\n error?: ReactNode;\n /** Standing advice — see {@link Combobox}'s `hint` (keksdose K4). */\n hint?: ReactNode;\n /** A picked/typed option emits its id; emptying the field, or the \"×\", emits\n * `clearValue`. */\n onChange: (v: V | C) => void;\n /** What a clear emits — `null` by default, `\"\"` for a schema that wants an empty\n * string. See {@link EntityCombobox}'s `clearValue`. */\n clearValue?: C;\n options: ComboOption<V>[];\n /** The visible label — see {@link Combobox}'s `label` (a ReactNode since 0.34, API-9). */\n label?: ReactNode;\n id?: string;\n placeholder?: string;\n className?: string;\n disabled?: boolean;\n /** Focus on mount, the way `<input autoFocus>` does — and, through `onFocus`\n * below, open the list with it. A click-the-value editor needs it: the cell\n * the user clicked names the field the editor should land the caret in, and\n * without a passthrough this component's fixed prop list puts the `<input>`\n * out of a caller's reach entirely (Keksdose feedback live #218). */\n autoFocus?: boolean;\n /** Phone sheet only — the anchored list has the field itself to type in. Falls\n * back to `placeholder`, then to `combobox.search` from the {@link UiKitProvider}. */\n searchPlaceholder?: string;\n /** Phone sheet only: a full screen showing nothing has to say why. The anchored\n * list simply does not open. Default: `combobox.noResults` from the provider. */\n emptyLabel?: string;\n closeLabel?: string;\n /** The phone sheet's heading (and, as a string, its accessible name) for a field\n * whose label is drawn by someone else — a bound field's label;\n * `RhfInlineEntityCombobox` hands its label over here (0.24). Default: `label`.\n * See {@link Combobox}'s `sheetTitle`. */\n sheetTitle?: ReactNode;\n /** Offer a clear \"×\" in place of the chevron whenever something is selected.\n *\n * Emptying the text already clears (see {@link reconcile}), and on a desktop that\n * is the fast way. It is not a way at all on a PHONE: touching the field opens the\n * full-screen sheet, which covers the very input the text would have been deleted\n * from — so a field that had been answered could not be UNanswered by any gesture\n * the screen offered (Keksdose live #236, filed from a phone). The \"×\" is the one\n * affordance both shells share, and it clears without opening anything. */\n clearable?: boolean;\n clearLabel?: string;\n /**\n * Why the choice cannot be changed — {@link Button}'s `disabledReason`, for a picker\n * that SAVES on change. keksdose G2: the statement review's account picker IS the\n * write (picking re-runs the duplicate check on the server), and on the read-only demo\n * the app wrapped it in a hand-made Tooltip over a native `disabled` — out of the tab\n * order, so the reason never reached a keyboard.\n *\n * With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the\n * settled grey), opens no list or sheet, offers no clear \"×\", and no pick, typed\n * label or clear reaches `onChange`; the reason is in the kit {@link Tooltip} and on\n * the input's `aria-describedby`. Wins over `disabled`. Use it CONTROLLED, as the\n * component always is: the shown label is `value`'s.\n */\n disabledReason?: ReactNode;\n /**\n * This picker COMMITS — choosing saves. Under a locked {@link WriteLockProvider} it is\n * locked the `disabledReason` way with the lock's reason (which wins over its own).\n * A picker inside a form with its own Save stays editable under the lock — leave this\n * off there and put `commit` on the Save. No provider, or an unlocked one: no effect.\n */\n commit?: CommitScope;\n /** Where the reason shows — {@link Button}'s `disabledReasonDisplay`: a line under the\n * field at Large and on a touch screen by default, the tooltip otherwise (DUP-3). */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * A last row that makes a new record from what was typed — keksdose G9, whose \"Create\n * cash account\" had to become a separate button because this picker had no such row.\n *\n * Shown while the typed query is non-empty and names no option exactly\n * (case-insensitive — an exact hit is a record the list already shows), worded\n * `createLabel(query)`, default `combobox.create`: \"Create “{query}”\". The arrow keys\n * reach it like an option; Enter or a click calls `onCreate(query)` and closes the\n * list. Making the record, and then passing its id as `value`, is the caller's: the\n * field shows the old selection until it does.\n *\n * The same rule as {@link EntityCombobox}'s `onCreate`, so the family agrees. It is a\n * row of the LIST, not an option: it is never `aria-selected`, never matched by a\n * typed label on blur, and never sorted among the records the way a sentinel option\n * would be (the reason keksdose gave for the separate button).\n */\n onCreate?: (query: string) => void;\n /** The create row's words for a query. Default: `combobox.create` from the\n * {@link UiKitProvider}, else English `Create “{query}”`. */\n createLabel?: (query: string) => string;\n /**\n * Offer the create row with NOTHING typed too, worded as this — keksdose's \"Create cash\n * account\", which mints an account under a default name and so needs no query.\n * `onCreate` then receives `\"\"`, and the caller supplies the name. Left out, the row\n * needs a query.\n *\n * One string rather than a switch: a row with no query has nothing to quote, so the\n * kit cannot word it — \"Create “”\" says nothing — and naming it is what turns it on.\n * With both, one handler serves both rows:\n *\n * ```tsx\n * onCreate={(name) => createCash(name || t(\"cash_account_default_name\"))}\n * createEmptyLabel={t(\"create_cash_account\")}\n * ```\n */\n createEmptyLabel?: string;\n /**\n * The create row WRITES while picking does not — keksdose dev#496: on the read-only\n * demo choosing the cash account is draft state and stays live, minting one is a\n * write. Under a locked {@link WriteLockProvider} the row stays in the list, dimmed and\n * passed over by the arrows, with the lock's reason as its second line (a hover is the\n * one explanation a phone cannot show), and `onCreate` is not called. Not needed with\n * `commit`: a locked field opens no list at all.\n */\n createCommit?: CommitScope;\n /**\n * The `<input>` — the focusable control, the one a reader types in — as a React 19\n * ref prop (0.24, kastlan). Its account picker is this component inside an\n * `RhfField` render, and react-hook-form's focus-on-error calls `focus()` on whatever\n * `field.ref` is handed: with no ref there was nothing to hand it, so a line-item form\n * whose account was missing failed its submit and left the caret where it was. Pass\n * `field.ref` here, or use `RhfInlineEntityCombobox`, which does. The wrapper is not\n * reachable this way — focus and selection are the input's. See {@link Combobox}'s `ref`.\n */\n ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * The id-keyed sibling of {@link Combobox}: identical anatomy (a real text input\n * with the shared field styling, chevron and inline as-you-type filtering) but\n * the value is an option id, not free text. Built so an entity picker can sit\n * next to a free-text combobox without any visible difference — a button-trigger\n * panel picker ({@link EntityCombobox}) never matches an input field exactly.\n *\n * Text-vs-value reconciliation: the input's text is transient. Picking a row or\n * typing an exact (unique) label commits that option; emptying the text commits\n * a clear; anything else reverts to the selected option's label on blur/Escape.\n *\n * **On a phone the list opens as a full-screen sheet**, exactly like {@link Combobox}\n * beside it (Keksdose live #200, extended by dev#477 — the account picker moved onto\n * this component and must not lose its sheet). The sheet carries its OWN query state\n * rather than reusing `text`: `text` is what {@link reconcile} judges on blur, so a\n * sheet that emptied it to show the full list would read as \"the user cleared the\n * field\" the moment it closed.\n */\nexport function InlineEntityCombobox<V extends string | number, C extends ComboClearValue = null>(\n props: InlineEntityComboboxProps<V, C>,\n) {\n const {\n value: rawValue,\n onChange,\n clearValue = null as C,\n options,\n label,\n id,\n placeholder,\n className,\n disabled,\n autoFocus,\n searchPlaceholder,\n emptyLabel,\n closeLabel,\n sheetTitle,\n clearable,\n clearLabel,\n disabledReason,\n disabledReasonDisplay,\n commit,\n onCreate,\n createLabel,\n createEmptyLabel,\n createCommit,\n invalid,\n error,\n hint,\n ref,\n \"aria-label\": ariaLabel,\n // Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s\n // render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and\n // on the wrapper div the hint and required state described nothing.\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n } = props;\n // The clear value reads as \"nothing selected\", whichever one the caller picked —\n // so from here down `value` is the id or `null`, as it always was.\n const value: V | null = rawValue == null || rawValue === clearValue ? null : (rawValue as V);\n const hintParts = useFieldHint(hint, ariaDescribedBy);\n const field = useFieldError(error, invalid, hintParts.describedBy, ariaInvalid);\n const generated = useId();\n const fieldId = id ?? generated;\n // See the twin above on why these hang off the generated id.\n const listboxId = `${generated}-listbox`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const isPhone = usePhoneLayout();\n // See the twin above: the phone sheet owns its own Back entry (live #309), and the\n // Escape layer is this field's own.\n const { open, setOpen, wrapperRef, panelRef } = useDropdown({ backCloses: !isPhone, escapeLayer: false });\n const primaryOnly = usePrimaryPressOnly();\n const [active, setActive] = useState(-1);\n // null = not editing → the input shows the selected option's label.\n const [text, setText] = useState<string | null>(null);\n const sheetInputRef = useRef<HTMLInputElement | null>(null);\n // The box the portalled list hangs off — see {@link SuggestionList}.\n const fieldRef = useRef<HTMLDivElement>(null);\n // The sheet's search box. Starts empty on every open, so a field that already\n // holds a value still offers the whole list — the shape a native <select> has\n // on a phone, and what the anchored panel gets from `query` below.\n const [sheetQuery, setSheetQuery] = useState(\"\");\n // Props first, provider second — see `EntityCombobox`. `emptyLabel` used to render\n // nothing when omitted, which left a phone user staring at a blank full-screen\n // sheet; now an omitted one says \"No results\" in the provider's language.\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n search: searchPlaceholder,\n noResults: emptyLabel,\n clear: clearLabel,\n create: createLabel,\n });\n // The lock (see `lockedKeyDown` above). A lock arriving mid-edit closes the list and\n // drops the loose text, adjusted while rendering: nothing typed before it may be\n // judged by `reconcile` after it.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const inert = locked || Boolean(disabled);\n if (locked && (open || text !== null)) {\n setOpen(false);\n setText(null);\n }\n // The create row's own lock (`createCommit`): the list stays live, the row does not.\n const createReason = useCommitReason(createCommit, undefined);\n const createLocked = hasContent(createReason);\n\n const selected = useMemo(\n () => (value == null ? null : (options.find((o) => o.value === value) ?? null)),\n [options, value],\n );\n const shown = text ?? selected?.label ?? \"\";\n // Nothing selected has nothing to clear, and the chevron comes back — the field\n // keeps exactly one trailing control, so the \"×\" never crowds the value it sits on.\n const showClear = Boolean(clearable && value != null && !inert);\n\n // Focusing select-alls the current label; filtering only kicks in once the\n // text actually differs from it, so an already-filled field still opens on\n // the FULL list instead of a single-row \"filter\" of its own value.\n const typedQuery = text !== null && text !== (selected?.label ?? \"\") ? text.trim() : \"\";\n const query = isPhone ? sheetQuery.trim() : typedQuery;\n // Folded as every kit list folds (DUP-5): accents off and the locale's case, so\n // \"zurich\" finds Zürich and, in Turkish, \"ı\" finds \"IRMAK\".\n const locale = useKitLocale();\n const matches = useMemo(() => {\n let ranked = options;\n if (query) {\n const q = foldForSearch(query, locale);\n const hit = (s: string | undefined) => s !== undefined && foldForSearch(s, locale).includes(q);\n // The label's whole name or its start first (`rankMatch` 0 and 1), then a match\n // anywhere in the label, the sublabel or the group.\n const leads = (o: ComboOption<V>) => {\n const rank = rankMatch(o.label, query, { locale });\n return rank === 0 || rank === 1;\n };\n const starts = options.filter(leads);\n const rest = options.filter((o) => !leads(o) && (hit(o.label) || hit(o.sublabel) || hit(o.group)));\n ranked = [...starts, ...rest];\n }\n if (!ranked.some((o) => o.group)) return ranked;\n // Keep each group contiguous so the headings below appear once instead of\n // re-appearing every time the ranking interleaves two groups — while preserving\n // \"best match first\": a Map keeps insertion order, so groups come out ordered by\n // their best-ranked member and members keep their rank order inside the group.\n const blocks = new Map<string, ComboOption<V>[]>();\n for (const o of ranked) {\n const key = o.group ?? \"\";\n if (!blocks.has(key)) blocks.set(key, []);\n blocks.get(key)!.push(o);\n }\n return [...blocks.values()].flat();\n }, [options, query, locale]);\n\n // The create row (`onCreate`), on the query as typed — not lowercased: it becomes a\n // name. Compared against EVERY option, not `matches`: an exact hit anywhere is a\n // record that exists, disabled or not. Restated from `text` rather than derived from\n // `typedQuery`: this value is handed to the caller's `onCreate`, and the React\n // Compiler cannot then keep `matches` memoised on anything it was derived from.\n const createQuery = isPhone\n ? sheetQuery.trim()\n : text !== null && text !== (selected?.label ?? \"\")\n ? text.trim()\n : \"\";\n const showCreate = offersCreate(\n onCreate,\n createQuery,\n options.map((o) => o.label),\n createEmptyLabel,\n );\n const createText = createQuery ? labels.create(createQuery) : createEmptyLabel;\n // Every row the keyboard can land on: the options, then the create row — passed over\n // like a disabled option while a write lock holds it.\n const rows: readonly { disabled?: boolean }[] = showCreate\n ? [...matches, { disabled: createLocked }]\n : matches;\n const createIndex = matches.length;\n\n // Pointer-device only: the phone's rows live in a {@link PickerSheet} whose own\n // search box holds focus, so the field behind it must not claim to be pointing at\n // one of them.\n // A disabled row is never the keyboard's, even if the list changed under it.\n const activeId = !isPhone && isOptionEnabled(rows[active]) ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n setSheetQuery(\"\");\n };\n // See the twin above: a layer while the list SHOWS, and Escape puts the label back.\n const showing = !isPhone && open && !inert && (matches.length > 0 || showCreate);\n useDismissLayer(showing, () => {\n setText(null);\n close();\n });\n const take = (o: ComboOption<V>) => {\n // A `disabled` option is listed, never taken — by any path; nor is anything while\n // the field is locked.\n if (o.disabled || locked) return;\n if (o.value !== value) onChange(o.value);\n setText(null);\n close();\n };\n /** Turn loose text into a decision: empty clears, an exact label match that names\n * ONE entity commits, anything else reverts to the selected label.\n *\n * Unique by VALUE, not by row. An option may deliberately appear twice — Keksdose\n * repeats recently-used categories in a \"Recent\" group at the top (live #203) — and\n * counting rows made every such option uncommittable: two hits, so nothing fired and\n * the field reverted to whatever was selected before. Typing a category you had just\n * used, then tabbing away, silently discarded it, and the more categories you used\n * the more of them stopped working. Two rows naming the same id are not an ambiguity;\n * two ids sharing a label are. */\n const create = () => {\n if (!onCreate || locked || createLocked) return;\n onCreate(createQuery);\n setText(null);\n close();\n };\n const reconcile = () => {\n // Locked, loose text is dropped rather than judged: nothing reaches `onChange`.\n if (text !== null && !locked) {\n const q = text.trim();\n if (!q) {\n if (value != null) onChange(clearValue);\n } else {\n // A disabled option's label typed out in full is not a way round `disabled`:\n // it is not a hit, so the text reverts like any other non-answer.\n const hits = options.filter(\n (o) => !o.disabled && o.label.toLowerCase() === q.toLowerCase(),\n );\n const ids = new Set(hits.map((h) => h.value));\n if (ids.size === 1 && hits[0].value !== value) onChange(hits[0].value);\n }\n }\n setText(null);\n close();\n };\n\n // The create row's inside, one for both shells: a \"+\" (it is an action, not a record),\n // the words, and — while a write lock holds it — the lock's reason as a second line.\n // Both lines wrap from Large up (text size §10.17, round 0.34 T1): the words hold what\n // the reader typed, which an ellipsis would cut.\n const createRowContent = (\n <>\n <Plus aria-hidden className=\"size-4 shrink-0\" />\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate-until-large\">{createText}</span>\n {createLocked && (\n <span className=\"block truncate-until-large text-xs font-normal text-placeholder\">\n {createReason}\n </span>\n )}\n </span>\n </>\n );\n // While locked the field sits in the reason's Tooltip — see Combobox.\n const withLock = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} display={disabledReasonDisplay} className=\"block\">\n {box}\n </LockedReason>\n ) : (\n box\n );\n\n return (\n // `rest` dresses the outer box — a `data-tour` anchor, a test id. Not the NAME,\n // description, invalid or required state: those belong on the <input> below,\n // which is the combobox a reader meets. Spread FIRST, so the field's ARIA and the\n // handlers carrying live #309 and dev#549 cannot be replaced from outside.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {/* A real <label for> — see {@link Combobox}; with a \"?\", in a row after the field. */}\n {label !== undefined && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel htmlFor={fieldId} className={inert ? \"opacity-50\" : undefined}>\n {label}\n </ComboboxFieldLabel>\n )}\n <EndHintRow {...endHintRowProps(\"hint\" in props, label !== undefined, hintParts.labelHint)}>\n {withLock(\n /* Inner wrapper for chevron centering — same reasoning as Combobox above.\n It is also what the portalled list anchors to.\n Dimmed as a whole when disabled (or locked), the way EntityCombobox dims its\n trigger. FIELD_BASE's grey alone left a disabled picker looking like a\n filled-in one beside the pickers that do dim (the label is not the input's\n `peer`, so it is dimmed by hand above). */\n <div ref={fieldRef} className={cn(\"relative\", inert && \"cursor-not-allowed opacity-50\")}>\n <input\n ref={ref}\n id={fieldId}\n value={shown}\n placeholder={placeholder}\n // The caller's name, over the <label for> above. Just the label, never\n // \"label: value\" the way a trigger button has to compose it: an input\n // already exposes its value separately.\n aria-label={ariaLabel}\n role=\"combobox\"\n aria-expanded={open}\n // Required by the role, and the half that was missing: the field said it\n // was expanded and never said what it had expanded (ESLint's\n // `role-has-required-aria-props`, the audit's §a11y).\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-autocomplete=\"list\"\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n aria-required={ariaRequired}\n autoComplete=\"off\"\n // Locked: focusable, `readOnly`, `aria-disabled` — see `lockedKeyDown`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || undefined}\n aria-disabled={locked || undefined}\n // eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.\n autoFocus={autoFocus}\n // `inputMode=\"none\"` rather than readOnly, for the same reason Combobox\n // above gives: the sheet carries the keyboard, and a readOnly field would\n // take FIELD_BASE's settled look on a field that is perfectly editable.\n inputMode={isPhone ? \"none\" : undefined}\n onMouseDown={primaryOnly.onMouseDown}\n onFocus={(e) => {\n // See {@link usePrimaryPressOnly}: a back/forward button lands here on\n // its way to navigating, and neither the list nor the select-all is\n // anything it asked for (live #309 rework). A locked field takes focus to\n // say why, and opens nothing.\n if (primaryOnly.fromAuxButton() || locked) return;\n setText(shown);\n e.currentTarget.select();\n setOpen(true);\n // The sheet has its own input; the field behind it must not also pull up\n // the keyboard and scroll the page under the dialog.\n if (isPhone) sheetInputRef.current?.focus();\n }}\n onBlur={() => {\n // On a phone the blur is the SHEET taking focus, not the user leaving the\n // field — reconciling there would close the sheet the instant it opened.\n if (!isPhone) reconcile();\n }}\n onChange={(e) => {\n if (locked) return;\n setText(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n if (locked) return lockedKeyDown(e);\n // Disabled rows are passed over; Up from \"nothing highlighted\" lands on\n // the first takeable row, as it always landed on row 0. The create row is\n // the last row the arrows reach.\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setOpen(true);\n setActive((i) => stepEnabled(rows, i, 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => (i < 0 ? stepEnabled(rows, -1, 1) : stepEnabled(rows, i, -1)));\n } else if (e.key === \"Enter\") {\n // An IME's Enter takes nothing, and judges no loose text (BUG-9).\n if (isImeEnter(e)) return;\n if (open && isOptionEnabled(rows[active])) {\n e.preventDefault();\n if (active === createIndex && showCreate) create();\n else take(matches[active]);\n } else {\n reconcile();\n }\n } else if (e.key === \"Escape\") {\n // A showing list takes the key and stops it — see the twin in\n // {@link Combobox} (round 0.34 BUG-1).\n if (showing && takeEscape(e)) return;\n setText(null);\n close();\n } else if (e.key === \"Tab\") {\n // Focus is in THIS input and stays there, so the browser's own Tab is\n // left alone; all that is needed is that the list stop covering what\n // the user is tabbing to. `reconcile` rather than `close`, because\n // leaving the field is exactly when loose text has to be judged.\n reconcile();\n }\n // Home/End stay with the caret — see the note in {@link Combobox}: this\n // field is editable, and its text is what `reconcile` judges.\n }}\n className={cn(\n FIELD_BASE,\n label !== undefined && FIELD_FLOATING_PAD,\n \"pe-9\",\n field.isInvalid && FIELD_INVALID,\n )}\n />\n {showClear ? (\n <button\n type=\"button\"\n // Out of the tab order, like the clear on `EntityCombobox`: the keyboard\n // already clears this field by selecting its text and deleting, and a\n // second stop between every picker and the next field is a worse trade\n // than the one gesture it saves.\n tabIndex={-1}\n aria-label={labels.clear}\n // preventDefault, exactly as the chevron does: without it the press\n // focuses the input, which on a phone opens the sheet over the field the\n // press was clearing.\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => {\n setText(null);\n close();\n if (!locked) onChange(clearValue);\n }}\n className={cn(\n \"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-placeholder\",\n \"hover:text-secondary\",\n )}\n >\n <X aria-hidden className=\"size-4\" />\n </button>\n ) : (\n <ChevronDown\n aria-hidden\n onMouseDown={(e) => {\n // Toggle on the chevron without stealing focus from the input — and\n // not at all on a disabled or locked field, which the input's own\n // `disabled`/`readOnly` does not stop from here.\n e.preventDefault();\n if (!inert) setOpen((o) => !o);\n }}\n className={cn(\n \"absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-placeholder\",\n // The box above does the dimming; dimming here too would halve it again.\n !inert && \"cursor-pointer\",\n )}\n />\n )}\n </div>,\n )}\n </EndHintRow>\n {label !== undefined && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel htmlFor={fieldId} className={cn(LABEL_IN_ROW, inert && \"opacity-50\")}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {isPhone && (\n <PickerSheet\n open={open && !inert}\n // Closing without choosing keeps the value: `text` was never emptied, so\n // reconcile has nothing to undo — it just puts the label back.\n onClose={reconcile}\n title={sheetTitle ?? label}\n query={sheetQuery}\n onQueryChange={(v) => {\n setSheetQuery(v);\n setActive(-1);\n }}\n searchPlaceholder={searchPlaceholder ?? placeholder ?? labels.search}\n inputRef={sheetInputRef}\n closeLabel={closeLabel}\n >\n <ul id={listboxId} role=\"listbox\">\n {matches.map((o, i) => (\n // Keyed by group AND value, like combobox-core.tsx: an option may\n // deliberately appear twice (see `reconcile`), and a bare value key\n // would collide.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {o.group && o.group !== matches[i - 1]?.group && (\n <li\n role=\"presentation\"\n className=\"px-4 pb-0.5 pt-3 text-caption font-semibold uppercase tracking-wide text-muted\"\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={o.value === value}\n aria-disabled={o.disabled || undefined}\n onClick={() => take(o)}\n className={cn(\n SHEET_ROW_CLASS,\n o.value === value && \"font-medium\",\n o.disabled && DISABLED_ROW_CLASS,\n )}\n >\n {o.label}\n </button>\n </li>\n </Fragment>\n ))}\n {showCreate && (\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n // An action, never chosen — see `onCreate`.\n aria-selected={false}\n aria-disabled={createLocked || undefined}\n onClick={create}\n className={cn(\n SHEET_ROW_CLASS,\n CREATE_ROW_CLASS,\n createLocked && DISABLED_ROW_CLASS,\n )}\n >\n {createRowContent}\n </button>\n </li>\n )}\n {/* \"Nothing matched\" only when there is nothing to do either: with a create\n row the list is not empty, it is offering to fill itself. */}\n {matches.length === 0 && !showCreate && (\n <li className=\"px-4 py-3 text-sm text-muted\">{labels.noResults}</li>\n )}\n </ul>\n </PickerSheet>\n )}\n {showing && (\n <SuggestionList id={listboxId} anchorRef={fieldRef} panelRef={panelRef}>\n {matches.map((o, i) => (\n // A group heading is emitted at each group boundary rather than repeating\n // the group on every row (feedback #136). `matches` is group-contiguous,\n // so comparing with the previous row is enough. Fragment key sits here;\n // the heading and the option carry their own list semantics — keyed by\n // group AND value, because an option may deliberately appear twice (see\n // `reconcile`).\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {o.group && o.group !== matches[i - 1]?.group && (\n <li\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-caption font-semibold uppercase tracking-wide text-muted first:pt-1\"\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n // See {@link Combobox}: the role rides the button, `aria-selected`\n // is the CHOSEN row, and the keyboard's row is the field's\n // `aria-activedescendant`.\n role=\"option\"\n aria-selected={o.value === value}\n aria-disabled={o.disabled || undefined}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown (not click) so the input's blur can't close the\n // list before the selection registers. Prevented on a disabled\n // row too, so pressing one leaves the field focused and open.\n e.preventDefault();\n take(o);\n }}\n onMouseEnter={() => {\n if (!o.disabled) setActive(i);\n }}\n className={cn(\n rowClass(i === active && !o.disabled),\n o.disabled && DISABLED_ROW_CLASS,\n // Indented under its heading, so the hierarchy is readable at a\n // glance instead of inferred from grey trailing text.\n o.group && \"ps-6\",\n o.value === value && \"font-medium\",\n )}\n >\n {o.label}\n {o.sublabel && (\n <span className=\"ms-2 text-xs text-placeholder\">\n {o.sublabel}\n </span>\n )}\n </button>\n </li>\n </Fragment>\n ))}\n {showCreate && (\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(createIndex)}\n role=\"option\"\n // An action, never chosen; whether the keyboard is ON it is the field's\n // `aria-activedescendant`'s to say.\n aria-selected={false}\n aria-disabled={createLocked || undefined}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown, like the rows above: the input's blur would otherwise\n // reconcile and close the list before the click landed.\n e.preventDefault();\n create();\n }}\n onMouseEnter={() => {\n if (!createLocked) setActive(createIndex);\n }}\n className={cn(\n rowClass(active === createIndex && !createLocked),\n CREATE_ROW_CLASS,\n createLocked && DISABLED_ROW_CLASS,\n )}\n >\n {createRowContent}\n </button>\n </li>\n )}\n </SuggestionList>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["Fragment","rest"]}
@@ -1,4 +1,4 @@
1
- import type { ComponentPropsWithoutRef, KeyboardEvent, MouseEvent as ReactMouseEvent, ReactNode } from "react";
1
+ import type { ComponentPropsWithoutRef, KeyboardEvent, MouseEvent as ReactMouseEvent, ReactNode, Ref } from "react";
2
2
  import { type DialogParam } from "../hooks/use-search-param-state.js";
3
3
  /**
4
4
  * The panel's own ANIMATED dismissal, for chrome rendered inside it. `DialogFrame`'s X
@@ -130,6 +130,14 @@ export interface ModalProps extends Omit<ComponentPropsWithoutRef<"div">, "role"
130
130
  * `onClose` still runs, after the param is cleared. Requires a react-router Router.
131
131
  */
132
132
  urlParam?: DialogParam;
133
+ /**
134
+ * The PANEL — the `role="dialog"` element the rest of the props go on, not the
135
+ * backdrop around it (0.35, round 0.35 R4, the follow-up to API-2; as on
136
+ * `FullBleedDialog`). Merged with the panel's own ref, which the focus
137
+ * trap holds. Set while the panel is in the document: `null` while a dialog with
138
+ * `open={false}` is closed, and after a `urlParam` dialog's param is cleared.
139
+ */
140
+ ref?: Ref<HTMLDivElement>;
133
141
  }
134
142
  /**
135
143
  * Centered (bottom-sheet on mobile) modal dialog: dimmed backdrop, single
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
- import { createContext, useEffect, useRef, useState } from "react";
3
+ import { createContext, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { createPortal } from "react-dom";
5
5
  import { cn } from "../lib/cn.js";
6
+ import { mergeRefs } from "./field-parts.js";
6
7
  import { useFocusTrap } from "../hooks/use-focus-trap.js";
7
8
  import { useBodyScrollLock } from "../hooks/use-body-scroll-lock.js";
8
9
  import { useOverlayHistory } from "../hooks/use-overlay-history.js";
@@ -94,9 +95,11 @@ function ModalPanel({
94
95
  role = "dialog",
95
96
  urlParam: _urlParam,
96
97
  ownHistoryEntry = true,
98
+ ref,
97
99
  ...rest
98
100
  }) {
99
101
  const panelRef = useRef(null);
102
+ const setPanel = useMemo(() => mergeRefs(panelRef, ref), [ref]);
100
103
  const drag = useDragOffset(Boolean(draggable));
101
104
  const active = open ?? true;
102
105
  const [dismissed, setDismissed] = useState(false);
@@ -123,9 +126,9 @@ function ModalPanel({
123
126
  useBodyScrollLock(active);
124
127
  useFocusTrap(panelRef, { active, initialFocus: "container" });
125
128
  const leaving = closing || exiting;
126
- useDismissLayer(active, requestClose);
129
+ const layer = useDismissLayer(active, requestClose);
127
130
  const handleKeyDown = (e) => {
128
- runOwnKeyDown(e, onKeyDown);
131
+ runOwnKeyDown(e, onKeyDown, layer);
129
132
  takeEscape(e);
130
133
  };
131
134
  if (!active && !exiting) return null;
@@ -147,7 +150,7 @@ function ModalPanel({
147
150
  "div",
148
151
  {
149
152
  ...rest,
150
- ref: panelRef,
153
+ ref: setPanel,
151
154
  role: role === "alertdialog" ? "alertdialog" : "dialog",
152
155
  "aria-modal": "true",
153
156
  "aria-labelledby": labelledBy,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/modal.tsx"],"sourcesContent":["import { createContext, useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type {\n ComponentPropsWithoutRef,\n KeyboardEvent,\n MouseEvent as ReactMouseEvent,\n PointerEvent as ReactPointerEvent,\n ReactNode,\n} from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { useBodyScrollLock } from \"../hooks/use-body-scroll-lock\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { useDialogParam, type DialogParam } from \"../hooks/use-search-param-state\";\nimport { OVERLAY_EXIT_MS, skipsExitAnimation, useCloseTransition } from \"../hooks/use-close-transition\";\nimport { runOwnKeyDown, takeEscape, useDismissLayer } from \"../hooks/use-dismiss\";\n\n/**\n * Drag the panel by its top strip, in pointer space (dev#460).\n *\n * Deliberately not a library and not persisted: the offset is component state that\n * dies with the dialog. It starts only on a press that lands on the panel's own\n * chrome — never on a field, a button or a link, which is what keeps \"drag to select\n * text in the textarea\" working. Above `md` only: below it the panel is a\n * full-width bottom sheet with nothing beside it to uncover.\n *\n * The whole gesture stays on the PANEL — captured pointer, React handlers — rather than\n * on `window`. The first shape of this added `pointermove`/`pointerup` to `window` from\n * inside the `pointerdown` handler and took them off again from the `pointerup` handler,\n * so the teardown ran only if the gesture ended: React never sees a listener added from\n * an event handler, and an unmount mid-drag left a live `pointermove` on `window` for\n * the life of the page, pinning this hook's state with it. The one caller that enables\n * dragging is the feedback composer, which closes ITSELF on submit — a Ctrl+Enter sent\n * with the pointer still down is exactly that order of events, once per drag.\n */\nfunction useDragOffset(enabled: boolean) {\n const [offset, setOffset] = useState<{ x: number; y: number } | null>(null);\n const from = useRef<{ x: number; y: number; ox: number; oy: number } | null>(null);\n\n const endDrag = () => {\n from.current = null;\n };\n\n return {\n /** Back to where the panel opens. A `Modal` with an `open` prop outlives its own\n * closes, so \"the position resets when the dialog closes\" needs saying. */\n reset: () => setOffset(null),\n style: offset ? { transform: `translate(${offset.x}px, ${offset.y}px)` } : undefined,\n handlers: !enabled\n ? undefined\n : {\n onPointerDown: (e: ReactPointerEvent<HTMLDivElement>) => {\n if (e.button !== 0 || window.innerWidth < 768) return;\n const target = e.target as HTMLElement;\n // Only the panel itself and its non-interactive chrome start a drag; a\n // press inside any control belongs to that control.\n if (target.closest(\"input,textarea,select,button,a,[role='button']\")) return;\n from.current = { x: e.clientX, y: e.clientY, ox: offset?.x ?? 0, oy: offset?.y ?? 0 };\n try {\n // Capture is what lets the handlers live on the panel: every later\n // pointer event for this pointer retargets here, so the drag keeps\n // working once the pointer outruns the panel's own box — which it always\n // does, since the panel moves out from under it.\n e.currentTarget.setPointerCapture(e.pointerId);\n } catch {\n // jsdom implements no pointer capture, and a browser throws\n // NotFoundError for a pointerId that is already gone. Neither is worth\n // refusing the drag: uncaptured, the panel still gets the moves that\n // land on it.\n }\n },\n onPointerMove: (e: ReactPointerEvent<HTMLDivElement>) => {\n const start = from.current;\n if (!start) return;\n setOffset({ x: start.ox + e.clientX - start.x, y: start.oy + e.clientY - start.y });\n },\n onPointerUp: endDrag,\n // A cancel (the browser taking the pointer for a scroll or a gesture) and a\n // lost capture both mean no `pointerup` is coming. Without them the drag would\n // stay \"live\" and the next move over the panel would jump it.\n onPointerCancel: endDrag,\n onLostPointerCapture: endDrag,\n },\n };\n}\n\n/**\n * The panel's own ANIMATED dismissal, for chrome rendered inside it. `DialogFrame`'s X\n * and its `actions(close)` read this, so a Cancel or an X lowers the panel exactly the\n * way Escape does — calling `onClose` from inside unmounts with no exit, which is the\n * documented limit of {@link useCloseTransition}. `null` outside a `Modal`.\n */\nexport const ModalCloseContext = createContext<(() => void) | null>(null);\n\n/**\n * Backdrop handlers that close only when a press starts AND ends on the\n * backdrop itself. A plain onClick fires on mouseup, so dragging a text\n * selection that starts inside the panel and releases over the backdrop would\n * dismiss the dialog (feedback #254) — and the reverse drag would too, since\n * click targets the common ancestor of the down/up elements.\n */\nexport function useBackdropClose(onClose: () => void) {\n const pressStartedOnBackdrop = useRef(false);\n return {\n onMouseDown: (e: ReactMouseEvent<HTMLDivElement>) => {\n pressStartedOnBackdrop.current = e.target === e.currentTarget;\n },\n onMouseUp: (e: ReactMouseEvent<HTMLDivElement>) => {\n const started = pressStartedOnBackdrop.current;\n pressStartedOnBackdrop.current = false;\n if (started && e.target === e.currentTarget) onClose();\n },\n };\n}\n\n// Tab-cycle target set: the interactive descendants the focus trap rotates\n// through. Querying live (per keystroke) means dynamically rendered fields are\n// included automatically.\n/**\n * `extends ComponentPropsWithoutRef<\"div\">` so anything a `<div>` takes reaches the\n * PANEL — the element with `role=\"dialog\"`, which is the one a consumer means. That is\n * where a `data-tour` anchor, a test id, an `aria-describedby` or an `aria-label` has\n * to land; before this, a closed prop list dropped all four silently (audit §api-design).\n */\nexport interface ModalProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"role\"> {\n /** Invoked on backdrop click and on Escape. */\n onClose: () => void;\n /**\n * Who decides whether the dialog is up, when the caller wants to keep it MOUNTED.\n *\n * Left out, the dialog is open for as long as it is mounted — `{open && <Modal/>}`,\n * which is every caller before this prop and stays exactly as it was. That idiom has\n * one blind spot: a close the CALLER decides on (a save that succeeded, a\n * `setOpen(false)` from outside) unmounts the panel on the spot, with no exit,\n * because there is nothing left to animate. kastlan's `FormModal` was nothing but an\n * `if (!open) return null` gate in front of this.\n *\n * Passed, the component stays mounted while closed and renders nothing, and a flip\n * to `false` plays the same exit a dismissal does before the panel goes. The focus\n * trap, the scroll lock and the Back-gesture history entry are held only while\n * `open` is true — they are released the moment it turns false, so focus is back on\n * the trigger while the panel is still lowering. A dismissal (Escape, the backdrop,\n * Back, the frame's X) animates first and then calls `onClose`; the `false` that\n * comes back from it is not animated a second time.\n *\n * The body is unmounted once the exit has finished, as it is today, so a form inside\n * still starts fresh on the next open.\n */\n open?: boolean;\n /** `\"alertdialog\"` for a dialog that interrupts to ask a question the user must\n * answer (a confirmation) — screen readers announce it with more urgency. Only the\n * two dialog roles are allowed: the panel stays modal either way. */\n role?: \"dialog\" | \"alertdialog\";\n children: ReactNode;\n /**\n * Panel max-width: `md` (28rem), `lg` (32rem), `xl` (48rem), `2xl` (the display less\n * the backdrop's margin, capped at 110rem).\n *\n * `xl` exists because two callers had already written `className=\"max-w-3xl\"` to\n * reach it — the payee backfill's preview table and the price-merge dialog, both\n * of which hold a list and several fields rather than one question. A caller\n * patching a missing size IS the signal that the size is missing, and a\n * hand-written width in a `className` is a width nothing else can line up with.\n *\n * `2xl` is the same signal one size up (round 0.34 C3, KS U1): a dialog whose\n * content is a TABLE to be read. Kurvenschmiede's five part dialogs hold a measured\n * table pasted from a spreadsheet and had patched themselves to\n * `w-[min(110rem,94vw)] max-w-none`; Keksdose's report drilldown passed `size=\"lg\"`\n * and then `max-w-5xl`. The cap is the generous one of the two, because a table\n * squeezed into a box in the middle of a wide screen is a table scrolled sideways\n * (KS feedback #16); below the cap the panel is as wide as the backdrop lets it be.\n * In rem, so it grows with the text size until the screen stops it.\n *\n * The panel is `w-full` under every one of them, so the cap only bites once the\n * viewport is wider than it: on a phone all four are the same full-width bottom\n * sheet. Nothing here may introduce a `min-w-*`, which is what would break that.\n */\n size?: \"md\" | \"lg\" | \"xl\" | \"2xl\";\n /** Extra classes for the panel (e.g. `space-y-3` for form content spacing). */\n className?: string;\n /** id of the heading element, wired to `aria-labelledby`. */\n labelledBy?: string;\n /**\n * Extra key handling on the panel (e.g. Ctrl/Cmd+Enter to submit). Runs first; a\n * `preventDefault()` on Escape HERE keeps the dialog open — the owner's claim (see\n * `use-dismiss.ts`). One a field inside makes does not, since 0.34: the rule\n * `FullBleedDialog` already had, so a search box keeping Chromium from clearing itself\n * no longer holds the dialog open (Wave 2, DUP-1).\n */\n onKeyDown?: (e: KeyboardEvent<HTMLDivElement>) => void;\n /**\n * Drop the backdrop's mobile margin so the panel can reach the screen edges\n * (a full-screen sheet on phones). The panel still needs its own\n * `h-[100dvh] max-w-full rounded-none` classes via `className` to fill it; the\n * desktop `md:p-4` margin is kept. Defaults to false (padded on all sizes).\n */\n fullBleed?: boolean;\n /**\n * Let the user drag the panel out of the way by its top edge (Keksdose feedback\n * dev#460: *\"Make the feedback dialog draggable so I can see behind it if it\n * blocks something.\"*).\n *\n * Opt-in, because it only makes sense for a dialog whose content is ABOUT the page\n * behind it — the feedback composer is the case, an ordinary form is not, and a\n * draggable panel that has nothing to reveal is a control that can only get lost.\n *\n * Pointer-only by design: it is a \"move this out of the way\" gesture, not a layout\n * choice worth persisting, and the position resets when the dialog closes. On a\n * phone the panel is a bottom sheet the width of the screen, so dragging is\n * suppressed there — there is nothing beside it to see.\n */\n draggable?: boolean;\n /**\n * Bind the open state to a URL search param (0.12.0): `\"edit\"` is open while\n * `?edit` is in the query, `{ key: \"dialog\", value: \"create-lease\" }` while\n * `?dialog=create-lease` is. The URL is then the only state — `open` is ignored —\n * and the dialog opens from anywhere that sets the param: {@link useDialogParam}'s\n * setter, a `<Link>`, a shared link, a reload.\n *\n * kastlan's `useDialogParam` did this beside the dialog, so its open state could be\n * deep-linked and captured in a feedback report. Here it also owns the history:\n * opening through `useDialogParam` pushes an entry, so the Back gesture closes the\n * dialog by leaving it, and a close (Escape, the backdrop, the X) goes back over\n * that entry rather than stacking a second one. The dialog's own Back handling\n * ({@link useOverlayHistory}'s throwaway entry) stays off in this mode — the param's\n * entry already is one, and two would take two Back presses to close one dialog.\n *\n * `onClose` still runs, after the param is cleared. Requires a react-router Router.\n */\n urlParam?: DialogParam;\n}\n\n/**\n * The URL-bound dialog: the param is the open state, and closing clears it. A\n * component of its own so the router hook is only called by a `Modal` that asked for\n * it — a plain `Modal` must keep working with no Router above it.\n */\nfunction UrlParamModal({ urlParam, onClose, ...rest }: ModalProps & { urlParam: DialogParam }) {\n const [open, setOpen] = useDialogParam(urlParam);\n // Closed from here the moment the dismissal is decided. Going back is a traversal\n // the browser lands a task later, and between the two the param still says \"open\":\n // without this the panel, already lowered, would pop back up for that task.\n const [leaving, setLeaving] = useState(false);\n if (leaving && !open) setLeaving(false);\n return (\n <ModalPanel\n {...rest}\n open={open && !leaving}\n ownHistoryEntry={false}\n onClose={() => {\n setLeaving(true);\n setOpen(false);\n onClose();\n }}\n />\n );\n}\n\n/**\n * Centered (bottom-sheet on mobile) modal dialog: dimmed backdrop, single\n * rounded panel. Owns the overlay mechanics that the hand-rolled dialogs each\n * re-implemented or lacked — backdrop-click + Escape to close, the platform Back\n * gesture (Keksdose feedback #172), background scroll lock (feedback #204), focus\n * into the panel on open and back to the trigger on close, a Tab focus trap, and\n * `role=\"dialog\"`/`aria-modal`.\n */\nexport function Modal(props: ModalProps) {\n return props.urlParam !== undefined ? (\n <UrlParamModal {...props} urlParam={props.urlParam} />\n ) : (\n <ModalPanel {...props} />\n );\n}\n\nfunction ModalPanel({\n onClose,\n open,\n children,\n size = \"md\",\n className,\n labelledBy,\n onKeyDown,\n fullBleed,\n draggable,\n style,\n role = \"dialog\",\n urlParam: _urlParam,\n ownHistoryEntry = true,\n ...rest\n}: ModalProps & {\n /** False when the URL already holds an entry for this dialog — see `urlParam`. */\n ownHistoryEntry?: boolean;\n}) {\n const panelRef = useRef<HTMLDivElement>(null);\n const drag = useDragOffset(Boolean(draggable));\n // Without `open`, mounted means open — the constant `true` every hook below used to\n // be given. With it, `open` itself.\n const active = open ?? true;\n\n // Whether the last close came through `requestClose`, i.e. has ALREADY been\n // animated. Set in the same batch as the caller's `onClose`, so the render that sees\n // `open` turn false also sees this and does not lower the panel a second time.\n const [dismissed, setDismissed] = useState(false);\n // The exit of a close the caller decided on (only reachable with `open`). Adjusted\n // during render when `open` flips — `Collapse`'s pattern — so the very render that\n // closes it already paints the `-out` classes rather than a frame of nothing.\n const [exiting, setExiting] = useState(false);\n const [prevOpen, setPrevOpen] = useState(open);\n if (open !== prevOpen) {\n setPrevOpen(open);\n setDismissed(false);\n setExiting(open === false && prevOpen === true && !dismissed && !skipsExitAnimation());\n if (open) drag.reset();\n }\n useEffect(() => {\n if (!exiting) return;\n const timer = setTimeout(() => setExiting(false), OVERLAY_EXIT_MS);\n return () => clearTimeout(timer);\n }, [exiting]);\n\n // The panel lowers itself before the caller unmounts it (live #320 rework). Every\n // dismissal below goes through `requestClose`; `onClose` still does the closing, one\n // animation later. A caller that closes the dialog ITSELF — after a save, say —\n // unmounts with no exit unless it passes `open`, which is what that prop is for.\n const { closing, requestClose } = useCloseTransition(() => {\n setDismissed(true);\n onClose();\n });\n // A panel on its way out after `open` turned false is already closed: a press on\n // its fading backdrop must not report a second close.\n const backdropClose = useBackdropClose(active ? requestClose : () => {});\n // Back means the same as Escape here. On a phone Escape doesn't exist, so without\n // this the only way out of a dialog is finding its close button — and Back, the\n // gesture everyone reaches for, navigated the page underneath instead (#172).\n // Held only while open: a closed `open={false}` dialog must not own a history entry.\n useOverlayHistory(active && ownHistoryEntry, requestClose);\n\n // Through the shared hook and not by hand: this component's own save/restore copy\n // was one half of the pair that left the page permanently unscrollable when a dialog\n // containing an open sheet was closed — see the note in use-body-scroll-lock.ts.\n useBodyScrollLock(active);\n\n // Focus the panel itself (not the first field) so opening doesn't pop the mobile\n // keyboard, while still moving focus into the dialog for keyboard and screen-reader\n // users. Through the shared hook since this component is where it came from — the\n // hook adds per-keystroke recomputation, a guard on restoring to a detached node,\n // and nesting, none of which this copy had. Released when `open` turns false, which\n // hands focus back to the trigger while the panel is still on its way out.\n //\n // \"The panel itself\" only when nothing inside asked for focus: a field the app\n // autofocuses on purpose keeps it (0.33.1, keksdose live #402 — the same trap took\n // an autofocused amount straight back in `FullBleedDialog`). Its keyboard is then the\n // one the app chose to show; the trigger still gets focus back on close.\n useFocusTrap(panelRef, { active, initialFocus: \"container\" });\n const leaving = closing || exiting;\n\n // One layer on the Escape stack while open (Wave 2, DUP-1): a list, a popover or a\n // tooltip opened inside takes the key before the dialog does, wherever focus is —\n // and the dialog gets it even when focus is not in the panel.\n useDismissLayer(active, requestClose);\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n runOwnKeyDown(e, onKeyDown);\n // To whatever is on top — this dialog, unless something opened over it — and\n // stopped here. Tab containment lives in useFocusTrap, which listens on the panel.\n takeEscape(e);\n };\n\n // Portal to <body> so the `fixed inset-0 z-50` overlay escapes whatever stacking\n // context its caller sits in. Rendered inline, a modal opened from e.g. the top\n // bar (a z-30 context) could paint BELOW a sibling z-30 sticky element like the\n // expanded transaction editor (feedback #320). The tour/command-palette overlays\n // already portal for the same reason.\n if (!active && !exiting) return null;\n return createPortal(\n <div\n // Out of the accessibility tree for the length of a caller-driven exit: focus is\n // already back on the page, and an `aria-modal` panel that is leaving would hide\n // that page from a screen reader for 220ms. Not `inert` — that would let a tap\n // fall through the fading backdrop to the row underneath.\n aria-hidden={exiting || undefined}\n className={cn(\n // Live #320. The panel below rises only where it is bottom-anchored: from md up\n // it is centred, and a centred box sliding up from off-screen reads as a\n // different component arriving rather than as the same one settling.\n leaving ? \"animate-overlay-out\" : \"animate-overlay\",\n \"fixed inset-0 z-50 flex items-end justify-center bg-black/40 md:items-center\",\n fullBleed ? \"p-0 md:p-4\" : \"p-4\",\n )}\n {...backdropClose}\n >\n {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- always\n role=\"dialog\" or \"alertdialog\" (the ternary hides that from the rule): `onKeyDown`\n is Escape-to-close per the APG dialog pattern, and the drag handlers are\n swipe-to-dismiss, whose keyboard path is that same Escape. */}\n <div\n // `...rest` FIRST, everything the dialog needs to be a dialog after it. A\n // wrapper that spreads its own props through to this one cannot then take away\n // the role, the modality or the `tabIndex={-1}` that `useFocusTrap` needs —\n // losing any of the three is silent, and the symptom (a dialog a screen reader\n // walks straight out of) shows up nowhere near the call site that caused it.\n {...rest}\n ref={panelRef}\n // Normalised at runtime too: an untyped caller's `role=\"presentation\"` must not\n // take the dialog role away (props-passthrough pins that).\n role={role === \"alertdialog\" ? \"alertdialog\" : \"dialog\"}\n aria-modal=\"true\"\n aria-labelledby={labelledBy}\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n {...drag.handlers}\n // MERGED, not replaced: `drag.style` is undefined until a drag starts, so\n // assigning it outright would drop a caller's own `style` on every render but\n // the ones where the panel is being dragged.\n style={{ ...style, ...drag.style }}\n className={cn(\n leaving ? \"animate-sheet-out md:animate-none\" : \"animate-sheet md:animate-none\",\n \"w-full rounded-lg border border-subtle bg-surface shadow-sm outline-none\",\n { md: \"max-w-md\", lg: \"max-w-lg\", xl: \"max-w-3xl\", \"2xl\": \"max-w-[110rem]\" }[size],\n // A panel taller than the screen has to scroll ITSELF. The backdrop is\n // `fixed inset-0` and the body is scroll-locked while a dialog is open, so\n // without this the overflow simply had nowhere to go: on a phone the panel\n // is bottom-anchored, so it grew UPWARD past the top edge and the part that\n // went past it could not be reached by any gesture (Keksdose live #254, on\n // the price-merge dialog — but it was every dialog with more than a screen\n // of content). `max-h-full` resolves against the backdrop's content box,\n // which is the viewport minus its own padding, so the panel stops exactly\n // where the screen does; `overscroll-contain` keeps the fling inside it.\n //\n // Three callers had already patched themselves with `max-h-[90vh]` +\n // an inner scroller. Those still win — `cn` is tailwind-merge — and this is\n // the default they should not have needed.\n \"max-h-full overflow-y-auto overscroll-contain\",\n \"p-4\",\n // The grab affordance sits on the panel's own top strip: a drag handle of\n // its own would be one more control in a dialog that is mostly one field.\n draggable && \"md:[&>*:first-child]:cursor-grab\",\n className,\n )}\n >\n <ModalCloseContext.Provider value={requestClose}>{children}</ModalCloseContext.Provider>\n </div>\n </div>,\n document.body,\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/modal.tsx"],"sourcesContent":["import { createContext, useEffect, useMemo, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type {\n ComponentPropsWithoutRef,\n KeyboardEvent,\n MouseEvent as ReactMouseEvent,\n PointerEvent as ReactPointerEvent,\n ReactNode,\n Ref,\n} from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { mergeRefs } from \"./field-parts\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { useBodyScrollLock } from \"../hooks/use-body-scroll-lock\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { useDialogParam, type DialogParam } from \"../hooks/use-search-param-state\";\nimport { OVERLAY_EXIT_MS, skipsExitAnimation, useCloseTransition } from \"../hooks/use-close-transition\";\nimport { runOwnKeyDown, takeEscape, useDismissLayer } from \"../hooks/use-dismiss\";\n\n/**\n * Drag the panel by its top strip, in pointer space (dev#460).\n *\n * Deliberately not a library and not persisted: the offset is component state that\n * dies with the dialog. It starts only on a press that lands on the panel's own\n * chrome — never on a field, a button or a link, which is what keeps \"drag to select\n * text in the textarea\" working. Above `md` only: below it the panel is a\n * full-width bottom sheet with nothing beside it to uncover.\n *\n * The whole gesture stays on the PANEL — captured pointer, React handlers — rather than\n * on `window`. The first shape of this added `pointermove`/`pointerup` to `window` from\n * inside the `pointerdown` handler and took them off again from the `pointerup` handler,\n * so the teardown ran only if the gesture ended: React never sees a listener added from\n * an event handler, and an unmount mid-drag left a live `pointermove` on `window` for\n * the life of the page, pinning this hook's state with it. The one caller that enables\n * dragging is the feedback composer, which closes ITSELF on submit — a Ctrl+Enter sent\n * with the pointer still down is exactly that order of events, once per drag.\n */\nfunction useDragOffset(enabled: boolean) {\n const [offset, setOffset] = useState<{ x: number; y: number } | null>(null);\n const from = useRef<{ x: number; y: number; ox: number; oy: number } | null>(null);\n\n const endDrag = () => {\n from.current = null;\n };\n\n return {\n /** Back to where the panel opens. A `Modal` with an `open` prop outlives its own\n * closes, so \"the position resets when the dialog closes\" needs saying. */\n reset: () => setOffset(null),\n style: offset ? { transform: `translate(${offset.x}px, ${offset.y}px)` } : undefined,\n handlers: !enabled\n ? undefined\n : {\n onPointerDown: (e: ReactPointerEvent<HTMLDivElement>) => {\n if (e.button !== 0 || window.innerWidth < 768) return;\n const target = e.target as HTMLElement;\n // Only the panel itself and its non-interactive chrome start a drag; a\n // press inside any control belongs to that control.\n if (target.closest(\"input,textarea,select,button,a,[role='button']\")) return;\n from.current = { x: e.clientX, y: e.clientY, ox: offset?.x ?? 0, oy: offset?.y ?? 0 };\n try {\n // Capture is what lets the handlers live on the panel: every later\n // pointer event for this pointer retargets here, so the drag keeps\n // working once the pointer outruns the panel's own box — which it always\n // does, since the panel moves out from under it.\n e.currentTarget.setPointerCapture(e.pointerId);\n } catch {\n // jsdom implements no pointer capture, and a browser throws\n // NotFoundError for a pointerId that is already gone. Neither is worth\n // refusing the drag: uncaptured, the panel still gets the moves that\n // land on it.\n }\n },\n onPointerMove: (e: ReactPointerEvent<HTMLDivElement>) => {\n const start = from.current;\n if (!start) return;\n setOffset({ x: start.ox + e.clientX - start.x, y: start.oy + e.clientY - start.y });\n },\n onPointerUp: endDrag,\n // A cancel (the browser taking the pointer for a scroll or a gesture) and a\n // lost capture both mean no `pointerup` is coming. Without them the drag would\n // stay \"live\" and the next move over the panel would jump it.\n onPointerCancel: endDrag,\n onLostPointerCapture: endDrag,\n },\n };\n}\n\n/**\n * The panel's own ANIMATED dismissal, for chrome rendered inside it. `DialogFrame`'s X\n * and its `actions(close)` read this, so a Cancel or an X lowers the panel exactly the\n * way Escape does — calling `onClose` from inside unmounts with no exit, which is the\n * documented limit of {@link useCloseTransition}. `null` outside a `Modal`.\n */\nexport const ModalCloseContext = createContext<(() => void) | null>(null);\n\n/**\n * Backdrop handlers that close only when a press starts AND ends on the\n * backdrop itself. A plain onClick fires on mouseup, so dragging a text\n * selection that starts inside the panel and releases over the backdrop would\n * dismiss the dialog (feedback #254) — and the reverse drag would too, since\n * click targets the common ancestor of the down/up elements.\n */\nexport function useBackdropClose(onClose: () => void) {\n const pressStartedOnBackdrop = useRef(false);\n return {\n onMouseDown: (e: ReactMouseEvent<HTMLDivElement>) => {\n pressStartedOnBackdrop.current = e.target === e.currentTarget;\n },\n onMouseUp: (e: ReactMouseEvent<HTMLDivElement>) => {\n const started = pressStartedOnBackdrop.current;\n pressStartedOnBackdrop.current = false;\n if (started && e.target === e.currentTarget) onClose();\n },\n };\n}\n\n// Tab-cycle target set: the interactive descendants the focus trap rotates\n// through. Querying live (per keystroke) means dynamically rendered fields are\n// included automatically.\n/**\n * `extends ComponentPropsWithoutRef<\"div\">` so anything a `<div>` takes reaches the\n * PANEL — the element with `role=\"dialog\"`, which is the one a consumer means. That is\n * where a `data-tour` anchor, a test id, an `aria-describedby` or an `aria-label` has\n * to land; before this, a closed prop list dropped all four silently (audit §api-design).\n */\nexport interface ModalProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"role\"> {\n /** Invoked on backdrop click and on Escape. */\n onClose: () => void;\n /**\n * Who decides whether the dialog is up, when the caller wants to keep it MOUNTED.\n *\n * Left out, the dialog is open for as long as it is mounted — `{open && <Modal/>}`,\n * which is every caller before this prop and stays exactly as it was. That idiom has\n * one blind spot: a close the CALLER decides on (a save that succeeded, a\n * `setOpen(false)` from outside) unmounts the panel on the spot, with no exit,\n * because there is nothing left to animate. kastlan's `FormModal` was nothing but an\n * `if (!open) return null` gate in front of this.\n *\n * Passed, the component stays mounted while closed and renders nothing, and a flip\n * to `false` plays the same exit a dismissal does before the panel goes. The focus\n * trap, the scroll lock and the Back-gesture history entry are held only while\n * `open` is true — they are released the moment it turns false, so focus is back on\n * the trigger while the panel is still lowering. A dismissal (Escape, the backdrop,\n * Back, the frame's X) animates first and then calls `onClose`; the `false` that\n * comes back from it is not animated a second time.\n *\n * The body is unmounted once the exit has finished, as it is today, so a form inside\n * still starts fresh on the next open.\n */\n open?: boolean;\n /** `\"alertdialog\"` for a dialog that interrupts to ask a question the user must\n * answer (a confirmation) — screen readers announce it with more urgency. Only the\n * two dialog roles are allowed: the panel stays modal either way. */\n role?: \"dialog\" | \"alertdialog\";\n children: ReactNode;\n /**\n * Panel max-width: `md` (28rem), `lg` (32rem), `xl` (48rem), `2xl` (the display less\n * the backdrop's margin, capped at 110rem).\n *\n * `xl` exists because two callers had already written `className=\"max-w-3xl\"` to\n * reach it — the payee backfill's preview table and the price-merge dialog, both\n * of which hold a list and several fields rather than one question. A caller\n * patching a missing size IS the signal that the size is missing, and a\n * hand-written width in a `className` is a width nothing else can line up with.\n *\n * `2xl` is the same signal one size up (round 0.34 C3, KS U1): a dialog whose\n * content is a TABLE to be read. Kurvenschmiede's five part dialogs hold a measured\n * table pasted from a spreadsheet and had patched themselves to\n * `w-[min(110rem,94vw)] max-w-none`; Keksdose's report drilldown passed `size=\"lg\"`\n * and then `max-w-5xl`. The cap is the generous one of the two, because a table\n * squeezed into a box in the middle of a wide screen is a table scrolled sideways\n * (KS feedback #16); below the cap the panel is as wide as the backdrop lets it be.\n * In rem, so it grows with the text size until the screen stops it.\n *\n * The panel is `w-full` under every one of them, so the cap only bites once the\n * viewport is wider than it: on a phone all four are the same full-width bottom\n * sheet. Nothing here may introduce a `min-w-*`, which is what would break that.\n */\n size?: \"md\" | \"lg\" | \"xl\" | \"2xl\";\n /** Extra classes for the panel (e.g. `space-y-3` for form content spacing). */\n className?: string;\n /** id of the heading element, wired to `aria-labelledby`. */\n labelledBy?: string;\n /**\n * Extra key handling on the panel (e.g. Ctrl/Cmd+Enter to submit). Runs first; a\n * `preventDefault()` on Escape HERE keeps the dialog open — the owner's claim (see\n * `use-dismiss.ts`). One a field inside makes does not, since 0.34: the rule\n * `FullBleedDialog` already had, so a search box keeping Chromium from clearing itself\n * no longer holds the dialog open (Wave 2, DUP-1).\n */\n onKeyDown?: (e: KeyboardEvent<HTMLDivElement>) => void;\n /**\n * Drop the backdrop's mobile margin so the panel can reach the screen edges\n * (a full-screen sheet on phones). The panel still needs its own\n * `h-[100dvh] max-w-full rounded-none` classes via `className` to fill it; the\n * desktop `md:p-4` margin is kept. Defaults to false (padded on all sizes).\n */\n fullBleed?: boolean;\n /**\n * Let the user drag the panel out of the way by its top edge (Keksdose feedback\n * dev#460: *\"Make the feedback dialog draggable so I can see behind it if it\n * blocks something.\"*).\n *\n * Opt-in, because it only makes sense for a dialog whose content is ABOUT the page\n * behind it — the feedback composer is the case, an ordinary form is not, and a\n * draggable panel that has nothing to reveal is a control that can only get lost.\n *\n * Pointer-only by design: it is a \"move this out of the way\" gesture, not a layout\n * choice worth persisting, and the position resets when the dialog closes. On a\n * phone the panel is a bottom sheet the width of the screen, so dragging is\n * suppressed there — there is nothing beside it to see.\n */\n draggable?: boolean;\n /**\n * Bind the open state to a URL search param (0.12.0): `\"edit\"` is open while\n * `?edit` is in the query, `{ key: \"dialog\", value: \"create-lease\" }` while\n * `?dialog=create-lease` is. The URL is then the only state — `open` is ignored —\n * and the dialog opens from anywhere that sets the param: {@link useDialogParam}'s\n * setter, a `<Link>`, a shared link, a reload.\n *\n * kastlan's `useDialogParam` did this beside the dialog, so its open state could be\n * deep-linked and captured in a feedback report. Here it also owns the history:\n * opening through `useDialogParam` pushes an entry, so the Back gesture closes the\n * dialog by leaving it, and a close (Escape, the backdrop, the X) goes back over\n * that entry rather than stacking a second one. The dialog's own Back handling\n * ({@link useOverlayHistory}'s throwaway entry) stays off in this mode — the param's\n * entry already is one, and two would take two Back presses to close one dialog.\n *\n * `onClose` still runs, after the param is cleared. Requires a react-router Router.\n */\n urlParam?: DialogParam;\n /**\n * The PANEL — the `role=\"dialog\"` element the rest of the props go on, not the\n * backdrop around it (0.35, round 0.35 R4, the follow-up to API-2; as on\n * `FullBleedDialog`). Merged with the panel's own ref, which the focus\n * trap holds. Set while the panel is in the document: `null` while a dialog with\n * `open={false}` is closed, and after a `urlParam` dialog's param is cleared.\n */\n ref?: Ref<HTMLDivElement>;\n}\n\n/**\n * The URL-bound dialog: the param is the open state, and closing clears it. A\n * component of its own so the router hook is only called by a `Modal` that asked for\n * it — a plain `Modal` must keep working with no Router above it.\n */\nfunction UrlParamModal({ urlParam, onClose, ...rest }: ModalProps & { urlParam: DialogParam }) {\n const [open, setOpen] = useDialogParam(urlParam);\n // Closed from here the moment the dismissal is decided. Going back is a traversal\n // the browser lands a task later, and between the two the param still says \"open\":\n // without this the panel, already lowered, would pop back up for that task.\n const [leaving, setLeaving] = useState(false);\n if (leaving && !open) setLeaving(false);\n return (\n <ModalPanel\n {...rest}\n open={open && !leaving}\n ownHistoryEntry={false}\n onClose={() => {\n setLeaving(true);\n setOpen(false);\n onClose();\n }}\n />\n );\n}\n\n/**\n * Centered (bottom-sheet on mobile) modal dialog: dimmed backdrop, single\n * rounded panel. Owns the overlay mechanics that the hand-rolled dialogs each\n * re-implemented or lacked — backdrop-click + Escape to close, the platform Back\n * gesture (Keksdose feedback #172), background scroll lock (feedback #204), focus\n * into the panel on open and back to the trigger on close, a Tab focus trap, and\n * `role=\"dialog\"`/`aria-modal`.\n */\nexport function Modal(props: ModalProps) {\n return props.urlParam !== undefined ? (\n <UrlParamModal {...props} urlParam={props.urlParam} />\n ) : (\n <ModalPanel {...props} />\n );\n}\n\nfunction ModalPanel({\n onClose,\n open,\n children,\n size = \"md\",\n className,\n labelledBy,\n onKeyDown,\n fullBleed,\n draggable,\n style,\n role = \"dialog\",\n urlParam: _urlParam,\n ownHistoryEntry = true,\n ref,\n ...rest\n}: ModalProps & {\n /** False when the URL already holds an entry for this dialog — see `urlParam`. */\n ownHistoryEntry?: boolean;\n}) {\n const panelRef = useRef<HTMLDivElement>(null);\n // The caller's `ref` beside the trap's (round 0.35 R4): memoised, so the panel is not\n // detached and re-attached on every render.\n const setPanel = useMemo(() => mergeRefs(panelRef, ref), [ref]);\n const drag = useDragOffset(Boolean(draggable));\n // Without `open`, mounted means open — the constant `true` every hook below used to\n // be given. With it, `open` itself.\n const active = open ?? true;\n\n // Whether the last close came through `requestClose`, i.e. has ALREADY been\n // animated. Set in the same batch as the caller's `onClose`, so the render that sees\n // `open` turn false also sees this and does not lower the panel a second time.\n const [dismissed, setDismissed] = useState(false);\n // The exit of a close the caller decided on (only reachable with `open`). Adjusted\n // during render when `open` flips — `Collapse`'s pattern — so the very render that\n // closes it already paints the `-out` classes rather than a frame of nothing.\n const [exiting, setExiting] = useState(false);\n const [prevOpen, setPrevOpen] = useState(open);\n if (open !== prevOpen) {\n setPrevOpen(open);\n setDismissed(false);\n setExiting(open === false && prevOpen === true && !dismissed && !skipsExitAnimation());\n if (open) drag.reset();\n }\n useEffect(() => {\n if (!exiting) return;\n const timer = setTimeout(() => setExiting(false), OVERLAY_EXIT_MS);\n return () => clearTimeout(timer);\n }, [exiting]);\n\n // The panel lowers itself before the caller unmounts it (live #320 rework). Every\n // dismissal below goes through `requestClose`; `onClose` still does the closing, one\n // animation later. A caller that closes the dialog ITSELF — after a save, say —\n // unmounts with no exit unless it passes `open`, which is what that prop is for.\n const { closing, requestClose } = useCloseTransition(() => {\n setDismissed(true);\n onClose();\n });\n // A panel on its way out after `open` turned false is already closed: a press on\n // its fading backdrop must not report a second close.\n const backdropClose = useBackdropClose(active ? requestClose : () => {});\n // Back means the same as Escape here. On a phone Escape doesn't exist, so without\n // this the only way out of a dialog is finding its close button — and Back, the\n // gesture everyone reaches for, navigated the page underneath instead (#172).\n // Held only while open: a closed `open={false}` dialog must not own a history entry.\n useOverlayHistory(active && ownHistoryEntry, requestClose);\n\n // Through the shared hook and not by hand: this component's own save/restore copy\n // was one half of the pair that left the page permanently unscrollable when a dialog\n // containing an open sheet was closed — see the note in use-body-scroll-lock.ts.\n useBodyScrollLock(active);\n\n // Focus the panel itself (not the first field) so opening doesn't pop the mobile\n // keyboard, while still moving focus into the dialog for keyboard and screen-reader\n // users. Through the shared hook since this component is where it came from — the\n // hook adds per-keystroke recomputation, a guard on restoring to a detached node,\n // and nesting, none of which this copy had. Released when `open` turns false, which\n // hands focus back to the trigger while the panel is still on its way out.\n //\n // \"The panel itself\" only when nothing inside asked for focus: a field the app\n // autofocuses on purpose keeps it (0.33.1, keksdose live #402 — the same trap took\n // an autofocused amount straight back in `FullBleedDialog`). Its keyboard is then the\n // one the app chose to show; the trigger still gets focus back on close.\n useFocusTrap(panelRef, { active, initialFocus: \"container\" });\n const leaving = closing || exiting;\n\n // One layer on the Escape stack while open (Wave 2, DUP-1): a list, a popover or a\n // tooltip opened inside takes the key before the dialog does, wherever focus is —\n // and the dialog gets it even when focus is not in the panel.\n const layer = useDismissLayer(active, requestClose);\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n // The caller's handler sees Escape only while this dialog is the layer on top: a\n // tooltip or a list open over it takes that press first (0.35, KS's gear wizard).\n runOwnKeyDown(e, onKeyDown, layer);\n // To whatever is on top — this dialog, unless something opened over it — and\n // stopped here. Tab containment lives in useFocusTrap, which listens on the panel.\n takeEscape(e);\n };\n\n // Portal to <body> so the `fixed inset-0 z-50` overlay escapes whatever stacking\n // context its caller sits in. Rendered inline, a modal opened from e.g. the top\n // bar (a z-30 context) could paint BELOW a sibling z-30 sticky element like the\n // expanded transaction editor (feedback #320). The tour/command-palette overlays\n // already portal for the same reason.\n if (!active && !exiting) return null;\n return createPortal(\n <div\n // Out of the accessibility tree for the length of a caller-driven exit: focus is\n // already back on the page, and an `aria-modal` panel that is leaving would hide\n // that page from a screen reader for 220ms. Not `inert` — that would let a tap\n // fall through the fading backdrop to the row underneath.\n aria-hidden={exiting || undefined}\n className={cn(\n // Live #320. The panel below rises only where it is bottom-anchored: from md up\n // it is centred, and a centred box sliding up from off-screen reads as a\n // different component arriving rather than as the same one settling.\n leaving ? \"animate-overlay-out\" : \"animate-overlay\",\n \"fixed inset-0 z-50 flex items-end justify-center bg-black/40 md:items-center\",\n fullBleed ? \"p-0 md:p-4\" : \"p-4\",\n )}\n {...backdropClose}\n >\n {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- always\n role=\"dialog\" or \"alertdialog\" (the ternary hides that from the rule): `onKeyDown`\n is Escape-to-close per the APG dialog pattern, and the drag handlers are\n swipe-to-dismiss, whose keyboard path is that same Escape. */}\n <div\n // `...rest` FIRST, everything the dialog needs to be a dialog after it. A\n // wrapper that spreads its own props through to this one cannot then take away\n // the role, the modality or the `tabIndex={-1}` that `useFocusTrap` needs —\n // losing any of the three is silent, and the symptom (a dialog a screen reader\n // walks straight out of) shows up nowhere near the call site that caused it.\n {...rest}\n ref={setPanel}\n // Normalised at runtime too: an untyped caller's `role=\"presentation\"` must not\n // take the dialog role away (props-passthrough pins that).\n role={role === \"alertdialog\" ? \"alertdialog\" : \"dialog\"}\n aria-modal=\"true\"\n aria-labelledby={labelledBy}\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n {...drag.handlers}\n // MERGED, not replaced: `drag.style` is undefined until a drag starts, so\n // assigning it outright would drop a caller's own `style` on every render but\n // the ones where the panel is being dragged.\n style={{ ...style, ...drag.style }}\n className={cn(\n leaving ? \"animate-sheet-out md:animate-none\" : \"animate-sheet md:animate-none\",\n \"w-full rounded-lg border border-subtle bg-surface shadow-sm outline-none\",\n { md: \"max-w-md\", lg: \"max-w-lg\", xl: \"max-w-3xl\", \"2xl\": \"max-w-[110rem]\" }[size],\n // A panel taller than the screen has to scroll ITSELF. The backdrop is\n // `fixed inset-0` and the body is scroll-locked while a dialog is open, so\n // without this the overflow simply had nowhere to go: on a phone the panel\n // is bottom-anchored, so it grew UPWARD past the top edge and the part that\n // went past it could not be reached by any gesture (Keksdose live #254, on\n // the price-merge dialog — but it was every dialog with more than a screen\n // of content). `max-h-full` resolves against the backdrop's content box,\n // which is the viewport minus its own padding, so the panel stops exactly\n // where the screen does; `overscroll-contain` keeps the fling inside it.\n //\n // Three callers had already patched themselves with `max-h-[90vh]` +\n // an inner scroller. Those still win — `cn` is tailwind-merge — and this is\n // the default they should not have needed.\n \"max-h-full overflow-y-auto overscroll-contain\",\n \"p-4\",\n // The grab affordance sits on the panel's own top strip: a drag handle of\n // its own would be one more control in a dialog that is mostly one field.\n draggable && \"md:[&>*:first-child]:cursor-grab\",\n className,\n )}\n >\n <ModalCloseContext.Provider value={requestClose}>{children}</ModalCloseContext.Provider>\n </div>\n </div>,\n document.body,\n );\n}\n"],"mappings":"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","names":[]}
@@ -27,32 +27,21 @@ export interface MultiSelectProps<V extends string | number = string | number> e
27
27
  options: MultiSelectOption<V>[];
28
28
  /** The chosen values — the select family's `value` (API-4). */
29
29
  value?: V[];
30
- /** @deprecated 0.34 — use `value`; removed in 0.35 (refactor audit 2026-10, API-4). */
31
- values?: V[];
32
30
  onChange: (next: V[]) => void;
33
- /** @deprecated 0.34 — use `labels.all`; removed in 0.35 (API-4). Displayed when no
34
- * value is selected (i.e. "all"); wins over `placeholder`, as `labels.all` does. */
35
- allLabel?: string;
36
31
  /** Singular/plural countable label, used when 1+ items are selected. Default:
37
32
  * `labels.selectedCount`, else `multiSelect.selectedCount` from the provider — the
38
33
  * bare count in English. */
39
34
  itemLabel?: (count: number) => string;
40
- /** Shown when nothing is picked, unless `labels.all` (or `allLabel`) says otherwise —
41
- * it has always doubled as the "nothing picked" text. */
35
+ /** Shown when nothing is picked, unless `labels.all` says otherwise — it has always
36
+ * doubled as the "nothing picked" text. */
42
37
  placeholder?: string;
43
38
  /** Embedded top-boundary label, matching the native Input/Select fields. */
44
39
  label?: ReactNode;
45
- /** @deprecated 0.34 — use `labels.search`; removed in 0.35 (API-4). */
46
- searchLabel?: string;
47
- /** @deprecated 0.34 — use `labels.selectAll`; removed in 0.35 (API-4). */
48
- selectAllLabel?: string;
49
- /** @deprecated 0.34 — use `labels.clear`; removed in 0.35 (API-4). */
50
- clearLabel?: string;
51
40
  /**
52
41
  * Per-instance overrides of the provider's `multiSelect` namespace, over English — the
53
- * kit's `labels` shape, where this took four string props (API-4). `all` is the trigger
54
- * when nothing is picked: `labels.all`, then `allLabel`, then `placeholder`, then the
55
- * provider's.
42
+ * kit's `labels` shape, where this took four string props until 0.35 (API-4: `allLabel`,
43
+ * `searchLabel`, `selectAllLabel`, `clearLabel`). `all` is the trigger when nothing is
44
+ * picked: `labels.all`, then `placeholder`, then the provider's.
56
45
  */
57
46
  labels?: LabelOverride<MultiSelectLabels>;
58
47
  /**
@@ -23,15 +23,10 @@ function MultiSelect(props) {
23
23
  const {
24
24
  options,
25
25
  value: valueProp,
26
- values,
27
26
  onChange,
28
- allLabel,
29
27
  itemLabel,
30
28
  placeholder,
31
29
  label,
32
- searchLabel,
33
- selectAllLabel,
34
- clearLabel,
35
30
  labels: labelsProp,
36
31
  className,
37
32
  panelClassName,
@@ -54,7 +49,7 @@ function MultiSelect(props) {
54
49
  "aria-required": ariaRequired,
55
50
  ...rest
56
51
  } = props;
57
- const value = useMemo(() => valueProp ?? values ?? [], [valueProp, values]);
52
+ const value = useMemo(() => valueProp ?? [], [valueProp]);
58
53
  const comboOptions = useMemo(
59
54
  () => options.map((o) => ({ value: o.value, label: o.label, sublabel: o.hint, disabled: o.disabled })),
60
55
  [options]
@@ -69,9 +64,9 @@ function MultiSelect(props) {
69
64
  [ref, triggerRef]
70
65
  );
71
66
  const text = useKitLabels("multiSelect", DEFAULT_MULTI_SELECT_LABELS, {
72
- search: labelsProp?.search ?? searchLabel,
73
- selectAll: labelsProp?.selectAll ?? selectAllLabel,
74
- clear: labelsProp?.clear ?? clearLabel,
67
+ search: labelsProp?.search,
68
+ selectAll: labelsProp?.selectAll,
69
+ clear: labelsProp?.clear,
75
70
  selectedCount: labelsProp?.selectedCount ?? itemLabel
76
71
  });
77
72
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
@@ -89,7 +84,7 @@ function MultiSelect(props) {
89
84
  const listboxId = `${useId()}-listbox`;
90
85
  const valueSet = useMemo(() => new Set(value), [value]);
91
86
  const allChecked = options.length > 0 && options.every((o) => valueSet.has(o.value));
92
- const selectedSummary = value.length === 0 || allChecked ? labelsProp?.all ?? allLabel ?? placeholder ?? text.all : text.selectedCount(value.length);
87
+ const selectedSummary = value.length === 0 || allChecked ? labelsProp?.all ?? placeholder ?? text.all : text.selectedCount(value.length);
93
88
  const toggle = (o) => {
94
89
  if (lock.locked) return;
95
90
  if (valueSet.has(o.value)) onChange(value.filter((v) => v !== o.value));