@mendylanda/ui 0.1.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 (64) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +5 -0
  3. package/README.md +49 -0
  4. package/THIRD_PARTY_LICENSES/shadcn-ui.txt +21 -0
  5. package/THIRD_PARTY_LICENSES/startercn.txt +21 -0
  6. package/dist/class-names.d.ts +1 -0
  7. package/dist/class-names.js +1 -0
  8. package/dist/customization.d.ts +31 -0
  9. package/dist/customization.js +42 -0
  10. package/dist/filters/core.d.ts +3 -0
  11. package/dist/filters/core.js +3 -0
  12. package/dist/filters/filter-bar.d.ts +49 -0
  13. package/dist/filters/filter-bar.js +435 -0
  14. package/dist/filters/filter-date-editor.d.ts +10 -0
  15. package/dist/filters/filter-date-editor.js +31 -0
  16. package/dist/filters/filter-definition.d.ts +220 -0
  17. package/dist/filters/filter-definition.js +227 -0
  18. package/dist/filters/filter-menu-panel.d.ts +21 -0
  19. package/dist/filters/filter-menu-panel.js +164 -0
  20. package/dist/filters/filter-select-editor.d.ts +25 -0
  21. package/dist/filters/filter-select-editor.js +46 -0
  22. package/dist/filters/filter-state.d.ts +34 -0
  23. package/dist/filters/filter-state.js +107 -0
  24. package/dist/filters/filter-text-editor.d.ts +11 -0
  25. package/dist/filters/filter-text-editor.js +34 -0
  26. package/dist/filters/filter-utils.d.ts +1 -0
  27. package/dist/filters/filter-utils.js +8 -0
  28. package/dist/filters/filters.d.ts +38 -0
  29. package/dist/filters/filters.js +94 -0
  30. package/dist/filters/index.d.ts +10 -0
  31. package/dist/filters/index.js +10 -0
  32. package/dist/filters/nuqs.d.ts +1 -0
  33. package/dist/filters/nuqs.js +2 -0
  34. package/dist/filters/use-filter-options.d.ts +24 -0
  35. package/dist/filters/use-filter-options.js +221 -0
  36. package/dist/filters/use-filters.d.ts +78 -0
  37. package/dist/filters/use-filters.js +158 -0
  38. package/dist/filters/use-url-filters.d.ts +41 -0
  39. package/dist/filters/use-url-filters.js +197 -0
  40. package/dist/filters/use-value-draft.d.ts +2 -0
  41. package/dist/filters/use-value-draft.js +16 -0
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.js +1 -0
  44. package/dist/primitives/button.d.ts +10 -0
  45. package/dist/primitives/button.js +36 -0
  46. package/dist/primitives/calendar.d.ts +8 -0
  47. package/dist/primitives/calendar.js +75 -0
  48. package/dist/primitives/checkbox.d.ts +4 -0
  49. package/dist/primitives/checkbox.js +10 -0
  50. package/dist/primitives/dropdown-menu.d.ts +25 -0
  51. package/dist/primitives/dropdown-menu.js +54 -0
  52. package/dist/primitives/index.d.ts +8 -0
  53. package/dist/primitives/index.js +8 -0
  54. package/dist/primitives/input.d.ts +3 -0
  55. package/dist/primitives/input.js +7 -0
  56. package/dist/primitives/label.d.ts +4 -0
  57. package/dist/primitives/label.js +9 -0
  58. package/dist/primitives/textarea.d.ts +3 -0
  59. package/dist/primitives/textarea.js +7 -0
  60. package/dist/styles.css +1775 -0
  61. package/dist/styles.css.d.ts +1 -0
  62. package/dist/utils.d.ts +3 -0
  63. package/dist/utils.js +19 -0
  64. package/package.json +98 -0
