@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.
- package/README.md +219 -0
- package/dist/components/account-settings.d.ts +71 -0
- package/dist/components/account-settings.js +159 -0
- package/dist/components/account-settings.js.map +1 -0
- package/dist/components/alert-banner.d.ts +20 -0
- package/dist/components/alert-banner.js +39 -0
- package/dist/components/alert-banner.js.map +1 -0
- package/dist/components/amount-input.d.ts +117 -0
- package/dist/components/amount-input.js +243 -0
- package/dist/components/amount-input.js.map +1 -0
- package/dist/components/calculator.d.ts +42 -0
- package/dist/components/calculator.js +161 -0
- package/dist/components/calculator.js.map +1 -0
- package/dist/components/chart.d.ts +58 -0
- package/dist/components/chart.js +158 -0
- package/dist/components/chart.js.map +1 -0
- package/dist/components/combobox-core.d.ts +77 -0
- package/dist/components/combobox-core.js +263 -0
- package/dist/components/combobox-core.js.map +1 -0
- package/dist/components/combobox.d.ts +141 -0
- package/dist/components/combobox.js +550 -0
- package/dist/components/combobox.js.map +1 -0
- package/dist/components/currency-select.d.ts +37 -0
- package/dist/components/currency-select.js +131 -0
- package/dist/components/currency-select.js.map +1 -0
- package/dist/components/data-table-filter-popover.d.ts +28 -0
- package/dist/components/data-table-filter-popover.js +201 -0
- package/dist/components/data-table-filter-popover.js.map +1 -0
- package/dist/components/data-table-filters.d.ts +5 -0
- package/dist/components/data-table-filters.js +118 -0
- package/dist/components/data-table-filters.js.map +1 -0
- package/dist/components/data-table-labels.d.ts +33 -0
- package/dist/components/data-table-labels.js +53 -0
- package/dist/components/data-table-labels.js.map +1 -0
- package/dist/components/data-table-pagination.d.ts +18 -0
- package/dist/components/data-table-pagination.js +92 -0
- package/dist/components/data-table-pagination.js.map +1 -0
- package/dist/components/data-table-sort.d.ts +23 -0
- package/dist/components/data-table-sort.js +55 -0
- package/dist/components/data-table-sort.js.map +1 -0
- package/dist/components/data-table.d.ts +5 -0
- package/dist/components/data-table.js +926 -0
- package/dist/components/data-table.js.map +1 -0
- package/dist/components/date-picker.d.ts +85 -0
- package/dist/components/date-picker.js +253 -0
- package/dist/components/date-picker.js.map +1 -0
- package/dist/components/dropdown.d.ts +98 -0
- package/dist/components/dropdown.js +122 -0
- package/dist/components/dropdown.js.map +1 -0
- package/dist/components/entity-combobox.d.ts +49 -0
- package/dist/components/entity-combobox.js +115 -0
- package/dist/components/entity-combobox.js.map +1 -0
- package/dist/components/file-dropzone.d.ts +18 -0
- package/dist/components/file-dropzone.js +101 -0
- package/dist/components/file-dropzone.js.map +1 -0
- package/dist/components/full-bleed-dialog.d.ts +57 -0
- package/dist/components/full-bleed-dialog.js +68 -0
- package/dist/components/full-bleed-dialog.js.map +1 -0
- package/dist/components/grouped-picker.d.ts +37 -0
- package/dist/components/grouped-picker.js +87 -0
- package/dist/components/grouped-picker.js.map +1 -0
- package/dist/components/hover-menu.d.ts +16 -0
- package/dist/components/hover-menu.js +113 -0
- package/dist/components/hover-menu.js.map +1 -0
- package/dist/components/mini-calendar.d.ts +33 -0
- package/dist/components/mini-calendar.js +133 -0
- package/dist/components/mini-calendar.js.map +1 -0
- package/dist/components/modal.d.ts +76 -0
- package/dist/components/modal.js +156 -0
- package/dist/components/modal.js.map +1 -0
- package/dist/components/multi-entity-combobox.d.ts +47 -0
- package/dist/components/multi-entity-combobox.js +116 -0
- package/dist/components/multi-entity-combobox.js.map +1 -0
- package/dist/components/multi-select.d.ts +39 -0
- package/dist/components/multi-select.js +132 -0
- package/dist/components/multi-select.js.map +1 -0
- package/dist/components/number-input.d.ts +74 -0
- package/dist/components/number-input.js +114 -0
- package/dist/components/number-input.js.map +1 -0
- package/dist/components/numpad-sheet.d.ts +45 -0
- package/dist/components/numpad-sheet.js +102 -0
- package/dist/components/numpad-sheet.js.map +1 -0
- package/dist/components/picker-sheet.d.ts +44 -0
- package/dist/components/picker-sheet.js +78 -0
- package/dist/components/picker-sheet.js.map +1 -0
- package/dist/components/popover.d.ts +20 -0
- package/dist/components/popover.js +46 -0
- package/dist/components/popover.js.map +1 -0
- package/dist/components/search-field.d.ts +52 -0
- package/dist/components/search-field.js +64 -0
- package/dist/components/search-field.js.map +1 -0
- package/dist/components/settings-fields.d.ts +39 -0
- package/dist/components/settings-fields.js +17 -0
- package/dist/components/settings-fields.js.map +1 -0
- package/dist/components/swipeable-row.d.ts +60 -0
- package/dist/components/swipeable-row.js +115 -0
- package/dist/components/swipeable-row.js.map +1 -0
- package/dist/components/toggle-group.d.ts +34 -0
- package/dist/components/toggle-group.js +90 -0
- package/dist/components/toggle-group.js.map +1 -0
- package/dist/components/tooltip.d.ts +86 -0
- package/dist/components/tooltip.js +171 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/components/ui.d.ts +239 -0
- package/dist/components/ui.js +512 -0
- package/dist/components/ui.js.map +1 -0
- package/dist/components/use-mobile-reveal.d.ts +31 -0
- package/dist/components/use-mobile-reveal.js +33 -0
- package/dist/components/use-mobile-reveal.js.map +1 -0
- package/dist/components/use-table-state.d.ts +54 -0
- package/dist/components/use-table-state.js +153 -0
- package/dist/components/use-table-state.js.map +1 -0
- package/dist/components/user-avatar.d.ts +23 -0
- package/dist/components/user-avatar.js +33 -0
- package/dist/components/user-avatar.js.map +1 -0
- package/dist/components/wizard-stepper.d.ts +14 -0
- package/dist/components/wizard-stepper.js +27 -0
- package/dist/components/wizard-stepper.js.map +1 -0
- package/dist/data-table-filters-aXyf0Xub.d.ts +261 -0
- package/dist/feedback/feedback-attachment.d.ts +63 -0
- package/dist/feedback/feedback-attachment.js +160 -0
- package/dist/feedback/feedback-attachment.js.map +1 -0
- package/dist/feedback/feedback-dialog.d.ts +72 -0
- package/dist/feedback/feedback-dialog.js +109 -0
- package/dist/feedback/feedback-dialog.js.map +1 -0
- package/dist/feedback/feedback-inbox.d.ts +226 -0
- package/dist/feedback/feedback-inbox.js +315 -0
- package/dist/feedback/feedback-inbox.js.map +1 -0
- package/dist/hooks/use-anchored-panel.d.ts +80 -0
- package/dist/hooks/use-anchored-panel.js +71 -0
- package/dist/hooks/use-anchored-panel.js.map +1 -0
- package/dist/hooks/use-anchored-rect.d.ts +24 -0
- package/dist/hooks/use-anchored-rect.js +40 -0
- package/dist/hooks/use-anchored-rect.js.map +1 -0
- package/dist/hooks/use-body-scroll-lock.d.ts +9 -0
- package/dist/hooks/use-body-scroll-lock.js +41 -0
- package/dist/hooks/use-body-scroll-lock.js.map +1 -0
- package/dist/hooks/use-close-transition.d.ts +41 -0
- package/dist/hooks/use-close-transition.js +36 -0
- package/dist/hooks/use-close-transition.js.map +1 -0
- package/dist/hooks/use-dismiss.d.ts +34 -0
- package/dist/hooks/use-dismiss.js +41 -0
- package/dist/hooks/use-dismiss.js.map +1 -0
- package/dist/hooks/use-media-query.d.ts +8 -0
- package/dist/hooks/use-media-query.js +18 -0
- package/dist/hooks/use-media-query.js.map +1 -0
- package/dist/hooks/use-overlay-history.d.ts +10 -0
- package/dist/hooks/use-overlay-history.js +99 -0
- package/dist/hooks/use-overlay-history.js.map +1 -0
- package/dist/hooks/use-row-swipe.d.ts +30 -0
- package/dist/hooks/use-row-swipe.js +128 -0
- package/dist/hooks/use-row-swipe.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +90 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/calc.d.ts +89 -0
- package/dist/lib/calc.js +180 -0
- package/dist/lib/calc.js.map +1 -0
- package/dist/lib/cn.d.ts +6 -0
- package/dist/lib/cn.js +7 -0
- package/dist/lib/cn.js.map +1 -0
- package/dist/lib/dates.d.ts +84 -0
- package/dist/lib/dates.js +108 -0
- package/dist/lib/dates.js.map +1 -0
- package/dist/lib/logger.d.ts +17 -0
- package/dist/lib/logger.js +30 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/safe-storage.d.ts +23 -0
- package/dist/lib/safe-storage.js +20 -0
- package/dist/lib/safe-storage.js.map +1 -0
- package/dist/search/command-palette.d.ts +71 -0
- package/dist/search/command-palette.js +198 -0
- package/dist/search/command-palette.js.map +1 -0
- package/dist/shell/app-shell.d.ts +64 -0
- package/dist/shell/app-shell.js +237 -0
- package/dist/shell/app-shell.js.map +1 -0
- package/dist/shell/option-switcher-menu.d.ts +24 -0
- package/dist/shell/option-switcher-menu.js +44 -0
- package/dist/shell/option-switcher-menu.js.map +1 -0
- package/dist/shell/role-switcher.d.ts +19 -0
- package/dist/shell/role-switcher.js +28 -0
- package/dist/shell/role-switcher.js.map +1 -0
- package/dist/shell/top-bar.d.ts +15 -0
- package/dist/shell/top-bar.js +29 -0
- package/dist/shell/top-bar.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +44 -0
- package/dist/shell/topbar-action-menu.js +49 -0
- package/dist/shell/topbar-action-menu.js.map +1 -0
- package/dist/shell/topbar-controls.d.ts +54 -0
- package/dist/shell/topbar-controls.js +152 -0
- package/dist/shell/topbar-controls.js.map +1 -0
- package/dist/theme/chart-palette.d.ts +60 -0
- package/dist/theme/chart-palette.js +79 -0
- package/dist/theme/chart-palette.js.map +1 -0
- package/dist/theme/palette-presets.d.ts +39 -0
- package/dist/theme/palette-presets.js +440 -0
- package/dist/theme/palette-presets.js.map +1 -0
- package/dist/theme/palette-store.d.ts +45 -0
- package/dist/theme/palette-store.js +52 -0
- package/dist/theme/palette-store.js.map +1 -0
- package/dist/theme/theme-store.d.ts +57 -0
- package/dist/theme/theme-store.js +96 -0
- package/dist/theme/theme-store.js.map +1 -0
- package/dist/tour/tour.d.ts +104 -0
- package/dist/tour/tour.js +351 -0
- package/dist/tour/tour.js.map +1 -0
- package/dist/wizard/stepper-nav.d.ts +30 -0
- package/dist/wizard/stepper-nav.js +146 -0
- package/dist/wizard/stepper-nav.js.map +1 -0
- package/dist/wizard/types.d.ts +124 -0
- package/dist/wizard/types.js +24 -0
- package/dist/wizard/types.js.map +1 -0
- package/dist/wizard/use-wizard.d.ts +6 -0
- package/dist/wizard/use-wizard.js +222 -0
- package/dist/wizard/use-wizard.js.map +1 -0
- package/dist/wizard/validation.d.ts +23 -0
- package/dist/wizard/validation.js +13 -0
- package/dist/wizard/validation.js.map +1 -0
- package/dist/wizard/wizard-context.d.ts +19 -0
- package/dist/wizard/wizard-context.js +15 -0
- package/dist/wizard/wizard-context.js.map +1 -0
- package/dist/wizard/wizard-step.d.ts +11 -0
- package/dist/wizard/wizard-step.js +12 -0
- package/dist/wizard/wizard-step.js.map +1 -0
- package/dist/wizard/wizard-summary.d.ts +18 -0
- package/dist/wizard/wizard-summary.js +41 -0
- package/dist/wizard/wizard-summary.js.map +1 -0
- package/package.json +87 -0
- 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
|