@eifi1/ui-kit 0.4.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 (229) hide show
  1. package/README.md +219 -0
  2. package/dist/components/account-settings.d.ts +71 -0
  3. package/dist/components/account-settings.js +159 -0
  4. package/dist/components/account-settings.js.map +1 -0
  5. package/dist/components/alert-banner.d.ts +20 -0
  6. package/dist/components/alert-banner.js +39 -0
  7. package/dist/components/alert-banner.js.map +1 -0
  8. package/dist/components/amount-input.d.ts +117 -0
  9. package/dist/components/amount-input.js +243 -0
  10. package/dist/components/amount-input.js.map +1 -0
  11. package/dist/components/calculator.d.ts +42 -0
  12. package/dist/components/calculator.js +161 -0
  13. package/dist/components/calculator.js.map +1 -0
  14. package/dist/components/chart.d.ts +58 -0
  15. package/dist/components/chart.js +158 -0
  16. package/dist/components/chart.js.map +1 -0
  17. package/dist/components/combobox-core.d.ts +77 -0
  18. package/dist/components/combobox-core.js +263 -0
  19. package/dist/components/combobox-core.js.map +1 -0
  20. package/dist/components/combobox.d.ts +141 -0
  21. package/dist/components/combobox.js +550 -0
  22. package/dist/components/combobox.js.map +1 -0
  23. package/dist/components/currency-select.d.ts +37 -0
  24. package/dist/components/currency-select.js +131 -0
  25. package/dist/components/currency-select.js.map +1 -0
  26. package/dist/components/data-table-filter-popover.d.ts +28 -0
  27. package/dist/components/data-table-filter-popover.js +201 -0
  28. package/dist/components/data-table-filter-popover.js.map +1 -0
  29. package/dist/components/data-table-filters.d.ts +5 -0
  30. package/dist/components/data-table-filters.js +118 -0
  31. package/dist/components/data-table-filters.js.map +1 -0
  32. package/dist/components/data-table-labels.d.ts +33 -0
  33. package/dist/components/data-table-labels.js +53 -0
  34. package/dist/components/data-table-labels.js.map +1 -0
  35. package/dist/components/data-table-pagination.d.ts +18 -0
  36. package/dist/components/data-table-pagination.js +92 -0
  37. package/dist/components/data-table-pagination.js.map +1 -0
  38. package/dist/components/data-table-sort.d.ts +23 -0
  39. package/dist/components/data-table-sort.js +55 -0
  40. package/dist/components/data-table-sort.js.map +1 -0
  41. package/dist/components/data-table.d.ts +5 -0
  42. package/dist/components/data-table.js +926 -0
  43. package/dist/components/data-table.js.map +1 -0
  44. package/dist/components/date-picker.d.ts +85 -0
  45. package/dist/components/date-picker.js +253 -0
  46. package/dist/components/date-picker.js.map +1 -0
  47. package/dist/components/dropdown.d.ts +98 -0
  48. package/dist/components/dropdown.js +122 -0
  49. package/dist/components/dropdown.js.map +1 -0
  50. package/dist/components/entity-combobox.d.ts +49 -0
  51. package/dist/components/entity-combobox.js +115 -0
  52. package/dist/components/entity-combobox.js.map +1 -0
  53. package/dist/components/file-dropzone.d.ts +18 -0
  54. package/dist/components/file-dropzone.js +101 -0
  55. package/dist/components/file-dropzone.js.map +1 -0
  56. package/dist/components/full-bleed-dialog.d.ts +57 -0
  57. package/dist/components/full-bleed-dialog.js +68 -0
  58. package/dist/components/full-bleed-dialog.js.map +1 -0
  59. package/dist/components/grouped-picker.d.ts +37 -0
  60. package/dist/components/grouped-picker.js +87 -0
  61. package/dist/components/grouped-picker.js.map +1 -0
  62. package/dist/components/hover-menu.d.ts +16 -0
  63. package/dist/components/hover-menu.js +113 -0
  64. package/dist/components/hover-menu.js.map +1 -0
  65. package/dist/components/mini-calendar.d.ts +33 -0
  66. package/dist/components/mini-calendar.js +133 -0
  67. package/dist/components/mini-calendar.js.map +1 -0
  68. package/dist/components/modal.d.ts +76 -0
  69. package/dist/components/modal.js +156 -0
  70. package/dist/components/modal.js.map +1 -0
  71. package/dist/components/multi-entity-combobox.d.ts +47 -0
  72. package/dist/components/multi-entity-combobox.js +116 -0
  73. package/dist/components/multi-entity-combobox.js.map +1 -0
  74. package/dist/components/multi-select.d.ts +39 -0
  75. package/dist/components/multi-select.js +132 -0
  76. package/dist/components/multi-select.js.map +1 -0
  77. package/dist/components/number-input.d.ts +74 -0
  78. package/dist/components/number-input.js +114 -0
  79. package/dist/components/number-input.js.map +1 -0
  80. package/dist/components/numpad-sheet.d.ts +45 -0
  81. package/dist/components/numpad-sheet.js +102 -0
  82. package/dist/components/numpad-sheet.js.map +1 -0
  83. package/dist/components/picker-sheet.d.ts +44 -0
  84. package/dist/components/picker-sheet.js +78 -0
  85. package/dist/components/picker-sheet.js.map +1 -0
  86. package/dist/components/popover.d.ts +20 -0
  87. package/dist/components/popover.js +46 -0
  88. package/dist/components/popover.js.map +1 -0
  89. package/dist/components/search-field.d.ts +52 -0
  90. package/dist/components/search-field.js +64 -0
  91. package/dist/components/search-field.js.map +1 -0
  92. package/dist/components/settings-fields.d.ts +39 -0
  93. package/dist/components/settings-fields.js +17 -0
  94. package/dist/components/settings-fields.js.map +1 -0
  95. package/dist/components/swipeable-row.d.ts +60 -0
  96. package/dist/components/swipeable-row.js +115 -0
  97. package/dist/components/swipeable-row.js.map +1 -0
  98. package/dist/components/toggle-group.d.ts +34 -0
  99. package/dist/components/toggle-group.js +90 -0
  100. package/dist/components/toggle-group.js.map +1 -0
  101. package/dist/components/tooltip.d.ts +86 -0
  102. package/dist/components/tooltip.js +171 -0
  103. package/dist/components/tooltip.js.map +1 -0
  104. package/dist/components/ui.d.ts +239 -0
  105. package/dist/components/ui.js +512 -0
  106. package/dist/components/ui.js.map +1 -0
  107. package/dist/components/use-mobile-reveal.d.ts +31 -0
  108. package/dist/components/use-mobile-reveal.js +33 -0
  109. package/dist/components/use-mobile-reveal.js.map +1 -0
  110. package/dist/components/use-table-state.d.ts +54 -0
  111. package/dist/components/use-table-state.js +153 -0
  112. package/dist/components/use-table-state.js.map +1 -0
  113. package/dist/components/user-avatar.d.ts +23 -0
  114. package/dist/components/user-avatar.js +33 -0
  115. package/dist/components/user-avatar.js.map +1 -0
  116. package/dist/components/wizard-stepper.d.ts +14 -0
  117. package/dist/components/wizard-stepper.js +27 -0
  118. package/dist/components/wizard-stepper.js.map +1 -0
  119. package/dist/data-table-filters-aXyf0Xub.d.ts +261 -0
  120. package/dist/feedback/feedback-attachment.d.ts +63 -0
  121. package/dist/feedback/feedback-attachment.js +160 -0
  122. package/dist/feedback/feedback-attachment.js.map +1 -0
  123. package/dist/feedback/feedback-dialog.d.ts +72 -0
  124. package/dist/feedback/feedback-dialog.js +109 -0
  125. package/dist/feedback/feedback-dialog.js.map +1 -0
  126. package/dist/feedback/feedback-inbox.d.ts +226 -0
  127. package/dist/feedback/feedback-inbox.js +315 -0
  128. package/dist/feedback/feedback-inbox.js.map +1 -0
  129. package/dist/hooks/use-anchored-panel.d.ts +80 -0
  130. package/dist/hooks/use-anchored-panel.js +71 -0
  131. package/dist/hooks/use-anchored-panel.js.map +1 -0
  132. package/dist/hooks/use-anchored-rect.d.ts +24 -0
  133. package/dist/hooks/use-anchored-rect.js +40 -0
  134. package/dist/hooks/use-anchored-rect.js.map +1 -0
  135. package/dist/hooks/use-body-scroll-lock.d.ts +9 -0
  136. package/dist/hooks/use-body-scroll-lock.js +41 -0
  137. package/dist/hooks/use-body-scroll-lock.js.map +1 -0
  138. package/dist/hooks/use-close-transition.d.ts +41 -0
  139. package/dist/hooks/use-close-transition.js +36 -0
  140. package/dist/hooks/use-close-transition.js.map +1 -0
  141. package/dist/hooks/use-dismiss.d.ts +34 -0
  142. package/dist/hooks/use-dismiss.js +41 -0
  143. package/dist/hooks/use-dismiss.js.map +1 -0
  144. package/dist/hooks/use-media-query.d.ts +8 -0
  145. package/dist/hooks/use-media-query.js +18 -0
  146. package/dist/hooks/use-media-query.js.map +1 -0
  147. package/dist/hooks/use-overlay-history.d.ts +10 -0
  148. package/dist/hooks/use-overlay-history.js +99 -0
  149. package/dist/hooks/use-overlay-history.js.map +1 -0
  150. package/dist/hooks/use-row-swipe.d.ts +30 -0
  151. package/dist/hooks/use-row-swipe.js +128 -0
  152. package/dist/hooks/use-row-swipe.js.map +1 -0
  153. package/dist/index.d.ts +75 -0
  154. package/dist/index.js +90 -0
  155. package/dist/index.js.map +1 -0
  156. package/dist/lib/calc.d.ts +89 -0
  157. package/dist/lib/calc.js +180 -0
  158. package/dist/lib/calc.js.map +1 -0
  159. package/dist/lib/cn.d.ts +6 -0
  160. package/dist/lib/cn.js +7 -0
  161. package/dist/lib/cn.js.map +1 -0
  162. package/dist/lib/dates.d.ts +84 -0
  163. package/dist/lib/dates.js +108 -0
  164. package/dist/lib/dates.js.map +1 -0
  165. package/dist/lib/logger.d.ts +17 -0
  166. package/dist/lib/logger.js +30 -0
  167. package/dist/lib/logger.js.map +1 -0
  168. package/dist/lib/safe-storage.d.ts +23 -0
  169. package/dist/lib/safe-storage.js +20 -0
  170. package/dist/lib/safe-storage.js.map +1 -0
  171. package/dist/search/command-palette.d.ts +71 -0
  172. package/dist/search/command-palette.js +198 -0
  173. package/dist/search/command-palette.js.map +1 -0
  174. package/dist/shell/app-shell.d.ts +64 -0
  175. package/dist/shell/app-shell.js +237 -0
  176. package/dist/shell/app-shell.js.map +1 -0
  177. package/dist/shell/option-switcher-menu.d.ts +24 -0
  178. package/dist/shell/option-switcher-menu.js +44 -0
  179. package/dist/shell/option-switcher-menu.js.map +1 -0
  180. package/dist/shell/role-switcher.d.ts +19 -0
  181. package/dist/shell/role-switcher.js +28 -0
  182. package/dist/shell/role-switcher.js.map +1 -0
  183. package/dist/shell/top-bar.d.ts +15 -0
  184. package/dist/shell/top-bar.js +29 -0
  185. package/dist/shell/top-bar.js.map +1 -0
  186. package/dist/shell/topbar-action-menu.d.ts +44 -0
  187. package/dist/shell/topbar-action-menu.js +49 -0
  188. package/dist/shell/topbar-action-menu.js.map +1 -0
  189. package/dist/shell/topbar-controls.d.ts +54 -0
  190. package/dist/shell/topbar-controls.js +152 -0
  191. package/dist/shell/topbar-controls.js.map +1 -0
  192. package/dist/theme/chart-palette.d.ts +60 -0
  193. package/dist/theme/chart-palette.js +79 -0
  194. package/dist/theme/chart-palette.js.map +1 -0
  195. package/dist/theme/palette-presets.d.ts +39 -0
  196. package/dist/theme/palette-presets.js +440 -0
  197. package/dist/theme/palette-presets.js.map +1 -0
  198. package/dist/theme/palette-store.d.ts +45 -0
  199. package/dist/theme/palette-store.js +52 -0
  200. package/dist/theme/palette-store.js.map +1 -0
  201. package/dist/theme/theme-store.d.ts +57 -0
  202. package/dist/theme/theme-store.js +96 -0
  203. package/dist/theme/theme-store.js.map +1 -0
  204. package/dist/tour/tour.d.ts +104 -0
  205. package/dist/tour/tour.js +351 -0
  206. package/dist/tour/tour.js.map +1 -0
  207. package/dist/wizard/stepper-nav.d.ts +30 -0
  208. package/dist/wizard/stepper-nav.js +146 -0
  209. package/dist/wizard/stepper-nav.js.map +1 -0
  210. package/dist/wizard/types.d.ts +124 -0
  211. package/dist/wizard/types.js +24 -0
  212. package/dist/wizard/types.js.map +1 -0
  213. package/dist/wizard/use-wizard.d.ts +6 -0
  214. package/dist/wizard/use-wizard.js +222 -0
  215. package/dist/wizard/use-wizard.js.map +1 -0
  216. package/dist/wizard/validation.d.ts +23 -0
  217. package/dist/wizard/validation.js +13 -0
  218. package/dist/wizard/validation.js.map +1 -0
  219. package/dist/wizard/wizard-context.d.ts +19 -0
  220. package/dist/wizard/wizard-context.js +15 -0
  221. package/dist/wizard/wizard-context.js.map +1 -0
  222. package/dist/wizard/wizard-step.d.ts +11 -0
  223. package/dist/wizard/wizard-step.js +12 -0
  224. package/dist/wizard/wizard-step.js.map +1 -0
  225. package/dist/wizard/wizard-summary.d.ts +18 -0
  226. package/dist/wizard/wizard-summary.js +41 -0
  227. package/dist/wizard/wizard-summary.js.map +1 -0
  228. package/package.json +87 -0
  229. package/tokens.css +254 -0