@@ -0,0 +1,435 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useId, useRef, useState, } from "react";
4
+ import { ListFilter, Search, X, Circle, ListChecks, Type, Hash, CalendarDays, SlidersHorizontal, Layers, } from "lucide-react";
5
+ import { Button } from "../customization.js";
6
+ import { Input } from "../customization.js";
7
+ import { Label } from "../customization.js";
8
+ import { FilterDateEditor } from "./filter-date-editor.js";
9
+ import { Textarea } from "../customization.js";
10
+ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuRadioGroup, DropdownMenuRadioItem, } from "../primitives/dropdown-menu.js";
11
+ import { AppliedFilter, FilterCheckboxItem } from "./filters.js";
12
+ import { FilterOptionCache, useFilterOptions } from "./use-filter-options.js";
13
+ import { classifyPaste, resolvePasteAmbiguity } from "./filter-state.js";
14
+ import { cn } from "../utils.js";
15
+ import { FilterMenuPanel } from "./filter-menu-panel.js";
16
+ import { useValueDraft } from "./use-value-draft.js";
17
+ import { MendyUIProvider, useMendyUI } from "../customization.js";
18
+ const defaultGroups = [];
19
+ const defaultSummary = { mode: "count", limit: 3 };
20
+ const Context = createContext(null);
21
+ function useRoot() {
22
+ const context = useContext(Context);
23
+ if (!context)
24
+ throw new Error("Filter components must be inside FilterRoot.");
25
+ return context;
26
+ }
27
+ export function FilterRoot({ classNames, ...props }) {
28
+ return (_jsx(MendyUIProvider, { classNames: classNames, children: _jsx(FilterRootContent, { ...props }) }));
29
+ }
30
+ function FilterRootContent({ filters, summary = defaultSummary, suggestions = "always", closeMenuOnApply = false, groups = defaultGroups, disabled = false, className, children, }) {
31
+ const { classNames } = useMendyUI();
32
+ const [cache] = useState(() => new Map());
33
+ const [ambiguous, setAmbiguous] = useState([]);
34
+ const trigger = useRef(null);
35
+ const context = useMemo(() => ({
36
+ filters,
37
+ summary,
38
+ suggestions,
39
+ closeMenuOnApply,
40
+ groups,
41
+ disabled,
42
+ trigger,
43
+ ambiguous,
44
+ setAmbiguous,
45
+ }), [filters, summary, suggestions, closeMenuOnApply, groups, disabled, ambiguous]);
46
+ return (_jsx(FilterOptionCache.Provider, { value: cache, children: _jsx(Context.Provider, { value: context, children: _jsx("div", { "data-mendy-ui": "", className: cn("mui-222f930b8752 mui-faa8a23c68f6 mui-71556df3b421 mui-074569488cca", classNames?.root, className), children: children }) }) }));
47
+ }
48
+ export function FilterBar(props) {
49
+ return (_jsxs(FilterRoot, { ...props, children: [_jsx(FilterSearch, { label: props.searchLabel, placeholder: props.searchPlaceholder }), _jsx(FilterList, {}), _jsx(FilterClear, {}), _jsx(FilterFeedback, {})] }));
50
+ }
51
+ export function FilterSearch({ label = "Search", placeholder = "Search or filter", }) {
52
+ const { filters, trigger, disabled, setAmbiguous } = useRoot();
53
+ const { classNames } = useMendyUI();
54
+ const anchor = useRef(null);
55
+ const input = useRef(null);
56
+ const shift = useRef(false);
57
+ return (_jsxs(DropdownMenu, { modal: false, open: filters.menuOpen, onOpenChange: filters.setMenuOpen, children: [_jsxs("div", { ref: anchor, className: cn("mui-d2d9e1f13413 mui-58d0413dd93c mui-27ead27a81df mui-8c14b4d2e932", classNames?.search), children: [_jsx(Search, { className: "mui-33228ff7d583 mui-747355bdc2a2 mui-ea31f8b3a958 mui-4e45f4841abb mui-3c7e3f82f336 mui-5dde0fd996f0", "aria-hidden": "true" }), _jsx(Input, { ref: input, type: "text", role: "searchbox", disabled: disabled, "aria-label": label, placeholder: placeholder, value: filters.search, autoComplete: "off", autoCapitalize: "none", autoCorrect: "off", spellCheck: false, className: cn("mui-58d0413dd93c mui-86486d7e416f mui-c74ab393b96d mui-4f05a238500e", filters.search ? "mui-83ffc494bac4" : "mui-0359a07b64f9", classNames?.searchInput), onKeyDown: (event) => {
58
+ shift.current = event.shiftKey;
59
+ if (event.key === "Enter" && !event.nativeEvent.isComposing && !event.shiftKey) {
60
+ const classified = classifyPaste(event.currentTarget.value, filters.entries);
61
+ if (Object.keys(classified.changes).length || classified.ambiguous.length) {
62
+ event.preventDefault();
63
+ const result = filters.paste(event.currentTarget.value, { before: "", after: "" });
64
+ setAmbiguous(result.ambiguous);
65
+ }
66
+ }
67
+ }, onKeyUp: (event) => {
68
+ shift.current = event.shiftKey;
69
+ }, onChange: (event) => {
70
+ setAmbiguous([]);
71
+ filters.setSearch(event.target.value);
72
+ }, onPaste: (event) => {
73
+ if (shift.current)
74
+ return;
75
+ const text = event.clipboardData.getData("text");
76
+ if (!text.trim() || !filters.entries.some((entry) => entry.field.recognize))
77
+ return;
78
+ const classified = classifyPaste(text, filters.entries);
79
+ if (!Object.keys(classified.changes).length && !classified.ambiguous.length)
80
+ return;
81
+ event.preventDefault();
82
+ const element = event.currentTarget;
83
+ // Search inputs do not expose selectionStart in every browser. Track a full-field
84
+ // replacement when selected text matches the field; otherwise append safely.
85
+ const selected = window.getSelection()?.toString();
86
+ const start = element.selectionStart ?? (selected === element.value ? 0 : element.value.length);
87
+ const end = element.selectionEnd ?? (selected === element.value ? element.value.length : start);
88
+ const result = filters.paste(text, {
89
+ before: element.value.slice(0, start),
90
+ after: element.value.slice(end),
91
+ });
92
+ setAmbiguous(result.ambiguous);
93
+ } }), filters.search && (_jsx(Button, { variant: "ghost", size: "icon", type: "button", disabled: disabled, "aria-label": "Clear search", className: "mui-747355bdc2a2 mui-5a6edca9f43a mui-4e45f4841abb mui-222f930b8752 mui-0712570dcc9f mui-5dde0fd996f0 mui-71556df3b421 mui-a503dd374cca mui-02e603944040 mui-0b5a4d1257ab mui-8da02faac748 mui-2e357972fc10 mui-39be5a15b766 mui-db549d38ff19", onClick: () => {
94
+ setAmbiguous([]);
95
+ filters.setSearch("");
96
+ input.current?.focus();
97
+ }, children: _jsx(X, { className: "mui-9104ce433bba", "aria-hidden": "true" }) })), _jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", ref: trigger, type: "button", disabled: disabled, "aria-label": "Open filters", "aria-haspopup": "dialog", className: cn("mui-747355bdc2a2 mui-71fb77dd8ea7 mui-4e45f4841abb mui-79ede756e761 mui-5dde0fd996f0 mui-2cdd01663c76 mui-8da02faac748 mui-2e357972fc10 mui-39be5a15b766 mui-db549d38ff19 mui-790ed041a953", filters.active.length ? "mui-65694092f002" : "mui-0b5a4d1257ab", classNames?.menuTrigger), children: _jsx(ListFilter, { className: "mui-3c7e3f82f336", "aria-hidden": "true" }) }) })] }), filters.menuOpen && _jsx(FilterMenuContent, { anchor: anchor })] }));
98
+ }
99
+ /** A standalone menu button for layouts without a search field. */
100
+ export function FilterMenu({ children = "Add filter", asChild = false, }) {
101
+ const { filters, trigger, disabled } = useRoot();
102
+ const { classNames } = useMendyUI();
103
+ return (_jsxs(DropdownMenu, { modal: false, open: filters.menuOpen, onOpenChange: filters.setMenuOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { ref: trigger, asChild: asChild, className: classNames?.menuTrigger, variant: "outline", disabled: disabled, "aria-haspopup": "dialog", children: children }) }), filters.menuOpen && _jsx(FilterMenuContent, {})] }));
104
+ }
105
+ const fieldIcons = {
106
+ single: Circle,
107
+ multi: ListChecks,
108
+ text: Type,
109
+ tokens: Hash,
110
+ numberRange: Hash,
111
+ dateRange: CalendarDays,
112
+ custom: SlidersHorizontal,
113
+ };
114
+ function FilterMenuContent({ anchor }) {
115
+ const { filters, groups, disabled, trigger } = useRoot();
116
+ const [clearEpoch, setClearEpoch] = useState(0);
117
+ const grouped = new Set(groups.flatMap((group) => group.fields));
118
+ const visible = filters.entries.filter((entry) => !entry.field.hidden && entry.field.menu !== false);
119
+ const sections = [
120
+ ...visible.flatMap((entry) => grouped.has(entry.id)
121
+ ? []
122
+ : [
123
+ {
124
+ id: entry.id,
125
+ label: entry.field.label,
126
+ icon: entry.field.icon,
127
+ entries: [entry],
128
+ },
129
+ ]),
130
+ ...groups.flatMap((group) => {
131
+ const entries = visible.filter((entry) => group.fields.includes(entry.id));
132
+ return entries.length ? [{ ...group, entries }] : [];
133
+ }),
134
+ ].map((section) => {
135
+ const single = section.entries.length === 1 ? section.entries[0].field : undefined;
136
+ const Icon = single ? fieldIcons[single.kind] : Layers;
137
+ const active = section.entries.some((entry) => entry.field.isActive(entry.value));
138
+ const clearable = section.entries.filter((entry) => entry.field.isActive(entry.value) &&
139
+ !entry.field.disabled &&
140
+ entry.field.removable !== false);
141
+ return {
142
+ id: section.id,
143
+ label: section.label,
144
+ icon: section.icon ?? _jsx(Icon, {}),
145
+ editorLabel: single?.editorLabel ??
146
+ (single?.kind === "text" || single?.kind === "tokens"
147
+ ? `Set ${section.label.toLowerCase()} filter`
148
+ : `Choose ${section.label.toLowerCase()}`),
149
+ disabled: disabled || section.entries.every((entry) => entry.field.disabled),
150
+ active,
151
+ clear: !disabled && clearable.length
152
+ ? () => {
153
+ const error = filters.batch(Object.fromEntries(clearable.map((entry) => [entry.id, entry.field.clearValue])), undefined, "remove");
154
+ if (!error)
155
+ setClearEpoch((epoch) => epoch + 1);
156
+ }
157
+ : undefined,
158
+ content: section.entries.map((entry) => (_jsxs("div", { className: "mui-f157ee6bae99", children: [!single && (_jsx("p", { className: "mui-894c3815a57f mui-0f9e6672913a mui-0a702d97ed7d mui-3992de70b033 mui-daaac3fbf55e mui-4fa4c190281c", children: entry.field.label })), _jsx(FieldEditor, { entry: entry, active: true, autoFocus: false, showDateLabel: false, disabled: disabled || entry.field.disabled, close: () => filters.setMenuOpen(false), location: "menu" }, `${entry.id}:${clearEpoch}`)] }, entry.id))),
159
+ };
160
+ });
161
+ return (_jsx(FilterMenuPanel, { sections: sections, selectedId: filters.openField, onSelect: filters.setOpenField, anchor: anchor, trigger: trigger }));
162
+ }
163
+ export function FilterList() {
164
+ const { filters } = useRoot();
165
+ return (_jsx(_Fragment, { children: filters.entries.map((entry) => (_jsx(FieldChip, { entry: entry }, entry.id))) }));
166
+ }
167
+ function summarize(field, value, choices) {
168
+ if (field.kind === "numberRange" && Array.isArray(value))
169
+ return `${value[0] ?? "Any"} – ${value[1] ?? "Any"}`;
170
+ if (Array.isArray(value))
171
+ return value
172
+ .map((item) => choices.find((choice) => choice.value === item)?.label ?? String(item ?? "Any"))
173
+ .join(", ");
174
+ if (value && typeof value === "object" && "from" in value && "to" in value)
175
+ return `${value.from ?? "Any"} – ${value.to ?? "Any"}`;
176
+ return choices.find((choice) => choice.value === value)?.label ?? String(value ?? "");
177
+ }
178
+ export function FilterField({ id }) {
179
+ const { filters } = useRoot();
180
+ const entry = filters.entries.find((entry) => entry.id === id);
181
+ return entry ? _jsx(FieldChip, { entry: entry }) : null;
182
+ }
183
+ /** Place a built-in or custom field editor in an application-defined layout. */
184
+ export function FilterFieldEditor({ id, autoFocus = true }) {
185
+ const { filters, disabled } = useRoot();
186
+ const entry = filters.entries.find((entry) => entry.id === id);
187
+ return entry ? (_jsx(FieldEditor, { entry: entry, active: true, autoFocus: autoFocus, disabled: disabled || entry.field.disabled, close: () => filters.edit(null), location: "inline" }, id)) : null;
188
+ }
189
+ function FieldChip({ entry }) {
190
+ const { classNames } = useMendyUI();
191
+ const { filters, summary, suggestions, disabled, trigger } = useRoot();
192
+ const { id, field, value } = entry;
193
+ const descriptionId = useId();
194
+ const active = field.isActive(value);
195
+ const currentlyActive = useRef(active);
196
+ useLayoutEffect(() => {
197
+ currentlyActive.current = active;
198
+ }, [active]);
199
+ const suggestion = showSuggestion(entry, suggestions, filters.active.length);
200
+ const options = useFilterOptions(id, field, active ? value : field.suggestion?.value, false);
201
+ if (field.hidden || (!active && !suggestion))
202
+ return null;
203
+ return (_jsxs(AppliedFilter, { label: field.label, "data-mendy-ui": "", "data-slot": active ? "filter-chip" : "filter-suggestion", "data-state": active ? "applied" : "suggested", className: cn(!active && "mui-4f1a55de40bc mui-8450eb0f1857 mui-9b76f456b8c9 mui-8fca9236d191", (field.summary ?? summary).mode === "all" && "mui-a66e9985b093 mui-9974acedf3f7", classNames?.chip), disabled: chipDisabled(field, active, disabled), removeProps: { className: classNames?.chipRemove }, contentProps: {
204
+ className: classNames?.editor,
205
+ onCloseAutoFocus: (event) => {
206
+ if (!currentlyActive.current) {
207
+ event.preventDefault();
208
+ trigger.current?.focus();
209
+ }
210
+ },
211
+ }, triggerProps: { "aria-describedby": descriptionId, className: classNames?.chipTrigger }, editLabel: `${active ? "Edit" : "Apply"} ${field.label} filter`, open: filters.editField === id, onOpenChange: (open) => {
212
+ if (open && !active && field.suggestion && "value" in field.suggestion) {
213
+ filters.commit(id, field.suggestion.value, "suggestion");
214
+ return;
215
+ }
216
+ filters.edit(open ? id : null);
217
+ }, onRemove: active && field.removable !== false
218
+ ? () => {
219
+ filters.remove(id);
220
+ requestAnimationFrame(() => trigger.current?.focus());
221
+ }
222
+ : undefined, editor: _jsx(FieldEditor, { entry: entry, active: filters.editField === id, disabled: disabled || field.disabled, close: () => filters.edit(null), location: "chip" }), children: [_jsx("span", { id: descriptionId, className: "mui-32fb090591d9", children: summarize(field, active ? value : field.suggestion?.value, options.selected) }), _jsx(ChipSummary, { field: field, value: value, active: active, options: options, summary: summary })] }));
223
+ }
224
+ function showSuggestion(entry, mode, activeCount) {
225
+ return (!entry.field.isActive(entry.value) &&
226
+ entry.field.suggestion &&
227
+ !entry.field.hidden &&
228
+ mode !== "never" &&
229
+ (mode === "always" || activeCount === 0));
230
+ }
231
+ function chipDisabled(field, active, disabled) {
232
+ return (disabled ||
233
+ field.disabled ||
234
+ (!active && (field.suggestion?.disabled || field.suggestion?.loading)));
235
+ }
236
+ function summaryText(field, shownValue, choices, policy) {
237
+ const full = summarize(field, shownValue, choices);
238
+ let text = full;
239
+ if (policy.mode === "count" && field.kind !== "numberRange" && Array.isArray(shownValue)) {
240
+ const limit = Math.max(0, policy.limit ?? 3);
241
+ if (shownValue.length > limit)
242
+ text = `${summarize(field, shownValue.slice(0, limit), choices)}${limit ? " and " : ""}${shownValue.length - limit} more`;
243
+ }
244
+ return text;
245
+ }
246
+ function ChipSummary({ field, value, active, options, summary, }) {
247
+ const policy = field.summary ?? summary;
248
+ const shownValue = active ? value : field.suggestion?.value;
249
+ const full = summarize(field, shownValue, options.selected);
250
+ const text = summaryText(field, shownValue, options.selected, policy);
251
+ return (_jsxs(_Fragment, { children: [_jsxs("span", { children: [!active && field.suggestion?.label ? field.suggestion.label : field.label, full ? ":" : ""] }), field.renderSummary ? (field.renderSummary(shownValue, options.selected)) : (_jsx("span", { title: full, className: cn("mui-184ddc11e5f9", policy.mode === "ellipsis" && "mui-5a3a4ee420c2", policy.mode === "all" && "mui-726fca123972 mui-37cf9655f30a mui-1fcf28756bb1"), style: policy.mode === "ellipsis" ? { maxWidth: policy.maxWidth ?? 180 } : undefined, children: options.resolving ? _jsx("span", { className: "mui-7b3cd77eb999", children: text || "Loading…" }) : text }))] }));
252
+ }
253
+ export function FilterClear({ children = "Clear all" }) {
254
+ const { classNames } = useMendyUI();
255
+ const { filters, disabled, trigger, setAmbiguous } = useRoot();
256
+ if (!filters.active.length && !filters.search)
257
+ return null;
258
+ return (_jsx(Button, { type: "button", disabled: disabled, variant: "ghost", size: "sm", className: cn("mui-539ae144f076 mui-e7e01cc7f4df mui-52101fc7d8bb mui-35f35c41d134 mui-944969efb294 mui-71863717995b", classNames?.clear), onClick: () => {
259
+ setAmbiguous([]);
260
+ filters.clear();
261
+ requestAnimationFrame(() => trigger.current?.focus());
262
+ }, children: children }));
263
+ }
264
+ export function FilterFeedback() {
265
+ const { filters, ambiguous, setAmbiguous } = useRoot();
266
+ return (_jsxs(_Fragment, { children: [filters.error && !filters.menuOpen && !filters.editField && (_jsx("p", { role: "alert", className: "mui-58d0413dd93c mui-c74ab393b96d mui-887b9502d5c7", children: filters.error })), filters.persistenceMessage && (_jsx("p", { role: "status", className: "mui-58d0413dd93c mui-3992de70b033 mui-35f35c41d134", children: filters.persistenceMessage })), ambiguous.map((item) => (_jsxs("div", { "data-mendy-ui": "", className: "mui-222f930b8752 mui-58d0413dd93c mui-faa8a23c68f6 mui-71556df3b421 mui-074569488cca mui-3fa8c572949b mui-4f1a55de40bc mui-b97db4a9f432 mui-c74ab393b96d", children: [_jsxs("span", { children: ["Use ", item.token, " as:"] }), item.candidates.map((candidate) => (_jsx(Button, { size: "sm", variant: "outline", onClick: () => {
267
+ const entry = filters.entries.find((entry) => entry.id === candidate.id);
268
+ const resolved = resolvePasteAmbiguity(filters.search, item, ambiguous);
269
+ const error = filters.batch({ [candidate.id]: entry.field.merge(entry.value, candidate.value) }, resolved.search, "paste");
270
+ if (!error)
271
+ setAmbiguous(resolved.remaining);
272
+ }, children: candidate.label }, candidate.id))), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => setAmbiguous(ambiguous.filter((other) => other !== item)), children: "Keep in search" })] }, item.token)))] }));
273
+ }
274
+ function FieldEditor({ entry, active, disabled, close, location, autoFocus = true, showDateLabel = true, }) {
275
+ const { filters, trigger, closeMenuOnApply } = useRoot();
276
+ const { field, value } = entry;
277
+ const input = useRef(null);
278
+ const customRoot = useRef(null);
279
+ const [draft, updateDraft] = useValueDraft(value, (current) => current);
280
+ const draftRef = useRef(value);
281
+ useLayoutEffect(() => {
282
+ draftRef.current = draft;
283
+ }, [draft]);
284
+ function setDraft(next) {
285
+ draftRef.current = next;
286
+ updateDraft(next);
287
+ }
288
+ const [text, setText] = useValueDraft(value, (value) => field.kind === "tokens"
289
+ ? Array.isArray(value)
290
+ ? value.join(", ")
291
+ : ""
292
+ : typeof value === "string"
293
+ ? value
294
+ : "");
295
+ const [error, setError] = useState();
296
+ const id = useId();
297
+ const options = useFilterOptions(entry.id, field, value, active);
298
+ useEffect(() => {
299
+ if (!active || !autoFocus)
300
+ return;
301
+ const frame = requestAnimationFrame(() => (input.current ??
302
+ customRoot.current?.querySelector('input:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]'))?.focus());
303
+ return () => cancelAnimationFrame(frame);
304
+ }, [active, autoFocus]);
305
+ function apply(next, shouldClose = true) {
306
+ if (disabled)
307
+ return;
308
+ const problem = filters.commit(entry.id, next);
309
+ setError(problem);
310
+ if (!problem) {
311
+ setDraft(field.normalize(next));
312
+ const dismiss = location === "menu"
313
+ ? (field.closeMenuOnApply ?? closeMenuOnApply)
314
+ : shouldClose || !field.isActive(field.normalize(next));
315
+ if (dismiss)
316
+ close();
317
+ }
318
+ if (!problem && !field.isActive(field.normalize(next)) && location === "chip")
319
+ requestAnimationFrame(() => trigger.current?.focus());
320
+ }
321
+ if (field.renderEditor)
322
+ return (_jsxs("div", { "data-mendy-ui": "", ref: customRoot, className: "mui-34055e6f8c1b mui-094f5333853b", onKeyDown: (event) => {
323
+ if (event.key === "Escape")
324
+ return;
325
+ if (event.key === "Tab" ||
326
+ (event.target instanceof HTMLElement &&
327
+ event.target.matches("input, textarea, select, [contenteditable=true]")))
328
+ event.stopPropagation();
329
+ }, children: [field.renderEditor({
330
+ value,
331
+ draft,
332
+ setDraft,
333
+ setValue: (next) => apply(next, false),
334
+ apply: (next = draftRef.current) => apply(next),
335
+ close,
336
+ }), error && (_jsx("p", { role: "alert", className: "mui-c74ab393b96d mui-887b9502d5c7", children: error }))] }));
337
+ if (field.kind === "single" || field.kind === "multi")
338
+ return (_jsx(ChoiceEditor, { field: field, value: value, input: input, options: options, error: error, disabled: disabled, apply: apply, location: location }));
339
+ if (field.kind === "dateRange")
340
+ return (_jsx(FilterDateEditor, { autoFocus: autoFocus, showLabel: showDateLabel, field: field, value: value, disabled: disabled, apply: apply, error: error }));
341
+ return (_jsx(ValueEditor, { field: field, draft: draft, setDraft: setDraft, text: text, setText: setText, input: input, error: error, id: id, disabled: disabled, apply: apply }));
342
+ }
343
+ function ChoiceEditor({ field, value, input, options, disabled, apply, location, error, }) {
344
+ const { classNames } = useMendyUI();
345
+ const selected = Array.isArray(value) ? value : value === null ? [] : [value];
346
+ const selectedSet = new Set(selected);
347
+ const searchLabel = field.searchLabel ?? `Search ${field.label.toLowerCase()}`;
348
+ return (_jsxs("div", { "data-mendy-ui": "", className: "mui-72b37c1a040d mui-81b198a2db84", onKeyDown: (event) => {
349
+ if (event.key === "Tab")
350
+ event.stopPropagation();
351
+ }, children: [field.searchable && (_jsxs("div", { "data-mendy-ui": "", className: "mui-d2d9e1f13413 mui-bbe39cfb5cc6 mui-b97db4a9f432", children: [_jsx(Search, { className: "mui-33228ff7d583 mui-747355bdc2a2 mui-866c339ea145 mui-4e45f4841abb mui-5dde0fd996f0 mui-100c22d5776b mui-0b5a4d1257ab", strokeWidth: 1.5, "aria-hidden": "true" }), _jsx(Input, { ref: (node) => {
352
+ input.current = node;
353
+ }, "aria-label": searchLabel, type: "search", disabled: disabled, placeholder: searchLabel, value: options.query, className: "mui-75b27e813dcc mui-4f05a238500e", onChange: (event) => options.setQuery(event.target.value), onKeyDown: (event) => {
354
+ if (event.key === "ArrowDown") {
355
+ event.preventDefault();
356
+ event.currentTarget
357
+ .closest("[data-radix-menu-content]")
358
+ ?.querySelector('[role^="menuitem"]:not([data-disabled])')
359
+ ?.focus();
360
+ }
361
+ if (event.key !== "Escape" && event.key !== "Tab")
362
+ event.stopPropagation();
363
+ } })] })), _jsx("div", { role: location === "inline" ? "group" : "menu", "aria-label": field.label, className: "mui-1dee6e3ec67d", children: location === "inline" ? (options.items.map((choice) => (_jsx(Button, { variant: "ghost", type: "button", "aria-label": choice.label, "aria-pressed": selectedSet.has(choice.value), disabled: disabled || choice.disabled, className: cn("mui-58d0413dd93c mui-c62ec162662e", selectedSet.has(choice.value) && "mui-292affc1f780", classNames?.option), onClick: () => {
364
+ if (field.kind === "single")
365
+ apply(choice.value, false);
366
+ else {
367
+ const next = selectedSet.has(choice.value)
368
+ ? selected.filter((item) => item !== choice.value)
369
+ : [...selected, choice.value];
370
+ apply(next.length ? next : field.clearValue, false);
371
+ }
372
+ }, children: field.renderOption
373
+ ? field.renderOption(choice, { selected: selectedSet.has(choice.value) })
374
+ : choice.label }, choice.value)))) : field.kind === "single" ? (_jsxs(DropdownMenuRadioGroup, { value: typeof value === "string" ? value : "", onValueChange: (next) => apply(next || field.clearValue), children: [location === "chip" && field.removable !== false && (_jsxs(DropdownMenuRadioItem, { value: "", disabled: disabled, onSelect: (event) => event.preventDefault(), children: ["Any ", field.label.toLowerCase()] })), options.items.map((choice) => (_jsx(DropdownMenuRadioItem, { "aria-label": choice.label, className: classNames?.option, value: choice.value, onSelect: (event) => event.preventDefault(), disabled: disabled || choice.disabled, children: field.renderOption
375
+ ? field.renderOption(choice, { selected: selectedSet.has(choice.value) })
376
+ : choice.label }, choice.value)))] })) : (options.items.map((choice) => (_jsx(FilterCheckboxItem, { "aria-label": choice.label, className: classNames?.option, disabled: disabled || choice.disabled, checked: selectedSet.has(choice.value), onCheckedChange: (checked) => {
377
+ const next = checked
378
+ ? [...new Set([...selected, choice.value])]
379
+ : selected.filter((item) => item !== choice.value);
380
+ apply(next.length ? next : field.clearValue, false);
381
+ }, children: field.renderOption
382
+ ? field.renderOption(choice, { selected: selectedSet.has(choice.value) })
383
+ : choice.label }, choice.value)))) }), _jsx(OptionFeedback, { options: options, error: error })] }));
384
+ }
385
+ function OptionFeedback({ options, error, }) {
386
+ return (_jsxs(_Fragment, { children: [error && (_jsx("p", { role: "alert", className: "mui-0f9e6672913a mui-b5edc3ea7c91 mui-c74ab393b96d mui-887b9502d5c7", children: error })), options.loading && (_jsx("p", { role: "status", className: "mui-0f9e6672913a mui-b5edc3ea7c91 mui-c74ab393b96d mui-35f35c41d134", children: "Loading options\u2026" })), !options.loading && !options.error && options.items.length === 0 && (_jsx("p", { role: "status", className: "mui-094f5333853b mui-c74ab393b96d mui-35f35c41d134", children: "No options found." })), options.error && (_jsxs("div", { "data-mendy-ui": "", className: "mui-267171770524 mui-094f5333853b", children: [_jsx("p", { role: "alert", className: "mui-c74ab393b96d mui-887b9502d5c7", children: options.error }), _jsx(Button, { size: "sm", variant: "outline", onClick: options.retry, children: "Retry" })] })), options.hasMore && (_jsx(Button, { variant: "ghost", className: "mui-58d0413dd93c", disabled: options.loading, onClick: options.loadMore, children: "Load more" }))] }));
387
+ }
388
+ function ValueEditor({ field, draft, setDraft, text, setText, input, error, id, disabled, apply, }) {
389
+ const candidate = field.kind === "tokens"
390
+ ? text.split(/[\r\n\t,]+/).flatMap((item) => (item.trim() ? [item.trim()] : []))
391
+ : field.kind === "text"
392
+ ? text
393
+ : draft;
394
+ let validation;
395
+ try {
396
+ validation = field.validate(field.normalize(candidate));
397
+ }
398
+ catch {
399
+ validation = "Enter a valid value.";
400
+ }
401
+ return (_jsxs("div", { "data-mendy-ui": "", className: "mui-37535002f0f7 mui-81b198a2db84 mui-267171770524 mui-094f5333853b", onKeyDown: (event) => {
402
+ if (event.key === "Tab")
403
+ event.stopPropagation();
404
+ }, children: [field.kind === "numberRange" ? (_jsxs("fieldset", { disabled: disabled, className: "mui-267171770524", children: [_jsx("legend", { className: "mui-c74ab393b96d mui-daaac3fbf55e", children: field.label }), [0, 1].map((index) => {
405
+ const key = index === 0 ? "from" : "to";
406
+ const rangeValue = draft?.[index];
407
+ return (_jsxs(Label, { className: "mui-496aca80e4d8 mui-27e6c432192f mui-3992de70b033", children: [index === 0 ? "Minimum" : "Maximum", _jsx(Input, { ref: index === 0
408
+ ? (node) => {
409
+ input.current = node;
410
+ }
411
+ : undefined, "aria-invalid": Boolean(validation || error), "aria-describedby": validation || error ? `${id}-error` : undefined, type: "number", value: rangeValue ?? "", onKeyDown: (event) => {
412
+ if (event.key !== "Escape" && event.key !== "Tab")
413
+ event.stopPropagation();
414
+ }, onChange: (event) => {
415
+ // An unfinished minus sign or exponent is not an intentional clear.
416
+ if (event.target.validity.badInput)
417
+ return;
418
+ const next = event.target.value || null;
419
+ const range = [...(draft ?? [null, null])];
420
+ range[index] = next === null ? null : Number(next);
421
+ setDraft(range);
422
+ apply(range, false);
423
+ } })] }, key));
424
+ })] })) : (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: id, className: "mui-c74ab393b96d mui-daaac3fbf55e", children: field.searchLabel ?? field.label }), _jsx(Textarea, { id: id, ref: (node) => {
425
+ input.current = node;
426
+ }, disabled: disabled, placeholder: field.placeholder, value: text, "aria-invalid": Boolean(validation || error), "aria-describedby": validation || error ? `${id}-error` : undefined, onChange: (event) => setText(event.target.value), onKeyDown: (event) => {
427
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
428
+ event.preventDefault();
429
+ if (!validation)
430
+ apply(candidate);
431
+ }
432
+ if (event.key !== "Escape" && event.key !== "Tab")
433
+ event.stopPropagation();
434
+ } })] })), (validation || error) && (_jsx("p", { role: "alert", id: `${id}-error`, className: "mui-c74ab393b96d mui-887b9502d5c7", children: validation ?? error })), field.kind !== "numberRange" && (_jsx("p", { className: "mui-3992de70b033 mui-35f35c41d134", children: "Enter to save. Shift+Enter for a new line." }))] }));
435
+ }
@@ -0,0 +1,10 @@
1
+ import type { RuntimeField } from "./filter-definition.js";
2
+ export declare function FilterDateEditor({ field, value, disabled, apply, error, autoFocus, showLabel, }: {
3
+ autoFocus?: boolean;
4
+ showLabel?: boolean;
5
+ field: RuntimeField;
6
+ value: unknown;
7
+ disabled?: boolean;
8
+ apply(value: unknown, shouldClose?: boolean): void;
9
+ error?: string;
10
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useValueDraft } from "./use-value-draft.js";
4
+ import { Calendar } from "../customization.js";
5
+ import { Button } from "../customization.js";
6
+ // Use local calendar dates, without converting through a UTC timestamp.
7
+ function calendarDate(value) {
8
+ return value ? new Date(`${value}T12:00:00`) : undefined;
9
+ }
10
+ function dateString(value) {
11
+ if (!value)
12
+ return null;
13
+ return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, "0")}-${String(value.getDate()).padStart(2, "0")}`;
14
+ }
15
+ export function FilterDateEditor({ field, value, disabled, apply, error, autoFocus = true, showLabel = true, }) {
16
+ const [range, setRange] = useValueDraft(value, (current) => current ? { from: calendarDate(current.from), to: calendarDate(current.to) } : undefined);
17
+ return (_jsxs("div", { "data-mendy-ui": "", onKeyDown: (event) => {
18
+ // Calendar arrows navigate days. Escape still dismisses the surrounding editor.
19
+ if (event.key !== "Escape")
20
+ event.stopPropagation();
21
+ }, children: [showLabel && _jsx("p", { className: "mui-0f9e6672913a mui-0a702d97ed7d mui-c74ab393b96d mui-daaac3fbf55e", children: field.label }), _jsx(Calendar, { mode: "range", autoFocus: autoFocus, selected: range, onSelect: (next) => {
22
+ const selected = next ? { from: next.from, to: next.to ?? next.from } : undefined;
23
+ setRange(selected);
24
+ apply(selected
25
+ ? { from: dateString(selected.from), to: dateString(selected.to) }
26
+ : field.clearValue, false);
27
+ }, defaultMonth: range?.from ?? range?.to, disabled: disabled }), _jsxs("div", { "data-mendy-ui": "", className: "mui-267171770524 mui-894c3815a57f mui-094f5333853b", children: [_jsx("p", { className: "mui-3992de70b033 mui-35f35c41d134", children: "Pick a day, or two dates for a range." }), error && (_jsx("p", { role: "alert", className: "mui-c74ab393b96d mui-887b9502d5c7", children: error })), _jsx("div", { "data-mendy-ui": "", className: "mui-222f930b8752 mui-074569488cca", children: _jsx(Button, { type: "button", size: "sm", variant: "ghost", disabled: disabled, onClick: () => {
28
+ setRange(undefined);
29
+ apply(field.clearValue);
30
+ }, children: "Clear date" }) })] })] }));
31
+ }