@eifi1/ui-kit 0.49.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +11 -7
  2. package/dist/admin/user-roster.d.ts +16 -10
  3. package/dist/admin/user-roster.js +7 -3
  4. package/dist/admin/user-roster.js.map +1 -1
  5. package/dist/auth/verify-email.d.ts +9 -1
  6. package/dist/auth/verify-email.js +3 -1
  7. package/dist/auth/verify-email.js.map +1 -1
  8. package/dist/components/amount-input.d.ts +2 -1
  9. package/dist/components/amount-input.js +3 -2
  10. package/dist/components/amount-input.js.map +1 -1
  11. package/dist/components/autocomplete.js +1 -0
  12. package/dist/components/autocomplete.js.map +1 -1
  13. package/dist/components/combobox-core.js +1 -0
  14. package/dist/components/combobox-core.js.map +1 -1
  15. package/dist/components/combobox-suggestions.js +1 -0
  16. package/dist/components/combobox-suggestions.js.map +1 -1
  17. package/dist/components/currency-select.d.ts +28 -2
  18. package/dist/components/currency-select.js +56 -10
  19. package/dist/components/currency-select.js.map +1 -1
  20. package/dist/components/data-table-mobile.d.ts +11 -2
  21. package/dist/components/data-table-mobile.js +62 -8
  22. package/dist/components/data-table-mobile.js.map +1 -1
  23. package/dist/components/data-table-row.d.ts +1 -1
  24. package/dist/components/data-table-row.js.map +1 -1
  25. package/dist/components/data-table.js +3 -1
  26. package/dist/components/data-table.js.map +1 -1
  27. package/dist/components/dropdown.js +1 -0
  28. package/dist/components/dropdown.js.map +1 -1
  29. package/dist/components/full-bleed-dialog.js +1 -0
  30. package/dist/components/full-bleed-dialog.js.map +1 -1
  31. package/dist/components/modal.js +1 -0
  32. package/dist/components/modal.js.map +1 -1
  33. package/dist/components/number-input.d.ts +2 -1
  34. package/dist/components/number-input.js +6 -2
  35. package/dist/components/number-input.js.map +1 -1
  36. package/dist/components/numpad-sheet.js +2 -0
  37. package/dist/components/numpad-sheet.js.map +1 -1
  38. package/dist/components/picker-sheet.js +2 -0
  39. package/dist/components/picker-sheet.js.map +1 -1
  40. package/dist/components/popover.js +1 -0
  41. package/dist/components/popover.js.map +1 -1
  42. package/dist/eslint/index.js +1 -1
  43. package/dist/i18n/defaults.js +2 -0
  44. package/dist/i18n/defaults.js.map +1 -1
  45. package/dist/i18n/german.js +14 -1
  46. package/dist/i18n/german.js.map +1 -1
  47. package/dist/i18n/kit-labels.d.ts +3 -0
  48. package/dist/i18n/kit-labels.js.map +1 -1
  49. package/dist/i18n/locales/en.js +16 -1
  50. package/dist/i18n/locales/en.js.map +3 -3
  51. package/dist/i18n/locales/es.js +14 -1
  52. package/dist/i18n/locales/es.js.map +1 -1
  53. package/dist/i18n/locales/fr.js +14 -1
  54. package/dist/i18n/locales/fr.js.map +1 -1
  55. package/dist/i18n/locales/hu.js +14 -1
  56. package/dist/i18n/locales/hu.js.map +1 -1
  57. package/dist/i18n/locales/it.js +14 -1
  58. package/dist/i18n/locales/it.js.map +1 -1
  59. package/dist/i18n/locales/zh.js +14 -1
  60. package/dist/i18n/locales/zh.js.map +1 -1
  61. package/dist/i18n/review.js +1 -0
  62. package/dist/i18n/review.js.map +1 -1
  63. package/dist/lib/data-attributes.d.ts +11 -0
  64. package/dist/lib/data-attributes.js +10 -0
  65. package/dist/lib/data-attributes.js.map +1 -0
  66. package/dist/lib/format.d.ts +1 -0
  67. package/dist/lib/format.js +2 -0
  68. package/dist/lib/format.js.map +1 -1
  69. package/dist/lib/minus.d.ts +10 -0
  70. package/dist/lib/minus.js.map +1 -1
  71. package/dist/testing/react.d.ts +25 -0
  72. package/dist/testing/react.js +19 -0
  73. package/dist/testing/react.js.map +1 -0
  74. package/dist/tour/tour-dock.d.ts +42 -0
  75. package/dist/tour/tour-dock.js +80 -0
  76. package/dist/tour/tour-dock.js.map +1 -0
  77. package/dist/tour/tour-gates.d.ts +71 -0
  78. package/dist/tour/tour-gates.js +114 -0
  79. package/dist/tour/tour-gates.js.map +1 -0
  80. package/dist/tour/tour-guide-button.d.ts +27 -0
  81. package/dist/tour/tour-guide-button.js +45 -0
  82. package/dist/tour/tour-guide-button.js.map +1 -0
  83. package/dist/tour/tour-guide-labels.d.ts +8 -0
  84. package/dist/tour/tour-guide-labels.js +9 -0
  85. package/dist/tour/tour-guide-labels.js.map +1 -0
  86. package/dist/tour/tour-labels.js +9 -1
  87. package/dist/tour/tour-labels.js.map +1 -1
  88. package/dist/tour/tour.d.ts +133 -9
  89. package/dist/tour/tour.js +594 -70
  90. package/dist/tour/tour.js.map +1 -1
  91. package/dist/tour/use-tour-url-sync.d.ts +5 -2
  92. package/dist/tour/use-tour-url-sync.js +7 -1
  93. package/dist/tour/use-tour-url-sync.js.map +1 -1
  94. package/dist/tour.d.ts +2 -0
  95. package/dist/tour.js +3 -0
  96. package/dist/tour.js.map +1 -1
  97. package/dist/wizard/stepper-nav.js +15 -2
  98. package/dist/wizard/stepper-nav.js.map +1 -1
  99. package/dist/wizard/types.d.ts +11 -0
  100. package/dist/wizard/types.js.map +1 -1
  101. package/dist/wizard/use-wizard.js +11 -5
  102. package/dist/wizard/use-wizard.js.map +1 -1
  103. package/package.json +9 -1
  104. package/src/admin/user-roster.tsx +22 -12
  105. package/src/auth/verify-email.tsx +10 -0
  106. package/src/components/amount-input.tsx +5 -4
  107. package/src/components/autocomplete.tsx +2 -0
  108. package/src/components/combobox-core.tsx +2 -0
  109. package/src/components/combobox-suggestions.tsx +2 -0
  110. package/src/components/currency-select.tsx +95 -6
  111. package/src/components/data-table-mobile.tsx +99 -8
  112. package/src/components/data-table-row.tsx +1 -0
  113. package/src/components/data-table.tsx +9 -1
  114. package/src/components/dropdown.tsx +2 -0
  115. package/src/components/full-bleed-dialog.tsx +3 -0
  116. package/src/components/modal.tsx +3 -0
  117. package/src/components/number-input.tsx +8 -3
  118. package/src/components/numpad-sheet.tsx +4 -0
  119. package/src/components/picker-sheet.tsx +4 -0
  120. package/src/components/popover.tsx +2 -0
  121. package/src/i18n/defaults.ts +2 -0
  122. package/src/i18n/german.ts +13 -0
  123. package/src/i18n/kit-labels.tsx +3 -0
  124. package/src/i18n/locales/es.ts +13 -0
  125. package/src/i18n/locales/fr.ts +13 -0
  126. package/src/i18n/locales/hu.ts +13 -0
  127. package/src/i18n/locales/it.ts +13 -0
  128. package/src/i18n/locales/zh.ts +13 -0
  129. package/src/i18n/review.ts +1 -0
  130. package/src/lib/data-attributes.ts +14 -0
  131. package/src/lib/format.ts +2 -0
  132. package/src/lib/minus.ts +10 -0
  133. package/src/testing/react.ts +59 -0
  134. package/src/tour/tour-dock.ts +132 -0
  135. package/src/tour/tour-gates.ts +195 -0
  136. package/src/tour/tour-guide-button.tsx +64 -0
  137. package/src/tour/tour-guide-labels.ts +15 -0
  138. package/src/tour/tour-labels.ts +8 -0
  139. package/src/tour/tour.tsx +949 -86
  140. package/src/tour/use-tour-url-sync.ts +14 -4
  141. package/src/tour.ts +3 -0
  142. package/src/wizard/stepper-nav.tsx +23 -3
  143. package/src/wizard/types.ts +12 -0
  144. package/src/wizard/use-wizard.ts +16 -9
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/amount-input.tsx"],"sourcesContent":["import { useId, useMemo, useRef } from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport type { ReactNode, Ref } from \"react\";\nimport { CurrencyOptionList, getCurrency } from \"./currency-option-list\";\nimport { useComboboxCore } from \"./combobox-core\";\nimport {\n FIELD_BASE,\n FIELD_DISPLAY,\n FIELD_INVALID,\n FLOATING_INPUT_CLASS,\n FloatingField,\n} from \"./field-base\";\nimport { cn } from \"../lib/cn\";\nimport { currencyMinorDigits } from \"../lib/format\";\nimport { roundHalfAway } from \"../lib/round\";\nimport { CalculatorButton, type CalculatorButtonLabels } from \"./calculator\";\nimport { NumberPadSheet, type NumberPadSheetLabels } from \"./numpad-sheet\";\nimport {\n DEFAULT_COMMON_LABELS,\n DEFAULT_CURRENCY_LABELS,\n useKitLabels,\n useKitLocale,\n type CurrencyLabels,\n} from \"../i18n/kit-labels\";\nimport {\n commitExpression,\n evaluateExpression,\n formatResult,\n isBareAmount,\n looksLikeExpression,\n sanitizeLive,\n splitLeadingSign,\n} from \"../lib/calc\";\nimport {\n FieldCaption,\n LockedReason,\n mergeRefs,\n type DisabledReasonDisplay,\n type FieldHelpProps,\n type LockReason,\n} from \"./field-parts\";\nimport { useEndWidth, useNumericField } from \"./numeric-field\";\nimport type { CommitScope } from \"./write-lock\";\n\n/** {@link AmountInput}'s props — exported since 0.34 (refactor audit 2026-10, API-9): a\n * wrapper typed itself off `ComponentProps<typeof AmountInput>` until then. */\nexport interface AmountInputProps extends FieldHelpProps {\n value: string;\n onChange: (value: string) => void;\n currency?: string;\n onCurrencyChange?: (code: string) => void;\n placeholder?: string;\n label?: ReactNode;\n disabled?: boolean;\n /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /**\n * What is wrong with the amount, in the caller's own words — {@link Input}'s `error`\n * (keksdose K4: \"exceeds the outstanding balance\" was a loose `<p>` beside the field\n * that nothing pointed at). Rendered under the field, after a text `hint`; merged into\n * `aria-describedby` after the caller's ids and the hint's; implies `invalid`, so the\n * field paints as well as announces. `null`, `false` and `\"\"` are no message.\n * {@link MoneyField} takes it too.\n */\n error?: ReactNode;\n /**\n * Show the desktop calculator trigger (default `true`). `false` for an amount in an\n * ephemeral close-on-blur editor — a budget's assigned cell, a VAT cell (keksdose K6)\n * — where a click on the icon blurs the field and tears the editor down before the\n * popover can open. Typing a calculation (\"200+50\", then Enter) still evaluates, and\n * a phone keeps its numpad, exactly as {@link NumberInput}'s `calculator={false}`.\n */\n calculator?: boolean;\n /** The field's WRAPPER — width, a margin, a flex share. The `<input>` is\n * `inputClassName`'s. */\n className?: string;\n /**\n * Classes for the `<input>` itself — {@link NumberInput}'s and {@link Input}'s prop of\n * the same name, merged the same way: after everything the field sets for LOOKS — the\n * base classes, the plain end padding, the display shape's centring and the `tone`\n * colour — so it can restyle the figure (`text-end` for a money column whose decimal\n * points line up, `font-medium`, a tighter `py-1` or a symmetric `px-2` in a table\n * cell, `text-transparent` while a figure is being re-fetched), and before what the\n * field needs to WORK: the end padding the calculator and the currency chip reserve,\n * and the invalid border. Neither of those can be taken away by accident — a caller's\n * `px-2` keeps its start edge and the digits still stop short of the chip, and a\n * borderless cell's `border-transparent` still turns red when the value is wrong.\n *\n * keksdose G7: the invoice's VAT cell right-aligns its figure, and with only\n * `className` (the wrapper) it reached the element through `[&_input]:text-end` — a\n * descendant selector that depends on the field's inner markup, which the kit is free\n * to change, and that a reader of the call site has to decode. {@link MoneyField}\n * passes it through, and `RhfMoneyField` takes it too.\n *\n * keksdose 0.24: until then it sat BEFORE the plain end padding (`pe-3`) and the tone,\n * so the budget's assigned cell could not make its padding symmetric (`px-2` came out\n * `px-2 pe-3`) and the holdings price could not hide an outflow-toned figure while\n * re-fetching — both were back on `[&_input]:` selectors on the wrapper.\n */\n inputClassName?: string;\n id?: string;\n /** Names the field when it has no visible `label` (a table cell, the display shape). */\n \"aria-label\"?: string;\n /**\n * Set on the `<input>`, with `id` above. Declared so `Field`'s render-prop spread —\n * `{(ids) => <AmountInput {...ids} … />}` — is typed and reaches the input: the\n * hint and error are described, a required amount is announced as required, and\n * `aria-invalid` paints {@link FIELD_INVALID} the way `invalid` does.\n */\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: boolean | \"true\" | \"false\";\n \"aria-required\"?: boolean | \"true\" | \"false\";\n /**\n * BCP 47 tag for this field — {@link NumberInput}'s `locale` (0.34, DUP-10): the decimal\n * mark the field SHOWS and reads typing in (\"1,5\" for fr-CH, \"1.5\" for de-CH, as `Intl`\n * decides), and the language of the currency picker's search. Default the\n * `<UiKitProvider locale>`, else the runtime's. `value` and `onChange` stay\n * dot-decimal.\n */\n locale?: string;\n /** {@link NumberInput}'s `hint`, the same rule: a {@link FieldHint} \"?\" rides the\n * label's own line, beside the label (not at the far end, where the calculator and\n * the currency chip live); plain TEXT (a string or a number) is a caption UNDER\n * the field, attached through `aria-describedby` after the caller's own. A phone's\n * `variant=\"display\"` has no visible label, so a FieldHint is dropped there; a\n * caption still shows.\n *\n * Since 0.34 a FieldHint is that caption at Large and on a touch screen, as on\n * NumberInput since 0.32 (`useInlineHint`, docs/text-size-harmonization.md §4: a fact\n * a finger cannot hover open is printed). A `HelpPopover` — help, not a fact (§10.23)\n * — stays on the label line at every size. */\n hint?: ReactNode;\n /** Every user-facing string this component and the two it composes own, so a\n * translating host can supply its own. Each key optional, and each a per-field\n * override of the kit-wide translation in `<UiKitProvider labels>`: `currency`\n * and `currencySearch` of its `currency` namespace, the rest of `calculator`.\n * `currency` names the picker chip, composed with the selected code through\n * `common.fieldValue`; the rest are pass-through. */\n labels?: {\n currency?: string;\n currencySearch?: string;\n /** Names the calculator TRIGGER this component renders. `calculator` below\n * names the controls inside the popover it opens. */\n calculatorTrigger?: string;\n pad?: NumberPadSheetLabels;\n calculator?: CalculatorButtonLabels;\n };\n /** Translated currency names for the chip's picker, keyed by ISO code. The same\n * sparse override `CurrencySelect` takes, and for the same reason: the names in\n * `CURRENCIES` are shipped data, so without this they are the one English word\n * left in a translated row. Not part of `labels` because it is keyed by DATA\n * rather than by a fixed set of slots, and an app passes the same map to both\n * pickers. */\n currencyNames?: Record<string, string>;\n /** Offer only these ISO codes in the chip's list, in this order — `CurrencySelect`'s\n * `options`, since the chip renders CurrencySelect's list (round 0.34 Wave 2, DUP-4).\n * Default every currency the kit knows. */\n currencyOptions?: string[];\n /** Focus the field on mount — on mobile this also opens the numpad sheet, so a\n * new-transaction form can jump straight to amount entry (feedback #70). */\n autoFocus?: boolean;\n /**\n * Tint the typed figure by what it will DO (Keksdose feedback #167).\n *\n * The direction is chosen with a toggle beside the field rather than typed, so\n * the digits alone say nothing about which way the money goes — on the one screen\n * where getting that backwards is the expensive mistake. Colour says it\n * continuously, and it costs no layout. It stays the MAIN indication (live #201\n * rework: *\"The main indication shall still be the color\"*); {@link negative}\n * renders the sign as a second, quieter signal in front of the figure.\n *\n * Never the ONLY signal: the toggle it mirrors is right there, labelled, so this\n * is reinforcement rather than the sole carrier of the meaning.\n */\n tone?: \"neutral\" | \"outflow\" | \"inflow\";\n /**\n * The SIGN of the figure, owned by the caller's direction control (Keksdose live\n * #201 rework: *\"Outflow should be negative and inflow positive … the sign shall\n * be displayed but not necessarily be typed in\"*).\n *\n * `value` stays the MAGNITUDE. The minus is glued on at render time and stripped\n * off again before anything is reported back, so the caller never sees its own\n * prefix echoed and no code path can end up applying it twice. That is the whole\n * cure: there is exactly ONE place the sign lives, and it is not the text.\n *\n * Because it is rendered rather than stored it cannot be deleted (the next render\n * puts it back), a select-all-and-retype keeps the direction, and the caret needs\n * no minding.\n *\n * Since 0.15.5 it is drawn on its own too, for a read-only or disabled signed figure\n * (an \"Items total\" of −5.18): no `onNegativeChange` needed just to show the minus.\n */\n negative?: boolean;\n /**\n * A sign the user TYPED, or a calculation that resolved to a signed number.\n *\n * SET, never flip. A \"-\" means *\"this is an outflow\"*, so typing it twice says the\n * same thing twice — which is exactly what the rework asks for (*\"Subtracting\n * values twice resulting in inflow -> outflow and back to inflow seems not\n * reasonable\"*). \"+\" says the other direction.\n *\n * Its presence is what ARMS the whole sign behaviour. A field with no direction\n * control — an account's starting balance, a loan payment, a holding's cost —\n * passes neither prop and keeps a text that is the whole truth, minus included.\n */\n onNegativeChange?: (negative: boolean) => void;\n /**\n * How the field presents itself ON A PHONE (Keksdose feedback #176).\n *\n * `\"field\"` (default) is the labelled, bordered input used everywhere else.\n *\n * `\"display\"` keeps every mechanic — the same `<input>`, the same numpad sheet,\n * the same currency picker, the same commit/blur contract — and only drops the\n * chrome: no border, no fill, no floating label, the figure set at display size.\n * On a form whose whole point is one number, a bordered `text-sm` box draws the\n * amount at the same weight as the memo beside it; this makes it the headline\n * instead. It stays fully editable — the underline and the caret are what say so.\n *\n * Phone-only by construction: it keys off the same breakpoint as the numpad, so\n * from `md` up the field is byte-for-byte the normal one and a desktop grid keeps\n * its column rhythm.\n */\n variant?: \"field\" | \"display\";\n /**\n * Where the figure sits in its row, for `variant=\"display\"` only (Keksdose\n * feedback #176 rework).\n *\n * `\"start\"` (default) keeps it flush left, in the reading column the fields\n * below it share — and it is what the entry forms use, because a figure that\n * starts at a known edge is legible at any width: when the value outgrows the\n * field, the digits that scroll out of sight are the trailing ones, not the\n * magnitude (Keksdose feedback #430 rework).\n *\n * `\"center\"` makes it a centred headline. It centres in the width that REMAINS\n * after the trailing currency control, not in the whole box: the control is\n * pinned to the right edge, so a plain `text-center` would centre the figure\n * under it and the last digits would sit behind the chip. This used to mirror\n * the control's reservation onto the left instead, which centred correctly but\n * spent twice the control's width on padding — fatal on a 375px screen, where\n * it left a 36px figure 16px to live in.\n */\n align?: \"start\" | \"center\";\n /**\n * Decimals the committed amount is rounded to (kastlan 0.12: \"12.345\" and a\n * calculator's \"100/3\" reached a `Numeric(10,2)` column unrounded and failed with a\n * 422). Default: the currency's minor unit, from `Intl` (CHF and EUR 2, JPY 0). With\n * no `currency`, nothing is rounded unless this is set. Rounding is half away from\n * zero (12.345 → 12.35, -12.345 → -12.35) and happens when the figure settles: on\n * blur, on Enter, and on every result the calculator writes back. Keystrokes are left\n * alone, so \"12.3\" can still become \"12.34\".\n *\n * ⚠️ **A unit price or a rate needs `digits`.** The default is right for an amount of\n * money and silently wrong for a price per unit: a fund's NAV of 123.4567 or fuel at\n * 1.789 €/l is stored as 123.46 / 1.79, and no test fails (keksdose G2, a\n * `Numeric(18,6)` holding price and a `Numeric(18,4)` unit price). Pass the column's\n * scale: `digits={4}`, `digits={6}`.\n *\n * **Set `digits` for any sub-currency scale** — not only a price: any field whose\n * column holds more decimals than the currency's minor unit. A split line stored at\n * the ledger's 4dp (`Numeric(18,4)`) and loaded as 12.3456 is SHOWN as 12.35, and\n * committed as 12.35 as soon as the user tabs through it (blur settles) — so the form\n * no longer equals what the server holds: the lines stop summing to the 4dp total,\n * and Save is held (keksdose live #240). With a `currency`, every commit rounds to that currency's\n * minor unit unless `digits` says otherwise: pass the column's scale (`digits={4}`).\n */\n digits?: number;\n /** The smallest amount the field settles on. A lower figure is raised to it when it\n * settles, like `digits`. Compared with the signed figure. */\n min?: number;\n /** The largest amount the field settles on (a deposit release capped at the\n * balance). A higher figure is lowered to it when it settles. */\n max?: number;\n /**\n * The SETTLED figure, dot-decimal, sign included — fired on blur, on Enter, on the\n * numpad's \"=\" and on every result the calculator writes back, never on a keystroke\n * (\"1200+\" is not a figure yet). `onChange` still reports every keystroke; this is\n * for a host that stores a number and must not see drafts (kastlan 52; see\n * {@link MoneyField}, which is built on it).\n */\n onCommit?: (value: string) => void;\n /**\n * Why the amount cannot be changed — {@link NumberInput}'s `disabledReason` (0.34,\n * round 0.34 C2), for an amount that SAVES itself: a budget row's assigned cell, a\n * price committed on blur. `disabled` takes the field out of the tab order, so the\n * reason never reached a keyboard.\n *\n * With a reason the field stays in the tab order and shows its figure, `readOnly` (the\n * settled grey FIELD_BASE gives a `[readonly]` field) and `aria-disabled`. Nothing\n * changes the amount: the calculator and the phone's numpad are not offered, the\n * currency chip is a read-out rather than a picker, and neither blur nor Enter\n * settles — so `onChange`, `onCommit`, `onNegativeChange` and `onCurrencyChange` are\n * never called. Enter is swallowed, so the field does not submit the form around it.\n * The reason is a line under the field at Large and on a touch screen, else in the kit\n * {@link Tooltip} (`disabledReasonDisplay`), and on the input's `aria-describedby`,\n * between the caption and the error. Wins over `disabled`. {@link MoneyField} and\n * `RhfMoneyField` take it too.\n */\n disabledReason?: ReactNode;\n /**\n * Where the reason shows — {@link Button}'s `disabledReasonDisplay`: by default a line\n * under the field at Large and on a touch screen, the tooltip otherwise (§4 \"No fact\n * only in a tooltip\"; DUP-3, refactor audit 2026-10).\n */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * This amount COMMITS — {@link Button}'s `commit`, NumberInput's: under a locked\n * {@link WriteLockProvider} it is locked the `disabledReason` way with the lock's reason\n * (which wins over its own). An amount that only reaches the server through its form's\n * Save leaves this off and stays editable under the lock; the Save takes `commit`. No\n * provider, or an unlocked one: no effect.\n */\n commit?: CommitScope;\n /** The `<input>`. A React 19 ref prop since 0.34 (MOD-4, refactor audit 2026-10); it\n * was `forwardRef`. */\n ref?: Ref<HTMLInputElement>;\n}\n\n\n/**\n * A settled figure, rounded to `digits` (half away from zero) and clamped to\n * `min`/`max`. Only a plain number is touched. A half-typed draft or an expression\n * comes back as it was. Rounding is `lib/round.ts`'s, shared with `roundToCurrency`\n * (BUG-11: this copy blanked \"0.0000001\" and a 22-digit figure on blur).\n */\nfunction settleAmount(text: string, digits: number | undefined, min?: number, max?: number): string {\n const t = text.trim();\n if (!/^-?\\d*\\.?\\d*$/.test(t) || !/\\d/.test(t)) return text;\n let n = Number(t);\n if (!Number.isFinite(n)) return text;\n if (digits !== undefined) n = roundHalfAway(n, digits);\n if (min !== undefined && n < min) n = min;\n if (max !== undefined && n > max) n = max;\n const out = formatResult(n);\n // Unchanged figures keep their own spelling (\"12.50\" stays \"12.50\").\n return Number(out) === Number(t) ? text : out;\n}\n\n// The locale's decimal mark and the grouping parse (kastlan 40, keksdose G1) live in\n// `lib/decimal-marks.ts`, read through the engine NumberInput and this field share\n// (`numeric-field.ts`, DUP-10).\n\n// The consuming app's ONE money palette (`--money-expense` / `--money-income`),\n// not a bespoke rose/emerald pairing: a figure being typed has to wear the same\n// colour the same figure will wear once it is a row in the table behind the form\n// (Keksdose dev#434). The utilities are unlayered and theme-aware, so no `dark:`\n// variant is needed and they win over the field's own text colour regardless of\n// where `cn` puts them.\nconst TONE_CLASS: Record<\"neutral\" | \"outflow\" | \"inflow\", string> = {\n neutral: \"\",\n outflow: \"text-money-neg\",\n inflow: \"text-money-pos\",\n};\n\n// The shared chrome-less treatment at money size. `tabular-nums` so digits don't\n// reflow the figure as they are typed; FIELD_DISPLAY's own muted placeholder\n// colour is what keeps an EMPTY form from painting its zero in the direction tint\n// (`tone` is computed from 0, which reads as an inflow).\nconst DISPLAY_INPUT_CLASS = cn(\n FIELD_DISPLAY,\n \"text-4xl font-semibold leading-tight tracking-tight tabular-nums\",\n);\n\n/**\n * Is `text` the ANSWER to `previous`, or something typed OVER it?\n *\n * \"The old text was a sum and the new one is bare\" cannot tell those apart, and both\n * gestures are ordinary: select-all-and-retype after a typo, or backspacing \"-20+\"\n * back down to a fresh figure. Read as an answer, they move the direction chip — so\n * an outflow of 7 quietly booked +7.00 (live #201 rework).\n *\n * The question that does separate them is arithmetic, not shape: evaluate what the\n * text replaced and see whether this IS that number. \"-12+30\" → \"18\" still is, so a\n * sum that comes out positive still takes the chip with it; \"-20+50\" → \"7\" is not,\n * and \"-20+\" evaluates to nothing at all.\n */\nfunction isResultOf(previous: string, text: string, settle: (text: string) => string): boolean {\n const n = evaluateExpression(previous);\n // Compared settled, since the field rounds what the calculator hands it.\n return n !== null && settle(formatResult(n)) === text.trim();\n}\n\nexport function AmountInput(props: AmountInputProps) {\n const { ref, value, onChange, currency, onCurrencyChange, placeholder, label, className, inputClassName, id, \"aria-label\": ariaLabel, \"aria-required\": ariaRequired, autoFocus, tone = \"neutral\", negative = false, onNegativeChange, align = \"start\", labels, currencyNames, currencyOptions, digits: digitsProp, min, max, onCommit, disabledReasonDisplay } = props;\n const generatedId = useId();\n const fieldId = id ?? generatedId;\n // The sign is DISPLAYED, not stored (live #201 rework). `value` is the\n // magnitude; the minus is glued on here and taken off again in handleText, so\n // the two can never disagree and the caller can never double-apply it. Only in\n // front of a bare amount: an expression being typed (\"12-30\") owns its own\n // minus and has to survive keystroke-for-keystroke.\n const signOwned = onNegativeChange !== undefined;\n // The sign is DRAWN whenever `negative` says so, handler or not (keksdose live #366):\n // a read-only or disabled total (\"−5.18\") used to need a dummy `onNegativeChange`\n // just to show its minus. Without the handler there is nothing to toggle — a typed\n // minus simply stays in the text, as in any field without a direction control.\n const shown = negative && isBareAmount(value) ? `-${value}` : value;\n // What the user SEES is `shown` with the locale's decimal mark (kastlan 40: a\n // fr-CH user typed \"1,5\" and watched it turn into \"1.5\" under their fingers). Display only — `value`, `onChange`, the sign split and the settle all\n // stay dot-decimal, because that string is the contract: RhfMoneyField's\n // `parseAmount`, every app's `Number(value)` and the calculator's evaluator read\n // it. The way back is free: {@link sanitizeLive} already folds a typed \",\" into\n // \".\", so a comma typed, pasted or tapped on the numpad lands as a dot.\n //\n // One character for one character, and no digit grouping: the caret stays where\n // the typist put it, and \"1.000\" cannot be read back as a thousand in one locale\n // and as one in the other. de-CH and it-CH are \".\" — CLDR says so, and this\n // follows `Intl` rather than a guess about a country. The field's own `locale`,\n // else the provider's (0.34, DUP-10).\n const locale = useKitLocale(props.locale);\n // Decimals the figure settles to: `digits`, else the currency's minor unit.\n const digits = digitsProp ?? currencyMinorDigits(currency);\n const settle = (text: string) => settleAmount(text, digits, min, max);\n // The engine NumberInput shares (DUP-10): the lock (focusable, `readOnly`, nothing\n // changes or settles — see `disabledReason`), the hint — a FieldHint is its words at\n // Large and on touch, §4 — the error, the description's order, the typist's draft\n // in the locale's mark, the calculator and the numpad. On a phone the OS keyboard\n // is suppressed for the numpad sheet (feedback #334), and the display shape keys off\n // the SAME phone layout, so the two can never drift apart on the breakpoint.\n //\n // A value the HOST set (not one typed here) is shown at the currency's minor unit\n // while the field is not being edited: an FX estimate of 93.4213 showed four\n // decimals (keksdose live #356). Display only — `value` is the host's, untouched.\n const numeric = useNumericField(props, {\n settle,\n display: (focused) => (focused ? shown : settleAmount(shown, digits)),\n });\n const { lock, locked, off, isMobile, asDisplay, showCalc, showNumpad, setFocused, mark, draft, setDraft, derived } =\n numeric;\n const invalid = numeric.isInvalid;\n // A locked chip is a read-out: picking a currency is a change too.\n const editable = !!onCurrencyChange && !locked;\n // Room for the trailing controls — the calculator and the currency chip — once\n // measured, as NumberInput reserves its own (keksdose live #367, DUP-10): their\n // width, the track's `end-1` inset in the boxed shape, and NumberInput's 4px gap.\n // Before that (the first paint, and wherever there is no layout) the classes below\n // stand in. showCalc is always false on a phone, so the display shape only has to\n // clear the chip, by its own width rather than the boxed shape's room (feedback #430\n // rework: on a 375px screen 16px decide between a readable figure and a clipped one).\n const hasEnd = showCalc || Boolean(currency);\n const [endRef, endWidth] = useEndWidth(hasEnd);\n const trailingRoom = asDisplay\n ? currency ? (editable ? \"pe-16\" : \"pe-12\") : undefined\n : showCalc\n ? currency ? (editable ? \"pe-24\" : \"pe-16\") : \"pe-10\"\n : currency ? (editable ? \"pe-20\" : \"pe-14\") : undefined;\n const innerRef = useRef<HTMLInputElement>(null);\n const setRefs = useMemo(() => mergeRefs(innerRef, ref), [ref]);\n // While the typist is typing, the field shows THEIR text (\"1.234,5\" stays exactly\n // that), not the value re-spelt: re-spelling turned a grouping \".\" into a \",\" under\n // their fingers, after which no rule could tell the two apart (keksdose G1). The\n // draft only while it says what the field holds AND holds nothing the live\n // sanitising would drop — a letter, a second decimal point (see `showsTyped`); a\n // value set from outside, a calculator result or a commit shows the derived spelling.\n const display =\n draft === null\n ? derived\n : (numeric.typedFor(shown) ??\n // The caller owns the sign: the typed \"12\" is shown as the \"-12\" it is.\n (signOwned && negative && shown.startsWith(\"-\") && numeric.typedFor(shown.slice(1)) !== null\n ? `-${draft}`\n : derived));\n // Every route into the field — typing, the numpad sheet, the desktop\n // calculator, the blur/Enter commit — funnels through here, so the split is\n // written once and the four entry paths cannot drift.\n //\n // `previous` is the text this one REPLACES, and it is what says whether a bare\n // figure arriving here is an answer or a magnitude. For the three paths that\n // edit the field's own text that is simply what the field was showing; the\n // desktop calculator keeps its expression in a popover the field never sees,\n // so it passes it in (see `CalculatorButton`'s `onChange`).\n const handleText = (raw: string, previous: string = shown) => {\n const text = sanitizeLive(raw);\n if (!signOwned) {\n onChange(text);\n return;\n }\n const { sign, rest } = splitLeadingSign(text);\n if (sign) {\n // SET, not flip: re-reporting the same sign on every keystroke is a no-op.\n onNegativeChange(sign === \"-\");\n } else if (looksLikeExpression(previous) && isBareAmount(text) && isResultOf(previous, text, settle)) {\n // A calculation that just RESOLVED states its sign in both directions. The\n // figure it ran on is the SIGNED one the user could see — \"-12+30\" is +18 —\n // so a positive result has to move the chip too, or the field would answer\n // that sum with \"-18\" and contradict the arithmetic it just showed.\n //\n // Only when it really resolved, though: see {@link isResultOf}. A bare figure\n // arriving on top of an expression is far more often a replacement than an\n // answer, and reading it as an answer flips the direction of money.\n onNegativeChange(false);\n }\n onChange(sign ? rest : text);\n };\n const commit = () => {\n setDraft(null);\n const settled = settle(commitExpression(shown));\n handleText(settled);\n onCommit?.(settled);\n };\n const typed = (raw: string) => handleText(numeric.read(raw));\n // The currency list is CurrencySelect's (round 0.34 Wave 2, DUP-4) on the picker\n // family's core: the chip was a hand copy of it and had drifted (no wrap at Large, no\n // `options` subset, no check mark, no listbox id, no #308 width), and both were plain\n // buttons under a listbox promise. `filter: false`: the list filters its own rows.\n const core = useComboboxCore<string>({ filter: false, backCloses: true });\n // `chipRef`: the chip the currency list hangs off (Keksdose dev#548: portalled, so the\n // panel needs a real trigger rect rather than a relative parent). It is also where\n // Escape and a pick hand focus back (round 0.34 BUG-1): Safari does not focus a\n // clicked button, so \"whatever held focus\" is not the chip.\n const { open, setOpen, closeToTrigger, triggerRef: chipRef } = core;\n const listboxId = `${generatedId}-currencies`;\n // A lock arriving while the list is up closes it, as on CountrySelect — adjusted\n // while rendering, so no frame shows a list that can no longer be used.\n if (!editable && open) setOpen(false);\n\n // The currency chip's strings are the `currency` namespace — the same two\n // `CurrencySelect` resolves, under this component's older key names. The\n // calculator and the numpad resolve `calculator` themselves; `labels.calculator`\n // and `labels.pad` are handed straight through to them as overrides.\n // The two strings read out first, so the memo depends on them and not on the\n // caller's `labels` object (a new one every render) — what the React Compiler infers.\n const currencyLabel = labels?.currency;\n const currencySearchLabel = labels?.currencySearch;\n const currencyOverrides = useMemo(() => {\n const out: Partial<CurrencyLabels> = {};\n if (currencyLabel !== undefined) out.currency = currencyLabel;\n if (currencySearchLabel !== undefined) out.search = currencySearchLabel;\n return out;\n }, [currencyLabel, currencySearchLabel]);\n const currencyText = useKitLabels(\"currency\", DEFAULT_CURRENCY_LABELS, currencyOverrides);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n\n const selected = getCurrency(currency);\n\n return (\n <div className={cn(\"w-full\", className)}>\n {/* Inner relative box holds the input + its absolutely-positioned\n trailing controls (calculator / currency), so those stay anchored to\n the INPUT even when the mobile math bar is rendered below (#334). It is\n the kit's FloatingField, which draws the label — floating, or sr-only in\n the display shape — and puts a FieldHint on the label's line, as it does\n for NumberInput. */}\n {/* Locked, the field sits in the reason's frame (LockedReason: the line, or the\n Tooltip), as NumberInput does; the root above keeps `className`, so nothing\n reflows. */}\n <LockedOrNot lock={lock} display={disabledReasonDisplay}>\n <FloatingField\n className=\"w-full\"\n htmlFor={fieldId}\n label={label}\n srOnlyLabel={asDisplay}\n hint={numeric.hint.labelHint}\n >\n <input\n ref={setRefs}\n id={fieldId}\n aria-label={ariaLabel}\n type=\"text\"\n // eslint-disable-next-line jsx-a11y/no-autofocus -- opt-in per call site (feedback #70)\n autoFocus={autoFocus}\n // On mobile suppress the OS keyboard so our numpad sheet owns entry; the\n // field keeps focus/caret. Desktop keeps the native decimal keypad.\n inputMode={isMobile ? \"none\" : \"decimal\"}\n autoComplete=\"off\"\n disabled={off}\n // Locked: focusable, `readOnly`, `aria-disabled` — see `disabledReason`.\n readOnly={locked || undefined}\n aria-disabled={locked || undefined}\n // The floating-label pattern needs a blank placeholder for its\n // peer-placeholder-shown trick. The display shape has no floating label,\n // so it spends the placeholder on what an empty borderless form actually\n // needs: a zero to aim at, at full size. The caller supplies it because\n // only the app knows the locale's decimal separator.\n placeholder={asDisplay ? (placeholder ?? \"0\") : label !== undefined ? \" \" : placeholder}\n value={display}\n onChange={(e) => {\n if (!locked) typed(e.target.value);\n }}\n onFocus={() => setFocused(true)}\n onBlur={() => {\n // Nothing settles under the lock — not even a draft typed before it arrived,\n // which stays in the field for when it lifts.\n if (!locked) commit();\n setFocused(false);\n }}\n onKeyDown={(e) => {\n if (e.key !== \"Enter\") return;\n // Locked: Enter submits no form and settles nothing.\n if (locked) e.preventDefault();\n else commit();\n }}\n className={cn(\n // With a label, use the floating-input padding (pt-4 pb-1) like every\n // other labelled field so the value sits BELOW the floated label rather\n // than vertically centred where it overlaps the resting label\n // (feedback #314). FLOATING_INPUT_CLASS already bundles the peer +\n // transparent-placeholder bits.\n asDisplay ? DISPLAY_INPUT_CLASS : label !== undefined ? FLOATING_INPUT_CLASS : FIELD_BASE,\n // The plain end padding, with nothing trailing: cosmetic, so the caller's\n // to change (keksdose 0.24 — a symmetric `px-2` in a budget cell).\n !trailingRoom && (asDisplay ? \"pe-0\" : \"pe-3\"),\n // Centred display shape: centre in what is LEFT of the currency chip.\n // `text-center` alone would centre the figure in the whole box, i.e.\n // partly underneath the chip; the padding below is what takes the chip\n // out of the centring, and nothing is added at the start because a\n // mirrored reservation would spend the width twice (see `align`).\n asDisplay && align === \"center\" && \"text-center\",\n // Over the base class's own text colour.\n TONE_CLASS[tone],\n // The caller's, over all of the above — base, plain padding, alignment and\n // tone — and under only what the field needs to work (see `inputClassName`).\n inputClassName,\n // Room for the trailing controls, after the caller's so a `px-2` cannot run\n // the digits under the chip (NumberInput's order, which reserves its unit\n // and calculator the same way).\n trailingRoom,\n invalid && FIELD_INVALID,\n locked && \"cursor-not-allowed\",\n )}\n // The classes above are the fallback; once measured the field reserves exactly\n // its trailing controls' width, inline so it wins over every class (see\n // `trailingRoom`).\n style={hasEnd && endWidth != null ? { paddingInlineEnd: endWidth + (asDisplay ? 4 : 8) } : undefined}\n aria-invalid={invalid || undefined}\n aria-describedby={numeric.describedBy}\n aria-required={ariaRequired}\n />\n {/* The label is FloatingField's, after these children (the floating trick\n reads the input through `peer-*`). The display shape drops it VISUALLY,\n not from the accessibility tree: a 36px tinted figure at the top of a\n transaction form is legibly the amount, but a screen reader still needs\n the name, and callers that pass `label` without `aria-label` would\n otherwise be left with none. */}\n {/* Trailing controls share one flex track so the calculator icon and the\n currency suffix/picker sit side by side without overlapping. */}\n {/* The display shape has no box to inset from, so the controls align to the\n figure's own end edge and its baseline strip (`bottom-1`, matching the\n input's pb-1) instead of floating inside a field. */}\n <div\n // Measured for the input's end padding (see `trailingRoom`).\n ref={endRef}\n className={cn(\n \"absolute flex items-center gap-0.5\",\n // Logical `end-*`: the chip and the calculator are the figure's trailing\n // controls, so a right-to-left form carries them on the left.\n asDisplay ? \"bottom-1 end-0\" : \"inset-y-1 end-1\",\n )}\n >\n {showCalc && (\n <CalculatorButton\n // The mark the field shows but not the typist's grouping: the evaluator\n // reads either mark, and \"1.234,5\" is two of them. The result comes back\n // dot-decimal.\n value={derived}\n onChange={(result, expression) => {\n setDraft(null);\n const settled = settle(result);\n handleText(settled, expression);\n onCommit?.(settled);\n }}\n className=\"px-1.5\"\n aria-label={labels?.calculatorTrigger}\n labels={labels?.calculator}\n />\n )}\n {currency && !editable && (\n <span\n aria-hidden\n className={cn(\n \"pointer-events-none px-1 font-medium text-muted\",\n // A text-xs chip next to a 36px figure reads as a stray footnote.\n asDisplay ? \"text-sm\" : \"text-xs\",\n )}\n >\n {currency}\n </span>\n )}\n {currency && editable && (\n <button\n ref={chipRef}\n type=\"button\"\n onClick={() => setOpen((v) => !v)}\n // A button that opens a listbox, and says so: the chip is a control inside\n // a text field, not a field of its own, so not a combobox.\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-controls={listboxId}\n aria-label={\n // \"Currency: EUR\" through `common.fieldValue`, not a template literal:\n // the colon-and-space is punctuation a translation has to own.\n selected ? common.fieldValue(currencyText.currency, selected.code) : currencyText.currency\n }\n className={cn(\n \"flex items-center gap-1 rounded px-2 py-1 font-medium text-secondary hover:bg-hover\",\n asDisplay ? \"text-sm\" : \"text-xs\",\n )}\n >\n {currency}\n <ChevronDown className=\"size-3\" />\n </button>\n )}\n </div>\n {editable && (\n <CurrencyOptionList\n core={core}\n listboxId={listboxId}\n value={currency}\n codes={currencyOptions}\n currencyNames={currencyNames}\n locale={locale}\n searchPlaceholder={currencyText.search}\n // The phone sheet's title: what the chip is for.\n sheetTitle={currencyText.currency}\n onChoose={(code) => {\n onCurrencyChange?.(code);\n closeToTrigger();\n }}\n />\n )}\n </FloatingField>\n </LockedOrNot>\n {/* Outside the relative box, as NumberInput's caption is: the trailing\n controls are `inset-y-1` in it and would stretch down over a second line. */}\n <FieldCaption parts={numeric.hint} />\n {/* The news after the standing advice, as Select orders them. The root is\n already a steady box, so the message coming and going remounts nothing. */}\n {numeric.errorEl}\n {showNumpad && (\n <NumberPadSheet\n // Localised like the field it mirrors. Keys are typing, read by the\n // locale's marks like the keyboard's; \"=\" is a result, dot-decimal.\n value={display}\n onChange={typed}\n onResult={(result) => {\n setDraft(null);\n const settled = settle(result);\n handleText(settled);\n onCommit?.(settled);\n }}\n onDone={() => innerRef.current?.blur()}\n label={label}\n labels={labels?.pad}\n decimalMark={mark}\n />\n )}\n </div>\n );\n}\n\n/** The field in its lock's {@link LockedReason} while locked, as it is otherwise. A\n * component rather than a ternary so the field's JSX is written once. `block w-full`:\n * the frame's box stands where the field's own did. */\nfunction LockedOrNot({\n lock,\n display,\n children,\n}: {\n lock: LockReason;\n display: DisabledReasonDisplay | undefined;\n children: ReactNode;\n}) {\n if (!lock.locked) return <>{children}</>;\n return (\n <LockedReason lock={lock} display={display} className=\"block w-full\">\n {children}\n </LockedReason>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/amount-input.tsx"],"sourcesContent":["import { useId, useMemo, useRef } from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport type { ReactNode, Ref } from \"react\";\nimport { CurrencyOptionList, getCurrency } from \"./currency-option-list\";\nimport { useComboboxCore } from \"./combobox-core\";\nimport {\n FIELD_BASE,\n FIELD_DISPLAY,\n FIELD_INVALID,\n FLOATING_INPUT_CLASS,\n FloatingField,\n} from \"./field-base\";\nimport { cn } from \"../lib/cn\";\nimport { currencyMinorDigits } from \"../lib/format\";\nimport { roundHalfAway } from \"../lib/round\";\nimport { CalculatorButton, type CalculatorButtonLabels } from \"./calculator\";\nimport { NumberPadSheet, type NumberPadSheetLabels } from \"./numpad-sheet\";\nimport {\n DEFAULT_COMMON_LABELS,\n DEFAULT_CURRENCY_LABELS,\n useKitLabels,\n useKitLocale,\n type CurrencyLabels,\n} from \"../i18n/kit-labels\";\nimport {\n commitExpression,\n evaluateExpression,\n formatResult,\n isBareAmount,\n looksLikeExpression,\n sanitizeLive,\n splitLeadingSign,\n} from \"../lib/calc\";\nimport {\n FieldCaption,\n LockedReason,\n mergeRefs,\n type DisabledReasonDisplay,\n type FieldHelpProps,\n type LockReason,\n} from \"./field-parts\";\nimport { useEndWidth, useNumericField } from \"./numeric-field\";\nimport type { CommitScope } from \"./write-lock\";\nimport { dataAttributes, type DataAttributes } from \"../lib/data-attributes\";\n\n/** {@link AmountInput}'s props — exported since 0.34 (refactor audit 2026-10, API-9): a\n * wrapper typed itself off `ComponentProps<typeof AmountInput>` until then. */\nexport interface AmountInputProps extends FieldHelpProps, DataAttributes {\n value: string;\n onChange: (value: string) => void;\n currency?: string;\n onCurrencyChange?: (code: string) => void;\n placeholder?: string;\n label?: ReactNode;\n disabled?: boolean;\n /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /**\n * What is wrong with the amount, in the caller's own words — {@link Input}'s `error`\n * (keksdose K4: \"exceeds the outstanding balance\" was a loose `<p>` beside the field\n * that nothing pointed at). Rendered under the field, after a text `hint`; merged into\n * `aria-describedby` after the caller's ids and the hint's; implies `invalid`, so the\n * field paints as well as announces. `null`, `false` and `\"\"` are no message.\n * {@link MoneyField} takes it too.\n */\n error?: ReactNode;\n /**\n * Show the desktop calculator trigger (default `true`). `false` for an amount in an\n * ephemeral close-on-blur editor — a budget's assigned cell, a VAT cell (keksdose K6)\n * — where a click on the icon blurs the field and tears the editor down before the\n * popover can open. Typing a calculation (\"200+50\", then Enter) still evaluates, and\n * a phone keeps its numpad, exactly as {@link NumberInput}'s `calculator={false}`.\n */\n calculator?: boolean;\n /** The field's WRAPPER — width, a margin, a flex share. The `<input>` is\n * `inputClassName`'s. */\n className?: string;\n /**\n * Classes for the `<input>` itself — {@link NumberInput}'s and {@link Input}'s prop of\n * the same name, merged the same way: after everything the field sets for LOOKS — the\n * base classes, the plain end padding, the display shape's centring and the `tone`\n * colour — so it can restyle the figure (`text-end` for a money column whose decimal\n * points line up, `font-medium`, a tighter `py-1` or a symmetric `px-2` in a table\n * cell, `text-transparent` while a figure is being re-fetched), and before what the\n * field needs to WORK: the end padding the calculator and the currency chip reserve,\n * and the invalid border. Neither of those can be taken away by accident — a caller's\n * `px-2` keeps its start edge and the digits still stop short of the chip, and a\n * borderless cell's `border-transparent` still turns red when the value is wrong.\n *\n * keksdose G7: the invoice's VAT cell right-aligns its figure, and with only\n * `className` (the wrapper) it reached the element through `[&_input]:text-end` — a\n * descendant selector that depends on the field's inner markup, which the kit is free\n * to change, and that a reader of the call site has to decode. {@link MoneyField}\n * passes it through, and `RhfMoneyField` takes it too.\n *\n * keksdose 0.24: until then it sat BEFORE the plain end padding (`pe-3`) and the tone,\n * so the budget's assigned cell could not make its padding symmetric (`px-2` came out\n * `px-2 pe-3`) and the holdings price could not hide an outflow-toned figure while\n * re-fetching — both were back on `[&_input]:` selectors on the wrapper.\n */\n inputClassName?: string;\n id?: string;\n /** Names the field when it has no visible `label` (a table cell, the display shape). */\n \"aria-label\"?: string;\n /**\n * Set on the `<input>`, with `id` above. Declared so `Field`'s render-prop spread —\n * `{(ids) => <AmountInput {...ids} … />}` — is typed and reaches the input: the\n * hint and error are described, a required amount is announced as required, and\n * `aria-invalid` paints {@link FIELD_INVALID} the way `invalid` does.\n */\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: boolean | \"true\" | \"false\";\n \"aria-required\"?: boolean | \"true\" | \"false\";\n /**\n * BCP 47 tag for this field — {@link NumberInput}'s `locale` (0.34, DUP-10): the decimal\n * mark the field SHOWS and reads typing in (\"1,5\" for fr-CH, \"1.5\" for de-CH, as `Intl`\n * decides), and the language of the currency picker's search. Default the\n * `<UiKitProvider locale>`, else the runtime's. `value` and `onChange` stay\n * dot-decimal.\n */\n locale?: string;\n /** {@link NumberInput}'s `hint`, the same rule: a {@link FieldHint} \"?\" rides the\n * label's own line, beside the label (not at the far end, where the calculator and\n * the currency chip live); plain TEXT (a string or a number) is a caption UNDER\n * the field, attached through `aria-describedby` after the caller's own. A phone's\n * `variant=\"display\"` has no visible label, so a FieldHint is dropped there; a\n * caption still shows.\n *\n * Since 0.34 a FieldHint is that caption at Large and on a touch screen, as on\n * NumberInput since 0.32 (`useInlineHint`, docs/text-size-harmonization.md §4: a fact\n * a finger cannot hover open is printed). A `HelpPopover` — help, not a fact (§10.23)\n * — stays on the label line at every size. */\n hint?: ReactNode;\n /** Every user-facing string this component and the two it composes own, so a\n * translating host can supply its own. Each key optional, and each a per-field\n * override of the kit-wide translation in `<UiKitProvider labels>`: `currency`\n * and `currencySearch` of its `currency` namespace, the rest of `calculator`.\n * `currency` names the picker chip, composed with the selected code through\n * `common.fieldValue`; the rest are pass-through. */\n labels?: {\n currency?: string;\n currencySearch?: string;\n /** Names the calculator TRIGGER this component renders. `calculator` below\n * names the controls inside the popover it opens. */\n calculatorTrigger?: string;\n pad?: NumberPadSheetLabels;\n calculator?: CalculatorButtonLabels;\n };\n /** Translated currency names for the chip's picker, keyed by ISO code. The same\n * sparse override `CurrencySelect` takes, and for the same reason: the names in\n * `CURRENCIES` are shipped data, so without this they are the one English word\n * left in a translated row. Not part of `labels` because it is keyed by DATA\n * rather than by a fixed set of slots, and an app passes the same map to both\n * pickers. */\n currencyNames?: Record<string, string>;\n /** Offer only these ISO codes in the chip's list, in this order — `CurrencySelect`'s\n * `options`, since the chip renders CurrencySelect's list (round 0.34 Wave 2, DUP-4).\n * Default every currency the kit knows. */\n currencyOptions?: string[];\n /** Focus the field on mount — on mobile this also opens the numpad sheet, so a\n * new-transaction form can jump straight to amount entry (feedback #70). */\n autoFocus?: boolean;\n /**\n * Tint the typed figure by what it will DO (Keksdose feedback #167).\n *\n * The direction is chosen with a toggle beside the field rather than typed, so\n * the digits alone say nothing about which way the money goes — on the one screen\n * where getting that backwards is the expensive mistake. Colour says it\n * continuously, and it costs no layout. It stays the MAIN indication (live #201\n * rework: *\"The main indication shall still be the color\"*); {@link negative}\n * renders the sign as a second, quieter signal in front of the figure.\n *\n * Never the ONLY signal: the toggle it mirrors is right there, labelled, so this\n * is reinforcement rather than the sole carrier of the meaning.\n */\n tone?: \"neutral\" | \"outflow\" | \"inflow\";\n /**\n * The SIGN of the figure, owned by the caller's direction control (Keksdose live\n * #201 rework: *\"Outflow should be negative and inflow positive … the sign shall\n * be displayed but not necessarily be typed in\"*).\n *\n * `value` stays the MAGNITUDE. The minus is glued on at render time and stripped\n * off again before anything is reported back, so the caller never sees its own\n * prefix echoed and no code path can end up applying it twice. That is the whole\n * cure: there is exactly ONE place the sign lives, and it is not the text.\n *\n * Because it is rendered rather than stored it cannot be deleted (the next render\n * puts it back), a select-all-and-retype keeps the direction, and the caret needs\n * no minding.\n *\n * Since 0.15.5 it is drawn on its own too, for a read-only or disabled signed figure\n * (an \"Items total\" of −5.18): no `onNegativeChange` needed just to show the minus.\n */\n negative?: boolean;\n /**\n * A sign the user TYPED, or a calculation that resolved to a signed number.\n *\n * SET, never flip. A \"-\" means *\"this is an outflow\"*, so typing it twice says the\n * same thing twice — which is exactly what the rework asks for (*\"Subtracting\n * values twice resulting in inflow -> outflow and back to inflow seems not\n * reasonable\"*). \"+\" says the other direction.\n *\n * Its presence is what ARMS the whole sign behaviour. A field with no direction\n * control — an account's starting balance, a loan payment, a holding's cost —\n * passes neither prop and keeps a text that is the whole truth, minus included.\n */\n onNegativeChange?: (negative: boolean) => void;\n /**\n * How the field presents itself ON A PHONE (Keksdose feedback #176).\n *\n * `\"field\"` (default) is the labelled, bordered input used everywhere else.\n *\n * `\"display\"` keeps every mechanic — the same `<input>`, the same numpad sheet,\n * the same currency picker, the same commit/blur contract — and only drops the\n * chrome: no border, no fill, no floating label, the figure set at display size.\n * On a form whose whole point is one number, a bordered `text-sm` box draws the\n * amount at the same weight as the memo beside it; this makes it the headline\n * instead. It stays fully editable — the underline and the caret are what say so.\n *\n * Phone-only by construction: it keys off the same breakpoint as the numpad, so\n * from `md` up the field is byte-for-byte the normal one and a desktop grid keeps\n * its column rhythm.\n */\n variant?: \"field\" | \"display\";\n /**\n * Where the figure sits in its row, for `variant=\"display\"` only (Keksdose\n * feedback #176 rework).\n *\n * `\"start\"` (default) keeps it flush left, in the reading column the fields\n * below it share — and it is what the entry forms use, because a figure that\n * starts at a known edge is legible at any width: when the value outgrows the\n * field, the digits that scroll out of sight are the trailing ones, not the\n * magnitude (Keksdose feedback #430 rework).\n *\n * `\"center\"` makes it a centred headline. It centres in the width that REMAINS\n * after the trailing currency control, not in the whole box: the control is\n * pinned to the right edge, so a plain `text-center` would centre the figure\n * under it and the last digits would sit behind the chip. This used to mirror\n * the control's reservation onto the left instead, which centred correctly but\n * spent twice the control's width on padding — fatal on a 375px screen, where\n * it left a 36px figure 16px to live in.\n */\n align?: \"start\" | \"center\";\n /**\n * Decimals the committed amount is rounded to (kastlan 0.12: \"12.345\" and a\n * calculator's \"100/3\" reached a `Numeric(10,2)` column unrounded and failed with a\n * 422). Default: the currency's minor unit, from `Intl` (CHF and EUR 2, JPY 0). With\n * no `currency`, nothing is rounded unless this is set. Rounding is half away from\n * zero (12.345 → 12.35, -12.345 → -12.35) and happens when the figure settles: on\n * blur, on Enter, and on every result the calculator writes back. Keystrokes are left\n * alone, so \"12.3\" can still become \"12.34\".\n *\n * ⚠️ **A unit price or a rate needs `digits`.** The default is right for an amount of\n * money and silently wrong for a price per unit: a fund's NAV of 123.4567 or fuel at\n * 1.789 €/l is stored as 123.46 / 1.79, and no test fails (keksdose G2, a\n * `Numeric(18,6)` holding price and a `Numeric(18,4)` unit price). Pass the column's\n * scale: `digits={4}`, `digits={6}`.\n *\n * **Set `digits` for any sub-currency scale** — not only a price: any field whose\n * column holds more decimals than the currency's minor unit. A split line stored at\n * the ledger's 4dp (`Numeric(18,4)`) and loaded as 12.3456 is SHOWN as 12.35, and\n * committed as 12.35 as soon as the user tabs through it (blur settles) — so the form\n * no longer equals what the server holds: the lines stop summing to the 4dp total,\n * and Save is held (keksdose live #240). With a `currency`, every commit rounds to that currency's\n * minor unit unless `digits` says otherwise: pass the column's scale (`digits={4}`).\n */\n digits?: number;\n /** The smallest amount the field settles on. A lower figure is raised to it when it\n * settles, like `digits`. Compared with the signed figure. */\n min?: number;\n /** The largest amount the field settles on (a deposit release capped at the\n * balance). A higher figure is lowered to it when it settles. */\n max?: number;\n /**\n * The SETTLED figure, dot-decimal, sign included — fired on blur, on Enter, on the\n * numpad's \"=\" and on every result the calculator writes back, never on a keystroke\n * (\"1200+\" is not a figure yet). `onChange` still reports every keystroke; this is\n * for a host that stores a number and must not see drafts (kastlan 52; see\n * {@link MoneyField}, which is built on it).\n */\n onCommit?: (value: string) => void;\n /**\n * Why the amount cannot be changed — {@link NumberInput}'s `disabledReason` (0.34,\n * round 0.34 C2), for an amount that SAVES itself: a budget row's assigned cell, a\n * price committed on blur. `disabled` takes the field out of the tab order, so the\n * reason never reached a keyboard.\n *\n * With a reason the field stays in the tab order and shows its figure, `readOnly` (the\n * settled grey FIELD_BASE gives a `[readonly]` field) and `aria-disabled`. Nothing\n * changes the amount: the calculator and the phone's numpad are not offered, the\n * currency chip is a read-out rather than a picker, and neither blur nor Enter\n * settles — so `onChange`, `onCommit`, `onNegativeChange` and `onCurrencyChange` are\n * never called. Enter is swallowed, so the field does not submit the form around it.\n * The reason is a line under the field at Large and on a touch screen, else in the kit\n * {@link Tooltip} (`disabledReasonDisplay`), and on the input's `aria-describedby`,\n * between the caption and the error. Wins over `disabled`. {@link MoneyField} and\n * `RhfMoneyField` take it too.\n */\n disabledReason?: ReactNode;\n /**\n * Where the reason shows — {@link Button}'s `disabledReasonDisplay`: by default a line\n * under the field at Large and on a touch screen, the tooltip otherwise (§4 \"No fact\n * only in a tooltip\"; DUP-3, refactor audit 2026-10).\n */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * This amount COMMITS — {@link Button}'s `commit`, NumberInput's: under a locked\n * {@link WriteLockProvider} it is locked the `disabledReason` way with the lock's reason\n * (which wins over its own). An amount that only reaches the server through its form's\n * Save leaves this off and stays editable under the lock; the Save takes `commit`. No\n * provider, or an unlocked one: no effect.\n */\n commit?: CommitScope;\n /** The `<input>`. A React 19 ref prop since 0.34 (MOD-4, refactor audit 2026-10); it\n * was `forwardRef`. */\n ref?: Ref<HTMLInputElement>;\n}\n\n\n/**\n * A settled figure, rounded to `digits` (half away from zero) and clamped to\n * `min`/`max`. Only a plain number is touched. A half-typed draft or an expression\n * comes back as it was. Rounding is `lib/round.ts`'s, shared with `roundToCurrency`\n * (BUG-11: this copy blanked \"0.0000001\" and a 22-digit figure on blur).\n */\nfunction settleAmount(text: string, digits: number | undefined, min?: number, max?: number): string {\n const t = text.trim();\n if (!/^-?\\d*\\.?\\d*$/.test(t) || !/\\d/.test(t)) return text;\n let n = Number(t);\n if (!Number.isFinite(n)) return text;\n if (digits !== undefined) n = roundHalfAway(n, digits);\n if (min !== undefined && n < min) n = min;\n if (max !== undefined && n > max) n = max;\n const out = formatResult(n);\n // Unchanged figures keep their own spelling (\"12.50\" stays \"12.50\").\n return Number(out) === Number(t) ? text : out;\n}\n\n// The locale's decimal mark and the grouping parse (kastlan 40, keksdose G1) live in\n// `lib/decimal-marks.ts`, read through the engine NumberInput and this field share\n// (`numeric-field.ts`, DUP-10).\n\n// The consuming app's ONE money palette (`--money-expense` / `--money-income`),\n// not a bespoke rose/emerald pairing: a figure being typed has to wear the same\n// colour the same figure will wear once it is a row in the table behind the form\n// (Keksdose dev#434). The utilities are unlayered and theme-aware, so no `dark:`\n// variant is needed and they win over the field's own text colour regardless of\n// where `cn` puts them.\nconst TONE_CLASS: Record<\"neutral\" | \"outflow\" | \"inflow\", string> = {\n neutral: \"\",\n outflow: \"text-money-neg\",\n inflow: \"text-money-pos\",\n};\n\n// The shared chrome-less treatment at money size. `tabular-nums` so digits don't\n// reflow the figure as they are typed; FIELD_DISPLAY's own muted placeholder\n// colour is what keeps an EMPTY form from painting its zero in the direction tint\n// (`tone` is computed from 0, which reads as an inflow).\nconst DISPLAY_INPUT_CLASS = cn(\n FIELD_DISPLAY,\n \"text-4xl font-semibold leading-tight tracking-tight tabular-nums\",\n);\n\n/**\n * Is `text` the ANSWER to `previous`, or something typed OVER it?\n *\n * \"The old text was a sum and the new one is bare\" cannot tell those apart, and both\n * gestures are ordinary: select-all-and-retype after a typo, or backspacing \"-20+\"\n * back down to a fresh figure. Read as an answer, they move the direction chip — so\n * an outflow of 7 quietly booked +7.00 (live #201 rework).\n *\n * The question that does separate them is arithmetic, not shape: evaluate what the\n * text replaced and see whether this IS that number. \"-12+30\" → \"18\" still is, so a\n * sum that comes out positive still takes the chip with it; \"-20+50\" → \"7\" is not,\n * and \"-20+\" evaluates to nothing at all.\n */\nfunction isResultOf(previous: string, text: string, settle: (text: string) => string): boolean {\n const n = evaluateExpression(previous);\n // Compared settled, since the field rounds what the calculator hands it.\n return n !== null && settle(formatResult(n)) === text.trim();\n}\n\nexport function AmountInput(props: AmountInputProps) {\n const { ref, value, onChange, currency, onCurrencyChange, placeholder, label, className, inputClassName, id, \"aria-label\": ariaLabel, \"aria-required\": ariaRequired, autoFocus, tone = \"neutral\", negative = false, onNegativeChange, align = \"start\", labels, currencyNames, currencyOptions, digits: digitsProp, min, max, onCommit, disabledReasonDisplay } = props;\n const generatedId = useId();\n const fieldId = id ?? generatedId;\n // The sign is DISPLAYED, not stored (live #201 rework). `value` is the\n // magnitude; the minus is glued on here and taken off again in handleText, so\n // the two can never disagree and the caller can never double-apply it. Only in\n // front of a bare amount: an expression being typed (\"12-30\") owns its own\n // minus and has to survive keystroke-for-keystroke.\n const signOwned = onNegativeChange !== undefined;\n // The sign is DRAWN whenever `negative` says so, handler or not (keksdose live #366):\n // a read-only or disabled total (\"−5.18\") used to need a dummy `onNegativeChange`\n // just to show its minus. Without the handler there is nothing to toggle — a typed\n // minus simply stays in the text, as in any field without a direction control.\n const shown = negative && isBareAmount(value) ? `-${value}` : value;\n // What the user SEES is `shown` with the locale's decimal mark (kastlan 40: a\n // fr-CH user typed \"1,5\" and watched it turn into \"1.5\" under their fingers). Display only — `value`, `onChange`, the sign split and the settle all\n // stay dot-decimal, because that string is the contract: RhfMoneyField's\n // `parseAmount`, every app's `Number(value)` and the calculator's evaluator read\n // it. The way back is free: {@link sanitizeLive} already folds a typed \",\" into\n // \".\", so a comma typed, pasted or tapped on the numpad lands as a dot.\n //\n // One character for one character, and no digit grouping: the caret stays where\n // the typist put it, and \"1.000\" cannot be read back as a thousand in one locale\n // and as one in the other. de-CH and it-CH are \".\" — CLDR says so, and this\n // follows `Intl` rather than a guess about a country. The field's own `locale`,\n // else the provider's (0.34, DUP-10).\n const locale = useKitLocale(props.locale);\n // Decimals the figure settles to: `digits`, else the currency's minor unit.\n const digits = digitsProp ?? currencyMinorDigits(currency);\n const settle = (text: string) => settleAmount(text, digits, min, max);\n // The engine NumberInput shares (DUP-10): the lock (focusable, `readOnly`, nothing\n // changes or settles — see `disabledReason`), the hint — a FieldHint is its words at\n // Large and on touch, §4 — the error, the description's order, the typist's draft\n // in the locale's mark, the calculator and the numpad. On a phone the OS keyboard\n // is suppressed for the numpad sheet (feedback #334), and the display shape keys off\n // the SAME phone layout, so the two can never drift apart on the breakpoint.\n //\n // A value the HOST set (not one typed here) is shown at the currency's minor unit\n // while the field is not being edited: an FX estimate of 93.4213 showed four\n // decimals (keksdose live #356). Display only — `value` is the host's, untouched.\n const numeric = useNumericField(props, {\n settle,\n display: (focused) => (focused ? shown : settleAmount(shown, digits)),\n });\n const { lock, locked, off, isMobile, asDisplay, showCalc, showNumpad, setFocused, mark, draft, setDraft, derived } =\n numeric;\n const invalid = numeric.isInvalid;\n // A locked or disabled chip is a read-out: picking a currency is a change too.\n const editable = !!onCurrencyChange && !locked && !off;\n // Room for the trailing controls — the calculator and the currency chip — once\n // measured, as NumberInput reserves its own (keksdose live #367, DUP-10): their\n // width, the track's `end-1` inset in the boxed shape, and NumberInput's 4px gap.\n // Before that (the first paint, and wherever there is no layout) the classes below\n // stand in. showCalc is always false on a phone, so the display shape only has to\n // clear the chip, by its own width rather than the boxed shape's room (feedback #430\n // rework: on a 375px screen 16px decide between a readable figure and a clipped one).\n const hasEnd = showCalc || Boolean(currency);\n const [endRef, endWidth] = useEndWidth(hasEnd);\n const trailingRoom = asDisplay\n ? currency ? (editable ? \"pe-16\" : \"pe-12\") : undefined\n : showCalc\n ? currency ? (editable ? \"pe-24\" : \"pe-16\") : \"pe-10\"\n : currency ? (editable ? \"pe-20\" : \"pe-14\") : undefined;\n const innerRef = useRef<HTMLInputElement>(null);\n const setRefs = useMemo(() => mergeRefs(innerRef, ref), [ref]);\n // While the typist is typing, the field shows THEIR text (\"1.234,5\" stays exactly\n // that), not the value re-spelt: re-spelling turned a grouping \".\" into a \",\" under\n // their fingers, after which no rule could tell the two apart (keksdose G1). The\n // draft only while it says what the field holds AND holds nothing the live\n // sanitising would drop — a letter, a second decimal point (see `showsTyped`); a\n // value set from outside, a calculator result or a commit shows the derived spelling.\n const display =\n draft === null\n ? derived\n : (numeric.typedFor(shown) ??\n // The caller owns the sign: the typed \"12\" is shown as the \"-12\" it is.\n (signOwned && negative && shown.startsWith(\"-\") && numeric.typedFor(shown.slice(1)) !== null\n ? `-${draft}`\n : derived));\n // Every route into the field — typing, the numpad sheet, the desktop\n // calculator, the blur/Enter commit — funnels through here, so the split is\n // written once and the four entry paths cannot drift.\n //\n // `previous` is the text this one REPLACES, and it is what says whether a bare\n // figure arriving here is an answer or a magnitude. For the three paths that\n // edit the field's own text that is simply what the field was showing; the\n // desktop calculator keeps its expression in a popover the field never sees,\n // so it passes it in (see `CalculatorButton`'s `onChange`).\n const handleText = (raw: string, previous: string = shown) => {\n const text = sanitizeLive(raw);\n if (!signOwned) {\n onChange(text);\n return;\n }\n const { sign, rest } = splitLeadingSign(text);\n if (sign) {\n // SET, not flip: re-reporting the same sign on every keystroke is a no-op.\n onNegativeChange(sign === \"-\");\n } else if (looksLikeExpression(previous) && isBareAmount(text) && isResultOf(previous, text, settle)) {\n // A calculation that just RESOLVED states its sign in both directions. The\n // figure it ran on is the SIGNED one the user could see — \"-12+30\" is +18 —\n // so a positive result has to move the chip too, or the field would answer\n // that sum with \"-18\" and contradict the arithmetic it just showed.\n //\n // Only when it really resolved, though: see {@link isResultOf}. A bare figure\n // arriving on top of an expression is far more often a replacement than an\n // answer, and reading it as an answer flips the direction of money.\n onNegativeChange(false);\n }\n onChange(sign ? rest : text);\n };\n const commit = () => {\n setDraft(null);\n const settled = settle(commitExpression(shown));\n handleText(settled);\n onCommit?.(settled);\n };\n const typed = (raw: string) => handleText(numeric.read(raw));\n // The currency list is CurrencySelect's (round 0.34 Wave 2, DUP-4) on the picker\n // family's core: the chip was a hand copy of it and had drifted (no wrap at Large, no\n // `options` subset, no check mark, no listbox id, no #308 width), and both were plain\n // buttons under a listbox promise. `filter: false`: the list filters its own rows.\n const core = useComboboxCore<string>({ filter: false, backCloses: true });\n // `chipRef`: the chip the currency list hangs off (Keksdose dev#548: portalled, so the\n // panel needs a real trigger rect rather than a relative parent). It is also where\n // Escape and a pick hand focus back (round 0.34 BUG-1): Safari does not focus a\n // clicked button, so \"whatever held focus\" is not the chip.\n const { open, setOpen, closeToTrigger, triggerRef: chipRef } = core;\n const listboxId = `${generatedId}-currencies`;\n // A lock arriving while the list is up closes it, as on CountrySelect — adjusted\n // while rendering, so no frame shows a list that can no longer be used.\n if (!editable && open) setOpen(false);\n\n // The currency chip's strings are the `currency` namespace — the same two\n // `CurrencySelect` resolves, under this component's older key names. The\n // calculator and the numpad resolve `calculator` themselves; `labels.calculator`\n // and `labels.pad` are handed straight through to them as overrides.\n // The two strings read out first, so the memo depends on them and not on the\n // caller's `labels` object (a new one every render) — what the React Compiler infers.\n const currencyLabel = labels?.currency;\n const currencySearchLabel = labels?.currencySearch;\n const currencyOverrides = useMemo(() => {\n const out: Partial<CurrencyLabels> = {};\n if (currencyLabel !== undefined) out.currency = currencyLabel;\n if (currencySearchLabel !== undefined) out.search = currencySearchLabel;\n return out;\n }, [currencyLabel, currencySearchLabel]);\n const currencyText = useKitLabels(\"currency\", DEFAULT_CURRENCY_LABELS, currencyOverrides);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n\n const selected = getCurrency(currency);\n\n return (\n <div {...dataAttributes(props)} className={cn(\"w-full\", className)}>\n {/* Inner relative box holds the input + its absolutely-positioned\n trailing controls (calculator / currency), so those stay anchored to\n the INPUT even when the mobile math bar is rendered below (#334). It is\n the kit's FloatingField, which draws the label — floating, or sr-only in\n the display shape — and puts a FieldHint on the label's line, as it does\n for NumberInput. */}\n {/* Locked, the field sits in the reason's frame (LockedReason: the line, or the\n Tooltip), as NumberInput does; the root above keeps `className`, so nothing\n reflows. */}\n <LockedOrNot lock={lock} display={disabledReasonDisplay}>\n <FloatingField\n className=\"w-full\"\n htmlFor={fieldId}\n label={label}\n srOnlyLabel={asDisplay}\n hint={numeric.hint.labelHint}\n >\n <input\n ref={setRefs}\n id={fieldId}\n aria-label={ariaLabel}\n type=\"text\"\n // eslint-disable-next-line jsx-a11y/no-autofocus -- opt-in per call site (feedback #70)\n autoFocus={autoFocus}\n // On mobile suppress the OS keyboard so our numpad sheet owns entry; the\n // field keeps focus/caret. Desktop keeps the native decimal keypad.\n inputMode={isMobile ? \"none\" : \"decimal\"}\n autoComplete=\"off\"\n disabled={off}\n // Locked: focusable, `readOnly`, `aria-disabled` — see `disabledReason`.\n readOnly={locked || undefined}\n aria-disabled={locked || undefined}\n // The floating-label pattern needs a blank placeholder for its\n // peer-placeholder-shown trick. The display shape has no floating label,\n // so it spends the placeholder on what an empty borderless form actually\n // needs: a zero to aim at, at full size. The caller supplies it because\n // only the app knows the locale's decimal separator.\n placeholder={asDisplay ? (placeholder ?? \"0\") : label !== undefined ? \" \" : placeholder}\n value={display}\n onChange={(e) => {\n if (!locked) typed(e.target.value);\n }}\n onFocus={() => setFocused(true)}\n onBlur={() => {\n // Nothing settles under the lock — not even a draft typed before it arrived,\n // which stays in the field for when it lifts.\n if (!locked) commit();\n setFocused(false);\n }}\n onKeyDown={(e) => {\n if (e.key !== \"Enter\") return;\n // Locked: Enter submits no form and settles nothing.\n if (locked) e.preventDefault();\n else commit();\n }}\n className={cn(\n // With a label, use the floating-input padding (pt-4 pb-1) like every\n // other labelled field so the value sits BELOW the floated label rather\n // than vertically centred where it overlaps the resting label\n // (feedback #314). FLOATING_INPUT_CLASS already bundles the peer +\n // transparent-placeholder bits.\n asDisplay ? DISPLAY_INPUT_CLASS : label !== undefined ? FLOATING_INPUT_CLASS : FIELD_BASE,\n // The plain end padding, with nothing trailing: cosmetic, so the caller's\n // to change (keksdose 0.24 — a symmetric `px-2` in a budget cell).\n !trailingRoom && (asDisplay ? \"pe-0\" : \"pe-3\"),\n // Centred display shape: centre in what is LEFT of the currency chip.\n // `text-center` alone would centre the figure in the whole box, i.e.\n // partly underneath the chip; the padding below is what takes the chip\n // out of the centring, and nothing is added at the start because a\n // mirrored reservation would spend the width twice (see `align`).\n asDisplay && align === \"center\" && \"text-center\",\n // Over the base class's own text colour.\n TONE_CLASS[tone],\n // The caller's, over all of the above — base, plain padding, alignment and\n // tone — and under only what the field needs to work (see `inputClassName`).\n inputClassName,\n // Room for the trailing controls, after the caller's so a `px-2` cannot run\n // the digits under the chip (NumberInput's order, which reserves its unit\n // and calculator the same way).\n trailingRoom,\n invalid && FIELD_INVALID,\n locked && \"cursor-not-allowed\",\n )}\n // The classes above are the fallback; once measured the field reserves exactly\n // its trailing controls' width, inline so it wins over every class (see\n // `trailingRoom`).\n style={hasEnd && endWidth != null ? { paddingInlineEnd: endWidth + (asDisplay ? 4 : 8) } : undefined}\n aria-invalid={invalid || undefined}\n aria-describedby={numeric.describedBy}\n aria-required={ariaRequired}\n />\n {/* The label is FloatingField's, after these children (the floating trick\n reads the input through `peer-*`). The display shape drops it VISUALLY,\n not from the accessibility tree: a 36px tinted figure at the top of a\n transaction form is legibly the amount, but a screen reader still needs\n the name, and callers that pass `label` without `aria-label` would\n otherwise be left with none. */}\n {/* Trailing controls share one flex track so the calculator icon and the\n currency suffix/picker sit side by side without overlapping. */}\n {/* The display shape has no box to inset from, so the controls align to the\n figure's own end edge and its baseline strip (`bottom-1`, matching the\n input's pb-1) instead of floating inside a field. */}\n <div\n // Measured for the input's end padding (see `trailingRoom`).\n ref={endRef}\n className={cn(\n \"absolute flex items-center gap-0.5\",\n // Logical `end-*`: the chip and the calculator are the figure's trailing\n // controls, so a right-to-left form carries them on the left.\n asDisplay ? \"bottom-1 end-0\" : \"inset-y-1 end-1\",\n )}\n >\n {showCalc && (\n <CalculatorButton\n // The mark the field shows but not the typist's grouping: the evaluator\n // reads either mark, and \"1.234,5\" is two of them. The result comes back\n // dot-decimal.\n value={derived}\n onChange={(result, expression) => {\n setDraft(null);\n const settled = settle(result);\n handleText(settled, expression);\n onCommit?.(settled);\n }}\n className=\"px-1.5\"\n aria-label={labels?.calculatorTrigger}\n labels={labels?.calculator}\n />\n )}\n {currency && !editable && (\n <span\n aria-hidden\n className={cn(\n \"pointer-events-none px-1 font-medium text-muted\",\n // A text-xs chip next to a 36px figure reads as a stray footnote.\n asDisplay ? \"text-sm\" : \"text-xs\",\n )}\n >\n {currency}\n </span>\n )}\n {currency && editable && (\n <button\n ref={chipRef}\n type=\"button\"\n onClick={() => setOpen((v) => !v)}\n // A button that opens a listbox, and says so: the chip is a control inside\n // a text field, not a field of its own, so not a combobox.\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-controls={listboxId}\n aria-label={\n // \"Currency: EUR\" through `common.fieldValue`, not a template literal:\n // the colon-and-space is punctuation a translation has to own.\n selected ? common.fieldValue(currencyText.currency, selected.code) : currencyText.currency\n }\n className={cn(\n \"flex items-center gap-1 rounded px-2 py-1 font-medium text-secondary hover:bg-hover\",\n asDisplay ? \"text-sm\" : \"text-xs\",\n )}\n >\n {currency}\n <ChevronDown className=\"size-3\" />\n </button>\n )}\n </div>\n {editable && (\n <CurrencyOptionList\n core={core}\n listboxId={listboxId}\n value={currency}\n codes={currencyOptions}\n currencyNames={currencyNames}\n locale={locale}\n searchPlaceholder={currencyText.search}\n // The phone sheet's title: what the chip is for.\n sheetTitle={currencyText.currency}\n onChoose={(code) => {\n onCurrencyChange?.(code);\n closeToTrigger();\n }}\n />\n )}\n </FloatingField>\n </LockedOrNot>\n {/* Outside the relative box, as NumberInput's caption is: the trailing\n controls are `inset-y-1` in it and would stretch down over a second line. */}\n <FieldCaption parts={numeric.hint} />\n {/* The news after the standing advice, as Select orders them. The root is\n already a steady box, so the message coming and going remounts nothing. */}\n {numeric.errorEl}\n {showNumpad && (\n <NumberPadSheet\n // Localised like the field it mirrors. Keys are typing, read by the\n // locale's marks like the keyboard's; \"=\" is a result, dot-decimal.\n value={display}\n onChange={typed}\n onResult={(result) => {\n setDraft(null);\n const settled = settle(result);\n handleText(settled);\n onCommit?.(settled);\n }}\n onDone={() => innerRef.current?.blur()}\n label={label}\n labels={labels?.pad}\n decimalMark={mark}\n />\n )}\n </div>\n );\n}\n\n/** The field in its lock's {@link LockedReason} while locked, as it is otherwise. A\n * component rather than a ternary so the field's JSX is written once. `block w-full`:\n * the frame's box stands where the field's own did. */\nfunction LockedOrNot({\n lock,\n display,\n children,\n}: {\n lock: LockReason;\n display: DisabledReasonDisplay | undefined;\n children: ReactNode;\n}) {\n if (!lock.locked) return <>{children}</>;\n return (\n <LockedReason lock={lock} display={display} className=\"block w-full\">\n {children}\n </LockedReason>\n );\n}\n"],"mappings":"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","names":[]}
@@ -253,6 +253,7 @@ function Autocomplete(props) {
253
253
  "div",
254
254
  {
255
255
  role: "presentation",
256
+ "data-kit-layer": "",
256
257
  dir,
257
258
  onMouseDown: (e) => e.preventDefault(),
258
259
  className: cn(SUGGESTION_LIST_CLASS, "fixed z-50 flex flex-col overflow-hidden py-0"),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/autocomplete.tsx"],"sourcesContent":["import { Fragment, useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./field-base\";\nimport { Spinner } from \"./spinner\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { takeEscape, useDismissLayer } from \"../hooks/use-dismiss\";\nimport { isImeEnter } from \"../lib/ime\";\nimport { useAnchorDir } from \"./use-anchor-dir\";\nimport {\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n endHintRowProps,\n GROUP_HEADING_STICKY,\n isOptionEnabled,\n stepEnabled,\n SUGGESTION_LIST_CLASS,\n suggestionRowClass,\n useActiveOptionScroll,\n useOptionSource,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels } from \"../i18n/kit-labels\";\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 type { CommitScope } from \"./write-lock\";\n\n/**\n * `value`/`onChange` are the TEXT's, and `onSelect` is \"a suggestion was taken\" —\n * not the DOM's text-selection event — so the input's own spellings of those three\n * are omitted. Everything else reaches the `<input>`, the way it does on {@link Input}.\n */\nexport interface AutocompleteProps<V extends string | number = string>\n extends Omit<\n ComponentPropsWithoutRef<\"input\">,\n \"value\" | \"defaultValue\" | \"onChange\" | \"onSelect\" | \"children\" | \"type\" | \"size\"\n >,\n FieldHelpProps {\n /** What is in the field. Controlled, and never reset by the component: opening,\n * closing and a failed lookup all leave it exactly as typed. */\n value: string;\n /** Every keystroke — and, with `fillOnSelect`, the label of a taken suggestion. */\n onChange: (text: string) => void;\n /** Suggestions the caller already has (e.g. from its own query hook). Narrowed by\n * the text unless `filter={false}`. An option with `disabled: true` is listed but\n * cannot be taken — see {@link ComboOption.disabled}. */\n options?: ComboOption<V>[];\n /** Or: fetch them. Debounced (`debounceMs`), race-safe, only at `minChars` and up,\n * never while the field is idle; a rejection shows `loadErrorLabel` and empties\n * the list rather than leaving the previous text's rows under it. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** Narrow `options` by label/sublabel substring. Default `true`. Pass `false` for\n * a list a server has already searched and ranked — it is shown as given. */\n filter?: boolean;\n /** External loading flag (a caller-side fetch), OR-ed with `loadOptions`'. */\n loading?: boolean;\n /** Below this many characters nothing is offered or fetched. Default 1: an empty\n * field asks nothing — focusing it is not a search. */\n minChars?: number;\n /** Quiet time before `loadOptions` runs. Default 300 ms — an ordinary typist's\n * inter-key interval is 150–250 ms, so the pickers' 150 fires on nearly every\n * letter, which a rate-limited geocoder cannot afford. */\n debounceMs?: number;\n /**\n * Whether the list MAY show — controlled. Omit (or pass `undefined`) and the field\n * decides: open while focused, until Escape, Tab or a taken row. `false` keeps it\n * shut whatever the user does, and stops `loadOptions` asking — keksdose's address\n * search, with its confirm map up, where it used to pass `minChars={Infinity}`.\n * `true` shows it even unfocused.\n *\n * Even open, the list only APPEARS when it has something to show (rows, or a\n * status line); `aria-expanded` follows what appears, not this flag.\n */\n open?: boolean;\n /** The field wants the list open or shut — focus, typing, ↓/↑ open it; blur,\n * Escape, Tab and taking a row shut it. Called on every change, controlled or not. */\n onOpenChange?: (open: boolean) => void;\n /**\n * `\"sm\"`: the 28px, 12px-type field of {@link Select}'s `size=\"sm\"`, with the icon,\n * spinner and list rows scaled to match — for a card or toolbar whose buttons are\n * small. `\"md\"` (default) is the form field. Unlabelled only, as on `Select`: a\n * floating label needs the tall box, so a labelled field ignores `\"sm\"`.\n *\n * A NUMBER is still the native `size` attribute (the width in characters), passed\n * straight through.\n */\n size?: \"sm\" | \"md\" | number;\n /** A suggestion was taken (click, or Enter on the highlighted row). */\n onSelect?: (option: ComboOption<V>) => void;\n /**\n * Put the taken suggestion's label into the field (`onChange(label)`). Default\n * `true` — the free-text case, where the text IS the value and a suggestion only\n * completes it (an address field). Pass `false` where taking a row is an ACTION\n * and the field is only the search that found it; the text then stays as typed.\n */\n fillOnSelect?: boolean;\n /** A floating label, as on {@link Combobox} — a real `<label for>` on the input.\n * Names the field (`aria-labelledby`) unless an `aria-label` is given. */\n label?: ReactNode;\n /** Leading decoration inside the field (a pin, a magnifier). Decorative: the\n * label names the field. An `<svg>` child is sized by the field — 16px, or 14px\n * at `size=\"sm\"` — so a lucide icon needs no class of its own. */\n icon?: ReactNode;\n /** Required and unanswered — {@link FIELD_INVALID}. */\n invalid?: boolean;\n /** What is wrong with the VALUE, as {@link Input}'s `error`: under the field, on\n * its `aria-describedby`, implies `invalid`. Not a failed lookup — that is\n * `loadErrorLabel`, shown in the list. */\n error?: ReactNode;\n /**\n * Standing advice — {@link Combobox}'s `hint`, so this field reads like every other\n * (keksdose K4): plain TEXT is a caption under the field on the input's\n * `aria-describedby`, before any error; a {@link FieldHint} \"?\" rides the label line,\n * or with no label sits at the field's end edge, outside the box. Not the list's\n * `status` line, which talks about the lookup, not the field.\n *\n * Pass it — even as `undefined` — on a field whose hint comes and goes (0.24): the\n * unlabelled field then keeps its end-hint row whether or not a \"?\" is in it, so the\n * `<input>` is never re-parented, and focus and caret survive the toggle. The rule\n * the rest of the family has had since 0.23 (and the fields since 0.22).\n */\n hint?: ReactNode;\n /**\n * The caller's own line in the list, replacing the automatic one (loading / failed\n * / no results). For what only the caller knows: \"offline\", \"the address service\n * is switched off\", a privacy note, the hint a caller-fetched list needs below its\n * own minimum. While set, the list opens even with no rows. Announced politely.\n */\n status?: ReactNode;\n /** Default: `combobox.noResults` from the {@link UiKitProvider}, else English. */\n emptyLabel?: string;\n /** Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n /** Classes for the `<input>` itself; `className` styles the wrapper. */\n inputClassName?: string;\n /**\n * Why the field cannot be changed — {@link Button}'s `disabledReason`, the Combobox\n * family's write lock (keksdose G2, 0.23), for a field whose pick SAVES: an address\n * search whose taken row writes the address, a lookup that links a record.\n *\n * With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the\n * settled grey FIELD_BASE gives a `[readonly]` field), opens no list and asks no\n * `loadOptions` — whatever `open` says — and nothing reaches `onChange`, `onSelect` or\n * `onOpenChange(true)`. Enter is swallowed, so a locked field does not submit the form\n * around it, and a caller's `onKeyDown` does not hear it; every other key still\n * reaches the caller (an Escape that closes its panel) and the caret. The reason is in\n * the kit {@link Tooltip} and on the input's `aria-describedby`. Wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /**\n * This field COMMITS. Under a locked {@link WriteLockProvider} it is locked the\n * `disabledReason` way with the lock's reason (which wins over its own). A search\n * whose result is only draft state — the form's Save writes it — leaves this off and\n * stays live under the lock. 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 /** The `<input>`. A React 19 ref prop since 0.34 (MOD-4, refactor audit 2026-10): it was\n * `forwardRef`, cast back to a generic function, with a `displayName` line beside it. */\n ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * An inline, async-capable text field with a suggestion list under it — the\n * WAI-ARIA APG \"editable combobox with list autocomplete\".\n *\n * Two jobs, one control:\n * - **free text** (kastlan's address field): the text is the value, the list only\n * offers completions, and taking one fills the field (`fillOnSelect`, default);\n * - **search-then-act** (keksdose's address search): the text is a query, taking a\n * row calls `onSelect` and nothing is held (`fillOnSelect={false}`).\n *\n * How it differs from its siblings: {@link Combobox} is free text over a STATIC\n * string pool and opens on focus; {@link EntityCombobox} is a trigger holding an id.\n * This one keeps no value of its own, never resets the text, asks nothing below\n * `minChars`, and reports a failed lookup instead of showing stale rows.\n *\n * Focus never leaves the `<input>`: `aria-activedescendant` names the highlighted\n * row, `aria-controls` the list, and a polite live region says what the list now\n * holds. Keyboard: ↓/↑ move (↓ opens) over every row but a `disabled` one, Enter\n * takes the highlighted row (with none,\n * Enter is left to the form), Escape closes, Tab closes and moves on; Home/End stay\n * with the caret. An Escape that closes an open list is consumed and never reaches the\n * caller; with the list closed it does. For every other key a caller's `onKeyDown`\n * runs first and may `preventDefault()` to\n * keep a key for itself.\n *\n * The list is portalled and anchored under the field on every screen size: the\n * field already carries the keyboard, and `useAnchoredPanel` keeps the list clear\n * of it on a phone. There is no full-screen sheet as {@link Combobox} has.\n *\n * Generic over the option value (`ComboOption<V>`); the ref is the `<input>`'s.\n */\nexport function Autocomplete<V extends string | number = string>(props: AutocompleteProps<V>) {\n const {\n ref,\n value,\n onChange,\n options,\n loadOptions,\n filter,\n loading,\n minChars = 1,\n debounceMs = 300,\n onSelect,\n fillOnSelect = true,\n label,\n icon,\n invalid,\n error,\n hint,\n help,\n status,\n emptyLabel,\n loadErrorLabel,\n open: openProp,\n onOpenChange,\n size,\n className,\n inputClassName,\n id,\n disabled,\n disabledReason,\n disabledReasonDisplay,\n commit,\n onKeyDown,\n onFocus,\n onBlur,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n } = props;\n const generated = useId();\n const fieldId = id ?? generated;\n // Off the GENERATED id: a caller's `id` is theirs to collide with.\n const listboxId = `${generated}-listbox`;\n const labelId = `${generated}-label`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const fieldRef = useRef<HTMLDivElement>(null);\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n noResults: emptyLabel,\n loadError: loadErrorLabel,\n });\n const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });\n // A form library's `aria-invalid` paints as well as announces (DUP-8: it only\n // announced, through the spread below).\n const field = useFieldError(error, invalid, hintParts.describedBy, rest[\"aria-invalid\"]);\n\n /** The user's intent: the field is focused and the list has not been dismissed.\n * Whether anything SHOWS is decided below, from what there is to show. */\n const [ownOpen, setOwnOpen] = useState(false);\n // Controlled when the caller passes a boolean. The own state is kept up to date\n // either way, so a caller that controls only SOMETIMES (`open={confirming ? false :\n // undefined}`) hands back a field that is exactly as open as focus says it is.\n const open = openProp ?? ownOpen;\n const setOpen = (next: boolean) => {\n setOwnOpen(next);\n if (next !== open) onOpenChange?.(next);\n };\n const [active, setActive] = useState(-1);\n // The lock: focusable, `readOnly`, no list — see `disabledReason`. Folded into `live`,\n // so a locked field asks nothing and shows nothing even with `open` held true.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const live = open && !disabled && !locked;\n const { results, busy, failed, tooShort } = useOptionSource<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n query: value,\n active: live,\n });\n\n const hasStatus = status !== undefined && status !== null && status !== false && status !== \"\";\n // The visible line under the rows. Nothing below `minChars` unless the caller has\n // something to say — an empty field opening a box that says \"type more\" is noise.\n const statusLine: ReactNode = hasStatus\n ? status\n : tooShort\n ? null\n : busy && results.length === 0\n ? labels.loading\n : failed\n ? labels.loadError\n : !busy && results.length === 0\n ? labels.noResults\n : null;\n const expanded = live && (results.length > 0 || statusLine !== null);\n // A disabled row is never the keyboard's — not even when a list that changed under\n // the highlight put one there.\n const activeId = expanded && isOptionEnabled(results[active]) ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n const { rect, top, bottom, maxHeight, above } = useAnchoredPanel(fieldRef, expanded, { preferredHeight: 256 });\n const dir = useAnchorDir(fieldRef, expanded);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n };\n // The list is a layer on the kit's Escape stack while it shows (Wave 2, DUP-1).\n useDismissLayer(expanded, close);\n const take = (o: ComboOption<V>) => {\n if (o.disabled || locked) return;\n if (fillOnSelect) onChange(o.label);\n onSelect?.(o);\n close();\n };\n\n const hasLabel = label !== undefined;\n // `size=\"sm\"` is for an UNLABELLED field, as on Select: the floating label needs the\n // tall box. A number is the native attribute.\n const small = size === \"sm\" && !hasLabel;\n const nativeSize = typeof size === \"number\" ? size : undefined;\n // The caller's name wins; else the floating label, by reference so a non-string\n // label still names the field.\n const labelledBy =\n ariaLabel === undefined ? (ariaLabelledBy ?? (hasLabel ? labelId : undefined)) : undefined;\n // While locked the field 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 = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} display={disabledReasonDisplay} className=\"block\">\n {box}\n </LockedReason>\n ) : (\n box\n );\n\n return (\n // `relative`: the floating label and the live region's `sr-only` both need a\n // local containing block.\n <div className={cn(\"relative\", className)}>\n {/* A real <label for> (lenkbank), and still `aria-labelledby`'s target, so the\n name — and the listbox's, which borrows it — is exactly what it was. */}\n {hasLabel && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId}>\n {label}\n </ComboboxFieldLabel>\n )}\n {/* `\"hint\" in props`: the family's 0.22 rule (see `endHintRowProps`) — passed at\n all, even as `undefined`, a hint keeps the unlabelled field's end-hint row, so\n a \"?\" coming and going never re-parents the <input> under the caret (0.24, the\n last of the family to get it; the siblings had it in 0.23). */}\n <EndHintRow {...endHintRowProps(\"hint\" in props, hasLabel, hintParts.labelHint)}>\n {withLock(\n <div ref={fieldRef} className={cn(\"relative\", locked && \"cursor-not-allowed\")}>\n {icon && (\n <span\n aria-hidden\n className={cn(\n \"pointer-events-none absolute flex text-muted\",\n small ? \"start-2 [&>svg]:size-3.5\" : \"start-2.5 [&>svg]:size-4\",\n // Labelled, the icon sits on the VALUE's line — the one line box below\n // FIELD_FLOATING_PAD's `pt-4` and the 1px border — not on the middle of\n // the box. Centred on the box it rose into the top strip and sat on the\n // label's first letters, which float at the same start inset. On the\n // value line it is beside what it describes, and the label keeps the\n // start edge every other label in the form's column shares.\n // At Large the label stands above the field and the strip is gone\n // (0.33, §10.17): the value's line is under FIELD_BASE's `py-2`.\n hasLabel\n ? \"top-[calc(1rem+1px)] h-5 items-center large:top-[calc(0.5rem+1px)]\"\n : \"top-1/2 -translate-y-1/2\",\n )}\n >\n {icon}\n </span>\n )}\n <input\n {...rest}\n ref={ref}\n id={fieldId}\n size={nativeSize}\n type=\"text\"\n value={value}\n // Locked: focusable, `readOnly`, `aria-disabled` — see `disabledReason`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || rest.readOnly}\n aria-disabled={locked || rest[\"aria-disabled\"]}\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={expanded}\n // Required by the role, and set while closed too, as on the siblings.\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n aria-invalid={field.isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n autoComplete=\"off\"\n onFocus={(e) => {\n onFocus?.(e);\n // A locked field takes focus to say why, and opens nothing.\n if (locked) return;\n // A seeded field (keksdose's `initialQuery`) is looked up as it is\n // focused; an empty one asks nothing, because of `minChars`.\n setOpen(true);\n }}\n onBlur={(e) => {\n onBlur?.(e);\n close();\n }}\n onChange={(e) => {\n if (locked) return;\n onChange(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n // Locked: Enter is swallowed (no form submit, no caller's \"search now\");\n // every other key is the caller's and the caret's. No list to drive.\n if (locked) {\n if (e.key === \"Enter\") e.preventDefault();\n else onKeyDown?.(e);\n return;\n }\n // An Escape that closes the OPEN list is the list's, and is consumed before\n // the caller sees it. A caller whose Escape means \"close the panel\" (keksdose's\n // address search) otherwise closed the whole panel when the user only meant\n // to dismiss the suggestions — the opposite of what the docs promised.\n if (e.key === \"Escape\" && expanded && takeEscape(e)) return;\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n // Disabled rows are passed over; at either end the highlight stays put.\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n if (!open) setOpen(true);\n else setActive((i) => stepEnabled(results, i, 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n if (!open) setOpen(true);\n // From \"nothing highlighted\", Up goes to the bottom, as the APG has it.\n else setActive((i) => stepEnabled(results, i < 0 ? results.length : i, -1));\n } else if (e.key === \"Enter\") {\n // No row highlighted: the text is the answer, and a form's own submit\n // is left alone. Nor is a row taken by the Enter that confirms an IME\n // composition, which is the IME's (BUG-9).\n if (expanded && activeId && !isImeEnter(e)) {\n e.preventDefault();\n take(results[active]);\n }\n } else if (e.key === \"Tab\") {\n close();\n }\n }}\n className={cn(\n FIELD_BASE,\n hasLabel && FIELD_FLOATING_PAD,\n // Select's `SELECT_SM` box: a fixed 28px, so the caller's line height\n // cannot grow it.\n small && \"h-7 py-0 pe-2 ps-2 text-xs\",\n icon ? (small ? \"ps-7\" : \"ps-8\") : undefined,\n busy && (small ? \"pe-7\" : \"pe-9\"),\n field.isInvalid && FIELD_INVALID,\n inputClassName,\n )}\n />\n {busy && live && (\n // Decorative: the live region below already says \"Loading…\".\n <span\n className={cn(\n \"pointer-events-none absolute top-1/2 flex -translate-y-1/2\",\n small ? \"end-2\" : \"end-2.5\",\n )}\n >\n <Spinner label={null} className={small ? \"h-3.5 w-3.5\" : \"h-4 w-4\"} />\n </span>\n )}\n </div>,\n )}\n </EndHintRow>\n {/* With a \"?\" the label shares the top strip with it, after the field so the \"?\"\n follows the control in the tab order (as on Input and Select). */}\n {hasLabel && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId} className={LABEL_IN_ROW}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {/* The live region, kept mounted so a change IS an announcement: focus stays\n in the field, and without it a reader typed into a geocoder and heard only\n their own letters. The panel's visible line is aria-hidden in favour of\n this, which also carries the row count the eye reads off the list. */}\n <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {!live\n ? null\n : statusLine !== null\n ? statusLine\n : results.length > 0\n ? labels.resultCount(results.length)\n : null}\n </div>\n {expanded &&\n rect &&\n typeof document !== \"undefined\" &&\n createPortal(\n <div\n // Every press inside the list keeps focus in the field — rows, the status\n // line, the scrollbar — or the input's blur would close the list first.\n role=\"presentation\"\n // Portalled out of the form's `dir`; the field's is put back. The list is\n // exactly the field's width, so `left` places it in either direction.\n dir={dir}\n onMouseDown={(e) => e.preventDefault()}\n // The family's suggestion box (DUP-4), as a column: the list scrolls inside\n // it and the status line stays under the rows.\n className={cn(SUGGESTION_LIST_CLASS, \"fixed z-50 flex flex-col overflow-hidden py-0\")}\n // Flipped above, the list hangs from its bottom edge: three rows under a\n // 256 px cap, placed by `top`, floated ~176 px above the field (keksdose K1),\n // and the rows come and go as the user types.\n style={{ ...(above ? { bottom } : { top }), left: rect.left, width: rect.width, maxHeight }}\n >\n {results.length > 0 && (\n <ul\n id={listboxId}\n role=\"listbox\"\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n className=\"min-h-0 flex-1 overflow-y-auto py-1\"\n >\n {results.map((o, i) => {\n const startsGroup = o.group != null && o.group !== results[i - 1]?.group;\n return (\n // Keyed by group AND value, as in the core panel: an option may\n // appear once per group.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {startsGroup && (\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 role=\"option\"\n // CHOSEN, never \"highlighted\" — the family's rule. This\n // field holds no value, so the only row that can be chosen\n // is the one whose label the text already is.\n aria-selected={fillOnSelect && o.label === value}\n // Listed, announced as unavailable, never taken: `take`\n // refuses it and the arrows pass it over.\n aria-disabled={o.disabled || undefined}\n tabIndex={-1}\n onClick={() => take(o)}\n onMouseEnter={() => {\n if (!o.disabled) setActive(i);\n }}\n className={cn(\n // The family's suggestion row (DUP-4): it hand-rolled its\n // own with a plain `truncate`, so a long address was cut at\n // Large, where Combobox's rows wrap (text size §10.17, T1).\n // A flex row here, for the icon and the two lines.\n suggestionRowClass(i === active && !o.disabled),\n \"flex items-center gap-2\",\n small && \"py-1 text-xs\",\n o.disabled && DISABLED_ROW_CLASS,\n o.group != null && \"ps-6\",\n )}\n >\n {o.icon && (\n <span aria-hidden className=\"shrink-0 text-muted\">\n {o.icon}\n </span>\n )}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate-until-large text-primary\">\n {o.label}\n </span>\n {o.sublabel && (\n <span\n className={cn(\n \"block truncate-until-large text-placeholder\",\n small ? \"text-caption\" : \"text-xs\",\n )}\n >\n {o.sublabel}\n </span>\n )}\n </span>\n </button>\n </li>\n </Fragment>\n );\n })}\n </ul>\n )}\n {statusLine !== null && (\n // Visible, not live — the region above speaks for it.\n <div\n aria-hidden\n className={cn(\n small ? \"px-3 py-1.5 text-xs\" : \"px-3 py-2 text-sm\",\n failed && !hasStatus ? \"text-danger\" : \"text-muted\",\n results.length > 0 && \"border-t border-subtle text-xs\",\n )}\n >\n {statusLine}\n </div>\n )}\n </div>,\n document.body,\n )}\n </div>\n );\n}\n"],"mappings":";AAmVM,cAwBI,YAxBJ;AAnVN,SAAS,UAAU,OAAO,QAAQ,gBAAgB;AAElD,SAAS,oBAAoB;AAC7B,SAAS,UAAU;AACnB,SAAS,YAAY,oBAAoB,qBAAqB;AAC9D,SAAS,eAAe;AACxB,SAAS,wBAAwB;AACjC,SAAS,YAAY,uBAAuB;AAC5C,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,yBAAyB,oBAAoB;AACtD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AA2KA,SAAS,aAAiD,OAA6B;AAC5F,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,aAAa;AAAA,IACb;AAAA,IACA,eAAe;AAAA,IACf;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc;AAAA,IACd,mBAAmB;AAAA,IACnB,oBAAoB;AAAA,IACpB,GAAG;AAAA,EACL,IAAI;AACJ,QAAM,YAAY,MAAM;AACxB,QAAM,UAAU,MAAM;AAEtB,QAAM,YAAY,GAAG,SAAS;AAC9B,QAAM,UAAU,GAAG,SAAS;AAC5B,QAAM,WAAW,CAAC,UAAkB,GAAG,SAAS,WAAW,KAAK;AAChE,QAAM,WAAW,OAAuB,IAAI;AAC5C,QAAM,SAAS,aAAa,YAAY,yBAAyB;AAAA,IAC/D,WAAW;AAAA,IACX,WAAW;AAAA,EACb,CAAC;AACD,QAAM,YAAY,aAAa,MAAM,iBAAiB,EAAE,MAAM,MAAM,SAAS,UAAU,CAAC;AAGxF,QAAM,QAAQ,cAAc,OAAO,SAAS,UAAU,aAAa,KAAK,cAAc,CAAC;AAIvF,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAI5C,QAAM,OAAO,YAAY;AACzB,QAAM,UAAU,CAAC,SAAkB;AACjC,eAAW,IAAI;AACf,QAAI,SAAS,KAAM,gBAAe,IAAI;AAAA,EACxC;AACA,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,EAAE;AAGvC,QAAM,OAAO,cAAc,QAAQ,cAAc;AACjD,QAAM,SAAS,KAAK;AACpB,QAAM,OAAO,QAAQ,CAAC,YAAY,CAAC;AACnC,QAAM,EAAE,SAAS,MAAM,QAAQ,SAAS,IAAI,gBAAmB;AAAA,IAC7D;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP,QAAQ;AAAA,EACV,CAAC;AAED,QAAM,YAAY,WAAW,UAAa,WAAW,QAAQ,WAAW,SAAS,WAAW;AAG5F,QAAM,aAAwB,YAC1B,SACA,WACE,OACA,QAAQ,QAAQ,WAAW,IACzB,OAAO,UACP,SACE,OAAO,YACP,CAAC,QAAQ,QAAQ,WAAW,IAC1B,OAAO,YACP;AACZ,QAAM,WAAW,SAAS,QAAQ,SAAS,KAAK,eAAe;AAG/D,QAAM,WAAW,YAAY,gBAAgB,QAAQ,MAAM,CAAC,IAAI,SAAS,MAAM,IAAI;AACnF,wBAAsB,QAAQ;AAC9B,QAAM,EAAE,MAAM,KAAK,QAAQ,WAAW,MAAM,IAAI,iBAAiB,UAAU,UAAU,EAAE,iBAAiB,IAAI,CAAC;AAC7G,QAAM,MAAM,aAAa,UAAU,QAAQ;AAE3C,QAAM,QAAQ,MAAM;AAClB,YAAQ,KAAK;AACb,cAAU,EAAE;AAAA,EACd;AAEA,kBAAgB,UAAU,KAAK;AAC/B,QAAM,OAAO,CAAC,MAAsB;AAClC,QAAI,EAAE,YAAY,OAAQ;AAC1B,QAAI,aAAc,UAAS,EAAE,KAAK;AAClC,eAAW,CAAC;AACZ,UAAM;AAAA,EACR;AAEA,QAAM,WAAW,UAAU;AAG3B,QAAM,QAAQ,SAAS,QAAQ,CAAC;AAChC,QAAM,aAAa,OAAO,SAAS,WAAW,OAAO;AAGrD,QAAM,aACJ,cAAc,SAAa,mBAAmB,WAAW,UAAU,UAAc;AAGnF,QAAM,WAAW,CAAC,QAChB,SACE,oBAAC,gBAAa,MAAY,SAAS,uBAAuB,WAAU,SACjE,eACH,IAEA;AAGJ;AAAA;AAAA;AAAA,IAGE,qBAAC,SAAI,WAAW,GAAG,YAAY,SAAS,GAGrC;AAAA,kBAAY,UAAU,cAAc,UACnC,oBAAC,sBAAmB,IAAI,SAAS,SAAS,SACvC,iBACH;AAAA,MAMF,oBAAC,cAAY,GAAG,gBAAgB,UAAU,OAAO,UAAU,UAAU,SAAS,GAC3E;AAAA,QACC,qBAAC,SAAI,KAAK,UAAU,WAAW,GAAG,YAAY,UAAU,oBAAoB,GACzE;AAAA,kBACC;AAAA,YAAC;AAAA;AAAA,cACC,eAAW;AAAA,cACX,WAAW;AAAA,gBACT;AAAA,gBACA,QAAQ,6BAA6B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBASrC,WACI,uEACA;AAAA,cACN;AAAA,cAEC;AAAA;AAAA,UACH;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACE,GAAG;AAAA,cACJ;AAAA,cACA,IAAI;AAAA,cACJ,MAAM;AAAA,cACN,MAAK;AAAA,cACL;AAAA,cAEA,UAAU,SAAS,SAAY;AAAA,cAC/B,UAAU,UAAU,KAAK;AAAA,cACzB,iBAAe,UAAU,KAAK,eAAe;AAAA,cAC7C,MAAK;AAAA,cACL,qBAAkB;AAAA,cAClB,iBAAe;AAAA,cAEf,iBAAe;AAAA,cACf,yBAAuB;AAAA,cACvB,cAAY;AAAA,cACZ,mBAAiB;AAAA,cACjB,gBAAc,MAAM,aAAa,KAAK,cAAc,KAAK;AAAA,cACzD,oBAAkB,iBAAiB,MAAM,aAAa,UAAU,KAAK,QAAQ;AAAA,cAC7E,cAAa;AAAA,cACb,SAAS,CAAC,MAAM;AACd,0BAAU,CAAC;AAEX,oBAAI,OAAQ;AAGZ,wBAAQ,IAAI;AAAA,cACd;AAAA,cACA,QAAQ,CAAC,MAAM;AACb,yBAAS,CAAC;AACV,sBAAM;AAAA,cACR;AAAA,cACA,UAAU,CAAC,MAAM;AACf,oBAAI,OAAQ;AACZ,yBAAS,EAAE,OAAO,KAAK;AACvB,wBAAQ,IAAI;AACZ,0BAAU,EAAE;AAAA,cACd;AAAA,cACA,WAAW,CAAC,MAAM;AAGhB,oBAAI,QAAQ;AACV,sBAAI,EAAE,QAAQ,QAAS,GAAE,eAAe;AAAA,sBACnC,aAAY,CAAC;AAClB;AAAA,gBACF;AAKA,oBAAI,EAAE,QAAQ,YAAY,YAAY,WAAW,CAAC,EAAG;AACrD,4BAAY,CAAC;AACb,oBAAI,EAAE,iBAAkB;AAExB,oBAAI,EAAE,QAAQ,aAAa;AACzB,oBAAE,eAAe;AACjB,sBAAI,CAAC,KAAM,SAAQ,IAAI;AAAA,sBAClB,WAAU,CAAC,MAAM,YAAY,SAAS,GAAG,CAAC,CAAC;AAAA,gBAClD,WAAW,EAAE,QAAQ,WAAW;AAC9B,oBAAE,eAAe;AACjB,sBAAI,CAAC,KAAM,SAAQ,IAAI;AAAA,sBAElB,WAAU,CAAC,MAAM,YAAY,SAAS,IAAI,IAAI,QAAQ,SAAS,GAAG,EAAE,CAAC;AAAA,gBAC5E,WAAW,EAAE,QAAQ,SAAS;AAI5B,sBAAI,YAAY,YAAY,CAAC,WAAW,CAAC,GAAG;AAC1C,sBAAE,eAAe;AACjB,yBAAK,QAAQ,MAAM,CAAC;AAAA,kBACtB;AAAA,gBACF,WAAW,EAAE,QAAQ,OAAO;AAC1B,wBAAM;AAAA,gBACR;AAAA,cACF;AAAA,cACA,WAAW;AAAA,gBACT;AAAA,gBACA,YAAY;AAAA;AAAA;AAAA,gBAGZ,SAAS;AAAA,gBACT,OAAQ,QAAQ,SAAS,SAAU;AAAA,gBACnC,SAAS,QAAQ,SAAS;AAAA,gBAC1B,MAAM,aAAa;AAAA,gBACnB;AAAA,cACF;AAAA;AAAA,UACF;AAAA,UACC,QAAQ;AAAA,UAEP;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,QAAQ,UAAU;AAAA,cACpB;AAAA,cAEA,8BAAC,WAAQ,OAAO,MAAM,WAAW,QAAQ,gBAAgB,WAAW;AAAA;AAAA,UACtE;AAAA,WAEJ;AAAA,MACF,GACF;AAAA,MAGC,YAAY,UAAU,cAAc,UACnC,oBAAC,kBAAe,MAAM,UAAU,WAC9B,8BAAC,sBAAmB,IAAI,SAAS,SAAS,SAAS,WAAW,cAC3D,iBACH,GACF;AAAA,MAEF,oBAAC,gBAAa,OAAO,WAAW;AAAA,MAC/B,MAAM;AAAA,MAKP,oBAAC,SAAI,MAAK,UAAS,aAAU,UAAS,WAAU,WAC7C,WAAC,OACE,OACA,eAAe,OACb,aACA,QAAQ,SAAS,IACf,OAAO,YAAY,QAAQ,MAAM,IACjC,MACV;AAAA,MACC,YACC,QACA,OAAO,aAAa,eACpB;AAAA,QACE;AAAA,UAAC;AAAA;AAAA,YAGC,MAAK;AAAA,YAGL;AAAA,YACA,aAAa,CAAC,MAAM,EAAE,eAAe;AAAA,YAGrC,WAAW,GAAG,uBAAuB,+CAA+C;AAAA,YAIpF,OAAO,EAAE,GAAI,QAAQ,EAAE,OAAO,IAAI,EAAE,IAAI,GAAI,MAAM,KAAK,MAAM,OAAO,KAAK,OAAO,UAAU;AAAA,YAEzF;AAAA,sBAAQ,SAAS,KAChB;AAAA,gBAAC;AAAA;AAAA,kBACC,IAAI;AAAA,kBACJ,MAAK;AAAA,kBACL,cAAY;AAAA,kBACZ,mBAAiB;AAAA,kBACjB,WAAU;AAAA,kBAET,kBAAQ,IAAI,CAAC,GAAG,MAAM;AACrB,0BAAM,cAAc,EAAE,SAAS,QAAQ,EAAE,UAAU,QAAQ,IAAI,CAAC,GAAG;AACnE;AAAA;AAAA;AAAA,sBAGE,qBAAC,YACE;AAAA,uCACC;AAAA,0BAAC;AAAA;AAAA,4BACC,MAAK;AAAA,4BACL,WAAW;AAAA,8BACT;AAAA,8BACA;AAAA,4BACF;AAAA,4BAEC,YAAE;AAAA;AAAA,wBACL;AAAA,wBAEF,oBAAC,QAAG,MAAK,gBACP;AAAA,0BAAC;AAAA;AAAA,4BACC,MAAK;AAAA,4BACL,IAAI,SAAS,CAAC;AAAA,4BACd,MAAK;AAAA,4BAIL,iBAAe,gBAAgB,EAAE,UAAU;AAAA,4BAG3C,iBAAe,EAAE,YAAY;AAAA,4BAC7B,UAAU;AAAA,4BACV,SAAS,MAAM,KAAK,CAAC;AAAA,4BACrB,cAAc,MAAM;AAClB,kCAAI,CAAC,EAAE,SAAU,WAAU,CAAC;AAAA,4BAC9B;AAAA,4BACA,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,8BAKT,mBAAmB,MAAM,UAAU,CAAC,EAAE,QAAQ;AAAA,8BAC9C;AAAA,8BACA,SAAS;AAAA,8BACT,EAAE,YAAY;AAAA,8BACd,EAAE,SAAS,QAAQ;AAAA,4BACrB;AAAA,4BAEC;AAAA,gCAAE,QACD,oBAAC,UAAK,eAAW,MAAC,WAAU,uBACzB,YAAE,MACL;AAAA,8BAEF,qBAAC,UAAK,WAAU,kBACd;AAAA,oDAAC,UAAK,WAAU,2CACb,YAAE,OACL;AAAA,gCACC,EAAE,YACD;AAAA,kCAAC;AAAA;AAAA,oCACC,WAAW;AAAA,sCACT;AAAA,sCACA,QAAQ,iBAAiB;AAAA,oCAC3B;AAAA,oCAEC,YAAE;AAAA;AAAA,gCACL;AAAA,iCAEJ;AAAA;AAAA;AAAA,wBACF,GACF;AAAA,2BA9Da,GAAG,EAAE,SAAS,EAAE,IAAI,OAAO,EAAE,KAAK,CAAC,EA+DlD;AAAA;AAAA,kBAEJ,CAAC;AAAA;AAAA,cACH;AAAA,cAED,eAAe;AAAA,cAEd;AAAA,gBAAC;AAAA;AAAA,kBACC,eAAW;AAAA,kBACX,WAAW;AAAA,oBACT,QAAQ,wBAAwB;AAAA,oBAChC,UAAU,CAAC,YAAY,gBAAgB;AAAA,oBACvC,QAAQ,SAAS,KAAK;AAAA,kBACxB;AAAA,kBAEC;AAAA;AAAA,cACH;AAAA;AAAA;AAAA,QAEJ;AAAA,QACA,SAAS;AAAA,MACX;AAAA,OACJ;AAAA;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/autocomplete.tsx"],"sourcesContent":["import { Fragment, useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/cn\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./field-base\";\nimport { Spinner } from \"./spinner\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { takeEscape, useDismissLayer } from \"../hooks/use-dismiss\";\nimport { isImeEnter } from \"../lib/ime\";\nimport { useAnchorDir } from \"./use-anchor-dir\";\nimport {\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n endHintRowProps,\n GROUP_HEADING_STICKY,\n isOptionEnabled,\n stepEnabled,\n SUGGESTION_LIST_CLASS,\n suggestionRowClass,\n useActiveOptionScroll,\n useOptionSource,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels } from \"../i18n/kit-labels\";\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 type { CommitScope } from \"./write-lock\";\n\n/**\n * `value`/`onChange` are the TEXT's, and `onSelect` is \"a suggestion was taken\" —\n * not the DOM's text-selection event — so the input's own spellings of those three\n * are omitted. Everything else reaches the `<input>`, the way it does on {@link Input}.\n */\nexport interface AutocompleteProps<V extends string | number = string>\n extends Omit<\n ComponentPropsWithoutRef<\"input\">,\n \"value\" | \"defaultValue\" | \"onChange\" | \"onSelect\" | \"children\" | \"type\" | \"size\"\n >,\n FieldHelpProps {\n /** What is in the field. Controlled, and never reset by the component: opening,\n * closing and a failed lookup all leave it exactly as typed. */\n value: string;\n /** Every keystroke — and, with `fillOnSelect`, the label of a taken suggestion. */\n onChange: (text: string) => void;\n /** Suggestions the caller already has (e.g. from its own query hook). Narrowed by\n * the text unless `filter={false}`. An option with `disabled: true` is listed but\n * cannot be taken — see {@link ComboOption.disabled}. */\n options?: ComboOption<V>[];\n /** Or: fetch them. Debounced (`debounceMs`), race-safe, only at `minChars` and up,\n * never while the field is idle; a rejection shows `loadErrorLabel` and empties\n * the list rather than leaving the previous text's rows under it. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** Narrow `options` by label/sublabel substring. Default `true`. Pass `false` for\n * a list a server has already searched and ranked — it is shown as given. */\n filter?: boolean;\n /** External loading flag (a caller-side fetch), OR-ed with `loadOptions`'. */\n loading?: boolean;\n /** Below this many characters nothing is offered or fetched. Default 1: an empty\n * field asks nothing — focusing it is not a search. */\n minChars?: number;\n /** Quiet time before `loadOptions` runs. Default 300 ms — an ordinary typist's\n * inter-key interval is 150–250 ms, so the pickers' 150 fires on nearly every\n * letter, which a rate-limited geocoder cannot afford. */\n debounceMs?: number;\n /**\n * Whether the list MAY show — controlled. Omit (or pass `undefined`) and the field\n * decides: open while focused, until Escape, Tab or a taken row. `false` keeps it\n * shut whatever the user does, and stops `loadOptions` asking — keksdose's address\n * search, with its confirm map up, where it used to pass `minChars={Infinity}`.\n * `true` shows it even unfocused.\n *\n * Even open, the list only APPEARS when it has something to show (rows, or a\n * status line); `aria-expanded` follows what appears, not this flag.\n */\n open?: boolean;\n /** The field wants the list open or shut — focus, typing, ↓/↑ open it; blur,\n * Escape, Tab and taking a row shut it. Called on every change, controlled or not. */\n onOpenChange?: (open: boolean) => void;\n /**\n * `\"sm\"`: the 28px, 12px-type field of {@link Select}'s `size=\"sm\"`, with the icon,\n * spinner and list rows scaled to match — for a card or toolbar whose buttons are\n * small. `\"md\"` (default) is the form field. Unlabelled only, as on `Select`: a\n * floating label needs the tall box, so a labelled field ignores `\"sm\"`.\n *\n * A NUMBER is still the native `size` attribute (the width in characters), passed\n * straight through.\n */\n size?: \"sm\" | \"md\" | number;\n /** A suggestion was taken (click, or Enter on the highlighted row). */\n onSelect?: (option: ComboOption<V>) => void;\n /**\n * Put the taken suggestion's label into the field (`onChange(label)`). Default\n * `true` — the free-text case, where the text IS the value and a suggestion only\n * completes it (an address field). Pass `false` where taking a row is an ACTION\n * and the field is only the search that found it; the text then stays as typed.\n */\n fillOnSelect?: boolean;\n /** A floating label, as on {@link Combobox} — a real `<label for>` on the input.\n * Names the field (`aria-labelledby`) unless an `aria-label` is given. */\n label?: ReactNode;\n /** Leading decoration inside the field (a pin, a magnifier). Decorative: the\n * label names the field. An `<svg>` child is sized by the field — 16px, or 14px\n * at `size=\"sm\"` — so a lucide icon needs no class of its own. */\n icon?: ReactNode;\n /** Required and unanswered — {@link FIELD_INVALID}. */\n invalid?: boolean;\n /** What is wrong with the VALUE, as {@link Input}'s `error`: under the field, on\n * its `aria-describedby`, implies `invalid`. Not a failed lookup — that is\n * `loadErrorLabel`, shown in the list. */\n error?: ReactNode;\n /**\n * Standing advice — {@link Combobox}'s `hint`, so this field reads like every other\n * (keksdose K4): plain TEXT is a caption under the field on the input's\n * `aria-describedby`, before any error; a {@link FieldHint} \"?\" rides the label line,\n * or with no label sits at the field's end edge, outside the box. Not the list's\n * `status` line, which talks about the lookup, not the field.\n *\n * Pass it — even as `undefined` — on a field whose hint comes and goes (0.24): the\n * unlabelled field then keeps its end-hint row whether or not a \"?\" is in it, so the\n * `<input>` is never re-parented, and focus and caret survive the toggle. The rule\n * the rest of the family has had since 0.23 (and the fields since 0.22).\n */\n hint?: ReactNode;\n /**\n * The caller's own line in the list, replacing the automatic one (loading / failed\n * / no results). For what only the caller knows: \"offline\", \"the address service\n * is switched off\", a privacy note, the hint a caller-fetched list needs below its\n * own minimum. While set, the list opens even with no rows. Announced politely.\n */\n status?: ReactNode;\n /** Default: `combobox.noResults` from the {@link UiKitProvider}, else English. */\n emptyLabel?: string;\n /** Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n /** Classes for the `<input>` itself; `className` styles the wrapper. */\n inputClassName?: string;\n /**\n * Why the field cannot be changed — {@link Button}'s `disabledReason`, the Combobox\n * family's write lock (keksdose G2, 0.23), for a field whose pick SAVES: an address\n * search whose taken row writes the address, a lookup that links a record.\n *\n * With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the\n * settled grey FIELD_BASE gives a `[readonly]` field), opens no list and asks no\n * `loadOptions` — whatever `open` says — and nothing reaches `onChange`, `onSelect` or\n * `onOpenChange(true)`. Enter is swallowed, so a locked field does not submit the form\n * around it, and a caller's `onKeyDown` does not hear it; every other key still\n * reaches the caller (an Escape that closes its panel) and the caret. The reason is in\n * the kit {@link Tooltip} and on the input's `aria-describedby`. Wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /**\n * This field COMMITS. Under a locked {@link WriteLockProvider} it is locked the\n * `disabledReason` way with the lock's reason (which wins over its own). A search\n * whose result is only draft state — the form's Save writes it — leaves this off and\n * stays live under the lock. 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 /** The `<input>`. A React 19 ref prop since 0.34 (MOD-4, refactor audit 2026-10): it was\n * `forwardRef`, cast back to a generic function, with a `displayName` line beside it. */\n ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * An inline, async-capable text field with a suggestion list under it — the\n * WAI-ARIA APG \"editable combobox with list autocomplete\".\n *\n * Two jobs, one control:\n * - **free text** (kastlan's address field): the text is the value, the list only\n * offers completions, and taking one fills the field (`fillOnSelect`, default);\n * - **search-then-act** (keksdose's address search): the text is a query, taking a\n * row calls `onSelect` and nothing is held (`fillOnSelect={false}`).\n *\n * How it differs from its siblings: {@link Combobox} is free text over a STATIC\n * string pool and opens on focus; {@link EntityCombobox} is a trigger holding an id.\n * This one keeps no value of its own, never resets the text, asks nothing below\n * `minChars`, and reports a failed lookup instead of showing stale rows.\n *\n * Focus never leaves the `<input>`: `aria-activedescendant` names the highlighted\n * row, `aria-controls` the list, and a polite live region says what the list now\n * holds. Keyboard: ↓/↑ move (↓ opens) over every row but a `disabled` one, Enter\n * takes the highlighted row (with none,\n * Enter is left to the form), Escape closes, Tab closes and moves on; Home/End stay\n * with the caret. An Escape that closes an open list is consumed and never reaches the\n * caller; with the list closed it does. For every other key a caller's `onKeyDown`\n * runs first and may `preventDefault()` to\n * keep a key for itself.\n *\n * The list is portalled and anchored under the field on every screen size: the\n * field already carries the keyboard, and `useAnchoredPanel` keeps the list clear\n * of it on a phone. There is no full-screen sheet as {@link Combobox} has.\n *\n * Generic over the option value (`ComboOption<V>`); the ref is the `<input>`'s.\n */\nexport function Autocomplete<V extends string | number = string>(props: AutocompleteProps<V>) {\n const {\n ref,\n value,\n onChange,\n options,\n loadOptions,\n filter,\n loading,\n minChars = 1,\n debounceMs = 300,\n onSelect,\n fillOnSelect = true,\n label,\n icon,\n invalid,\n error,\n hint,\n help,\n status,\n emptyLabel,\n loadErrorLabel,\n open: openProp,\n onOpenChange,\n size,\n className,\n inputClassName,\n id,\n disabled,\n disabledReason,\n disabledReasonDisplay,\n commit,\n onKeyDown,\n onFocus,\n onBlur,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n } = props;\n const generated = useId();\n const fieldId = id ?? generated;\n // Off the GENERATED id: a caller's `id` is theirs to collide with.\n const listboxId = `${generated}-listbox`;\n const labelId = `${generated}-label`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const fieldRef = useRef<HTMLDivElement>(null);\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n noResults: emptyLabel,\n loadError: loadErrorLabel,\n });\n const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });\n // A form library's `aria-invalid` paints as well as announces (DUP-8: it only\n // announced, through the spread below).\n const field = useFieldError(error, invalid, hintParts.describedBy, rest[\"aria-invalid\"]);\n\n /** The user's intent: the field is focused and the list has not been dismissed.\n * Whether anything SHOWS is decided below, from what there is to show. */\n const [ownOpen, setOwnOpen] = useState(false);\n // Controlled when the caller passes a boolean. The own state is kept up to date\n // either way, so a caller that controls only SOMETIMES (`open={confirming ? false :\n // undefined}`) hands back a field that is exactly as open as focus says it is.\n const open = openProp ?? ownOpen;\n const setOpen = (next: boolean) => {\n setOwnOpen(next);\n if (next !== open) onOpenChange?.(next);\n };\n const [active, setActive] = useState(-1);\n // The lock: focusable, `readOnly`, no list — see `disabledReason`. Folded into `live`,\n // so a locked field asks nothing and shows nothing even with `open` held true.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const live = open && !disabled && !locked;\n const { results, busy, failed, tooShort } = useOptionSource<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n query: value,\n active: live,\n });\n\n const hasStatus = status !== undefined && status !== null && status !== false && status !== \"\";\n // The visible line under the rows. Nothing below `minChars` unless the caller has\n // something to say — an empty field opening a box that says \"type more\" is noise.\n const statusLine: ReactNode = hasStatus\n ? status\n : tooShort\n ? null\n : busy && results.length === 0\n ? labels.loading\n : failed\n ? labels.loadError\n : !busy && results.length === 0\n ? labels.noResults\n : null;\n const expanded = live && (results.length > 0 || statusLine !== null);\n // A disabled row is never the keyboard's — not even when a list that changed under\n // the highlight put one there.\n const activeId = expanded && isOptionEnabled(results[active]) ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n const { rect, top, bottom, maxHeight, above } = useAnchoredPanel(fieldRef, expanded, { preferredHeight: 256 });\n const dir = useAnchorDir(fieldRef, expanded);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n };\n // The list is a layer on the kit's Escape stack while it shows (Wave 2, DUP-1).\n useDismissLayer(expanded, close);\n const take = (o: ComboOption<V>) => {\n if (o.disabled || locked) return;\n if (fillOnSelect) onChange(o.label);\n onSelect?.(o);\n close();\n };\n\n const hasLabel = label !== undefined;\n // `size=\"sm\"` is for an UNLABELLED field, as on Select: the floating label needs the\n // tall box. A number is the native attribute.\n const small = size === \"sm\" && !hasLabel;\n const nativeSize = typeof size === \"number\" ? size : undefined;\n // The caller's name wins; else the floating label, by reference so a non-string\n // label still names the field.\n const labelledBy =\n ariaLabel === undefined ? (ariaLabelledBy ?? (hasLabel ? labelId : undefined)) : undefined;\n // While locked the field 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 = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} display={disabledReasonDisplay} className=\"block\">\n {box}\n </LockedReason>\n ) : (\n box\n );\n\n return (\n // `relative`: the floating label and the live region's `sr-only` both need a\n // local containing block.\n <div className={cn(\"relative\", className)}>\n {/* A real <label for> (lenkbank), and still `aria-labelledby`'s target, so the\n name — and the listbox's, which borrows it — is exactly what it was. */}\n {hasLabel && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId}>\n {label}\n </ComboboxFieldLabel>\n )}\n {/* `\"hint\" in props`: the family's 0.22 rule (see `endHintRowProps`) — passed at\n all, even as `undefined`, a hint keeps the unlabelled field's end-hint row, so\n a \"?\" coming and going never re-parents the <input> under the caret (0.24, the\n last of the family to get it; the siblings had it in 0.23). */}\n <EndHintRow {...endHintRowProps(\"hint\" in props, hasLabel, hintParts.labelHint)}>\n {withLock(\n <div ref={fieldRef} className={cn(\"relative\", locked && \"cursor-not-allowed\")}>\n {icon && (\n <span\n aria-hidden\n className={cn(\n \"pointer-events-none absolute flex text-muted\",\n small ? \"start-2 [&>svg]:size-3.5\" : \"start-2.5 [&>svg]:size-4\",\n // Labelled, the icon sits on the VALUE's line — the one line box below\n // FIELD_FLOATING_PAD's `pt-4` and the 1px border — not on the middle of\n // the box. Centred on the box it rose into the top strip and sat on the\n // label's first letters, which float at the same start inset. On the\n // value line it is beside what it describes, and the label keeps the\n // start edge every other label in the form's column shares.\n // At Large the label stands above the field and the strip is gone\n // (0.33, §10.17): the value's line is under FIELD_BASE's `py-2`.\n hasLabel\n ? \"top-[calc(1rem+1px)] h-5 items-center large:top-[calc(0.5rem+1px)]\"\n : \"top-1/2 -translate-y-1/2\",\n )}\n >\n {icon}\n </span>\n )}\n <input\n {...rest}\n ref={ref}\n id={fieldId}\n size={nativeSize}\n type=\"text\"\n value={value}\n // Locked: focusable, `readOnly`, `aria-disabled` — see `disabledReason`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || rest.readOnly}\n aria-disabled={locked || rest[\"aria-disabled\"]}\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={expanded}\n // Required by the role, and set while closed too, as on the siblings.\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n aria-invalid={field.isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n autoComplete=\"off\"\n onFocus={(e) => {\n onFocus?.(e);\n // A locked field takes focus to say why, and opens nothing.\n if (locked) return;\n // A seeded field (keksdose's `initialQuery`) is looked up as it is\n // focused; an empty one asks nothing, because of `minChars`.\n setOpen(true);\n }}\n onBlur={(e) => {\n onBlur?.(e);\n close();\n }}\n onChange={(e) => {\n if (locked) return;\n onChange(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n // Locked: Enter is swallowed (no form submit, no caller's \"search now\");\n // every other key is the caller's and the caret's. No list to drive.\n if (locked) {\n if (e.key === \"Enter\") e.preventDefault();\n else onKeyDown?.(e);\n return;\n }\n // An Escape that closes the OPEN list is the list's, and is consumed before\n // the caller sees it. A caller whose Escape means \"close the panel\" (keksdose's\n // address search) otherwise closed the whole panel when the user only meant\n // to dismiss the suggestions — the opposite of what the docs promised.\n if (e.key === \"Escape\" && expanded && takeEscape(e)) return;\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n // Disabled rows are passed over; at either end the highlight stays put.\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n if (!open) setOpen(true);\n else setActive((i) => stepEnabled(results, i, 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n if (!open) setOpen(true);\n // From \"nothing highlighted\", Up goes to the bottom, as the APG has it.\n else setActive((i) => stepEnabled(results, i < 0 ? results.length : i, -1));\n } else if (e.key === \"Enter\") {\n // No row highlighted: the text is the answer, and a form's own submit\n // is left alone. Nor is a row taken by the Enter that confirms an IME\n // composition, which is the IME's (BUG-9).\n if (expanded && activeId && !isImeEnter(e)) {\n e.preventDefault();\n take(results[active]);\n }\n } else if (e.key === \"Tab\") {\n close();\n }\n }}\n className={cn(\n FIELD_BASE,\n hasLabel && FIELD_FLOATING_PAD,\n // Select's `SELECT_SM` box: a fixed 28px, so the caller's line height\n // cannot grow it.\n small && \"h-7 py-0 pe-2 ps-2 text-xs\",\n icon ? (small ? \"ps-7\" : \"ps-8\") : undefined,\n busy && (small ? \"pe-7\" : \"pe-9\"),\n field.isInvalid && FIELD_INVALID,\n inputClassName,\n )}\n />\n {busy && live && (\n // Decorative: the live region below already says \"Loading…\".\n <span\n className={cn(\n \"pointer-events-none absolute top-1/2 flex -translate-y-1/2\",\n small ? \"end-2\" : \"end-2.5\",\n )}\n >\n <Spinner label={null} className={small ? \"h-3.5 w-3.5\" : \"h-4 w-4\"} />\n </span>\n )}\n </div>,\n )}\n </EndHintRow>\n {/* With a \"?\" the label shares the top strip with it, after the field so the \"?\"\n follows the control in the tab order (as on Input and Select). */}\n {hasLabel && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId} className={LABEL_IN_ROW}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {/* The live region, kept mounted so a change IS an announcement: focus stays\n in the field, and without it a reader typed into a geocoder and heard only\n their own letters. The panel's visible line is aria-hidden in favour of\n this, which also carries the row count the eye reads off the list. */}\n <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {!live\n ? null\n : statusLine !== null\n ? statusLine\n : results.length > 0\n ? labels.resultCount(results.length)\n : null}\n </div>\n {expanded &&\n rect &&\n typeof document !== \"undefined\" &&\n createPortal(\n <div\n // Every press inside the list keeps focus in the field — rows, the status\n // line, the scrollbar — or the input's blur would close the list first.\n role=\"presentation\"\n // A kit layer, for the tour's layer order (0.50, docs/tours-harmonization.md §11.6).\n data-kit-layer=\"\"\n // Portalled out of the form's `dir`; the field's is put back. The list is\n // exactly the field's width, so `left` places it in either direction.\n dir={dir}\n onMouseDown={(e) => e.preventDefault()}\n // The family's suggestion box (DUP-4), as a column: the list scrolls inside\n // it and the status line stays under the rows.\n className={cn(SUGGESTION_LIST_CLASS, \"fixed z-50 flex flex-col overflow-hidden py-0\")}\n // Flipped above, the list hangs from its bottom edge: three rows under a\n // 256 px cap, placed by `top`, floated ~176 px above the field (keksdose K1),\n // and the rows come and go as the user types.\n style={{ ...(above ? { bottom } : { top }), left: rect.left, width: rect.width, maxHeight }}\n >\n {results.length > 0 && (\n <ul\n id={listboxId}\n role=\"listbox\"\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n className=\"min-h-0 flex-1 overflow-y-auto py-1\"\n >\n {results.map((o, i) => {\n const startsGroup = o.group != null && o.group !== results[i - 1]?.group;\n return (\n // Keyed by group AND value, as in the core panel: an option may\n // appear once per group.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {startsGroup && (\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 role=\"option\"\n // CHOSEN, never \"highlighted\" — the family's rule. This\n // field holds no value, so the only row that can be chosen\n // is the one whose label the text already is.\n aria-selected={fillOnSelect && o.label === value}\n // Listed, announced as unavailable, never taken: `take`\n // refuses it and the arrows pass it over.\n aria-disabled={o.disabled || undefined}\n tabIndex={-1}\n onClick={() => take(o)}\n onMouseEnter={() => {\n if (!o.disabled) setActive(i);\n }}\n className={cn(\n // The family's suggestion row (DUP-4): it hand-rolled its\n // own with a plain `truncate`, so a long address was cut at\n // Large, where Combobox's rows wrap (text size §10.17, T1).\n // A flex row here, for the icon and the two lines.\n suggestionRowClass(i === active && !o.disabled),\n \"flex items-center gap-2\",\n small && \"py-1 text-xs\",\n o.disabled && DISABLED_ROW_CLASS,\n o.group != null && \"ps-6\",\n )}\n >\n {o.icon && (\n <span aria-hidden className=\"shrink-0 text-muted\">\n {o.icon}\n </span>\n )}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate-until-large text-primary\">\n {o.label}\n </span>\n {o.sublabel && (\n <span\n className={cn(\n \"block truncate-until-large text-placeholder\",\n small ? \"text-caption\" : \"text-xs\",\n )}\n >\n {o.sublabel}\n </span>\n )}\n </span>\n </button>\n </li>\n </Fragment>\n );\n })}\n </ul>\n )}\n {statusLine !== null && (\n // Visible, not live — the region above speaks for it.\n <div\n aria-hidden\n className={cn(\n small ? \"px-3 py-1.5 text-xs\" : \"px-3 py-2 text-sm\",\n failed && !hasStatus ? \"text-danger\" : \"text-muted\",\n results.length > 0 && \"border-t border-subtle text-xs\",\n )}\n >\n {statusLine}\n </div>\n )}\n </div>,\n document.body,\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -505,6 +505,7 @@ function ComboboxPanel({
505
505
  {
506
506
  ...rest,
507
507
  ref: panelRef,
508
+ "data-kit-layer": "",
508
509
  dir,
509
510
  onKeyDown,
510
511
  onPointerDownCapture: (e) => {