@eifi1/ui-kit 0.51.5 → 0.51.7

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 (129) hide show
  1. package/README.md +9 -7
  2. package/dist/admin/admin-action-confirm.js +2 -4
  3. package/dist/admin/admin-action-confirm.js.map +1 -1
  4. package/dist/auth/complete-name-dialog.d.ts +3 -2
  5. package/dist/auth/complete-name-dialog.js +1 -4
  6. package/dist/auth/complete-name-dialog.js.map +1 -1
  7. package/dist/billing/cancel-request.d.ts +11 -2
  8. package/dist/billing/cancel-request.js +15 -3
  9. package/dist/billing/cancel-request.js.map +1 -1
  10. package/dist/billing/cancel-subscription.d.ts +2 -2
  11. package/dist/billing/cancel-subscription.js +2 -7
  12. package/dist/billing/cancel-subscription.js.map +1 -1
  13. package/dist/billing/plan-switch.d.ts +2 -2
  14. package/dist/billing/plan-switch.js +2 -4
  15. package/dist/billing/plan-switch.js.map +1 -1
  16. package/dist/components/amount-input.d.ts +9 -4
  17. package/dist/components/amount-input.js +2 -3
  18. package/dist/components/amount-input.js.map +1 -1
  19. package/dist/components/calculator.d.ts +21 -2
  20. package/dist/components/calculator.js +53 -14
  21. package/dist/components/calculator.js.map +1 -1
  22. package/dist/components/combobox-core.d.ts +8 -5
  23. package/dist/components/combobox-core.js +3 -4
  24. package/dist/components/combobox-core.js.map +1 -1
  25. package/dist/components/combobox.js +2 -2
  26. package/dist/components/combobox.js.map +1 -1
  27. package/dist/components/data-table-mobile.d.ts +24 -0
  28. package/dist/components/data-table-mobile.js +20 -1
  29. package/dist/components/data-table-mobile.js.map +1 -1
  30. package/dist/components/data-table.d.ts +13 -5
  31. package/dist/components/data-table.js +35 -9
  32. package/dist/components/data-table.js.map +1 -1
  33. package/dist/components/dialog-frame.d.ts +2 -1
  34. package/dist/components/dialog-frame.js.map +1 -1
  35. package/dist/components/entity-combobox.js +4 -2
  36. package/dist/components/entity-combobox.js.map +1 -1
  37. package/dist/components/inline-entity-combobox.js +4 -3
  38. package/dist/components/inline-entity-combobox.js.map +1 -1
  39. package/dist/components/list.js +1 -1
  40. package/dist/components/list.js.map +1 -1
  41. package/dist/components/modal.d.ts +24 -0
  42. package/dist/components/modal.js +9 -3
  43. package/dist/components/modal.js.map +1 -1
  44. package/dist/components/money-draft.js +7 -5
  45. package/dist/components/money-draft.js.map +1 -1
  46. package/dist/components/money-field.d.ts +2 -2
  47. package/dist/components/money-field.js.map +1 -1
  48. package/dist/components/multi-entity-combobox.js +2 -1
  49. package/dist/components/multi-entity-combobox.js.map +1 -1
  50. package/dist/components/number-field.d.ts +6 -4
  51. package/dist/components/number-field.js +3 -2
  52. package/dist/components/number-field.js.map +1 -1
  53. package/dist/components/number-input.d.ts +4 -1
  54. package/dist/components/number-input.js +3 -2
  55. package/dist/components/number-input.js.map +1 -1
  56. package/dist/components/numpad-sheet.js.map +1 -1
  57. package/dist/components/reauth-dialog.d.ts +4 -5
  58. package/dist/components/reauth-dialog.js +1 -4
  59. package/dist/components/reauth-dialog.js.map +1 -1
  60. package/dist/components/series-chart-axes.d.ts +7 -0
  61. package/dist/components/series-chart-axes.js +1 -0
  62. package/dist/components/series-chart-axes.js.map +1 -1
  63. package/dist/components/tabs.js +12 -10
  64. package/dist/components/tabs.js.map +1 -1
  65. package/dist/components/treemap.d.ts +24 -2
  66. package/dist/components/treemap.js +40 -4
  67. package/dist/components/treemap.js.map +1 -1
  68. package/dist/eslint/index.js +1 -1
  69. package/dist/hooks/use-dismiss.js.map +1 -1
  70. package/dist/hooks/use-overlay-history.d.ts +7 -1
  71. package/dist/hooks/use-overlay-history.js +16 -2
  72. package/dist/hooks/use-overlay-history.js.map +1 -1
  73. package/dist/lib/calc.d.ts +7 -1
  74. package/dist/lib/calc.js +3 -1
  75. package/dist/lib/calc.js.map +1 -1
  76. package/dist/lib/clipboard-minus.d.ts +2 -2
  77. package/dist/lib/clipboard-minus.js +1 -1
  78. package/dist/lib/clipboard-minus.js.map +1 -1
  79. package/dist/lib/decimal-marks.d.ts +7 -1
  80. package/dist/lib/decimal-marks.js +5 -2
  81. package/dist/lib/decimal-marks.js.map +1 -1
  82. package/dist/lib/format.d.ts +5 -3
  83. package/dist/lib/format.js +12 -0
  84. package/dist/lib/format.js.map +1 -1
  85. package/dist/lib/text-match.d.ts +9 -0
  86. package/dist/lib/text-match.js +4 -0
  87. package/dist/lib/text-match.js.map +1 -1
  88. package/dist/theme/contrast.d.ts +2 -2
  89. package/dist/tour/tour.d.ts +3 -1
  90. package/dist/tour/tour.js +9 -3
  91. package/dist/tour/tour.js.map +1 -1
  92. package/dist/tour/use-tour-url-sync.js +2 -2
  93. package/dist/tour/use-tour-url-sync.js.map +1 -1
  94. package/package.json +3 -3
  95. package/src/admin/admin-action-confirm.tsx +4 -5
  96. package/src/auth/complete-name-dialog.tsx +5 -7
  97. package/src/billing/cancel-request.ts +28 -6
  98. package/src/billing/cancel-subscription.tsx +4 -11
  99. package/src/billing/plan-switch.tsx +5 -8
  100. package/src/components/amount-input.tsx +14 -7
  101. package/src/components/calculator.tsx +98 -17
  102. package/src/components/combobox-core.tsx +10 -7
  103. package/src/components/combobox.tsx +5 -6
  104. package/src/components/data-table-mobile.tsx +49 -1
  105. package/src/components/data-table.tsx +66 -11
  106. package/src/components/dialog-frame.tsx +2 -1
  107. package/src/components/entity-combobox.tsx +4 -1
  108. package/src/components/inline-entity-combobox.tsx +5 -3
  109. package/src/components/list.tsx +4 -1
  110. package/src/components/modal.tsx +39 -3
  111. package/src/components/money-draft.ts +16 -5
  112. package/src/components/money-field.tsx +2 -2
  113. package/src/components/multi-entity-combobox.tsx +1 -0
  114. package/src/components/number-field.tsx +14 -7
  115. package/src/components/number-input.tsx +9 -3
  116. package/src/components/numpad-sheet.tsx +4 -3
  117. package/src/components/reauth-dialog.tsx +6 -10
  118. package/src/components/series-chart-axes.ts +8 -0
  119. package/src/components/tabs.tsx +33 -17
  120. package/src/components/treemap.tsx +69 -7
  121. package/src/hooks/use-dismiss.ts +3 -2
  122. package/src/hooks/use-overlay-history.ts +38 -2
  123. package/src/lib/calc.ts +13 -2
  124. package/src/lib/clipboard-minus.ts +14 -4
  125. package/src/lib/decimal-marks.ts +12 -3
  126. package/src/lib/format.ts +30 -3
  127. package/src/lib/text-match.ts +12 -0
  128. package/src/tour/tour.tsx +21 -4
  129. package/src/tour/use-tour-url-sync.ts +7 -3
@@ -14,7 +14,8 @@ export { DEFAULT_DIALOG_FRAME_LABELS };
14
14
  * `PickerSheet` met first. Everything else, `size`, `draggable`, `fullBleed`,
15
15
  * `onKeyDown`, a `data-tour` anchor, still reaches the `Modal` — and since 0.35 `ref`
16
16
  * (round 0.35 R4): the panel, the `role="dialog"` element the frame's header, body and
17
- * actions sit in.
17
+ * actions sit in. `busy` too (DUP-3): it holds the frame's X and the `close` that
18
+ * `actions` and `headerActions` receive, since both are the `Modal`'s own close.
18
19
  */