@@ -0,0 +1,141 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { ComboOption } from './combobox-core.js';
4
+ import '../hooks/use-anchored-rect.js';
5
+ import '../hooks/use-anchored-panel.js';
6
+
7
+ /**
8
+ * Free-text combobox that looks like the shared {@link Select} (same FIELD_BASE
9
+ * styling, chevron and static floating label) but lets the user type a value
10
+ * that isn't in the list — e.g. naming a brand-new payee. Picking from the
11
+ * filtered suggestion list fills the value; typing keeps whatever was entered.
12
+ *
13
+ * Use this instead of an `<Input list="…">` + `<datalist>` so the control is
14
+ * visually and behaviourally consistent with the other dropdowns (feedback
15
+ * #230 — the payee field looked/behaved differently from every other select).
16
+ */
17
+ declare function Combobox({ value, onChange, options, label, id, placeholder, className, groupBy, maxSuggestions, searchPlaceholder, closeLabel, createLabel, invalid, optionAdornment, autoFocus, onBlur, onSubmit, "aria-label": ariaLabel, }: {
18
+ value: string;
19
+ onChange: (v: string) => void;
20
+ /** Suggestion pool (e.g. existing payee names). */
21
+ options: string[];
22
+ label?: string;
23
+ id?: string;
24
+ placeholder?: string;
25
+ className?: string;
26
+ /** Required and unanswered — see {@link Input}'s `invalid`. Set on the `<input>`
27
+ * itself rather than on the wrapper, which is what let Keksdose's account picker
28
+ * delete the `[&_input]:…` copy of {@link FIELD_INVALID} it had been carrying
29
+ * because this component had no `invalid` of its own. */
30
+ invalid?: boolean;
31
+ /** Heading an option belongs under. Supplying it makes this list read exactly
32
+ * like {@link InlineEntityCombobox}'s — one heading per group with its rows
33
+ * indented beneath — instead of a flat list (feedback #136 rework: the payee
34
+ * field sat next to the newly-grouped category field and no longer matched).
35
+ * Omit for a plain list. */
36
+ groupBy?: (option: string) => string;
37
+ /**
38
+ * Something to show at the far end of an option's row — a badge saying what the
39
+ * option IS, as distinct from what it is called.
40
+ *
41
+ * Keksdose's category-name field is the case: its proposals carry per-locale
42
+ * names, so picking one stores a category that follows the UI language, and
43
+ * picking a look-alike custom name does not. That difference is invisible in the
44
+ * label and decides what the row DOES, so the row has to show it. Returning
45
+ * `null` for an option renders nothing and costs no layout.
46
+ *
47
+ * Not rendered in the create row, which by definition names nothing in the pool.
48
+ */
49
+ optionAdornment?: (option: string) => ReactNode;
50
+ /** Focus on mount, the way `<input autoFocus>` does — and, through `onFocus`, open
51
+ * the list with it. A click-the-value inline editor needs it: the cell the user
52
+ * clicked names the field the caret should land in. {@link InlineEntityCombobox}
53
+ * has had it since live #218. */
54
+ autoFocus?: boolean;
55
+ /** Focus genuinely LEFT the field. Not fired by picking a row — the rows suppress
56
+ * `mousedown`, so the input never blurs — which is what makes it usable as a
57
+ * "close the inline editor" signal. */
58
+ onBlur?: () => void;
59
+ /** Enter pressed with no row highlighted, i.e. "I mean what I typed". The list
60
+ * closes either way; this is for a caller whose Enter also submits a row editor. */
61
+ onSubmit?: () => void;
62
+ "aria-label"?: string;
63
+ /** Rows to offer when the field is empty. 8 on a dropdown, where that is all
64
+ * that fits; the phone sheet asks for more because it has a screen. */
65
+ maxSuggestions?: number;
66
+ searchPlaceholder?: string;
67
+ closeLabel?: string;
68
+ /**
69
+ * Label for the row that COMMITS a value the list does not contain — e.g.
70
+ * `(v) => \`Add "${v}" as payee\``. Supplying it turns the free text into
71
+ * something you confirm rather than something you leave behind.
72
+ *
73
+ * Keksdose live #212: *"For the payee input have an apply button, when input is
74
+ * placed like 'add xxx as payee' to confirm. Now it can be put in and closed with
75
+ * the right top X, which seems not intuitive."* On the phone sheet the search box
76
+ * IS the value, so typing a brand-new name and dismissing the sheet did commit it —
77
+ * but the only control on offer was the close X, which reads as "discard". A named
78
+ * affordance says what the typing did.
79
+ *
80
+ * Omit and the row is not rendered, which is the right default for a list whose
81
+ * values are all supposed to already exist.
82
+ */
83
+ createLabel?: (value: string) => string;
84
+ }): react.JSX.Element;
85
+ /**
86
+ * The id-keyed sibling of {@link Combobox}: identical anatomy (a real text input
87
+ * with the shared field styling, chevron and inline as-you-type filtering) but
88
+ * the value is an option id, not free text. Built so an entity picker can sit
89
+ * next to a free-text combobox without any visible difference — a button-trigger
90
+ * panel picker ({@link EntityCombobox}) never matches an input field exactly.
91
+ *
92
+ * Text-vs-value reconciliation: the input's text is transient. Picking a row or
93
+ * typing an exact (unique) label commits that option; emptying the text commits
94
+ * a clear; anything else reverts to the selected option's label on blur/Escape.
95
+ *
96
+ * **On a phone the list opens as a full-screen sheet**, exactly like {@link Combobox}
97
+ * beside it (Keksdose live #200, extended by dev#477 — the account picker moved onto
98
+ * this component and must not lose its sheet). The sheet carries its OWN query state
99
+ * rather than reusing `text`: `text` is what {@link reconcile} judges on blur, so a
100
+ * sheet that emptied it to show the full list would read as "the user cleared the
101
+ * field" the moment it closed.
102
+ */
103
+ declare function InlineEntityCombobox<V extends string | number>({ value, onChange, options, label, id, placeholder, className, disabled, autoFocus, searchPlaceholder, emptyLabel, closeLabel, clearable, clearLabel, invalid, "aria-label": ariaLabel, }: {
104
+ /** Selected option id, or null when nothing is selected. */
105
+ value: V | null;
106
+ /** Required and unanswered — see {@link Combobox}'s `invalid`. */
107
+ invalid?: boolean;
108
+ /** A picked/typed option emits its id; emptying the field emits `null`. */
109
+ onChange: (v: V | null) => void;
110
+ options: ComboOption<V>[];
111
+ label?: string;
112
+ id?: string;
113
+ placeholder?: string;
114
+ className?: string;
115
+ disabled?: boolean;
116
+ /** Focus on mount, the way `<input autoFocus>` does — and, through `onFocus`
117
+ * below, open the list with it. A click-the-value editor needs it: the cell
118
+ * the user clicked names the field the editor should land the caret in, and
119
+ * without a passthrough this component's fixed prop list puts the `<input>`
120
+ * out of a caller's reach entirely (Keksdose feedback live #218). */
121
+ autoFocus?: boolean;
122
+ /** Phone sheet only — the anchored list has the field itself to type in. */
123
+ searchPlaceholder?: string;
124
+ /** Phone sheet only: a full screen showing nothing has to say why. The anchored
125
+ * list simply does not open. */
126
+ emptyLabel?: string;
127
+ closeLabel?: string;
128
+ /** Offer a clear "×" in place of the chevron whenever something is selected.
129
+ *
130
+ * Emptying the text already clears (see {@link reconcile}), and on a desktop that
131
+ * is the fast way. It is not a way at all on a PHONE: touching the field opens the
132
+ * full-screen sheet, which covers the very input the text would have been deleted
133
+ * from — so a field that had been answered could not be UNanswered by any gesture
134
+ * the screen offered (Keksdose live #236, filed from a phone). The "×" is the one
135
+ * affordance both shells share, and it clears without opening anything. */
136
+ clearable?: boolean;
137
+ clearLabel?: string;
138
+ "aria-label"?: string;
139
+ }): react.JSX.Element;
140
+
141
+ export { Combobox, InlineEntityCombobox };
@@ -0,0 +1,550 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, useId, useMemo, useRef, useState } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { ChevronDown, X } from "lucide-react";
5
+ import { FieldLabel, FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID, PHONE_QUERY } from "./ui.js";
6
+ import { cn } from "../lib/cn.js";
7
+ import { useDropdown } from "./dropdown.js";
8
+ import { useAnchoredPanel } from "../hooks/use-anchored-panel.js";
9
+ import { useMediaQuery } from "../hooks/use-media-query.js";
10
+ import { PickerSheet, SHEET_ROW_CLASS } from "./picker-sheet.js";
11
+ const LIST_CLASS = "max-h-64 overflow-y-auto rounded-md border border-slate-200 bg-white py-1 shadow-lg dark:border-slate-700 dark:bg-slate-900";
12
+ function usePrimaryPressOnly() {
13
+ const auxPress = useRef(false);
14
+ return {
15
+ onMouseDown: (e) => {
16
+ if (e.button === 0) return;
17
+ auxPress.current = true;
18
+ setTimeout(() => {
19
+ auxPress.current = false;
20
+ }, 0);
21
+ },
22
+ /** True while handling the focus a non-primary press just caused. */
23
+ fromAuxButton: () => auxPress.current
24
+ };
25
+ }
26
+ function SuggestionList({
27
+ anchorRef,
28
+ panelRef,
29
+ children
30
+ }) {
31
+ const { rect, top, maxHeight } = useAnchoredPanel(anchorRef, true, { preferredHeight: 256 });
32
+ if (!rect) return null;
33
+ return createPortal(
34
+ /* @__PURE__ */ jsx(
35
+ "div",
36
+ {
37
+ ref: panelRef,
38
+ className: "fixed z-50",
39
+ style: { top, left: rect.left, width: rect.width },
40
+ children: /* @__PURE__ */ jsx("ul", { role: "listbox", className: LIST_CLASS, style: { maxHeight }, children })
41
+ }
42
+ ),
43
+ document.body
44
+ );
45
+ }
46
+ const rowClass = (isActive) => cn(
47
+ "block w-full truncate px-3 py-1.5 text-left text-sm text-slate-900 dark:text-slate-100",
48
+ isActive ? "bg-slate-100 dark:bg-slate-800" : "hover:bg-slate-50 dark:hover:bg-slate-800"
49
+ );
50
+ function Combobox({
51
+ value,
52
+ onChange,
53
+ options,
54
+ label,
55
+ id,
56
+ placeholder,
57
+ className,
58
+ groupBy,
59
+ maxSuggestions,
60
+ searchPlaceholder,
61
+ closeLabel,
62
+ createLabel,
63
+ invalid,
64
+ optionAdornment,
65
+ autoFocus,
66
+ onBlur,
67
+ onSubmit,
68
+ "aria-label": ariaLabel
69
+ }) {
70
+ const generated = useId();
71
+ const fieldId = id ?? generated;
72
+ const isPhone = useMediaQuery(PHONE_QUERY, false);
73
+ const { open, setOpen, wrapperRef, panelRef } = useDropdown({ backCloses: !isPhone });
74
+ const primaryOnly = usePrimaryPressOnly();
75
+ const [active, setActive] = useState(-1);
76
+ const sheetInputRef = useRef(null);
77
+ const fieldRef = useRef(null);
78
+ const [typing, setTyping] = useState(false);
79
+ const query = typing ? value.trim().toLowerCase() : "";
80
+ const matches = useMemo(() => {
81
+ const seen = /* @__PURE__ */ new Set();
82
+ const uniq = options.filter((o) => o && !seen.has(o) && seen.add(o));
83
+ let ranked;
84
+ const cap = maxSuggestions ?? (isPhone ? 50 : 8);
85
+ if (!query) {
86
+ ranked = uniq.slice(0, cap);
87
+ } else {
88
+ const starts = uniq.filter((o) => o.toLowerCase().startsWith(query));
89
+ const contains = uniq.filter(
90
+ (o) => !o.toLowerCase().startsWith(query) && o.toLowerCase().includes(query)
91
+ );
92
+ ranked = [...starts, ...contains].slice(0, cap);
93
+ }
94
+ if (!groupBy) return ranked;
95
+ const byGroup = /* @__PURE__ */ new Map();
96
+ for (const o of ranked) {
97
+ const key = groupBy(o);
98
+ if (!byGroup.has(key)) byGroup.set(key, []);
99
+ byGroup.get(key).push(o);
100
+ }
101
+ return [...byGroup.values()].flat();
102
+ }, [options, query, groupBy, maxSuggestions, isPhone]);
103
+ const commit = (v) => {
104
+ onChange(v);
105
+ setOpen(false);
106
+ setActive(-1);
107
+ setTyping(false);
108
+ };
109
+ const typed = value.trim();
110
+ const createRow = createLabel && typed.length > 0 && !options.some((o) => o.toLowerCase() === typed.toLowerCase()) ? createLabel(typed) : null;
111
+ return /* @__PURE__ */ jsxs("div", { ref: wrapperRef, className: cn("relative", className), children: [
112
+ label !== void 0 && /* @__PURE__ */ jsx(FieldLabel, { children: label }),
113
+ /* @__PURE__ */ jsxs("div", { ref: fieldRef, className: "relative", children: [
114
+ /* @__PURE__ */ jsx(
115
+ "input",
116
+ {
117
+ id: fieldId,
118
+ value,
119
+ placeholder,
120
+ "aria-label": ariaLabel ?? label,
121
+ role: "combobox",
122
+ "aria-expanded": open,
123
+ "aria-autocomplete": "list",
124
+ "aria-invalid": invalid || void 0,
125
+ autoComplete: "off",
126
+ autoFocus,
127
+ onBlur,
128
+ onMouseDown: primaryOnly.onMouseDown,
129
+ onFocus: () => {
130
+ if (primaryOnly.fromAuxButton()) return;
131
+ setOpen(true);
132
+ setTyping(false);
133
+ if (isPhone) sheetInputRef.current?.focus();
134
+ },
135
+ onClick: () => {
136
+ setOpen(true);
137
+ setTyping(false);
138
+ },
139
+ inputMode: isPhone ? "none" : void 0,
140
+ onChange: (e) => {
141
+ onChange(e.target.value);
142
+ setOpen(true);
143
+ setActive(-1);
144
+ setTyping(true);
145
+ },
146
+ onKeyDown: (e) => {
147
+ if (e.key === "ArrowDown") {
148
+ e.preventDefault();
149
+ setOpen(true);
150
+ setActive((i) => Math.min(i + 1, matches.length - 1));
151
+ } else if (e.key === "ArrowUp") {
152
+ e.preventDefault();
153
+ setActive((i) => Math.max(i - 1, 0));
154
+ } else if (e.key === "Enter") {
155
+ if (open && active >= 0 && active < matches.length) {
156
+ e.preventDefault();
157
+ commit(matches[active]);
158
+ } else {
159
+ if (onSubmit) e.preventDefault();
160
+ setOpen(false);
161
+ onSubmit?.();
162
+ }
163
+ } else if (e.key === "Escape") {
164
+ setOpen(false);
165
+ setActive(-1);
166
+ }
167
+ },
168
+ className: cn(FIELD_BASE, label !== void 0 && FIELD_FLOATING_PAD, "pr-9", invalid && FIELD_INVALID)
169
+ }
170
+ ),
171
+ /* @__PURE__ */ jsx(
172
+ ChevronDown,
173
+ {
174
+ "aria-hidden": true,
175
+ onMouseDown: (e) => {
176
+ e.preventDefault();
177
+ setOpen((o) => !o);
178
+ },
179
+ className: "absolute right-2.5 top-1/2 size-4 -translate-y-1/2 cursor-pointer text-slate-400 dark:text-slate-500"
180
+ }
181
+ )
182
+ ] }),
183
+ isPhone && /* @__PURE__ */ jsx(
184
+ PickerSheet,
185
+ {
186
+ open,
187
+ onClose: () => setOpen(false),
188
+ title: label,
189
+ query: value,
190
+ onQueryChange: (v) => {
191
+ onChange(v);
192
+ setTyping(true);
193
+ },
194
+ searchPlaceholder: searchPlaceholder ?? placeholder,
195
+ inputRef: sheetInputRef,
196
+ closeLabel,
197
+ children: /* @__PURE__ */ jsxs("ul", { role: "listbox", children: [
198
+ createRow && /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": false, children: /* @__PURE__ */ jsx(
199
+ "button",
200
+ {
201
+ type: "button",
202
+ onClick: () => commit(typed),
203
+ className: cn(SHEET_ROW_CLASS, "font-medium text-teal-700 dark:text-teal-300"),
204
+ children: createRow
205
+ }
206
+ ) }),
207
+ matches.map((o) => {
208
+ const group = groupBy?.(o);
209
+ const startsGroup = group != null && group !== (matches[matches.indexOf(o) - 1] && groupBy?.(matches[matches.indexOf(o) - 1]));
210
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
211
+ startsGroup && /* @__PURE__ */ jsx(
212
+ "li",
213
+ {
214
+ role: "presentation",
215
+ className: "px-4 pb-0.5 pt-3 text-[11px] font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400",
216
+ children: group
217
+ }
218
+ ),
219
+ /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": o === value, children: /* @__PURE__ */ jsxs(
220
+ "button",
221
+ {
222
+ type: "button",
223
+ onClick: () => commit(o),
224
+ className: cn(
225
+ SHEET_ROW_CLASS,
226
+ optionAdornment && "flex items-center justify-between gap-2"
227
+ ),
228
+ children: [
229
+ optionAdornment ? /* @__PURE__ */ jsx("span", { className: "truncate", children: o }) : o,
230
+ optionAdornment?.(o)
231
+ ]
232
+ }
233
+ ) })
234
+ ] }, o);
235
+ })
236
+ ] })
237
+ }
238
+ ),
239
+ !isPhone && open && (matches.length > 0 || createRow) && /* @__PURE__ */ jsxs(SuggestionList, { anchorRef: fieldRef, panelRef, children: [
240
+ createRow && /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": false, children: /* @__PURE__ */ jsx(
241
+ "button",
242
+ {
243
+ type: "button",
244
+ onMouseDown: (e) => {
245
+ e.preventDefault();
246
+ commit(typed);
247
+ },
248
+ className: cn(rowClass(false), "font-medium text-teal-700 dark:text-teal-300"),
249
+ children: createRow
250
+ }
251
+ ) }),
252
+ matches.map((o, i) => {
253
+ const group = groupBy?.(o);
254
+ const startsGroup = group != null && group !== (matches[i - 1] && groupBy?.(matches[i - 1]));
255
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
256
+ startsGroup && /* @__PURE__ */ jsx(
257
+ "li",
258
+ {
259
+ role: "presentation",
260
+ className: "px-3 pb-0.5 pt-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500 first:pt-1 dark:text-slate-400",
261
+ children: group
262
+ }
263
+ ),
264
+ /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": i === active, children: /* @__PURE__ */ jsxs(
265
+ "button",
266
+ {
267
+ type: "button",
268
+ onMouseDown: (e) => {
269
+ e.preventDefault();
270
+ commit(o);
271
+ },
272
+ onMouseEnter: () => setActive(i),
273
+ className: cn(
274
+ rowClass(i === active),
275
+ group != null && "pl-6",
276
+ // A row with a badge is a flex row so the label truncates and the
277
+ // badge keeps its width; without one it stays the plain block the
278
+ // other lists render, so nothing shifts for callers that pass none.
279
+ optionAdornment && "flex items-center justify-between gap-2"
280
+ ),
281
+ children: [
282
+ optionAdornment ? /* @__PURE__ */ jsx("span", { className: "truncate", children: o }) : o,
283
+ optionAdornment?.(o)
284
+ ]
285
+ }
286
+ ) })
287
+ ] }, o);
288
+ })
289
+ ] })
290
+ ] });
291
+ }
292
+ function InlineEntityCombobox({
293
+ value,
294
+ onChange,
295
+ options,
296
+ label,
297
+ id,
298
+ placeholder,
299
+ className,
300
+ disabled,
301
+ autoFocus,
302
+ searchPlaceholder,
303
+ emptyLabel,
304
+ closeLabel,
305
+ clearable,
306
+ clearLabel,
307
+ invalid,
308
+ "aria-label": ariaLabel
309
+ }) {
310
+ const generated = useId();
311
+ const fieldId = id ?? generated;
312
+ const isPhone = useMediaQuery(PHONE_QUERY, false);
313
+ const { open, setOpen, wrapperRef, panelRef } = useDropdown({ backCloses: !isPhone });
314
+ const primaryOnly = usePrimaryPressOnly();
315
+ const [active, setActive] = useState(-1);
316
+ const [text, setText] = useState(null);
317
+ const sheetInputRef = useRef(null);
318
+ const fieldRef = useRef(null);
319
+ const [sheetQuery, setSheetQuery] = useState("");
320
+ const selected = useMemo(
321
+ () => value == null ? null : options.find((o) => o.value === value) ?? null,
322
+ [options, value]
323
+ );
324
+ const shown = text ?? selected?.label ?? "";
325
+ const showClear = Boolean(clearable && value != null && !disabled);
326
+ const typedQuery = text !== null && text !== (selected?.label ?? "") ? text.trim().toLowerCase() : "";
327
+ const query = isPhone ? sheetQuery.trim().toLowerCase() : typedQuery;
328
+ const matches = useMemo(() => {
329
+ let ranked = options;
330
+ if (query) {
331
+ const hit = (s) => s?.toLowerCase().includes(query) ?? false;
332
+ const starts = options.filter((o) => o.label.toLowerCase().startsWith(query));
333
+ const rest = options.filter(
334
+ (o) => !o.label.toLowerCase().startsWith(query) && (hit(o.label) || hit(o.sublabel) || hit(o.group))
335
+ );
336
+ ranked = [...starts, ...rest];
337
+ }
338
+ if (!ranked.some((o) => o.group)) return ranked;
339
+ const blocks = /* @__PURE__ */ new Map();
340
+ for (const o of ranked) {
341
+ const key = o.group ?? "";
342
+ if (!blocks.has(key)) blocks.set(key, []);
343
+ blocks.get(key).push(o);
344
+ }
345
+ return [...blocks.values()].flat();
346
+ }, [options, query]);
347
+ const close = () => {
348
+ setOpen(false);
349
+ setActive(-1);
350
+ setSheetQuery("");
351
+ };
352
+ const commit = (o) => {
353
+ if (o.value !== value) onChange(o.value);
354
+ setText(null);
355
+ close();
356
+ };
357
+ const reconcile = () => {
358
+ if (text !== null) {
359
+ const q = text.trim();
360
+ if (!q) {
361
+ if (value != null) onChange(null);
362
+ } else {
363
+ const hits = options.filter((o) => o.label.toLowerCase() === q.toLowerCase());
364
+ const ids = new Set(hits.map((h) => h.value));
365
+ if (ids.size === 1 && hits[0].value !== value) onChange(hits[0].value);
366
+ }
367
+ setText(null);
368
+ }
369
+ close();
370
+ };
371
+ return /* @__PURE__ */ jsxs("div", { ref: wrapperRef, className: cn("relative", className), children: [
372
+ label !== void 0 && /* @__PURE__ */ jsx(FieldLabel, { children: label }),
373
+ /* @__PURE__ */ jsxs("div", { ref: fieldRef, className: "relative", children: [
374
+ /* @__PURE__ */ jsx(
375
+ "input",
376
+ {
377
+ id: fieldId,
378
+ value: shown,
379
+ placeholder,
380
+ "aria-label": ariaLabel ?? (typeof label === "string" ? label : void 0),
381
+ role: "combobox",
382
+ "aria-expanded": open,
383
+ "aria-autocomplete": "list",
384
+ "aria-invalid": invalid || void 0,
385
+ autoComplete: "off",
386
+ disabled,
387
+ autoFocus,
388
+ inputMode: isPhone ? "none" : void 0,
389
+ onMouseDown: primaryOnly.onMouseDown,
390
+ onFocus: (e) => {
391
+ if (primaryOnly.fromAuxButton()) return;
392
+ setText(shown);
393
+ e.currentTarget.select();
394
+ setOpen(true);
395
+ if (isPhone) sheetInputRef.current?.focus();
396
+ },
397
+ onBlur: () => {
398
+ if (!isPhone) reconcile();
399
+ },
400
+ onChange: (e) => {
401
+ setText(e.target.value);
402
+ setOpen(true);
403
+ setActive(-1);
404
+ },
405
+ onKeyDown: (e) => {
406
+ if (e.key === "ArrowDown") {
407
+ e.preventDefault();
408
+ setOpen(true);
409
+ setActive((i) => Math.min(i + 1, matches.length - 1));
410
+ } else if (e.key === "ArrowUp") {
411
+ e.preventDefault();
412
+ setActive((i) => Math.max(i - 1, 0));
413
+ } else if (e.key === "Enter") {
414
+ if (open && active >= 0 && active < matches.length) {
415
+ e.preventDefault();
416
+ commit(matches[active]);
417
+ } else {
418
+ reconcile();
419
+ }
420
+ } else if (e.key === "Escape") {
421
+ setText(null);
422
+ close();
423
+ }
424
+ },
425
+ className: cn(FIELD_BASE, label !== void 0 && FIELD_FLOATING_PAD, "pr-9", invalid && FIELD_INVALID)
426
+ }
427
+ ),
428
+ showClear ? /* @__PURE__ */ jsx(
429
+ "button",
430
+ {
431
+ type: "button",
432
+ tabIndex: -1,
433
+ "aria-label": clearLabel ?? "Clear",
434
+ onMouseDown: (e) => e.preventDefault(),
435
+ onClick: () => {
436
+ setText(null);
437
+ close();
438
+ onChange(null);
439
+ },
440
+ className: cn(
441
+ "absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-slate-400",
442
+ "hover:text-slate-600 dark:text-slate-500 dark:hover:text-slate-300"
443
+ ),
444
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": true, className: "size-4" })
445
+ }
446
+ ) : /* @__PURE__ */ jsx(
447
+ ChevronDown,
448
+ {
449
+ "aria-hidden": true,
450
+ onMouseDown: (e) => {
451
+ e.preventDefault();
452
+ setOpen((o) => !o);
453
+ },
454
+ className: cn(
455
+ "absolute right-2.5 top-1/2 size-4 -translate-y-1/2 text-slate-400 dark:text-slate-500",
456
+ disabled ? "opacity-50" : "cursor-pointer"
457
+ )
458
+ }
459
+ )
460
+ ] }),
461
+ isPhone && /* @__PURE__ */ jsx(
462
+ PickerSheet,
463
+ {
464
+ open,
465
+ onClose: reconcile,
466
+ title: label,
467
+ query: sheetQuery,
468
+ onQueryChange: (v) => {
469
+ setSheetQuery(v);
470
+ setActive(-1);
471
+ },
472
+ searchPlaceholder: searchPlaceholder ?? placeholder,
473
+ inputRef: sheetInputRef,
474
+ closeLabel,
475
+ children: /* @__PURE__ */ jsxs("ul", { role: "listbox", children: [
476
+ matches.map((o, i) => (
477
+ // Keyed by group AND value, like combobox-core.tsx: an option may
478
+ // deliberately appear twice (see `reconcile`), and a bare value key
479
+ // would collide.
480
+ /* @__PURE__ */ jsxs(Fragment, { children: [
481
+ o.group && o.group !== matches[i - 1]?.group && /* @__PURE__ */ jsx(
482
+ "li",
483
+ {
484
+ role: "presentation",
485
+ className: "px-4 pb-0.5 pt-3 text-[11px] font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400",
486
+ children: o.group
487
+ }
488
+ ),
489
+ /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": o.value === value, children: /* @__PURE__ */ jsx(
490
+ "button",
491
+ {
492
+ type: "button",
493
+ onClick: () => commit(o),
494
+ className: cn(SHEET_ROW_CLASS, o.value === value && "font-medium"),
495
+ children: o.label
496
+ }
497
+ ) })
498
+ ] }, `${o.group ?? ""}|${String(o.value)}`)
499
+ )),
500
+ matches.length === 0 && emptyLabel && /* @__PURE__ */ jsx("li", { className: "px-4 py-3 text-sm text-slate-500 dark:text-slate-400", children: emptyLabel })
501
+ ] })
502
+ }
503
+ ),
504
+ !isPhone && open && matches.length > 0 && /* @__PURE__ */ jsx(SuggestionList, { anchorRef: fieldRef, panelRef, children: matches.map((o, i) => (
505
+ // A group heading is emitted at each group boundary rather than repeating
506
+ // the group on every row (feedback #136). `matches` is group-contiguous,
507
+ // so comparing with the previous row is enough. Fragment key sits here;
508
+ // the heading and the option carry their own list semantics — keyed by
509
+ // group AND value, because an option may deliberately appear twice (see
510
+ // `reconcile`).
511
+ /* @__PURE__ */ jsxs(Fragment, { children: [
512
+ o.group && o.group !== matches[i - 1]?.group && /* @__PURE__ */ jsx(
513
+ "li",
514
+ {
515
+ role: "presentation",
516
+ className: "px-3 pb-0.5 pt-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500 first:pt-1 dark:text-slate-400",
517
+ children: o.group
518
+ }
519
+ ),
520
+ /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": o.value === value, children: /* @__PURE__ */ jsxs(
521
+ "button",
522
+ {
523
+ type: "button",
524
+ onMouseDown: (e) => {
525
+ e.preventDefault();
526
+ commit(o);
527
+ },
528
+ onMouseEnter: () => setActive(i),
529
+ className: cn(
530
+ rowClass(i === active),
531
+ // Indented under its heading, so the hierarchy is readable at a
532
+ // glance instead of inferred from grey trailing text.
533
+ o.group && "pl-6",
534
+ o.value === value && "font-medium"
535
+ ),
536
+ children: [
537
+ o.label,
538
+ o.sublabel && /* @__PURE__ */ jsx("span", { className: "ml-2 text-xs text-slate-400 dark:text-slate-500", children: o.sublabel })
539
+ ]
540
+ }
541
+ ) })
542
+ ] }, `${o.group ?? ""}|${String(o.value)}`)
543
+ )) })
544
+ ] });
545
+ }
546
+ export {
547
+ Combobox,
548
+ InlineEntityCombobox
549
+ };
550
+ //# sourceMappingURL=combobox.js.map