@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,220 @@
1
+ import type { ReactNode } from "react";
2
+ export interface FilterCodec<V> {
3
+ parse(raw: string): V | null;
4
+ serialize(value: V): string;
5
+ }
6
+ export type SummaryPolicy = {
7
+ mode: "count";
8
+ limit?: number;
9
+ } | {
10
+ mode: "ellipsis";
11
+ maxWidth?: number;
12
+ } | {
13
+ mode: "all";
14
+ };
15
+ export interface Choice {
16
+ value: string;
17
+ label: string;
18
+ disabled?: boolean;
19
+ data?: unknown;
20
+ }
21
+ export interface OptionPage<T> {
22
+ items: readonly T[];
23
+ cursor?: string | null;
24
+ }
25
+ export interface RemoteOptions<T, P = unknown> {
26
+ kind: "remote";
27
+ scope: string;
28
+ params: P;
29
+ search(input: {
30
+ query: string;
31
+ cursor?: string;
32
+ params: P;
33
+ signal: AbortSignal;
34
+ }): Promise<OptionPage<T>>;
35
+ resolve(input: {
36
+ ids: string[];
37
+ params: P;
38
+ signal: AbortSignal;
39
+ }): Promise<readonly T[]>;
40
+ getValue(item: T): string;
41
+ getLabel(item: T): string;
42
+ debounceMs?: number;
43
+ }
44
+ export declare function remoteOptions<T, P>(source: Omit<RemoteOptions<T, P>, "kind">): RemoteOptions<T, P>;
45
+ export interface ExternalOptions<T> {
46
+ kind: "external";
47
+ items: readonly T[];
48
+ selectedItems?: readonly T[];
49
+ loading?: boolean;
50
+ error?: string | null;
51
+ retry?: () => void;
52
+ query?: string;
53
+ onQueryChange?: (query: string) => void;
54
+ hasMore?: boolean;
55
+ loadMore?: () => void;
56
+ }
57
+ export declare function externalOptions<T>(source: Omit<ExternalOptions<T>, "kind">): ExternalOptions<T>;
58
+ export interface EditorContext<V> {
59
+ value: V;
60
+ setValue(value: V): void;
61
+ close(): void;
62
+ /** Change the local draft without applying it. */
63
+ draft: V;
64
+ setDraft(value: V): void;
65
+ apply(value?: V): void;
66
+ }
67
+ export interface FieldConfig<V> {
68
+ label: string;
69
+ icon?: ReactNode;
70
+ defaultValue?: V;
71
+ clearValue?: V;
72
+ isActive?: (value: V) => boolean;
73
+ normalize?: (value: V) => V;
74
+ validate?: (value: V) => string | undefined;
75
+ codec?: FilterCodec<V>;
76
+ suggestion?: {
77
+ value?: V;
78
+ label?: string;
79
+ loading?: boolean;
80
+ disabled?: boolean;
81
+ };
82
+ summary?: SummaryPolicy;
83
+ renderSummary?: (value: V, choices: readonly Choice[]) => ReactNode;
84
+ renderEditor?: (context: EditorContext<V>) => ReactNode;
85
+ recognize?: (token: string) => V | undefined;
86
+ pastePriority?: number;
87
+ merge?: (current: V, incoming: V) => V;
88
+ urlKey?: string;
89
+ /** Hide only the menu entry; recognition and chip editing remain available. */
90
+ menu?: boolean;
91
+ hidden?: boolean;
92
+ disabled?: boolean;
93
+ removable?: boolean;
94
+ closeMenuOnApply?: boolean;
95
+ editorLabel?: string;
96
+ searchLabel?: string;
97
+ placeholder?: string;
98
+ }
99
+ export interface RuntimeSource {
100
+ kind: "local" | "external" | "remote";
101
+ items: Choice[];
102
+ selectedItems?: Choice[];
103
+ loading?: boolean;
104
+ error?: string | null;
105
+ retry?: () => void;
106
+ query?: string;
107
+ onQueryChange?: (query: string) => void;
108
+ hasMore?: boolean;
109
+ loadMore?: () => void;
110
+ scope?: string;
111
+ params?: unknown;
112
+ debounceMs?: number;
113
+ search?: (query: string, cursor: string | undefined, signal: AbortSignal) => Promise<OptionPage<Choice>>;
114
+ resolve?: (ids: string[], signal: AbortSignal) => Promise<readonly Choice[]>;
115
+ }
116
+ /** Runtime operations erase the value type only after the typed factory wraps callbacks. */
117
+ export interface RuntimeField {
118
+ renderOption?: (choice: Choice, context: {
119
+ selected: boolean;
120
+ }) => ReactNode;
121
+ kind: "single" | "multi" | "text" | "tokens" | "numberRange" | "dateRange" | "custom";
122
+ label: string;
123
+ icon?: ReactNode;
124
+ defaultValue: unknown;
125
+ clearValue: unknown;
126
+ isActive(value: unknown): boolean;
127
+ normalize(value: unknown): unknown;
128
+ validate(value: unknown): string | undefined;
129
+ codec: FilterCodec<unknown>;
130
+ suggestion?: {
131
+ value?: unknown;
132
+ label?: string;
133
+ loading?: boolean;
134
+ disabled?: boolean;
135
+ };
136
+ summary?: SummaryPolicy;
137
+ renderSummary?: (value: unknown, choices: readonly Choice[]) => ReactNode;
138
+ renderEditor?: (context: EditorContext<unknown>) => ReactNode;
139
+ recognize?: (token: string) => unknown;
140
+ pastePriority: number;
141
+ merge(current: unknown, incoming: unknown): unknown;
142
+ urlKey?: string;
143
+ source?: RuntimeSource;
144
+ searchable?: boolean;
145
+ /** Hide only the menu entry; recognition and chip editing remain available. */
146
+ menu?: boolean;
147
+ hidden?: boolean;
148
+ disabled?: boolean;
149
+ removable?: boolean;
150
+ closeMenuOnApply?: boolean;
151
+ editorLabel?: string;
152
+ searchLabel?: string;
153
+ placeholder?: string;
154
+ }
155
+ export interface Field<V> extends RuntimeField {
156
+ readonly valueType?: (value: V) => V;
157
+ }
158
+ export type FilterDefinitions = Record<string, RuntimeField>;
159
+ export type FilterValues<D extends FilterDefinitions> = {
160
+ [K in keyof D]: D[K] extends Field<infer V> ? V : never;
161
+ };
162
+ export declare function defineFilters<const D extends FilterDefinitions>(definitions: D): D;
163
+ export declare function valueIsActive(value: unknown): boolean;
164
+ export declare function equalValues(a: unknown, b: unknown): boolean;
165
+ export declare function jsonCodec<V>(accept: (value: unknown) => value is V): FilterCodec<V>;
166
+ interface SelectConfig<T, V> extends FieldConfig<V> {
167
+ renderOption?: (item: T, context: {
168
+ selected: boolean;
169
+ choice: Choice;
170
+ }) => ReactNode;
171
+ options: readonly T[] | ExternalOptions<T> | RemoteOptions<T, unknown>;
172
+ getValue?: (item: T) => string;
173
+ getLabel?: (item: T) => string;
174
+ searchable?: boolean;
175
+ loading?: boolean;
176
+ error?: string | null;
177
+ retry?: () => void;
178
+ }
179
+ export type OptionSource<T> = readonly T[] | ExternalOptions<T> | RemoteOptions<T, unknown>;
180
+ export type DateRange = {
181
+ from: string | null;
182
+ to: string | null;
183
+ };
184
+ export type NumberRange = [number | null, number | null];
185
+ export declare const filter: {
186
+ text(config: FieldConfig<string | null>): Field<string | null>;
187
+ tokens(config: FieldConfig<string[] | null>): Field<string[] | null>;
188
+ select<const T extends {
189
+ value: string;
190
+ label: string;
191
+ }>(config: SelectConfig<T, T["value"] | null>): Field<T["value"] | null>;
192
+ multiSelect<const T extends {
193
+ value: string;
194
+ label: string;
195
+ }>(config: SelectConfig<T, T["value"][] | null>): Field<T["value"][] | null>;
196
+ options<T>(config: SelectConfig<T, string[] | null>): Field<string[] | null>;
197
+ numberRange(config: FieldConfig<NumberRange | null>): Field<NumberRange | null>;
198
+ dateRange(config: FieldConfig<DateRange | null>): Field<DateRange | null>;
199
+ custom<V>(config: FieldConfig<V> & {
200
+ codec: FilterCodec<V>;
201
+ defaultValue: V;
202
+ clearValue: V;
203
+ }): Field<V>;
204
+ };
205
+ export interface BoundField<S> extends RuntimeField {
206
+ read(state: S): unknown;
207
+ write(value: unknown, state: S): Partial<S>;
208
+ }
209
+ export type BoundDefinitions<S> = Record<string, BoundField<S>>;
210
+ export declare function bindFilters<S>(): {
211
+ field<K extends keyof S>(key: K, definition: Field<NoInfer<Exclude<S[K], undefined>>>, options?: {
212
+ update(value: S[K], current: S): Partial<S>;
213
+ }): BoundField<S>;
214
+ composite<const K extends readonly (keyof S)[], V>(keys: K, config: {
215
+ field: Field<V>;
216
+ read(state: Pick<S, K[number]>): V;
217
+ write(value: V, current: Pick<S, K[number]>): Pick<S, K[number]>;
218
+ }): BoundField<S>;
219
+ };
220
+ export {};
@@ -0,0 +1,227 @@
1
+ export function remoteOptions(source) {
2
+ return { ...source, kind: "remote" };
3
+ }
4
+ export function externalOptions(source) {
5
+ return { ...source, kind: "external" };
6
+ }
7
+ export function defineFilters(definitions) {
8
+ return definitions;
9
+ }
10
+ export function valueIsActive(value) {
11
+ return (value !== null &&
12
+ value !== undefined &&
13
+ value !== "" &&
14
+ (!Array.isArray(value) || value.length > 0));
15
+ }
16
+ export function equalValues(a, b) {
17
+ return JSON.stringify(a) === JSON.stringify(b);
18
+ }
19
+ export function jsonCodec(accept) {
20
+ return {
21
+ parse(raw) {
22
+ try {
23
+ const value = JSON.parse(raw);
24
+ return accept(value) ? value : null;
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ },
30
+ serialize: (value) => JSON.stringify(value),
31
+ };
32
+ }
33
+ function makeField(kind, config, fallback, codec) {
34
+ const normalize = (value) => (config.normalize ? config.normalize(value) : value);
35
+ return {
36
+ ...config,
37
+ kind,
38
+ defaultValue: config.defaultValue === undefined ? fallback : config.defaultValue,
39
+ clearValue: config.clearValue === undefined ? fallback : config.clearValue,
40
+ normalize,
41
+ isActive: (value) => (config.isActive ? config.isActive(value) : valueIsActive(value)),
42
+ validate: (value) => config.validate?.(value),
43
+ codec: {
44
+ parse: (raw) => {
45
+ const value = (config.codec ?? codec).parse(raw);
46
+ return value === null ? null : normalize(value);
47
+ },
48
+ serialize: (value) => (config.codec ?? codec).serialize(value),
49
+ },
50
+ renderSummary: config.renderSummary
51
+ ? (value, choices) => config.renderSummary(value, choices)
52
+ : undefined,
53
+ renderEditor: config.renderEditor
54
+ ? (context) => config.renderEditor(context)
55
+ : undefined,
56
+ recognize: config.recognize,
57
+ pastePriority: config.pastePriority ?? 0,
58
+ merge: (current, incoming) => config.merge
59
+ ? config.merge(current, incoming)
60
+ : Array.isArray(current) && Array.isArray(incoming)
61
+ ? [...new Set([...current, ...incoming])]
62
+ : incoming,
63
+ };
64
+ }
65
+ function optionRenderer(config) {
66
+ return config.renderOption
67
+ ? (choice, context) => config.renderOption(choice.data, { ...context, choice })
68
+ : undefined;
69
+ }
70
+ function sourceFor(config) {
71
+ const options = config.options;
72
+ const choice = (item) => ({
73
+ value: config.getValue ? config.getValue(item) : item.value,
74
+ label: config.getLabel ? config.getLabel(item) : item.label,
75
+ disabled: item.disabled,
76
+ data: item,
77
+ });
78
+ if (Array.isArray(options))
79
+ return {
80
+ kind: "local",
81
+ items: options.map(choice),
82
+ loading: config.loading,
83
+ error: config.error,
84
+ retry: config.retry,
85
+ };
86
+ const source = options;
87
+ if (source.kind === "external")
88
+ return {
89
+ ...source,
90
+ items: source.items.map(choice),
91
+ selectedItems: source.selectedItems?.map(choice),
92
+ };
93
+ const remoteChoice = (item) => ({
94
+ value: source.getValue(item),
95
+ label: source.getLabel(item),
96
+ data: item,
97
+ });
98
+ return {
99
+ kind: "remote",
100
+ items: [],
101
+ scope: source.scope,
102
+ params: source.params,
103
+ debounceMs: source.debounceMs,
104
+ search: async (query, cursor, signal) => {
105
+ const page = await source.search({ query, cursor, signal, params: source.params });
106
+ return { items: page.items.map(remoteChoice), cursor: page.cursor };
107
+ },
108
+ resolve: async (ids, signal) => (await source.resolve({ ids, signal, params: source.params })).map(remoteChoice),
109
+ };
110
+ }
111
+ const stringCodec = {
112
+ parse: (raw) => raw,
113
+ serialize: (value) => value ?? "",
114
+ };
115
+ const stringsCodec = jsonCodec((value) => value === null || (Array.isArray(value) && value.every((item) => typeof item === "string")));
116
+ function validDate(value) {
117
+ return (value === null ||
118
+ (typeof value === "string" &&
119
+ /^\d{4}-\d{2}-\d{2}$/.test(value) &&
120
+ !Number.isNaN(Date.parse(value)) &&
121
+ new Date(value).toISOString().slice(0, 10) === value));
122
+ }
123
+ export const filter = {
124
+ text(config) {
125
+ return makeField("text", { normalize: (value) => value?.trim() || null, ...config }, null, stringCodec);
126
+ },
127
+ tokens(config) {
128
+ return makeField("tokens", {
129
+ normalize: (value) => value?.length
130
+ ? [...new Set(value.flatMap((item) => (item.trim() ? [item.trim()] : [])))]
131
+ : null,
132
+ ...config,
133
+ }, null, stringsCodec);
134
+ },
135
+ select(config) {
136
+ const allowed = Array.isArray(config.options)
137
+ ? new Set(config.options.map((item) => item.value))
138
+ : null;
139
+ const field = makeField("single", config, null, {
140
+ parse: (raw) => (!allowed || allowed.has(raw) ? raw : null),
141
+ serialize: stringCodec.serialize,
142
+ });
143
+ return {
144
+ ...field,
145
+ source: sourceFor(config),
146
+ renderOption: optionRenderer(config),
147
+ searchable: config.searchable,
148
+ };
149
+ },
150
+ multiSelect(config) {
151
+ const allowed = Array.isArray(config.options)
152
+ ? new Set(config.options.map((item) => item.value))
153
+ : null;
154
+ return {
155
+ ...makeField("multi", config, null, {
156
+ parse: (raw) => {
157
+ const values = stringsCodec.parse(raw);
158
+ return values && (!allowed || values.every((value) => allowed.has(value)))
159
+ ? values
160
+ : null;
161
+ },
162
+ serialize: stringsCodec.serialize,
163
+ }),
164
+ source: sourceFor(config),
165
+ renderOption: optionRenderer(config),
166
+ searchable: config.searchable,
167
+ };
168
+ },
169
+ options(config) {
170
+ return {
171
+ ...makeField("multi", config, null, stringsCodec),
172
+ source: sourceFor(config),
173
+ renderOption: optionRenderer(config),
174
+ searchable: config.searchable,
175
+ };
176
+ },
177
+ numberRange(config) {
178
+ return makeField("numberRange", {
179
+ normalize: (value) => (value?.some((bound) => bound !== null) ? value : null),
180
+ validate: (value) => value && value[0] !== null && value[1] !== null && value[0] > value[1]
181
+ ? "Minimum must not exceed maximum."
182
+ : undefined,
183
+ ...config,
184
+ }, null, jsonCodec((value) => value === null ||
185
+ (Array.isArray(value) &&
186
+ value.length === 2 &&
187
+ value.every((item) => item === null || (typeof item === "number" && Number.isFinite(item))))));
188
+ },
189
+ dateRange(config) {
190
+ return makeField("dateRange", {
191
+ normalize: (value) => (value?.from || value?.to ? value : null),
192
+ isActive: (value) => Boolean(value?.from || value?.to),
193
+ validate: (value) => value?.from && value.to && value.from > value.to
194
+ ? "Start date must not follow end date."
195
+ : undefined,
196
+ ...config,
197
+ }, null, jsonCodec((value) => value === null ||
198
+ (typeof value === "object" &&
199
+ value !== null &&
200
+ "from" in value &&
201
+ "to" in value &&
202
+ validDate(value.from) &&
203
+ validDate(value.to))));
204
+ },
205
+ custom(config) {
206
+ return makeField("custom", config, config.defaultValue, config.codec);
207
+ },
208
+ };
209
+ export function bindFilters() {
210
+ return {
211
+ field(key, definition, options) {
212
+ return {
213
+ ...definition,
214
+ read: (state) => (state[key] === undefined ? definition.defaultValue : state[key]),
215
+ write: (value, state) => options ? options.update(value, state) : { [key]: value },
216
+ };
217
+ },
218
+ composite(keys, config) {
219
+ const pick = (state) => Object.fromEntries(keys.map((key) => [key, state[key]]));
220
+ return {
221
+ ...config.field,
222
+ read: (state) => config.read(pick(state)),
223
+ write: (value, state) => config.write(value, pick(state)),
224
+ };
225
+ },
226
+ };
227
+ }
@@ -0,0 +1,21 @@
1
+ import type { ReactNode, RefObject } from "react";
2
+ export interface FilterMenuSection {
3
+ id: string;
4
+ label: string;
5
+ editorLabel: string;
6
+ icon?: ReactNode;
7
+ disabled: boolean;
8
+ active: boolean;
9
+ clear?: () => void;
10
+ content: ReactNode;
11
+ }
12
+ interface FilterMenuPanelProps {
13
+ sections: FilterMenuSection[];
14
+ selectedId: string | null;
15
+ onSelect(id: string | null): void;
16
+ anchor?: RefObject<HTMLDivElement | null>;
17
+ trigger: RefObject<HTMLButtonElement | null>;
18
+ }
19
+ /** One dialog contains the filter list and its editor, with a single-panel layout on phones. */
20
+ export declare function FilterMenuPanel({ sections, selectedId, onSelect, anchor, trigger, }: FilterMenuPanelProps): import("react/jsx-runtime").JSX.Element;
21
+ export {};
@@ -0,0 +1,164 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
4
+ import { ArrowLeft, ChevronRight } from "lucide-react";
5
+ import { Button } from "../customization.js";
6
+ import { DropdownMenuContent } from "../primitives/dropdown-menu.js";
7
+ import { useMendyUI } from "../customization.js";
8
+ import { cn } from "../utils.js";
9
+ const desktopQuery = "(min-width: 640px)";
10
+ function subscribeViewport(listener) {
11
+ const media = window.matchMedia(desktopQuery);
12
+ media.addEventListener("change", listener);
13
+ return () => media.removeEventListener("change", listener);
14
+ }
15
+ const isDesktop = () => window.matchMedia(desktopQuery).matches;
16
+ const serverDesktop = () => false;
17
+ /** One dialog contains the filter list and its editor, with a single-panel layout on phones. */
18
+ export function FilterMenuPanel({ sections, selectedId, onSelect, anchor, trigger, }) {
19
+ const { classNames } = useMendyUI();
20
+ const desktop = useSyncExternalStore(subscribeViewport, isDesktop, serverDesktop);
21
+ const selected = sections.find((section) => section.id === selectedId && !section.disabled) ??
22
+ (desktop ? sections.find((section) => !section.disabled) : undefined);
23
+ const content = useRef(null);
24
+ const editor = useRef(null);
25
+ const rows = useRef(new Map());
26
+ const pendingEditorFocus = useRef(false);
27
+ const [alignOffset, setAlignOffset] = useState(0);
28
+ const panelId = useId();
29
+ useLayoutEffect(() => {
30
+ const button = trigger.current;
31
+ if (!button)
32
+ return;
33
+ const update = () => {
34
+ const bounds = button.getBoundingClientRect();
35
+ const target = anchor?.current?.getBoundingClientRect();
36
+ // Desktop starts at the search field's edge; mobile stays close to the icon.
37
+ setAlignOffset(desktop ? (target?.left ?? bounds.left) - bounds.left : 0);
38
+ };
39
+ update();
40
+ const observer = new ResizeObserver(update);
41
+ observer.observe(anchor?.current ?? button);
42
+ window.addEventListener("resize", update);
43
+ return () => {
44
+ observer.disconnect();
45
+ window.removeEventListener("resize", update);
46
+ };
47
+ }, [anchor, desktop, trigger]);
48
+ useEffect(() => {
49
+ const frame = requestAnimationFrame(() => {
50
+ const first = [...rows.current.values()].find((button) => !button.disabled);
51
+ (first ?? content.current)?.focus();
52
+ });
53
+ return () => cancelAnimationFrame(frame);
54
+ }, []);
55
+ function focusEditor() {
56
+ const root = editor.current;
57
+ if (!root)
58
+ return;
59
+ // Prefer the current calendar day to month-navigation buttons.
60
+ const target = root.querySelector('input:not([disabled]), textarea:not([disabled]), [role="grid"] button[tabindex="0"], [role^="menuitem"]:not([data-disabled])') ?? root.querySelector('button:not([disabled]), [tabindex="0"]');
61
+ (target ?? root).focus();
62
+ }
63
+ useLayoutEffect(() => {
64
+ if (!pendingEditorFocus.current)
65
+ return;
66
+ pendingEditorFocus.current = false;
67
+ focusEditor();
68
+ });
69
+ function choose(id, enter = false) {
70
+ if (enter && selected?.id === id && editor.current)
71
+ focusEditor();
72
+ else
73
+ pendingEditorFocus.current = enter;
74
+ onSelect(id);
75
+ }
76
+ function back() {
77
+ const previous = selected?.id;
78
+ onSelect(null);
79
+ requestAnimationFrame(() => previous && rows.current.get(previous)?.focus());
80
+ }
81
+ const showList = desktop || !selected;
82
+ return (_jsx(DropdownMenuContent, { ref: content, "data-mendy-ui": "", "data-slot": "filter-menu-panel", role: "dialog", "aria-label": "Filters", "aria-labelledby": undefined, "aria-orientation": undefined, align: desktop ? "start" : "end", alignOffset: alignOffset, sideOffset: 7, collisionPadding: 12, onEscapeKeyDown: (event) => {
83
+ if (selected && !desktop) {
84
+ event.preventDefault();
85
+ back();
86
+ }
87
+ }, onCloseAutoFocus: (event) => {
88
+ const focused = document.activeElement;
89
+ if (event.target instanceof HTMLElement &&
90
+ focused &&
91
+ focused !== document.body &&
92
+ !event.target.contains(focused))
93
+ event.preventDefault();
94
+ }, onKeyDown: (event) => {
95
+ // Allow normal Tab traversal within the dialog; Radix menus normally cancel Tab.
96
+ if (event.key === "Tab")
97
+ event.stopPropagation();
98
+ const target = event.target;
99
+ const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
100
+ if (event.key === (rtl ? "ArrowRight" : "ArrowLeft") &&
101
+ target instanceof HTMLElement &&
102
+ !target.closest('input, textarea, select, [role="grid"], [contenteditable=true]')) {
103
+ event.preventDefault();
104
+ event.stopPropagation();
105
+ if (desktop)
106
+ rows.current.get(selected?.id ?? "")?.focus();
107
+ else
108
+ back();
109
+ }
110
+ }, className: cn("mui-981e5fc95664 mui-571ea69568d3 mui-a5c6864f064f mui-d5111d0e9f48 mui-04760bcd507f mui-94ea94fde25f mui-393df0d154e0 mui-548a450e8e53", desktop && selected ? "mui-9958eb2b312a" : "mui-a4cce7869436", classNames?.menu), children: _jsxs("div", { className: cn("mui-571ea69568d3", desktop && selected
111
+ ? "mui-0f2a693e93e2 mui-50f2bcd37640" : "mui-222f930b8752 mui-302c0d124a94"), children: [showList && (_jsx(FilterMenuList, { sections: sections, selectedId: selected?.id, panelId: panelId, desktop: desktop, rows: rows, choose: choose })), selected && (_jsxs("div", { className: "mui-222f930b8752 mui-410da8dfa8ac mui-184ddc11e5f9 mui-302c0d124a94", children: [_jsxs("div", { className: cn("mui-222f930b8752 mui-8423dc94ee06 mui-27ead27a81df mui-71556df3b421 mui-074569488cca mui-bbe39cfb5cc6 mui-0f9e6672913a mui-b5edc3ea7c91 mui-3992de70b033 mui-daaac3fbf55e", classNames?.menuHeader), children: [!desktop && (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: back, className: "mui-9b2a4c5f630c mui-70007b876865 mui-e7e01cc7f4df mui-3992de70b033 mui-52101fc7d8bb", children: [_jsx(ArrowLeft, { "aria-hidden": "true", className: "mui-61c000c8a98a mui-36b12e8339c7" }), "Filters"] }), _jsx("span", { "aria-hidden": "true", className: "mui-35f35c41d134", children: "/" })] })), _jsx("span", { className: "mui-184ddc11e5f9 mui-7bd5bab6d7f4 mui-726fca123972 mui-393883c1db68", children: selected.label }), selected.clear && (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
112
+ selected.clear?.();
113
+ requestAnimationFrame(focusEditor);
114
+ }, "aria-label": `Clear ${selected.label} filter`, className: "mui-ce1945804664 mui-1da2e8f173c5 mui-3992de70b033 mui-52101fc7d8bb mui-35f35c41d134", children: "Clear" }))] }), _jsx("div", { ref: editor, id: panelId, role: "group", "aria-label": selected.editorLabel, tabIndex: -1, className: cn("mui-410da8dfa8ac mui-184ddc11e5f9 mui-2368e909f3a5 mui-998780cdef87 mui-b30fc56058b6 mui-f157ee6bae99 mui-a1c8c43d9be2", classNames?.editor), children: selected.content }, selected.id)] }))] }) }));
115
+ }
116
+ function FilterMenuList({ sections, selectedId, panelId, desktop, rows, choose, }) {
117
+ const { classNames } = useMendyUI();
118
+ return (_jsxs("div", { role: "group", "aria-label": "Filter types", className: cn("mui-410da8dfa8ac mui-2368e909f3a5 mui-998780cdef87 mui-1dee6e3ec67d", desktop && selectedId && "mui-baf794b99c10", classNames?.menuList), children: [sections.length === 0 && (_jsx("p", { className: "mui-094f5333853b mui-c74ab393b96d mui-35f35c41d134", children: "No filters available." })), sections.map((section) => (_jsxs(Button, { ref: (node) => {
119
+ if (node)
120
+ rows.current.set(section.id, node);
121
+ else
122
+ rows.current.delete(section.id);
123
+ }, variant: "ghost", type: "button", "aria-label": section.label, "aria-expanded": selectedId === section.id, "aria-controls": selectedId === section.id ? panelId : undefined, disabled: section.disabled, className: cn("mui-a66e9985b093 mui-8423dc94ee06 mui-58d0413dd93c mui-c62ec162662e mui-074569488cca mui-02e603944040 mui-e7e01cc7f4df mui-b5edc3ea7c91 mui-c74ab393b96d mui-52101fc7d8bb", selectedId === section.id && "mui-292affc1f780 mui-cc209238d847", classNames?.menuRow), onPointerEnter: (event) => {
124
+ if (desktop && event.pointerType === "mouse" && !section.disabled)
125
+ choose(section.id);
126
+ }, onFocus: () => {
127
+ if (desktop)
128
+ choose(section.id);
129
+ }, onClick: () => choose(section.id, true), onKeyDown: (event) => {
130
+ const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
131
+ if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
132
+ event.preventDefault();
133
+ event.stopPropagation();
134
+ const enabled = sections.filter((item) => !item.disabled);
135
+ const index = enabled.findIndex((item) => item.id === section.id);
136
+ const next = event.key === "Home"
137
+ ? enabled[0]
138
+ : event.key === "End"
139
+ ? enabled.at(-1)
140
+ : enabled[(index + (event.key === "ArrowDown" ? 1 : -1) + enabled.length) %
141
+ enabled.length];
142
+ if (next)
143
+ rows.current.get(next.id)?.focus();
144
+ }
145
+ else if ([rtl ? "ArrowLeft" : "ArrowRight", "Enter", " "].includes(event.key)) {
146
+ event.preventDefault();
147
+ event.stopPropagation();
148
+ choose(section.id, true);
149
+ }
150
+ else if (event.key.length === 1 &&
151
+ !event.ctrlKey &&
152
+ !event.metaKey &&
153
+ !event.altKey) {
154
+ const start = sections.findIndex((item) => item.id === section.id) + 1;
155
+ const next = [...sections.slice(start), ...sections.slice(0, start)].find((item) => !item.disabled &&
156
+ item.label.toLocaleLowerCase().startsWith(event.key.toLocaleLowerCase()));
157
+ if (next) {
158
+ event.preventDefault();
159
+ event.stopPropagation();
160
+ rows.current.get(next.id)?.focus();
161
+ }
162
+ }
163
+ }, children: [section.icon && (_jsx("span", { "aria-hidden": "true", className: "mui-27ead27a81df mui-35f35c41d134 mui-c77732bd3ab4", children: section.icon })), _jsx("span", { className: "mui-184ddc11e5f9 mui-7bd5bab6d7f4 mui-726fca123972 mui-393883c1db68 mui-1a5d10977b93", title: section.label, children: section.label }), section.active && (_jsx("span", { "aria-hidden": "true", className: "mui-dc797fcc7165 mui-27ead27a81df mui-b005d61d0953 mui-0cf6c2e8fe7e" })), _jsx(ChevronRight, { "aria-hidden": "true", className: "mui-61c000c8a98a mui-27ead27a81df mui-0b5a4d1257ab mui-36b12e8339c7" })] }, section.id)))] }));
164
+ }
@@ -0,0 +1,25 @@
1
+ export interface FilterOption {
2
+ value: string;
3
+ label: string;
4
+ disabled?: boolean;
5
+ }
6
+ interface SharedProps {
7
+ label: string;
8
+ options: readonly FilterOption[];
9
+ searchable?: boolean;
10
+ searchPlaceholder?: string;
11
+ emptyMessage?: string;
12
+ }
13
+ export interface FilterSelectEditorProps extends SharedProps {
14
+ value: string;
15
+ onValueChange: (value: string) => void;
16
+ }
17
+ /** Single selection applies immediately and closes the dropdown. */
18
+ export declare function FilterSelectEditor({ value, onValueChange, ...props }: FilterSelectEditorProps): import("react/jsx-runtime").JSX.Element;
19
+ export interface FilterMultiSelectEditorProps extends SharedProps {
20
+ values: readonly string[];
21
+ onValuesChange: (values: string[]) => void;
22
+ }
23
+ /** Multiple selections apply immediately and keep the editor open. */
24
+ export declare function FilterMultiSelectEditor({ values, onValuesChange, ...props }: FilterMultiSelectEditorProps): import("react/jsx-runtime").JSX.Element;
25
+ export {};