19
20
  export interface DialogFrameProps extends Omit<ModalProps, "labelledBy" | "children" | "title"> {
20
21
  /** The heading, and therefore the dialog's accessible name (`aria-labelledby`). */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/dialog-frame.tsx"],"sourcesContent":["import { useContext, useId } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { Modal, ModalCloseContext } from \"./modal\";\nimport type { ModalProps } from \"./modal\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { DEFAULT_DIALOG_FRAME_LABELS } from \"./dialog-frame-labels\";\n\nexport interface DialogFrameLabels {\n /** Accessible name of the header's X, when {@link DialogFrameProps.closeButton} shows it. */\n close: string;\n}\n\n// Defined in ./dialog-frame-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_DIALOG_FRAME_LABELS };\n\n/**\n * `ModalProps` minus the three this component owns: the NAME (`labelledBy` — the\n * heading's id is generated inside and never reaches the caller), the content\n * (`children` is the body here, not the whole panel) and `title`, which is the dialog's\n * HEADING and a ReactNode rather than the browser's tooltip string — the collision\n * `PickerSheet` met first. Everything else, `size`, `draggable`, `fullBleed`,\n * `onKeyDown`, a `data-tour` anchor, still reaches the `Modal` — and since 0.35 `ref`\n * (round 0.35 R4): the panel, the `role=\"dialog\"` element the frame's header, body and\n * actions sit in.\n */\nexport interface DialogFrameProps extends Omit<ModalProps, \"labelledBy\" | \"children\" | \"title\"> {\n /** The heading, and therefore the dialog's accessible name (`aria-labelledby`). */\n title: ReactNode;\n /** The smaller line under the heading; wired to `aria-describedby`. */\n description?: ReactNode;\n /**\n * The heading's level. `h2` by default, which is what every dialog in both apps\n * writes; a prop for a page that nests its demos under a real heading.\n */\n headingAs?: \"h1\" | \"h2\" | \"h3\" | \"h4\";\n /**\n * The row under the body — buttons, in the caller's own order and variants (the two\n * apps disagree about the cancel button's variant, so the frame has no opinion on\n * it). Stays put while the body scrolls.\n *\n * A FUNCTION receives the panel's animated close: `(close) => <Button\n * onClick={close}>Cancel</Button>` lowers the panel the way Escape does, where\n * calling `onClose` directly unmounts it at once.\n */\n actions?: ReactNode | ((close: () => void) => ReactNode);\n /**\n * Controls that belong to the HEADER rather than to the actions row — an \"Edit\"\n * toggle, a status badge with a menu, a \"Copy link\" (kastlan). Rendered beside the\n * title, before the optional X; on a phone, where title and controls do not fit on\n * one line, they wrap under the title and the X keeps its corner.\n *\n * A function receives the animated close, as `actions` does.\n */\n headerActions?: ReactNode | ((close: () => void) => ReactNode);\n /**\n * Show an X in the header. Off by default: a centred dialog has a backdrop and\n * Escape, and a form dialog has a Cancel. On for a dialog that commits as it goes\n * and has no actions row, where the X is the only visible way out.\n */\n closeButton?: boolean;\n /** Default: `dialogFrame.close` from the {@link UiKitProvider}, else \"Close\". */\n closeLabel?: string;\n /** Extra classes for the scrolling body (default spacing `space-y-3`). */\n bodyClassName?: string;\n /**\n * Extra classes for the header row (default padding `px-4 pt-4 pb-3`), merged last —\n * `px-3` for a phone sheet whose body runs at `px-3`, so heading and fields share\n * one gutter.\n */\n headerClassName?: string;\n /**\n * A rule under the header, the twin of the one over the actions row — for a tall or\n * full-screen body that scrolls under a header which stays. The body then starts a\n * step below the rule instead of flush against it.\n */\n headerDivider?: boolean;\n /**\n * The body. Optional: a dialog whose title, description and actions are the whole\n * of it (a \"you have unsynced changes\" question) leaves it out, and the frame then\n * renders no body at all rather than an empty padded one.\n */\n children?: ReactNode;\n}\n\n/**\n * A {@link Modal} with the frame every caller was writing by hand: a heading, an\n * optional description, an optional X, a body that scrolls, and an actions row that\n * does not.\n *\n * 34 dialogs across the two apps framed themselves — a heading with an id invented per\n * file (and spelt three ways), four visible type sizes for one thing, ten spellings of\n * one right-aligned button row — and the package's own feedback dialog shipped with no\n * accessible name at all. This makes the name unforgettable: the heading's id comes\n * from `useId()` and goes straight to `Modal`'s `labelledBy`, so a framed dialog cannot\n * announce as just \"dialog\", and two open instances cannot share an id.\n *\n * ## It wraps, it does not change `Modal`\n *\n * `Modal` keeps `labelledBy` and every existing caller compiles untouched. What the\n * frame changes is inside the panel: the panel becomes a flex column with no padding\n * of its own, and only the BODY scrolls. The panel's `max-h-full` is still the outer\n * bound, so a tall form keeps its heading and its Save button on screen instead of\n * scrolling them away with the fields. A caller's own `className` still wins (it is\n * tailwind-merged last), which is how a full-screen phone sheet is spelt:\n * `fullBleed className=\"h-[100dvh] max-w-full rounded-none md:h-auto md:rounded-lg\"`.\n *\n * ## Kept mounted\n *\n * `open` reaches the `Modal` like every other prop, so `<DialogFrame open={open} …>`\n * replaces the `{open && <DialogFrame …/>}` gate (kastlan's `FormModal` was only\n * that gate) and gains the exit a caller-driven close otherwise skips. See\n * {@link ModalProps.open}.\n *\n * ## What it is not\n *\n * Not `FullBleedDialog`: that is the phone's full-screen editor with its own `open`,\n * Back handling and a required X, and it already draws a frame of its own. And not a\n * form: submit handling, a pending label and close-on-success stay the caller's.\n * Focus lands on the panel, as `Modal` decides — not on the first field, so opening\n * does not pop a phone's keyboard; an `autoFocus` in the body overrides that from the\n * caller's side, and should be a decision rather than a habit.\n */\nexport function DialogFrame({\n title,\n description,\n headingAs: Heading = \"h2\",\n actions,\n headerActions,\n closeButton = false,\n closeLabel,\n bodyClassName,\n headerClassName,\n headerDivider = false,\n className,\n children,\n \"aria-describedby\": describedBy,\n ...modal\n}: DialogFrameProps) {\n const titleId = useId();\n const descriptionId = useId();\n const hasDescription = description !== undefined && description !== null;\n // `false` too, so `{cond && <Body/>}` with a false `cond` means \"no body\".\n const hasBody = children !== undefined && children !== null && children !== false;\n\n return (\n <Modal\n {...modal}\n labelledBy={titleId}\n // The caller's own description (a warning inside the body, say) is ADDED to the\n // frame's, not traded for it: both are the dialog's.\n aria-describedby={\n [hasDescription ? descriptionId : undefined, describedBy].filter(Boolean).join(\" \") || undefined\n }\n // `overflow-hidden` replaces the panel's own `overflow-y-auto` (tailwind-merge\n // treats them as one group), `p-0` its `p-4`: the scroller and the padding move\n // to the body, which is the only part that should move.\n className={cn(\"flex flex-col overflow-hidden p-0\", className)}\n >\n <div\n className={cn(\n // `last:pb-4`: a frame with neither body nor actions closes on its header.\n \"flex shrink-0 items-start justify-between gap-2 px-4 pt-4 pb-3 last:pb-4\",\n headerDivider && \"border-b border-subtle\",\n headerClassName,\n )}\n >\n {/* Title and header actions share a wrapping row of their own, so it is THEY\n that wrap on a phone — the actions drop under a title that needs its 12rem —\n while the X stays outside it, pinned to the top-end corner. */}\n <div className=\"flex min-w-0 flex-1 flex-wrap items-start justify-between gap-x-3 gap-y-2\">\n <div className=\"min-w-0 grow basis-48\">\n <Heading id={titleId} className=\"text-lg font-semibold leading-snug text-primary\">\n {title}\n </Heading>\n {hasDescription && (\n <p id={descriptionId} className=\"mt-0.5 text-sm text-muted\">\n {description}\n </p>\n )}\n </div>\n {headerActions !== undefined && headerActions !== null && headerActions !== false && (\n <FrameHeaderActions actions={headerActions} onClose={modal.onClose} />\n )}\n </div>\n {closeButton && <FrameClose label={closeLabel} onClose={modal.onClose} />}\n </div>\n {hasBody && (\n <div\n className={cn(\n // `min-h-0` is what lets a flex child shrink below its content and scroll;\n // `last:pb-4` closes a frame that has no actions row under it.\n \"min-h-0 flex-1 space-y-3 overflow-y-auto overscroll-contain px-4 pb-3 last:pb-4\",\n headerDivider && \"pt-3\",\n bodyClassName,\n )}\n >\n {children}\n </div>\n )}\n {actions !== undefined && actions !== null && (\n <FrameActions actions={actions} onClose={modal.onClose} divider={hasBody} />\n )}\n </Modal>\n );\n}\n\n/** The X. A component of its own so it can read the panel's animated close, which\n * only exists INSIDE the `Modal` (the frame's own body runs outside it). */\nfunction FrameClose({ label, onClose }: { label?: string; onClose: () => void }) {\n const close = useContext(ModalCloseContext) ?? onClose;\n const labels = useKitLabels(\n \"dialogFrame\",\n DEFAULT_DIALOG_FRAME_LABELS,\n label === undefined ? undefined : { close: label },\n );\n return (\n <button\n type=\"button\"\n onClick={close}\n aria-label={labels.close}\n className={cn(\n \"-me-1.5 -mt-1 shrink-0 rounded p-1.5 text-muted outline-none hover:bg-hover hover:text-secondary\",\n FOCUS_RING,\n )}\n >\n <X aria-hidden className=\"size-5\" />\n </button>\n );\n}\n\n/** The header's own controls; a component for the same reason as {@link FrameClose}. */\nfunction FrameHeaderActions({\n actions,\n onClose,\n}: {\n actions: NonNullable<DialogFrameProps[\"headerActions\"]>;\n onClose: () => void;\n}) {\n const close = useContext(ModalCloseContext) ?? onClose;\n return (\n <div data-dialog-header-actions=\"\" className=\"flex shrink-0 flex-wrap items-center gap-2\">\n {typeof actions === \"function\" ? actions(close) : actions}\n </div>\n );\n}\n\nfunction FrameActions({\n actions,\n onClose,\n divider,\n}: {\n actions: NonNullable<DialogFrameProps[\"actions\"]>;\n onClose: () => void;\n /** Off when there is no body: nothing scrolls, so there is no edge to mark, and a\n * rule straight under the description would cut the question from its answers. */\n divider: boolean;\n}) {\n const close = useContext(ModalCloseContext) ?? onClose;\n return (\n // The border marks where the scrolling stops; `flex-wrap` keeps three long\n // translated labels on a phone from pushing the row wider than the sheet.\n <div\n className={cn(\n \"flex shrink-0 flex-wrap items-center justify-end gap-2 px-4 py-3\",\n divider && \"border-t border-subtle\",\n )}\n >\n {typeof actions === \"function\" ? actions(close) : actions}\n </div>\n );\n}\n"],"mappings":";AA8KU,SACE,KADF;AA9KV,SAAS,YAAY,aAAa;AAElC,SAAS,SAAS;AAElB,SAAS,UAAU;AACnB,SAAS,kBAAkB;AAC3B,SAAS,OAAO,yBAAyB;AAEzC,SAAS,oBAAoB;AAC7B,SAAS,mCAAmC;AAqHrC,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,WAAW,UAAU;AAAA,EACrB;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA,oBAAoB;AAAA,EACpB,GAAG;AACL,GAAqB;AACnB,QAAM,UAAU,MAAM;AACtB,QAAM,gBAAgB,MAAM;AAC5B,QAAM,iBAAiB,gBAAgB,UAAa,gBAAgB;AAEpE,QAAM,UAAU,aAAa,UAAa,aAAa,QAAQ,aAAa;AAE5E,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,YAAY;AAAA,MAGZ,oBACE,CAAC,iBAAiB,gBAAgB,QAAW,WAAW,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,KAAK;AAAA,MAKzF,WAAW,GAAG,qCAAqC,SAAS;AAAA,MAE5D;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA;AAAA,cAET;AAAA,cACA,iBAAiB;AAAA,cACjB;AAAA,YACF;AAAA,YAKA;AAAA,mCAAC,SAAI,WAAU,6EACb;AAAA,qCAAC,SAAI,WAAU,yBACb;AAAA,sCAAC,WAAQ,IAAI,SAAS,WAAU,mDAC7B,iBACH;AAAA,kBACC,kBACC,oBAAC,OAAE,IAAI,eAAe,WAAU,6BAC7B,uBACH;AAAA,mBAEJ;AAAA,gBACC,kBAAkB,UAAa,kBAAkB,QAAQ,kBAAkB,SAC1E,oBAAC,sBAAmB,SAAS,eAAe,SAAS,MAAM,SAAS;AAAA,iBAExE;AAAA,cACC,eAAe,oBAAC,cAAW,OAAO,YAAY,SAAS,MAAM,SAAS;AAAA;AAAA;AAAA,QACzE;AAAA,QACC,WACC;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA;AAAA;AAAA,cAGT;AAAA,cACA,iBAAiB;AAAA,cACjB;AAAA,YACF;AAAA,YAEC;AAAA;AAAA,QACH;AAAA,QAED,YAAY,UAAa,YAAY,QACpC,oBAAC,gBAAa,SAAkB,SAAS,MAAM,SAAS,SAAS,SAAS;AAAA;AAAA;AAAA,EAE9E;AAEJ;AAIA,SAAS,WAAW,EAAE,OAAO,QAAQ,GAA4C;AAC/E,QAAM,QAAQ,WAAW,iBAAiB,KAAK;AAC/C,QAAM,SAAS;AAAA,IACb;AAAA,IACA;AAAA,IACA,UAAU,SAAY,SAAY,EAAE,OAAO,MAAM;AAAA,EACnD;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS;AAAA,MACT,cAAY,OAAO;AAAA,MACnB,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEA,8BAAC,KAAE,eAAW,MAAC,WAAU,UAAS;AAAA;AAAA,EACpC;AAEJ;AAGA,SAAS,mBAAmB;AAAA,EAC1B;AAAA,EACA;AACF,GAGG;AACD,QAAM,QAAQ,WAAW,iBAAiB,KAAK;AAC/C,SACE,oBAAC,SAAI,8BAA2B,IAAG,WAAU,8CAC1C,iBAAO,YAAY,aAAa,QAAQ,KAAK,IAAI,SACpD;AAEJ;AAEA,SAAS,aAAa;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,QAAQ,WAAW,iBAAiB,KAAK;AAC/C;AAAA;AAAA;AAAA,IAGE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA,WAAW;AAAA,QACb;AAAA,QAEC,iBAAO,YAAY,aAAa,QAAQ,KAAK,IAAI;AAAA;AAAA,IACpD;AAAA;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/dialog-frame.tsx"],"sourcesContent":["import { useContext, useId } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { Modal, ModalCloseContext } from \"./modal\";\nimport type { ModalProps } from \"./modal\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { DEFAULT_DIALOG_FRAME_LABELS } from \"./dialog-frame-labels\";\n\nexport interface DialogFrameLabels {\n /** Accessible name of the header's X, when {@link DialogFrameProps.closeButton} shows it. */\n close: string;\n}\n\n// Defined in ./dialog-frame-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_DIALOG_FRAME_LABELS };\n\n/**\n * `ModalProps` minus the three this component owns: the NAME (`labelledBy` — the\n * heading's id is generated inside and never reaches the caller), the content\n * (`children` is the body here, not the whole panel) and `title`, which is the dialog's\n * HEADING and a ReactNode rather than the browser's tooltip string — the collision\n * `PickerSheet` met first. Everything else, `size`, `draggable`, `fullBleed`,\n * `onKeyDown`, a `data-tour` anchor, still reaches the `Modal` — and since 0.35 `ref`\n * (round 0.35 R4): the panel, the `role=\"dialog\"` element the frame's header, body and\n * actions sit in. `busy` too (DUP-3): it holds the frame's X and the `close` that\n * `actions` and `headerActions` receive, since both are the `Modal`'s own close.\n */\nexport interface DialogFrameProps extends Omit<ModalProps, \"labelledBy\" | \"children\" | \"title\"> {\n /** The heading, and therefore the dialog's accessible name (`aria-labelledby`). */\n title: ReactNode;\n /** The smaller line under the heading; wired to `aria-describedby`. */\n description?: ReactNode;\n /**\n * The heading's level. `h2` by default, which is what every dialog in both apps\n * writes; a prop for a page that nests its demos under a real heading.\n */\n headingAs?: \"h1\" | \"h2\" | \"h3\" | \"h4\";\n /**\n * The row under the body — buttons, in the caller's own order and variants (the two\n * apps disagree about the cancel button's variant, so the frame has no opinion on\n * it). Stays put while the body scrolls.\n *\n * A FUNCTION receives the panel's animated close: `(close) => <Button\n * onClick={close}>Cancel</Button>` lowers the panel the way Escape does, where\n * calling `onClose` directly unmounts it at once.\n */\n actions?: ReactNode | ((close: () => void) => ReactNode);\n /**\n * Controls that belong to the HEADER rather than to the actions row — an \"Edit\"\n * toggle, a status badge with a menu, a \"Copy link\" (kastlan). Rendered beside the\n * title, before the optional X; on a phone, where title and controls do not fit on\n * one line, they wrap under the title and the X keeps its corner.\n *\n * A function receives the animated close, as `actions` does.\n */\n headerActions?: ReactNode | ((close: () => void) => ReactNode);\n /**\n * Show an X in the header. Off by default: a centred dialog has a backdrop and\n * Escape, and a form dialog has a Cancel. On for a dialog that commits as it goes\n * and has no actions row, where the X is the only visible way out.\n */\n closeButton?: boolean;\n /** Default: `dialogFrame.close` from the {@link UiKitProvider}, else \"Close\". */\n closeLabel?: string;\n /** Extra classes for the scrolling body (default spacing `space-y-3`). */\n bodyClassName?: string;\n /**\n * Extra classes for the header row (default padding `px-4 pt-4 pb-3`), merged last —\n * `px-3` for a phone sheet whose body runs at `px-3`, so heading and fields share\n * one gutter.\n */\n headerClassName?: string;\n /**\n * A rule under the header, the twin of the one over the actions row — for a tall or\n * full-screen body that scrolls under a header which stays. The body then starts a\n * step below the rule instead of flush against it.\n */\n headerDivider?: boolean;\n /**\n * The body. Optional: a dialog whose title, description and actions are the whole\n * of it (a \"you have unsynced changes\" question) leaves it out, and the frame then\n * renders no body at all rather than an empty padded one.\n */\n children?: ReactNode;\n}\n\n/**\n * A {@link Modal} with the frame every caller was writing by hand: a heading, an\n * optional description, an optional X, a body that scrolls, and an actions row that\n * does not.\n *\n * 34 dialogs across the two apps framed themselves — a heading with an id invented per\n * file (and spelt three ways), four visible type sizes for one thing, ten spellings of\n * one right-aligned button row — and the package's own feedback dialog shipped with no\n * accessible name at all. This makes the name unforgettable: the heading's id comes\n * from `useId()` and goes straight to `Modal`'s `labelledBy`, so a framed dialog cannot\n * announce as just \"dialog\", and two open instances cannot share an id.\n *\n * ## It wraps, it does not change `Modal`\n *\n * `Modal` keeps `labelledBy` and every existing caller compiles untouched. What the\n * frame changes is inside the panel: the panel becomes a flex column with no padding\n * of its own, and only the BODY scrolls. The panel's `max-h-full` is still the outer\n * bound, so a tall form keeps its heading and its Save button on screen instead of\n * scrolling them away with the fields. A caller's own `className` still wins (it is\n * tailwind-merged last), which is how a full-screen phone sheet is spelt:\n * `fullBleed className=\"h-[100dvh] max-w-full rounded-none md:h-auto md:rounded-lg\"`.\n *\n * ## Kept mounted\n *\n * `open` reaches the `Modal` like every other prop, so `<DialogFrame open={open} …>`\n * replaces the `{open && <DialogFrame …/>}` gate (kastlan's `FormModal` was only\n * that gate) and gains the exit a caller-driven close otherwise skips. See\n * {@link ModalProps.open}.\n *\n * ## What it is not\n *\n * Not `FullBleedDialog`: that is the phone's full-screen editor with its own `open`,\n * Back handling and a required X, and it already draws a frame of its own. And not a\n * form: submit handling, a pending label and close-on-success stay the caller's.\n * Focus lands on the panel, as `Modal` decides — not on the first field, so opening\n * does not pop a phone's keyboard; an `autoFocus` in the body overrides that from the\n * caller's side, and should be a decision rather than a habit.\n */\nexport function DialogFrame({\n title,\n description,\n headingAs: Heading = \"h2\",\n actions,\n headerActions,\n closeButton = false,\n closeLabel,\n bodyClassName,\n headerClassName,\n headerDivider = false,\n className,\n children,\n \"aria-describedby\": describedBy,\n ...modal\n}: DialogFrameProps) {\n const titleId = useId();\n const descriptionId = useId();\n const hasDescription = description !== undefined && description !== null;\n // `false` too, so `{cond && <Body/>}` with a false `cond` means \"no body\".\n const hasBody = children !== undefined && children !== null && children !== false;\n\n return (\n <Modal\n {...modal}\n labelledBy={titleId}\n // The caller's own description (a warning inside the body, say) is ADDED to the\n // frame's, not traded for it: both are the dialog's.\n aria-describedby={\n [hasDescription ? descriptionId : undefined, describedBy].filter(Boolean).join(\" \") || undefined\n }\n // `overflow-hidden` replaces the panel's own `overflow-y-auto` (tailwind-merge\n // treats them as one group), `p-0` its `p-4`: the scroller and the padding move\n // to the body, which is the only part that should move.\n className={cn(\"flex flex-col overflow-hidden p-0\", className)}\n >\n <div\n className={cn(\n // `last:pb-4`: a frame with neither body nor actions closes on its header.\n \"flex shrink-0 items-start justify-between gap-2 px-4 pt-4 pb-3 last:pb-4\",\n headerDivider && \"border-b border-subtle\",\n headerClassName,\n )}\n >\n {/* Title and header actions share a wrapping row of their own, so it is THEY\n that wrap on a phone — the actions drop under a title that needs its 12rem —\n while the X stays outside it, pinned to the top-end corner. */}\n <div className=\"flex min-w-0 flex-1 flex-wrap items-start justify-between gap-x-3 gap-y-2\">\n <div className=\"min-w-0 grow basis-48\">\n <Heading id={titleId} className=\"text-lg font-semibold leading-snug text-primary\">\n {title}\n </Heading>\n {hasDescription && (\n <p id={descriptionId} className=\"mt-0.5 text-sm text-muted\">\n {description}\n </p>\n )}\n </div>\n {headerActions !== undefined && headerActions !== null && headerActions !== false && (\n <FrameHeaderActions actions={headerActions} onClose={modal.onClose} />\n )}\n </div>\n {closeButton && <FrameClose label={closeLabel} onClose={modal.onClose} />}\n </div>\n {hasBody && (\n <div\n className={cn(\n // `min-h-0` is what lets a flex child shrink below its content and scroll;\n // `last:pb-4` closes a frame that has no actions row under it.\n \"min-h-0 flex-1 space-y-3 overflow-y-auto overscroll-contain px-4 pb-3 last:pb-4\",\n headerDivider && \"pt-3\",\n bodyClassName,\n )}\n >\n {children}\n </div>\n )}\n {actions !== undefined && actions !== null && (\n <FrameActions actions={actions} onClose={modal.onClose} divider={hasBody} />\n )}\n </Modal>\n );\n}\n\n/** The X. A component of its own so it can read the panel's animated close, which\n * only exists INSIDE the `Modal` (the frame's own body runs outside it). */\nfunction FrameClose({ label, onClose }: { label?: string; onClose: () => void }) {\n const close = useContext(ModalCloseContext) ?? onClose;\n const labels = useKitLabels(\n \"dialogFrame\",\n DEFAULT_DIALOG_FRAME_LABELS,\n label === undefined ? undefined : { close: label },\n );\n return (\n <button\n type=\"button\"\n onClick={close}\n aria-label={labels.close}\n className={cn(\n \"-me-1.5 -mt-1 shrink-0 rounded p-1.5 text-muted outline-none hover:bg-hover hover:text-secondary\",\n FOCUS_RING,\n )}\n >\n <X aria-hidden className=\"size-5\" />\n </button>\n );\n}\n\n/** The header's own controls; a component for the same reason as {@link FrameClose}. */\nfunction FrameHeaderActions({\n actions,\n onClose,\n}: {\n actions: NonNullable<DialogFrameProps[\"headerActions\"]>;\n onClose: () => void;\n}) {\n const close = useContext(ModalCloseContext) ?? onClose;\n return (\n <div data-dialog-header-actions=\"\" className=\"flex shrink-0 flex-wrap items-center gap-2\">\n {typeof actions === \"function\" ? actions(close) : actions}\n </div>\n );\n}\n\nfunction FrameActions({\n actions,\n onClose,\n divider,\n}: {\n actions: NonNullable<DialogFrameProps[\"actions\"]>;\n onClose: () => void;\n /** Off when there is no body: nothing scrolls, so there is no edge to mark, and a\n * rule straight under the description would cut the question from its answers. */\n divider: boolean;\n}) {\n const close = useContext(ModalCloseContext) ?? onClose;\n return (\n // The border marks where the scrolling stops; `flex-wrap` keeps three long\n // translated labels on a phone from pushing the row wider than the sheet.\n <div\n className={cn(\n \"flex shrink-0 flex-wrap items-center justify-end gap-2 px-4 py-3\",\n divider && \"border-t border-subtle\",\n )}\n >\n {typeof actions === \"function\" ? actions(close) : actions}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -11,7 +11,7 @@ import {
11
11
  offersCreate,
12
12
  useComboboxCore
13
13
  } from "./combobox-core.js";
14
- import { DEFAULT_COMBOBOX_LABELS, DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels.js";
14
+ import { DEFAULT_COMBOBOX_LABELS, DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
15
15
  import {
16
16
  assignRef,
17
17
  EndHintRow,
@@ -103,11 +103,13 @@ function EntityCombobox(props) {
103
103
  const chosen = value == null || value === clearValue ? null : value;
104
104
  const selectedOption = chosen == null ? null : resolve(chosen);
105
105
  const q = query.trim();
106
+ const locale = useKitLocale();
106
107
  const showCreate = offersCreate(
107
108
  onCreate,
108
109
  q,
109
110
  results.map((o) => o.label),
110
- createEmptyLabel
111
+ createEmptyLabel,
112
+ locale
111
113
  );
112
114
  const showClear = Boolean(clearable && chosen != null && !inert);
113
115
  const triggerText = selectedOption?.label ?? placeholder ?? "";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/entity-combobox.tsx"],"sourcesContent":["import { useCallback, useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FieldChevron, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./field-base\";\nimport { FieldLabel } from \"./field-parts\";\nimport {\n ComboboxPanel,\n endHintRowProps,\n offersCreate,\n useComboboxCore,\n type ComboClearValue,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport {\n assignRef,\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n mergeDescribedBy,\n StaticLabelRow,\n useFieldError,\n useFieldHint,\n useLockReason,\n type DisabledReasonDisplay,\n type FieldHelpProps,\n} from \"./field-parts\";\nimport { useCommitReason, type CommitScope } from \"./write-lock\";\n\nexport type { ComboClearValue, ComboOption } from \"./combobox-core\";\n\n/** `onChange` is the kit's — \"a selection was made\", carrying values — rather\n * than the div's form event, so the DOM's spelling of it is omitted. */\nexport interface EntityComboboxProps<V extends string | number, C extends ComboClearValue = null>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\">, FieldHelpProps {\n /** Selected id; `null`, `undefined` or `clearValue` when nothing is selected. */\n value: V | C | null | undefined;\n /** Selecting an option emits its value; the clear button emits `clearValue`. */\n onChange: (value: V | C) => void;\n /**\n * What the clear button emits. Default `null`. Pass `\"\"` for a form whose schema\n * wants an empty string for \"no choice\" (a zod `z.string()` field), rather than\n * mapping `null` in every `onChange` — the type of `onChange` follows it, so the\n * mapping cannot be forgotten on one field and not another. The picker also reads\n * the value back as empty, so `value=\"\"` shows the placeholder and no \"×\".\n * See {@link ComboClearValue} for why the choice is closed.\n */\n clearValue?: C;\n /** Already-loaded options (client-side filtered). Also used to resolve the\n * trigger label for the current `value`. */\n options?: ComboOption<V>[];\n /** Async option source, debounced and called on open + as the query changes\n * (at `minChars` and up). Stale responses are ignored; a rejection empties the\n * list and says `loadErrorLabel` instead of leaving the last query's rows up. When set, `options` is used only for label\n * resolution, not as the result list. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** External loading flag (OR-ed with the internal async state). */\n loading?: boolean;\n label?: ReactNode;\n placeholder?: string;\n searchPlaceholder?: string;\n emptyLabel?: string;\n /** Offer a clear \"×\" in place of the chevron while something is chosen; it emits\n * `clearValue`. A pointer shortcut inside the trigger, out of the tab order — the\n * keyboard's way is Delete or Backspace on the CLOSED trigger (0.23, as\n * {@link CountrySelect}). Not offered while `disabled` or locked. */\n clearable?: boolean;\n clearLabel?: string;\n /** The phone sheet's close button. Its own prop rather than a reuse of\n * `clearLabel`: \"clear the selection\" and \"close this screen\" are different\n * actions, and on a full-screen sheet the close button is the only way out —\n * so it is the one control here that MUST be in the reader's language. */\n closeLabel?: string;\n /** The phone sheet's heading (and, as a string, its accessible name) for a picker\n * whose label is drawn by someone else — a bound field's label; `RhfCombobox`\n * hands its label over here (0.24). Default: `label`, then `placeholder`. */\n sheetTitle?: ReactNode;\n disabled?: boolean;\n /**\n * Why the choice cannot be changed — {@link Button}'s `disabledReason`, for a picker\n * that SAVES on change (keksdose G2: the statement's account, where picking IS the\n * write). The trigger stays focusable and `aria-disabled`, the list does not open,\n * the clear \"×\" is gone and `onChange` never fires; the reason is in the kit\n * {@link Tooltip} and on the trigger's `aria-describedby`. Wins over `disabled`.\n * {@link CountrySelect}'s lock, which is built on the same core.\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 /** When set, a \"create\" row appears for a non-empty query with no exact match. */\n onCreate?: (query: string) => void;\n createLabel?: (query: string) => string;\n /**\n * Offer the create row with NOTHING typed too, worded as this — keksdose G9's\n * \"Create cash account\", which mints a record under a default name and so needs no\n * query. `onCreate` then receives `\"\"`. Left out, the row needs a query, as before.\n * See {@link InlineEntityCombobox}'s `createEmptyLabel`.\n */\n createEmptyLabel?: string;\n /**\n * The create row WRITES (it mints the record on the server) while picking does not.\n * Under a locked {@link WriteLockProvider} the row stays in the list but cannot be\n * taken, with the lock's reason as its second line — the picker itself stays live.\n * keksdose dev#496: on the read-only demo choosing is draft state, minting is not.\n */\n createCommit?: CommitScope;\n /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** What is wrong with the value, as {@link Input}'s `error`: rendered under the\n * field, on the trigger's `aria-describedby`, and implies `invalid`. */\n error?: ReactNode;\n /**\n * Standing advice — {@link Combobox}'s `hint`, so the pickers read like every other\n * field (keksdose K4): plain TEXT is a caption under the field on the trigger's\n * `aria-describedby`, before any error; a {@link FieldHint} \"?\" rides the label line,\n * or with no label sits at the trigger's end edge, outside the box.\n */\n hint?: ReactNode;\n /** Narrow `options` client-side by the query. Default `true`; `false` shows them\n * as given (a server-ranked list). */\n filter?: boolean;\n /** Offer nothing, and call no `loadOptions`, below this many characters. Default\n * `0`, i.e. the list loads as the panel opens. */\n minChars?: number;\n /** `loadOptions` debounce. Default 150 ms. */\n debounceMs?: number;\n /** Shown when `loadOptions` rejects. Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n /**\n * The TRIGGER — the focusable `<button>` a reader meets — as a React 19 ref prop, as\n * on {@link CountrySelect}, which is built on the same core (0.24, kastlan: react-hook-\n * form's focus-on-error calls `focus()` on whatever `field.ref` is handed, and a\n * picker with no ref gave it nothing to focus). Not the wrapper: focus belongs on the\n * trigger, which is also where the panel hands it back after a pick.\n */\n ref?: Ref<HTMLButtonElement>;\n}\n\n/**\n * An id-keyed, searchable entity picker: a field-styled trigger showing the\n * selected item's label, and a portalled dropdown of `{label, sublabel, icon}`\n * options — loaded up front via `options` or lazily via `loadOptions`. Built on\n * the shared field/anchor/dismiss/search primitives (no cmdk/Radix). For picking\n * several entities use {@link MultiEntityCombobox}.\n */\nexport function EntityCombobox<V extends string | number, C extends ComboClearValue = null>(\n props: EntityComboboxProps<V, C>,\n) {\n const {\n value,\n onChange,\n clearValue = null as C,\n options,\n loadOptions,\n loading,\n label,\n placeholder,\n searchPlaceholder,\n emptyLabel,\n clearable,\n clearLabel,\n closeLabel,\n sheetTitle,\n disabled,\n disabledReason,\n disabledReasonDisplay,\n commit,\n onCreate,\n createLabel,\n createEmptyLabel,\n createCommit,\n className,\n invalid,\n error,\n hint,\n help,\n filter,\n minChars,\n debounceMs,\n loadErrorLabel,\n ref,\n \"aria-label\": ariaLabel,\n // The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than\n // the wrapper — see MultiEntityCombobox: `Field`'s render-prop spreads `{ id,\n // aria-describedby, aria-invalid, aria-required }`, and on the role-less wrapper\n // div the label's `htmlFor` named nothing and the hint described nothing.\n id,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n } = props;\n const core = useComboboxCore<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n });\n const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });\n const field = useFieldError(error, invalid, hintParts.describedBy, ariaInvalid);\n // The props are the per-instance overrides, the provider the app-wide ones; a\n // prop left `undefined` falls through to the provider rather than masking it.\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n search: searchPlaceholder,\n noResults: emptyLabel,\n clear: clearLabel,\n create: createLabel,\n });\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const { open, results, resolve, setOpen, rememberOption, query, triggerRef } = core;\n // The core anchors the panel to the trigger and hands focus back to it; the caller's\n // `ref` wants the same element. One callback feeds both — CountrySelect's.\n const setTrigger = useCallback(\n (el: HTMLButtonElement | null) => {\n triggerRef.current = el;\n assignRef(ref, el);\n },\n [ref, triggerRef],\n );\n // The lock: this picker's own reason, or the provider's under `commit`.\n const lock = useLockReason(commit, disabledReason);\n const inert = lock.locked || Boolean(disabled);\n // A lock arriving while the list is up closes it — adjusted while\n // rendering, as the core adjusts its own query, so no frame offers a pick that would\n // be thrown away.\n if (lock.locked && open) setOpen(false);\n // The create row's own lock, for a picker whose choosing is draft and whose create\n // is the write (`createCommit`).\n const createReason = useCommitReason(createCommit, undefined);\n // One id per instance, generated here rather than in the core: `aria-controls` on\n // the trigger has to name the list while the list is still closed, so the id\n // belongs to whoever renders both ends of it.\n const listboxId = useId();\n\n // The clear value is \"nothing selected\" too, whichever one the caller picked.\n const chosen: V | null = value == null || value === clearValue ? null : (value as V);\n const selectedOption = chosen == null ? null : resolve(chosen);\n const q = query.trim();\n const showCreate = offersCreate(\n onCreate,\n q,\n results.map((o) => o.label),\n createEmptyLabel,\n );\n const showClear = Boolean(clearable && chosen != null && !inert);\n /** What the closed control is showing — the second half of its accessible name. */\n const triggerText = selectedOption?.label ?? placeholder ?? \"\";\n // While locked the trigger sits in the reason's Tooltip — in a fragment with a hidden\n // copy of the reason, Button's anatomy — `block` so a full-width field stays full width.\n const withLock = (trigger: ReactNode) =>\n lock.locked ? (\n <LockedReason lock={lock} display={disabledReasonDisplay} className=\"block\">\n {trigger}\n </LockedReason>\n ) : (\n trigger\n );\n\n const choose = (o: ComboOption<V>) => {\n if (lock.locked) return;\n rememberOption(o);\n onChange(o.value);\n // Back to the trigger, not to <body>: the panel that held focus is about to\n // unmount, and a keyboard user who just answered this field should be standing\n // on it, ready to Tab to the next one.\n core.closeToTrigger();\n };\n\n return (\n // `rest` dresses the wrapper, which has no role; the accessible NAME — and the\n // id, description, invalid and required state above — go on the trigger, which\n // has one. Spread FIRST so the trigger's ARIA and the\n // handlers that open the panel cannot be clobbered from outside.\n <div {...rest} className={cn(\"relative\", className)}>\n {label !== undefined && hintParts.labelHint === undefined && <FieldLabel>{label}</FieldLabel>}\n <EndHintRow {...endHintRowProps(\"hint\" in props, label !== undefined, hintParts.labelHint)}>\n {withLock(\n <button\n ref={setTrigger}\n id={id}\n type=\"button\"\n // A combobox, not a button. The distinction is not pedantry: this control\n // carried `aria-invalid`, which `button` does not support, so a required\n // field left empty painted a rose border and told a reader nothing at all —\n // and ESLint flagged it as exactly that (`role-supports-aria-props`). The\n // fix the audit asked for is the role that describes what this IS: a closed\n // choice that expands into the list named below. `combobox` supports\n // `aria-invalid`, so the border and the announcement finally agree.\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listboxId}\n aria-haspopup=\"listbox\"\n // The label is a floating <span>, not a <label for>, so without this the\n // trigger's accessible name is whatever value happens to be selected —\n // \"Checking\" with nothing saying it is the account. The label AND the\n // value, because `aria-label` replaces the content rather than adding to\n // it, and a control that announces only its name has lost the answer.\n // A caller's own name wins over the composition: two fields labelled\n // \"Account\" on a transfer form are the from and the to, and only the\n // caller knows which is which.\n aria-label={ariaLabel ?? (typeof label === \"string\" ? common.fieldValue(label, triggerText) : undefined)}\n // The focusable kind of disabled while locked (Button's rule): a native\n // `disabled` would take the field out of the tab order, and the reason with it.\n disabled={lock.locked ? undefined : disabled}\n aria-disabled={lock.locked || undefined}\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, lock.locked && lock.reasonId)}\n aria-required={ariaRequired}\n onClick={() => !inert && setOpen((o) => !o)}\n // Down/Up opens the list from the closed trigger, per the APG. Enter and\n // Space already do it through the button's own click.\n onKeyDown={(e) => {\n if (inert) return;\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n } else if (showClear && !open && (e.key === \"Delete\" || e.key === \"Backspace\")) {\n // The keyboard's clear: the \"×\" is a pointer shortcut out of the tab\n // order, so without this a cleared-by-mouse field was set-only from the\n // keyboard. The closed trigger only — CountrySelect's rule (0.23).\n e.preventDefault();\n onChange(clearValue);\n }\n }}\n className={cn(\n FIELD_TRIGGER,\n \"pe-9\",\n label !== undefined && FIELD_FLOATING_PAD,\n inert && \"cursor-not-allowed opacity-50\",\n field.isInvalid && FIELD_INVALID,\n )}\n >\n <span className=\"flex min-w-0 items-center gap-2\">\n {selectedOption?.icon && <span className=\"shrink-0\">{selectedOption.icon}</span>}\n <span\n className={cn(\n // One line at Normal; from Large up the value wraps and the trigger\n // grows, as DatePicker's does (text size §10.17, round 0.34 T1).\n \"truncate-until-large\",\n // A chosen value is the field's VALUE, so it is set in the same ink an\n // <input>'s value is — FIELD_BASE's own text colour, inherited rather\n // than restated (Keksdose dev#477). It used to be one notch lighter\n // than the typeahead fields beside it, which is visible when a picker\n // and a text field share a form row. Nothing selected keeps the\n // placeholder tone (`--text-placeholder`), which every field here\n // agrees on.\n !selectedOption && \"text-placeholder\",\n )}\n >\n {selectedOption?.label ?? placeholder ?? \"\"}\n </span>\n </span>\n {showClear ? (\n /* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside\n the trigger <button>, so it never holds focus and a key handler here could\n never fire; the keys go to the trigger, where Delete and Backspace clear. */\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label={labels.clear}\n onClick={(e) => {\n e.stopPropagation();\n onChange(clearValue);\n }}\n className=\"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-placeholder hover:text-secondary\"\n >\n <X className=\"size-4\" />\n </span>\n ) : (\n <FieldChevron />\n )}\n </button>,\n )}\n </EndHintRow>\n {/* With a \"?\" the label shares the top strip with it — after the trigger, so the\n \"?\" follows the control in the tab order, as on Input and Select. */}\n {label !== undefined && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <FieldLabel className={LABEL_IN_ROW}>{label}</FieldLabel>\n </StaticLabelRow>\n )}\n <ComboboxPanel\n core={core}\n listboxId={listboxId}\n // On a phone the panel becomes a full-screen sheet, which needs the field's\n // own label to say what it is asking for (live #200).\n sheetTitle={sheetTitle ?? label ?? placeholder}\n searchPlaceholder={labels.search}\n emptyLabel={labels.noResults}\n closeLabel={closeLabel}\n loadErrorLabel={loadErrorLabel}\n isSelected={(v) => v === value}\n onChoose={choose}\n showCreate={showCreate}\n onCreate={() => {\n if (lock.locked) return;\n onCreate?.(q);\n core.closeToTrigger();\n }}\n createContent={q ? labels.create(q) : createEmptyLabel}\n createDisabledReason={createReason}\n />\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/entity-combobox.tsx"],"sourcesContent":["import { useCallback, useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FieldChevron, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./field-base\";\nimport { FieldLabel } from \"./field-parts\";\nimport {\n ComboboxPanel,\n endHintRowProps,\n offersCreate,\n useComboboxCore,\n type ComboClearValue,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport {\n assignRef,\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n mergeDescribedBy,\n StaticLabelRow,\n useFieldError,\n useFieldHint,\n useLockReason,\n type DisabledReasonDisplay,\n type FieldHelpProps,\n} from \"./field-parts\";\nimport { useCommitReason, type CommitScope } from \"./write-lock\";\n\nexport type { ComboClearValue, ComboOption } from \"./combobox-core\";\n\n/** `onChange` is the kit's — \"a selection was made\", carrying values — rather\n * than the div's form event, so the DOM's spelling of it is omitted. */\nexport interface EntityComboboxProps<V extends string | number, C extends ComboClearValue = null>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\">, FieldHelpProps {\n /** Selected id; `null`, `undefined` or `clearValue` when nothing is selected. */\n value: V | C | null | undefined;\n /** Selecting an option emits its value; the clear button emits `clearValue`. */\n onChange: (value: V | C) => void;\n /**\n * What the clear button emits. Default `null`. Pass `\"\"` for a form whose schema\n * wants an empty string for \"no choice\" (a zod `z.string()` field), rather than\n * mapping `null` in every `onChange` — the type of `onChange` follows it, so the\n * mapping cannot be forgotten on one field and not another. The picker also reads\n * the value back as empty, so `value=\"\"` shows the placeholder and no \"×\".\n * See {@link ComboClearValue} for why the choice is closed.\n */\n clearValue?: C;\n /** Already-loaded options (client-side filtered). Also used to resolve the\n * trigger label for the current `value`. */\n options?: ComboOption<V>[];\n /** Async option source, debounced and called on open + as the query changes\n * (at `minChars` and up). Stale responses are ignored; a rejection empties the\n * list and says `loadErrorLabel` instead of leaving the last query's rows up. When set, `options` is used only for label\n * resolution, not as the result list. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** External loading flag (OR-ed with the internal async state). */\n loading?: boolean;\n label?: ReactNode;\n placeholder?: string;\n searchPlaceholder?: string;\n emptyLabel?: string;\n /** Offer a clear \"×\" in place of the chevron while something is chosen; it emits\n * `clearValue`. A pointer shortcut inside the trigger, out of the tab order — the\n * keyboard's way is Delete or Backspace on the CLOSED trigger (0.23, as\n * {@link CountrySelect}). Not offered while `disabled` or locked. */\n clearable?: boolean;\n clearLabel?: string;\n /** The phone sheet's close button. Its own prop rather than a reuse of\n * `clearLabel`: \"clear the selection\" and \"close this screen\" are different\n * actions, and on a full-screen sheet the close button is the only way out —\n * so it is the one control here that MUST be in the reader's language. */\n closeLabel?: string;\n /** The phone sheet's heading (and, as a string, its accessible name) for a picker\n * whose label is drawn by someone else — a bound field's label; `RhfCombobox`\n * hands its label over here (0.24). Default: `label`, then `placeholder`. */\n sheetTitle?: ReactNode;\n disabled?: boolean;\n /**\n * Why the choice cannot be changed — {@link Button}'s `disabledReason`, for a picker\n * that SAVES on change (keksdose G2: the statement's account, where picking IS the\n * write). The trigger stays focusable and `aria-disabled`, the list does not open,\n * the clear \"×\" is gone and `onChange` never fires; the reason is in the kit\n * {@link Tooltip} and on the trigger's `aria-describedby`. Wins over `disabled`.\n * {@link CountrySelect}'s lock, which is built on the same core.\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 /** When set, a \"create\" row appears for a non-empty query with no exact match. */\n onCreate?: (query: string) => void;\n createLabel?: (query: string) => string;\n /**\n * Offer the create row with NOTHING typed too, worded as this — keksdose G9's\n * \"Create cash account\", which mints a record under a default name and so needs no\n * query. `onCreate` then receives `\"\"`. Left out, the row needs a query, as before.\n * See {@link InlineEntityCombobox}'s `createEmptyLabel`.\n */\n createEmptyLabel?: string;\n /**\n * The create row WRITES (it mints the record on the server) while picking does not.\n * Under a locked {@link WriteLockProvider} the row stays in the list but cannot be\n * taken, with the lock's reason as its second line — the picker itself stays live.\n * keksdose dev#496: on the read-only demo choosing is draft state, minting is not.\n */\n createCommit?: CommitScope;\n /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** What is wrong with the value, as {@link Input}'s `error`: rendered under the\n * field, on the trigger's `aria-describedby`, and implies `invalid`. */\n error?: ReactNode;\n /**\n * Standing advice — {@link Combobox}'s `hint`, so the pickers read like every other\n * field (keksdose K4): plain TEXT is a caption under the field on the trigger's\n * `aria-describedby`, before any error; a {@link FieldHint} \"?\" rides the label line,\n * or with no label sits at the trigger's end edge, outside the box.\n */\n hint?: ReactNode;\n /** Narrow `options` client-side by the query. Default `true`; `false` shows them\n * as given (a server-ranked list). */\n filter?: boolean;\n /** Offer nothing, and call no `loadOptions`, below this many characters. Default\n * `0`, i.e. the list loads as the panel opens. */\n minChars?: number;\n /** `loadOptions` debounce. Default 150 ms. */\n debounceMs?: number;\n /** Shown when `loadOptions` rejects. Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n /**\n * The TRIGGER — the focusable `<button>` a reader meets — as a React 19 ref prop, as\n * on {@link CountrySelect}, which is built on the same core (0.24, kastlan: react-hook-\n * form's focus-on-error calls `focus()` on whatever `field.ref` is handed, and a\n * picker with no ref gave it nothing to focus). Not the wrapper: focus belongs on the\n * trigger, which is also where the panel hands it back after a pick.\n */\n ref?: Ref<HTMLButtonElement>;\n}\n\n/**\n * An id-keyed, searchable entity picker: a field-styled trigger showing the\n * selected item's label, and a portalled dropdown of `{label, sublabel, icon}`\n * options — loaded up front via `options` or lazily via `loadOptions`. Built on\n * the shared field/anchor/dismiss/search primitives (no cmdk/Radix). For picking\n * several entities use {@link MultiEntityCombobox}.\n */\nexport function EntityCombobox<V extends string | number, C extends ComboClearValue = null>(\n props: EntityComboboxProps<V, C>,\n) {\n const {\n value,\n onChange,\n clearValue = null as C,\n options,\n loadOptions,\n loading,\n label,\n placeholder,\n searchPlaceholder,\n emptyLabel,\n clearable,\n clearLabel,\n closeLabel,\n sheetTitle,\n disabled,\n disabledReason,\n disabledReasonDisplay,\n commit,\n onCreate,\n createLabel,\n createEmptyLabel,\n createCommit,\n className,\n invalid,\n error,\n hint,\n help,\n filter,\n minChars,\n debounceMs,\n loadErrorLabel,\n ref,\n \"aria-label\": ariaLabel,\n // The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than\n // the wrapper — see MultiEntityCombobox: `Field`'s render-prop spreads `{ id,\n // aria-describedby, aria-invalid, aria-required }`, and on the role-less wrapper\n // div the label's `htmlFor` named nothing and the hint described nothing.\n id,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n } = props;\n const core = useComboboxCore<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n });\n const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });\n const field = useFieldError(error, invalid, hintParts.describedBy, ariaInvalid);\n // The props are the per-instance overrides, the provider the app-wide ones; a\n // prop left `undefined` falls through to the provider rather than masking it.\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n search: searchPlaceholder,\n noResults: emptyLabel,\n clear: clearLabel,\n create: createLabel,\n });\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const { open, results, resolve, setOpen, rememberOption, query, triggerRef } = core;\n // The core anchors the panel to the trigger and hands focus back to it; the caller's\n // `ref` wants the same element. One callback feeds both — CountrySelect's.\n const setTrigger = useCallback(\n (el: HTMLButtonElement | null) => {\n triggerRef.current = el;\n assignRef(ref, el);\n },\n [ref, triggerRef],\n );\n // The lock: this picker's own reason, or the provider's under `commit`.\n const lock = useLockReason(commit, disabledReason);\n const inert = lock.locked || Boolean(disabled);\n // A lock arriving while the list is up closes it — adjusted while\n // rendering, as the core adjusts its own query, so no frame offers a pick that would\n // be thrown away.\n if (lock.locked && open) setOpen(false);\n // The create row's own lock, for a picker whose choosing is draft and whose create\n // is the write (`createCommit`).\n const createReason = useCommitReason(createCommit, undefined);\n // One id per instance, generated here rather than in the core: `aria-controls` on\n // the trigger has to name the list while the list is still closed, so the id\n // belongs to whoever renders both ends of it.\n const listboxId = useId();\n\n // The clear value is \"nothing selected\" too, whichever one the caller picked.\n const chosen: V | null = value == null || value === clearValue ? null : (value as V);\n const selectedOption = chosen == null ? null : resolve(chosen);\n const q = query.trim();\n // The create row's \"is this name already listed\" folds as the list's match does (DUP-5).\n const locale = useKitLocale();\n const showCreate = offersCreate(\n onCreate,\n q,\n results.map((o) => o.label),\n createEmptyLabel,\n locale,\n );\n const showClear = Boolean(clearable && chosen != null && !inert);\n /** What the closed control is showing — the second half of its accessible name. */\n const triggerText = selectedOption?.label ?? placeholder ?? \"\";\n // While locked the trigger sits in the reason's Tooltip — in a fragment with a hidden\n // copy of the reason, Button's anatomy — `block` so a full-width field stays full width.\n const withLock = (trigger: ReactNode) =>\n lock.locked ? (\n <LockedReason lock={lock} display={disabledReasonDisplay} className=\"block\">\n {trigger}\n </LockedReason>\n ) : (\n trigger\n );\n\n const choose = (o: ComboOption<V>) => {\n if (lock.locked) return;\n rememberOption(o);\n onChange(o.value);\n // Back to the trigger, not to <body>: the panel that held focus is about to\n // unmount, and a keyboard user who just answered this field should be standing\n // on it, ready to Tab to the next one.\n core.closeToTrigger();\n };\n\n return (\n // `rest` dresses the wrapper, which has no role; the accessible NAME — and the\n // id, description, invalid and required state above — go on the trigger, which\n // has one. Spread FIRST so the trigger's ARIA and the\n // handlers that open the panel cannot be clobbered from outside.\n <div {...rest} className={cn(\"relative\", className)}>\n {label !== undefined && hintParts.labelHint === undefined && <FieldLabel>{label}</FieldLabel>}\n <EndHintRow {...endHintRowProps(\"hint\" in props, label !== undefined, hintParts.labelHint)}>\n {withLock(\n <button\n ref={setTrigger}\n id={id}\n type=\"button\"\n // A combobox, not a button. The distinction is not pedantry: this control\n // carried `aria-invalid`, which `button` does not support, so a required\n // field left empty painted a rose border and told a reader nothing at all —\n // and ESLint flagged it as exactly that (`role-supports-aria-props`). The\n // fix the audit asked for is the role that describes what this IS: a closed\n // choice that expands into the list named below. `combobox` supports\n // `aria-invalid`, so the border and the announcement finally agree.\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listboxId}\n aria-haspopup=\"listbox\"\n // The label is a floating <span>, not a <label for>, so without this the\n // trigger's accessible name is whatever value happens to be selected —\n // \"Checking\" with nothing saying it is the account. The label AND the\n // value, because `aria-label` replaces the content rather than adding to\n // it, and a control that announces only its name has lost the answer.\n // A caller's own name wins over the composition: two fields labelled\n // \"Account\" on a transfer form are the from and the to, and only the\n // caller knows which is which.\n aria-label={ariaLabel ?? (typeof label === \"string\" ? common.fieldValue(label, triggerText) : undefined)}\n // The focusable kind of disabled while locked (Button's rule): a native\n // `disabled` would take the field out of the tab order, and the reason with it.\n disabled={lock.locked ? undefined : disabled}\n aria-disabled={lock.locked || undefined}\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, lock.locked && lock.reasonId)}\n aria-required={ariaRequired}\n onClick={() => !inert && setOpen((o) => !o)}\n // Down/Up opens the list from the closed trigger, per the APG. Enter and\n // Space already do it through the button's own click.\n onKeyDown={(e) => {\n if (inert) return;\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n } else if (showClear && !open && (e.key === \"Delete\" || e.key === \"Backspace\")) {\n // The keyboard's clear: the \"×\" is a pointer shortcut out of the tab\n // order, so without this a cleared-by-mouse field was set-only from the\n // keyboard. The closed trigger only — CountrySelect's rule (0.23).\n e.preventDefault();\n onChange(clearValue);\n }\n }}\n className={cn(\n FIELD_TRIGGER,\n \"pe-9\",\n label !== undefined && FIELD_FLOATING_PAD,\n inert && \"cursor-not-allowed opacity-50\",\n field.isInvalid && FIELD_INVALID,\n )}\n >\n <span className=\"flex min-w-0 items-center gap-2\">\n {selectedOption?.icon && <span className=\"shrink-0\">{selectedOption.icon}</span>}\n <span\n className={cn(\n // One line at Normal; from Large up the value wraps and the trigger\n // grows, as DatePicker's does (text size §10.17, round 0.34 T1).\n \"truncate-until-large\",\n // A chosen value is the field's VALUE, so it is set in the same ink an\n // <input>'s value is — FIELD_BASE's own text colour, inherited rather\n // than restated (Keksdose dev#477). It used to be one notch lighter\n // than the typeahead fields beside it, which is visible when a picker\n // and a text field share a form row. Nothing selected keeps the\n // placeholder tone (`--text-placeholder`), which every field here\n // agrees on.\n !selectedOption && \"text-placeholder\",\n )}\n >\n {selectedOption?.label ?? placeholder ?? \"\"}\n </span>\n </span>\n {showClear ? (\n /* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside\n the trigger <button>, so it never holds focus and a key handler here could\n never fire; the keys go to the trigger, where Delete and Backspace clear. */\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label={labels.clear}\n onClick={(e) => {\n e.stopPropagation();\n onChange(clearValue);\n }}\n className=\"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-placeholder hover:text-secondary\"\n >\n <X className=\"size-4\" />\n </span>\n ) : (\n <FieldChevron />\n )}\n </button>,\n )}\n </EndHintRow>\n {/* With a \"?\" the label shares the top strip with it — after the trigger, so the\n \"?\" follows the control in the tab order, as on Input and Select. */}\n {label !== undefined && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <FieldLabel className={LABEL_IN_ROW}>{label}</FieldLabel>\n </StaticLabelRow>\n )}\n <ComboboxPanel\n core={core}\n listboxId={listboxId}\n // On a phone the panel becomes a full-screen sheet, which needs the field's\n // own label to say what it is asking for (live #200).\n sheetTitle={sheetTitle ?? label ?? placeholder}\n searchPlaceholder={labels.search}\n emptyLabel={labels.noResults}\n closeLabel={closeLabel}\n loadErrorLabel={loadErrorLabel}\n isSelected={(v) => v === value}\n onChoose={choose}\n showCreate={showCreate}\n onCreate={() => {\n if (lock.locked) return;\n onCreate?.(q);\n core.closeToTrigger();\n }}\n createContent={q ? labels.create(q) : createEmptyLabel}\n createDisabledReason={createReason}\n />\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -21,7 +21,7 @@ import {
21
21
  useActiveOptionScroll
22
22
  } from "./combobox-core.js";
23
23
  import { DEFAULT_COMBOBOX_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
24
- import { matchesSearch, rankMatch, searchWords } from "../lib/text-match.js";
24
+ import { matchesSearch, rankMatch, sameName, searchWords } from "../lib/text-match.js";
25
25
  import {
26
26
  EndHintRow,
27
27
  FieldCaption,
@@ -143,7 +143,8 @@ function InlineEntityCombobox(props) {
143
143
  onCreate,
144
144
  createQuery,
145
145
  options.map((o) => o.label),
146
- createEmptyLabel
146
+ createEmptyLabel,
147
+ locale
147
148
  );
148
149
  const createText = createQuery ? labels.create(createQuery) : createEmptyLabel;
149
150
  const rows = showCreate ? [...matches, { disabled: createLocked }] : matches;
@@ -178,7 +179,7 @@ function InlineEntityCombobox(props) {
178
179
  if (!q) {
179
180
  if (value != null) onChange(clearValue);
180
181
  } else {
181
- const hits = options.filter((o) => !o.disabled && o.label.toLowerCase() === q.toLowerCase());
182
+ const hits = options.filter((o) => !o.disabled && sameName(o.label, q, locale));
182
183
  const ids = new Set(hits.map((h) => h.value));
183
184
  if (ids.size === 1 && hits[0].value !== value) onChange(hits[0].value);
184
185
  }
@@ -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 { isQuietFocus } from \"../lib/quiet-focus\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { PickerSheet, SHEET_ROW_CLASS } from \"./picker-sheet\";\nimport {\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n endHintRowProps,\n GROUP_HEADING_STICKY,\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 { matchesSearch, rankMatch, searchWords } 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 type FieldHelpProps,\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\">, FieldHelpProps {\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 help,\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, { help, name: label ?? ariaLabel });\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 // The field took the tour's quiet focus (§11.8) and has not opened since.\n const quietRef = useRef(false);\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 // Every word of the query, in the label or the sublabel (0.51.4).\n const words = searchWords(query, locale);\n const hit = (o: ComboOption<V>) => matchesSearch(words, [o.label, o.sublabel], locale);\n // The label's whole name or its start first (`rankMatch` 0 and 1), then a match\n // anywhere in the label or the sublabel. Not the group (0.40, kk run 79): with\n // first-letter groups one typed letter matched its whole 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));\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 ? [...matches, { disabled: createLocked }] : 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((o) => !o.disabled && o.label.toLowerCase() === q.toLowerCase());\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\">{createReason}</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 // The tour's focus on arrival (§11.8) opens nothing: the list would cover\n // its card (0.50.1). A click, ArrowDown or typing opens it.\n if (isQuietFocus(e.currentTarget)) {\n quietRef.current = true;\n return;\n }\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 // After a quiet focus, the person's click on the already-focused field\n // fires no `focus`: the click is their \"open\" instead.\n onClick={() => {\n if (!quietRef.current || locked) return;\n quietRef.current = false;\n setOpen(true);\n }}\n onBlur={() => {\n quietRef.current = false;\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={cn(\n GROUP_HEADING_STICKY,\n \"px-4 pb-0.5 pt-3 text-caption font-semibold uppercase tracking-wide text-muted\",\n )}\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(SHEET_ROW_CLASS, CREATE_ROW_CLASS, createLocked && DISABLED_ROW_CLASS)}\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={cn(\n GROUP_HEADING_STICKY,\n \"px-3 pb-0.5 pt-2 text-caption font-semibold uppercase tracking-wide text-muted first:pt-1\",\n )}\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 && <span className=\"ms-2 text-xs text-placeholder\">{o.sublabel}</span>}\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 { isQuietFocus } from \"../lib/quiet-focus\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { PickerSheet, SHEET_ROW_CLASS } from \"./picker-sheet\";\nimport {\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n endHintRowProps,\n GROUP_HEADING_STICKY,\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 { matchesSearch, rankMatch, sameName, searchWords } 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 type FieldHelpProps,\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\">, FieldHelpProps {\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 help,\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, { help, name: label ?? ariaLabel });\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 // The field took the tour's quiet focus (§11.8) and has not opened since.\n const quietRef = useRef(false);\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 // Every word of the query, in the label or the sublabel (0.51.4).\n const words = searchWords(query, locale);\n const hit = (o: ComboOption<V>) => matchesSearch(words, [o.label, o.sublabel], locale);\n // The label's whole name or its start first (`rankMatch` 0 and 1), then a match\n // anywhere in the label or the sublabel. Not the group (0.40, kk run 79): with\n // first-letter groups one typed letter matched its whole 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));\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 locale,\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 ? [...matches, { disabled: createLocked }] : 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. A label is\n // typed out in full as the list matches it, so \"cafe\" commits Café (DUP-5).\n const hits = options.filter((o) => !o.disabled && sameName(o.label, q, locale));\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\">{createReason}</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 // The tour's focus on arrival (§11.8) opens nothing: the list would cover\n // its card (0.50.1). A click, ArrowDown or typing opens it.\n if (isQuietFocus(e.currentTarget)) {\n quietRef.current = true;\n return;\n }\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 // After a quiet focus, the person's click on the already-focused field\n // fires no `focus`: the click is their \"open\" instead.\n onClick={() => {\n if (!quietRef.current || locked) return;\n quietRef.current = false;\n setOpen(true);\n }}\n onBlur={() => {\n quietRef.current = false;\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={cn(\n GROUP_HEADING_STICKY,\n \"px-4 pb-0.5 pt-3 text-caption font-semibold uppercase tracking-wide text-muted\",\n )}\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(SHEET_ROW_CLASS, CREATE_ROW_CLASS, createLocked && DISABLED_ROW_CLASS)}\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={cn(\n GROUP_HEADING_STICKY,\n \"px-3 pb-0.5 pt-2 text-caption font-semibold uppercase tracking-wide text-muted first:pt-1\",\n )}\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 && <span className=\"ms-2 text-xs text-placeholder\">{o.sublabel}</span>}\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"]}
@@ -52,7 +52,7 @@ function List({
52
52
  className
53
53
  ),
54
54
  children: [
55
- folds && !open ? rows.slice(0, Math.max(0, limit)) : children,
55
+ folds && !open ? rows.slice(0, Math.max(0, limit)) : rows ?? children,
56
56
  folds && /* @__PURE__ */ jsx("li", { role: "none", className: MORE_PAD[density], children: /* @__PURE__ */ jsx(
57
57
  Button,
58
58
  {