@mendylanda/ui 0.3.2-dev.56.581b055ea0a9 → 0.3.2-dev.58.0ef9785a4046
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 +15 -0
- package/dist/calendar-locale.d.ts +7 -0
- package/dist/calendar-locale.js +31 -0
- package/dist/class-names.js +1 -1
- package/dist/customization.d.ts +6 -1
- package/dist/customization.js +20 -2
- package/dist/filters/filter-bar.d.ts +1 -1
- package/dist/filters/filter-bar.js +56 -30
- package/dist/filters/filter-collection.js +3 -1
- package/dist/filters/filter-date-editor.js +5 -3
- package/dist/filters/filter-definition.d.ts +3 -2
- package/dist/filters/filter-definition.js +5 -6
- package/dist/filters/filter-menu-focus.js +1 -1
- package/dist/filters/filter-menu-panel.js +13 -9
- package/dist/filters/filter-select-editor.js +6 -4
- package/dist/filters/filter-text-editor.js +5 -3
- package/dist/filters/filters.js +8 -5
- package/dist/filters/index.d.ts +3 -0
- package/dist/filters/index.js +2 -0
- package/dist/filters/use-filter-options.d.ts +1 -1
- package/dist/filters/use-filter-options.js +13 -7
- package/dist/filters/use-filters.js +6 -2
- package/dist/filters/use-url-filters.js +9 -7
- package/dist/locale-context.d.ts +29 -0
- package/dist/locale-context.js +8 -0
- package/dist/locale.d.ts +37 -0
- package/dist/locale.js +26 -0
- package/dist/locales/en.d.ts +113 -0
- package/dist/locales/en.js +112 -0
- package/dist/locales/he.d.ts +116 -0
- package/dist/locales/he.js +116 -0
- package/dist/primitives/calendar.d.ts +1 -1
- package/dist/primitives/calendar.js +15 -10
- package/dist/primitives/dropdown-menu.js +7 -5
- package/dist/primitives/popover.js +3 -1
- package/dist/styles.css +39 -39
- package/dist/styles.tailwind3.css +39 -39
- package/dist/table/table-column-settings.js +27 -13
- package/dist/table/table-controls.js +21 -8
- package/dist/table/table-feedback.js +6 -3
- package/dist/table/table-frame.js +3 -1
- package/dist/table/table-layout.d.ts +1 -1
- package/dist/table/table-layout.js +5 -5
- package/dist/table/table-loading.js +3 -1
- package/dist/table/table-parts.js +15 -11
- package/dist/table/table-view.js +6 -4
- package/dist/table/use-column-window.js +1 -1
- package/dist/table/use-data-table.js +3 -0
- package/dist/table/use-table-interaction.js +12 -8
- package/package.json +8 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MendyMessages } from "../locale.js";
|
|
1
2
|
import type { ReactNode } from "react";
|
|
2
3
|
export interface FilterCodec<V> {
|
|
3
4
|
parse(raw: string): V | null;
|
|
@@ -74,7 +75,7 @@ export interface FieldConfig<V> {
|
|
|
74
75
|
clearValue?: V;
|
|
75
76
|
isActive?: (value: V) => boolean;
|
|
76
77
|
normalize?: (value: V) => V;
|
|
77
|
-
validate?: (value: V) => string | undefined;
|
|
78
|
+
validate?: (value: V, messages?: MendyMessages) => string | undefined;
|
|
78
79
|
codec?: FilterCodec<V>;
|
|
79
80
|
suggestion?: {
|
|
80
81
|
value?: V;
|
|
@@ -133,7 +134,7 @@ export interface RuntimeField {
|
|
|
133
134
|
clearValue: unknown;
|
|
134
135
|
isActive(value: unknown): boolean;
|
|
135
136
|
normalize(value: unknown): unknown;
|
|
136
|
-
validate(value: unknown): string | undefined;
|
|
137
|
+
validate(value: unknown, messages?: MendyMessages): string | undefined;
|
|
137
138
|
codec: FilterCodec<unknown>;
|
|
138
139
|
suggestion?: {
|
|
139
140
|
value?: unknown;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { englishMessages } from "../locales/en.js";
|
|
1
2
|
export function remoteOptions(source) {
|
|
2
3
|
return { ...source, kind: "remote" };
|
|
3
4
|
}
|
|
@@ -40,7 +41,7 @@ function makeField(kind, config, fallback, codec) {
|
|
|
40
41
|
clearValue: config.clearValue === undefined ? fallback : config.clearValue,
|
|
41
42
|
normalize,
|
|
42
43
|
isActive: (value) => (config.isActive ? config.isActive(value) : valueIsActive(value)),
|
|
43
|
-
validate: (value) => config.validate?.(value),
|
|
44
|
+
validate: (value, messages) => config.validate?.(value, messages),
|
|
44
45
|
codec: {
|
|
45
46
|
parse: (raw) => {
|
|
46
47
|
const value = (config.codec ?? codec).parse(raw);
|
|
@@ -178,8 +179,8 @@ export const filter = {
|
|
|
178
179
|
numberRange(config) {
|
|
179
180
|
return makeField("numberRange", {
|
|
180
181
|
normalize: (value) => (value?.some((bound) => bound !== null) ? value : null),
|
|
181
|
-
validate: (value) => value && value[0] !== null && value[1] !== null && value[0] > value[1]
|
|
182
|
-
?
|
|
182
|
+
validate: (value, messages = englishMessages) => value && value[0] !== null && value[1] !== null && value[0] > value[1]
|
|
183
|
+
? messages.numberRangeError
|
|
183
184
|
: undefined,
|
|
184
185
|
...config,
|
|
185
186
|
}, null, jsonCodec((value) => value === null ||
|
|
@@ -191,9 +192,7 @@ export const filter = {
|
|
|
191
192
|
return makeField("dateRange", {
|
|
192
193
|
normalize: (value) => (value?.from || value?.to ? value : null),
|
|
193
194
|
isActive: (value) => Boolean(value?.from || value?.to),
|
|
194
|
-
validate: (value) => value?.from && value.to && value.from > value.to
|
|
195
|
-
? "Start date must not follow end date."
|
|
196
|
-
: undefined,
|
|
195
|
+
validate: (value, messages = englishMessages) => value?.from && value.to && value.from > value.to ? messages.dateRangeError : undefined,
|
|
197
196
|
...config,
|
|
198
197
|
}, null, jsonCodec((value) => value === null ||
|
|
199
198
|
(typeof value === "object" &&
|
|
@@ -49,7 +49,7 @@ export function handleMenuTab(event, { trigger, editor, onClose, hasSelection, }
|
|
|
49
49
|
(element.matches('[role^="menuitem"]') ? menuStops.has(element) : element.tabIndex >= 0));
|
|
50
50
|
const index = stops.indexOf(target);
|
|
51
51
|
const next = index < 0 ? undefined : stops[index + (event.shiftKey ? -1 : 1)];
|
|
52
|
-
if (target.closest('[
|
|
52
|
+
if (target.closest('[data-slot="filter-menu-list"]') &&
|
|
53
53
|
!event.shiftKey &&
|
|
54
54
|
hasSelection &&
|
|
55
55
|
(!next || editor?.contains(next)))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useMendyLocale } from "../locale-context.js";
|
|
3
4
|
import { useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
5
|
import { ArrowLeft, ChevronRight } from "lucide-react";
|
|
5
6
|
import { Button, Input } from "../customization.js";
|
|
@@ -32,6 +33,7 @@ function selectedSection(sections, selectedId) {
|
|
|
32
33
|
/** One dialog contains the filter list and its editor, with a single-panel layout on phones. */
|
|
33
34
|
export function FilterMenuPanel({ sections, selectedId, onSelect, onClose, anchor, trigger, }) {
|
|
34
35
|
const { classNames, menuLayout } = useMendyUI();
|
|
36
|
+
const { t } = useMendyLocale();
|
|
35
37
|
const desktop = useSyncExternalStore(subscribeViewport, isDesktop, serverDesktop);
|
|
36
38
|
const selected = selectedSection(sections, selectedId);
|
|
37
39
|
const content = useRef(null);
|
|
@@ -93,7 +95,7 @@ export function FilterMenuPanel({ sections, selectedId, onSelect, onClose, ancho
|
|
|
93
95
|
}
|
|
94
96
|
const showList = desktop || !selected;
|
|
95
97
|
const sideBySide = desktop && Boolean(selected);
|
|
96
|
-
return (_jsx(DropdownMenuContent, { ref: setContent, "data-mendy-ui": "", "data-slot": "filter-menu-panel", role: "dialog", "aria-label": "
|
|
98
|
+
return (_jsx(DropdownMenuContent, { ref: setContent, "data-mendy-ui": "", "data-slot": "filter-menu-panel", role: "dialog", "aria-label": t("filters"), "aria-labelledby": undefined, "aria-orientation": undefined, align: desktop ? "start" : "end", alignOffset: alignOffset, side: side, style: anchorStyle, sideOffset: 7, collisionPadding: 16, onEscapeKeyDown: (event) => {
|
|
97
99
|
if (selected && !desktop) {
|
|
98
100
|
event.preventDefault();
|
|
99
101
|
back();
|
|
@@ -136,10 +138,11 @@ export function FilterMenuPanel({ sections, selectedId, onSelect, onClose, ancho
|
|
|
136
138
|
}
|
|
137
139
|
function EditorHeading({ section, desktop, back, onCleared, }) {
|
|
138
140
|
const { classNames } = useMendyUI();
|
|
139
|
-
|
|
141
|
+
const { t } = useMendyLocale();
|
|
142
|
+
return (_jsxs("div", { className: cn("mui-222f930b8752 mui-8423dc94ee06 mui-27ead27a81df mui-71556df3b421 mui-074569488cca mui-bbe39cfb5cc6 mui-0f9e6672913a mui-b5edc3ea7c91 mui-3992de70b033 mui-daaac3fbf55e mui-aa0e3c036805 mui-0288c4ea71ea", classNames?.menuHeader), children: [!desktop && (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: back, className: "mui-958e2cfcf5b6 mui-70007b876865 mui-e7e01cc7f4df mui-3992de70b033 mui-52101fc7d8bb", children: [_jsx(ArrowLeft, { "aria-hidden": "true", className: "mui-61c000c8a98a mui-36b12e8339c7" }), t("filters")] }), _jsx("span", { "aria-hidden": "true", className: "mui-35f35c41d134", children: "/" })] })), _jsx(MenuHeading, { label: section.label }), section.clear && (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
|
|
140
143
|
section.clear?.();
|
|
141
144
|
requestAnimationFrame(onCleared);
|
|
142
|
-
}, "aria-label":
|
|
145
|
+
}, "aria-label": t("clearFilter", { label: section.label }), className: "mui-8cc46fa87f41 mui-ce1945804664 mui-1da2e8f173c5 mui-3992de70b033 mui-52101fc7d8bb mui-35f35c41d134", children: t("clear") }))] }));
|
|
143
146
|
}
|
|
144
147
|
function MenuHeading({ label }) {
|
|
145
148
|
const ref = useRef(null);
|
|
@@ -167,30 +170,31 @@ function MenuHeading({ label }) {
|
|
|
167
170
|
function FilterMenuList({ sections, selectedId, initialKey, panelId, desktop, detached, rows, choose, keyboardNavigation, onPointerMove, }) {
|
|
168
171
|
const { classNames } = useMendyUI();
|
|
169
172
|
const [query, setQuery] = useValueDraft("types", () => "", "__menu:query");
|
|
173
|
+
const { t, code } = useMendyLocale();
|
|
170
174
|
const collection = useRef(null);
|
|
171
175
|
const matches = [];
|
|
172
|
-
const term = query.toLocaleLowerCase();
|
|
176
|
+
const term = query.toLocaleLowerCase(code);
|
|
173
177
|
for (const section of sections) {
|
|
174
|
-
if (section.label.toLocaleLowerCase().includes(term))
|
|
178
|
+
if (section.label.toLocaleLowerCase(code).includes(term))
|
|
175
179
|
matches.push({ ...section, key: section.id });
|
|
176
180
|
}
|
|
177
|
-
return (_jsxs("div", { "data-slot": "filter-menu-list", className: cn("mui-222f930b8752 mui-410da8dfa8ac mui-302c0d124a94", desktop && selectedId && !detached && "mui-3b6d5fc7b061", detached && "mui-cb20bd519346 mui-d5111d0e9f48 mui-3fa8c572949b mui-4f1a55de40bc mui-e593c38256e0 mui-5bd2afe89f0b"), children: [sections.length > 20 && (_jsx("div", { className: "mui-27ead27a81df mui-bbe39cfb5cc6 mui-b97db4a9f432", children: _jsx(Input, { type: "search", "aria-label": "
|
|
181
|
+
return (_jsxs("div", { "data-slot": "filter-menu-list", className: cn("mui-222f930b8752 mui-410da8dfa8ac mui-302c0d124a94", desktop && selectedId && !detached && "mui-3b6d5fc7b061", detached && "mui-cb20bd519346 mui-d5111d0e9f48 mui-3fa8c572949b mui-4f1a55de40bc mui-e593c38256e0 mui-5bd2afe89f0b"), children: [sections.length > 20 && (_jsx("div", { className: "mui-27ead27a81df mui-bbe39cfb5cc6 mui-b97db4a9f432", children: _jsx(Input, { type: "search", "aria-label": t("findFilter"), placeholder: t("findFilterPlaceholder"), value: query, className: "mui-b51243872d85", onChange: (event) => setQuery(event.target.value), onKeyDown: (event) => {
|
|
178
182
|
if (event.key === "ArrowDown") {
|
|
179
183
|
event.preventDefault();
|
|
180
184
|
collection.current?.focusFirst();
|
|
181
185
|
}
|
|
182
186
|
if (event.key !== "Escape" && event.key !== "Tab")
|
|
183
187
|
event.stopPropagation();
|
|
184
|
-
} }) })), matches.length === 0 && (_jsx("p", { className: "mui-094f5333853b mui-c74ab393b96d mui-35f35c41d134", children: sections.length ? "
|
|
188
|
+
} }) })), matches.length === 0 && (_jsx("p", { className: "mui-094f5333853b mui-c74ab393b96d mui-35f35c41d134", children: t(sections.length ? "noMatchingFilters" : "noFilters") })), _jsx(FilterCollection, { items: matches, role: "group", label: t("filterTypes"), collectionRef: collection, initialKey: initialKey, renderBefore: (section, index) => index > 0 && section.separatorBefore && !query ? (_jsx("div", { "data-slot": "filter-menu-separator", className: "mui-1fcf28756bb1", children: _jsx("hr", { className: "mui-a2526bc2efa3 mui-c7d5500b77ab mui-dad556abfe15 mui-894c3815a57f" }) })) : null, className: cn("mui-80baa5d03af7", classNames?.menuList), children: (section, index, row) => (_jsxs(Button, { ...row, ref: (node) => {
|
|
185
189
|
row.ref(node);
|
|
186
190
|
if (node)
|
|
187
191
|
rows.current.set(section.id, node);
|
|
188
192
|
else
|
|
189
193
|
rows.current.delete(section.id);
|
|
190
194
|
}, variant: "ghost", type: "button", "aria-label": section.label, "aria-description": matches.length > 100
|
|
191
|
-
? `${section.active ? "
|
|
195
|
+
? `${section.active ? t("filterApplied") + ". " : ""}${t("position", { index: index + 1, count: matches.length })}`
|
|
192
196
|
: section.active
|
|
193
|
-
? "
|
|
197
|
+
? t("filterApplied")
|
|
194
198
|
: undefined, "aria-expanded": selectedId === section.id, "aria-controls": selectedId === section.id ? `${panelId}-${section.id}` : undefined, "data-navigation": keyboardNavigation ? "keyboard" : "pointer", "data-separator": section.separatorBefore && !query ? "true" : 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 mui-aa0e3c036805 mui-41d1475cf71a mui-dfc89afd7885", selectedId === section.id && "mui-292affc1f780 mui-cc209238d847", classNames?.menuRow), onPointerMove: (event) => {
|
|
195
199
|
if (!section.disabled)
|
|
196
200
|
onPointerMove(section.id, event);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMendyLocale } from "../locale-context.js";
|
|
3
4
|
import { useEffect, useRef, useState } from "react";
|
|
4
5
|
import { FilterOptionSearch } from "./filter-option-search.js";
|
|
5
6
|
import { DropdownMenuRadioGroup, DropdownMenuRadioItem } from "../primitives/dropdown-menu.js";
|
|
@@ -15,8 +16,9 @@ function handleSearchKey(event) {
|
|
|
15
16
|
if (event.key !== "Escape" && event.key !== "Tab")
|
|
16
17
|
event.stopPropagation();
|
|
17
18
|
}
|
|
18
|
-
function SearchableOptions({ label, options, searchable = true, searchPlaceholder, emptyMessage
|
|
19
|
+
function SearchableOptions({ label, options, searchable = true, searchPlaceholder, emptyMessage, autoFocus = true, children, }) {
|
|
19
20
|
const [query, setQuery] = useState("");
|
|
21
|
+
const { t, direction, code, configured } = useMendyLocale();
|
|
20
22
|
const inputRef = useRef(null);
|
|
21
23
|
useEffect(() => {
|
|
22
24
|
if (!searchable || !autoFocus)
|
|
@@ -26,11 +28,11 @@ function SearchableOptions({ label, options, searchable = true, searchPlaceholde
|
|
|
26
28
|
}, [searchable, autoFocus]);
|
|
27
29
|
const filtered = !searchable
|
|
28
30
|
? options
|
|
29
|
-
: options.filter((option) => option.label.toLocaleLowerCase().includes(query.toLocaleLowerCase()));
|
|
30
|
-
return (_jsxs("div", { className: "mui-58d0413dd93c", "data-mendy-ui": "", "data-slot": "filter-options", onKeyDown: (event) => {
|
|
31
|
+
: options.filter((option) => option.label.toLocaleLowerCase(code).includes(query.toLocaleLowerCase(code)));
|
|
32
|
+
return (_jsxs("div", { className: "mui-58d0413dd93c", "data-mendy-ui": "", "data-slot": "filter-options", dir: configured ? direction : undefined, lang: code, onKeyDown: (event) => {
|
|
31
33
|
if (event.key === "Tab")
|
|
32
34
|
event.stopPropagation();
|
|
33
|
-
}, children: [searchable && (_jsx(FilterOptionSearch, { ref: inputRef, "aria-label": label, placeholder: searchPlaceholder ?? label, value: query, onChange: (event) => setQuery(event.target.value), onKeyDown: handleSearchKey })), filtered.length ? (_jsx("div", { role: "menu", "aria-label": label, className: "mui-1dee6e3ec67d", children: children(filtered) })) : (_jsx("p", { role: "status", className: "mui-094f5333853b mui-c74ab393b96d mui-35f35c41d134", children: emptyMessage }))] }));
|
|
35
|
+
}, children: [searchable && (_jsx(FilterOptionSearch, { ref: inputRef, "aria-label": label, placeholder: searchPlaceholder ?? label, value: query, onChange: (event) => setQuery(event.target.value), onKeyDown: handleSearchKey })), filtered.length ? (_jsx("div", { role: "menu", "aria-label": label, className: "mui-1dee6e3ec67d", children: children(filtered) })) : (_jsx("p", { role: "status", className: "mui-094f5333853b mui-c74ab393b96d mui-35f35c41d134", children: emptyMessage ?? t("noOptions") }))] }));
|
|
34
36
|
}
|
|
35
37
|
/** Apply immediately, toggle the current choice off, and keep the dropdown open by default. */
|
|
36
38
|
export function FilterSelectEditor({ value, onValueChange, clearValue = "", removable = true, closeOnSelect = false, ...props }) {
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMendyLocale } from "../locale-context.js";
|
|
3
4
|
import { useEffect, useId, useRef, useState } from "react";
|
|
4
5
|
import { Button } from "../customization.js";
|
|
5
6
|
import { Label } from "../customization.js";
|
|
6
7
|
import { Textarea } from "../customization.js";
|
|
7
8
|
/** A draft is local to the open editor. Enter commits it by default; explicit button submission is optional. */
|
|
8
|
-
export function FilterTextEditor({ label, defaultValue, onApply, validate, applyLabel
|
|
9
|
+
export function FilterTextEditor({ label, defaultValue, onApply, validate, applyLabel, commitMode = "enter", placeholder, }) {
|
|
9
10
|
const id = useId();
|
|
10
11
|
const inputRef = useRef(null);
|
|
12
|
+
const { t, direction, code, configured } = useMendyLocale();
|
|
11
13
|
const [draft, setDraft] = useState(defaultValue);
|
|
12
14
|
const error = validate?.(draft);
|
|
13
15
|
useEffect(() => {
|
|
@@ -15,7 +17,7 @@ export function FilterTextEditor({ label, defaultValue, onApply, validate, apply
|
|
|
15
17
|
const frame = requestAnimationFrame(() => inputRef.current?.focus());
|
|
16
18
|
return () => cancelAnimationFrame(frame);
|
|
17
19
|
}, []);
|
|
18
|
-
return (_jsxs("div", { "data-mendy-ui": "", className: "mui-37535002f0f7 mui-81b198a2db84 mui-267171770524 mui-094f5333853b", onKeyDown: (event) => {
|
|
20
|
+
return (_jsxs("div", { "data-mendy-ui": "", dir: configured ? direction : undefined, lang: code, className: "mui-37535002f0f7 mui-81b198a2db84 mui-267171770524 mui-094f5333853b", onKeyDown: (event) => {
|
|
19
21
|
if (event.key === "Tab")
|
|
20
22
|
event.stopPropagation();
|
|
21
23
|
}, children: [_jsx(Label, { htmlFor: id, className: "mui-c74ab393b96d mui-daaac3fbf55e", children: label }), _jsx(Textarea, { ref: inputRef, id: id, value: draft, placeholder: placeholder, "aria-invalid": Boolean(error), "aria-describedby": error ? `${id}-error` : undefined, onChange: (event) => setDraft(event.target.value), onKeyDown: (event) => {
|
|
@@ -30,5 +32,5 @@ export function FilterTextEditor({ label, defaultValue, onApply, validate, apply
|
|
|
30
32
|
// Let the textarea handle typing and cursor movement, not menu typeahead.
|
|
31
33
|
if (event.key !== "Escape" && event.key !== "Tab")
|
|
32
34
|
event.stopPropagation();
|
|
33
|
-
} }), error && (_jsx("p", { id: `${id}-error`, className: "mui-c74ab393b96d mui-887b9502d5c7", children: error })), commitMode === "apply" ? (_jsx(Button, { type: "button", size: "sm", disabled: Boolean(error), onClick: () => onApply(draft), children: applyLabel })) : (_jsx("p", { className: "mui-3992de70b033 mui-35f35c41d134", children: "
|
|
35
|
+
} }), error && (_jsx("p", { id: `${id}-error`, className: "mui-c74ab393b96d mui-887b9502d5c7", children: error })), commitMode === "apply" ? (_jsx(Button, { type: "button", size: "sm", disabled: Boolean(error), onClick: () => onApply(draft), children: applyLabel ?? t("apply") })) : (_jsx("p", { className: "mui-3992de70b033 mui-35f35c41d134", children: t("saveHint") }))] }));
|
|
34
36
|
}
|
package/dist/filters/filters.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMendyLocale } from "../locale-context.js";
|
|
3
4
|
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
|
4
5
|
import { m, LazyMotion, domAnimation, useReducedMotion } from "motion/react";
|
|
5
6
|
import { XIcon } from "lucide-react";
|
|
@@ -32,9 +33,10 @@ export function FilterChipList({ children }) {
|
|
|
32
33
|
return (_jsx(LazyMotion, { features: domAnimation, children: _jsx(ChipListContext.Provider, { value: true, children: _jsx(m.div, { variants: reducedMotion ? reducedVariant : listVariant, initial: "hidden", animate: "show", className: "mui-d1b2a59fbea7", children: children }) }) }));
|
|
33
34
|
}
|
|
34
35
|
export function FilterChip({ className, entranceDelay, ...props }) {
|
|
36
|
+
const locale = useMendyLocale();
|
|
35
37
|
const inList = useContext(ChipListContext);
|
|
36
38
|
const reducedMotion = useReducedMotion();
|
|
37
|
-
const content = (_jsx(m.div, { "data-mendy-ui": "", "data-slot": "filter-chip-entrance", variants: reducedMotion ? reducedVariant : itemVariant, initial: inList ? undefined : "hidden", animate: inList ? undefined : "show", transition: entranceDelay === undefined || reducedMotion ? undefined : { delay: entranceDelay }, className: "mui-184ddc11e5f9 mui-81b198a2db84", children: _jsx("div", { "data-mendy-ui": "", "data-slot": "filter-chip", className: cn("mui-123306850fe1 mui-539ae144f076 mui-81b198a2db84 mui-d5111d0e9f48 mui-3fa8c572949b mui-71556df3b421 mui-d191cb569aac mui-c74ab393b96d mui-35f35c41d134", className), ...props }) }));
|
|
39
|
+
const content = (_jsx(m.div, { "data-mendy-ui": "", "data-slot": "filter-chip-entrance", variants: reducedMotion ? reducedVariant : itemVariant, initial: inList ? undefined : "hidden", animate: inList ? undefined : "show", transition: entranceDelay === undefined || reducedMotion ? undefined : { delay: entranceDelay }, className: "mui-184ddc11e5f9 mui-81b198a2db84", children: _jsx("div", { "data-mendy-ui": "", "data-slot": "filter-chip", dir: locale.configured ? locale.direction : undefined, lang: locale.code, className: cn("mui-123306850fe1 mui-539ae144f076 mui-81b198a2db84 mui-d5111d0e9f48 mui-3fa8c572949b mui-71556df3b421 mui-d191cb569aac mui-c74ab393b96d mui-35f35c41d134", className), ...props }) }));
|
|
38
40
|
return inList ? content : _jsx(LazyMotion, { features: domAnimation, children: content });
|
|
39
41
|
}
|
|
40
42
|
export function FilterEditorTrigger({ className, asChild, ...props }) {
|
|
@@ -51,14 +53,15 @@ export function FilterRemove({ className, children, ...props }) {
|
|
|
51
53
|
return (_jsx(Button, { variant: "ghost", type: "button", "data-mendy-ui": "", "data-slot": "filter-remove", className: cn("mui-222f930b8752 mui-57c930dc1c31 mui-52417259a4f5 mui-04760bcd507f mui-8867d85ed290 mui-27ead27a81df mui-71556df3b421 mui-a503dd374cca mui-55860251be20 mui-163a623b3716 mui-37ad15a95595 mui-4d6720d47806 mui-2e357972fc10 mui-355efe22aedb mui-50aafa8694bb mui-db549d38ff19 mui-8d3c1014b967 mui-4600b77303d6", className), ...props, children: children ?? _jsx(XIcon, { className: "mui-61c000c8a98a", "aria-hidden": "true" }) }));
|
|
52
54
|
}
|
|
53
55
|
/** Editing and removal are independent. Values remain owned by the caller. */
|
|
54
|
-
export function AppliedFilter({ label, editor, onRemove, disabled, open, onOpenChange, editLabel
|
|
55
|
-
|
|
56
|
+
export function AppliedFilter({ label, editor, onRemove, disabled, open, onOpenChange, editLabel, removeLabel, contentProps, triggerProps, removeProps, children, ...props }) {
|
|
57
|
+
const { t } = useMendyLocale();
|
|
58
|
+
return (_jsx(FilterEditor, { open: open, onOpenChange: onOpenChange, children: _jsxs(FilterChip, { ...props, children: [editor ? (_jsx(FilterEditorTrigger, { disabled: disabled, "aria-label": editLabel ?? t("editFilter", { label }), ...triggerProps, onClick: (event) => {
|
|
56
59
|
event.stopPropagation();
|
|
57
60
|
triggerProps?.onClick?.(event);
|
|
58
|
-
}, children: children })) : (_jsx("span", { className: "mui-184ddc11e5f9 mui-e7e01cc7f4df", children: children })), onRemove && (_jsx(FilterRemove, { ...removeProps, disabled: disabled, "aria-label": removeLabel, onClick: (event) => {
|
|
61
|
+
}, children: children })) : (_jsx("span", { className: "mui-184ddc11e5f9 mui-e7e01cc7f4df", children: children })), onRemove && (_jsx(FilterRemove, { ...removeProps, disabled: disabled, "aria-label": removeLabel ?? t("removeFilter", { label }), onClick: (event) => {
|
|
59
62
|
event.stopPropagation();
|
|
60
63
|
onRemove();
|
|
61
|
-
} })), editor && (_jsx(FilterEditorContent, { "aria-label":
|
|
64
|
+
} })), editor && (_jsx(FilterEditorContent, { "aria-label": t("filterLabel", { label }), ...contentProps, children: editor }))] }) }));
|
|
62
65
|
}
|
|
63
66
|
export function FilterMenuItem({ label, icon, children, contentProps, open, defaultOpen, onOpenChange, ...props }) {
|
|
64
67
|
const [internalOpen, setInternalOpen] = useState(defaultOpen ?? false);
|
package/dist/filters/index.d.ts
CHANGED
|
@@ -9,3 +9,6 @@ export * from "./filter-text-editor.js";
|
|
|
9
9
|
export * from "./filter-select-editor.js";
|
|
10
10
|
export * from "../customization.js";
|
|
11
11
|
export * from "./use-filter-search.js";
|
|
12
|
+
export { defineLocale } from "../locale.js";
|
|
13
|
+
export type { MendyLocale, MendyMessages } from "../locale.js";
|
|
14
|
+
export { useMendyLocale } from "../locale-context.js";
|
package/dist/filters/index.js
CHANGED
|
@@ -9,3 +9,5 @@ export * from "./filter-text-editor.js";
|
|
|
9
9
|
export * from "./filter-select-editor.js";
|
|
10
10
|
export * from "../customization.js";
|
|
11
11
|
export * from "./use-filter-search.js";
|
|
12
|
+
export { defineLocale } from "../locale.js";
|
|
13
|
+
export { useMendyLocale } from "../locale-context.js";
|
|
@@ -16,7 +16,7 @@ export declare function useFilterOptions(id: string, field: RuntimeField, value:
|
|
|
16
16
|
selected: Choice[];
|
|
17
17
|
loading: boolean;
|
|
18
18
|
resolving: boolean;
|
|
19
|
-
error: string |
|
|
19
|
+
error: string | undefined;
|
|
20
20
|
retry(): void;
|
|
21
21
|
hasMore: boolean;
|
|
22
22
|
loadMore(): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { useMendyLocale } from "../locale-context.js";
|
|
2
3
|
import { useValueDraft } from "./use-value-draft.js";
|
|
3
4
|
import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
4
5
|
export const FilterOptionCache = createContext(null);
|
|
@@ -55,7 +56,7 @@ function acquire(cache, key, load) {
|
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
function errorMessage(error) {
|
|
58
|
-
return error instanceof Error ? error.message :
|
|
59
|
+
return error instanceof Error ? error.message : null;
|
|
59
60
|
}
|
|
60
61
|
export function selectedIds(value) {
|
|
61
62
|
return Array.isArray(value)
|
|
@@ -70,6 +71,7 @@ function optionSearchQuery(field, localQuery) {
|
|
|
70
71
|
}
|
|
71
72
|
export function useFilterOptions(id, field, value, enabled) {
|
|
72
73
|
const sharedCache = useContext(FilterOptionCache);
|
|
74
|
+
const { t, code } = useMendyLocale();
|
|
73
75
|
const privateCache = useRef(new Map());
|
|
74
76
|
const cache = sharedCache ?? privateCache.current;
|
|
75
77
|
const source = field.source;
|
|
@@ -160,7 +162,7 @@ export function useFilterOptions(id, field, value, enabled) {
|
|
|
160
162
|
: { items: [], loading: enabled, cursor: null, error: undefined };
|
|
161
163
|
const matchingResolved = resolved.key === resolveKey;
|
|
162
164
|
const items = remote ? currentPage.items : (source?.items ?? []);
|
|
163
|
-
const { selected, shown, knownSelected, missingLabels } = optionPresentation(source, items, ids, resolved, matchingResolved, resolved.identity === identity, remote, query, retainedSelection.current.identity === identity ? retainedSelection.current.choices : []);
|
|
165
|
+
const { selected, shown, knownSelected, missingLabels } = optionPresentation(source, items, ids, resolved, matchingResolved, resolved.identity === identity, remote, query, retainedSelection.current.identity === identity ? retainedSelection.current.choices : [], t, code);
|
|
164
166
|
useLayoutEffect(() => {
|
|
165
167
|
// Keep only actual labels for the current selection, never synthesized ID fallbacks.
|
|
166
168
|
retainedSelection.current = { identity, choices: knownSelected };
|
|
@@ -177,7 +179,11 @@ export function useFilterOptions(id, field, value, enabled) {
|
|
|
177
179
|
selected,
|
|
178
180
|
loading: remote ? currentPage.loading : (source?.loading ?? false),
|
|
179
181
|
resolving: missingLabels && (remote ? !matchingResolved : Boolean(source?.loading)),
|
|
180
|
-
error: currentPage.error
|
|
182
|
+
error: [currentPage.error, matchingResolved ? resolved.error : undefined]
|
|
183
|
+
.map((error) => (error === null ? t("optionsError") : error))
|
|
184
|
+
.find((error) => error !== undefined) ??
|
|
185
|
+
source?.error ??
|
|
186
|
+
undefined,
|
|
181
187
|
retry() {
|
|
182
188
|
source?.retry?.();
|
|
183
189
|
retry((key) => key + 1);
|
|
@@ -214,7 +220,7 @@ export function useFilterOptions(id, field, value, enabled) {
|
|
|
214
220
|
},
|
|
215
221
|
};
|
|
216
222
|
}
|
|
217
|
-
function optionPresentation(source, items, ids, resolved, matchingResolved, retainedLabels, remote, query, previousSelection) {
|
|
223
|
+
function optionPresentation(source, items, ids, resolved, matchingResolved, retainedLabels, remote, query, previousSelection, t, code) {
|
|
218
224
|
const known = new Map();
|
|
219
225
|
for (const item of [
|
|
220
226
|
...previousSelection,
|
|
@@ -225,13 +231,13 @@ function optionPresentation(source, items, ids, resolved, matchingResolved, reta
|
|
|
225
231
|
known.set(item.value, item);
|
|
226
232
|
const selected = ids.map((id) => known.get(id) ?? {
|
|
227
233
|
value: id,
|
|
228
|
-
label: remote && matchingResolved && resolved.done &&
|
|
229
|
-
?
|
|
234
|
+
label: remote && matchingResolved && resolved.done && resolved.error === undefined
|
|
235
|
+
? t("unavailable", { id })
|
|
230
236
|
: id,
|
|
231
237
|
});
|
|
232
238
|
const shown = remote || source?.onQueryChange
|
|
233
239
|
? items
|
|
234
|
-
: items.filter((item) => item.label.toLocaleLowerCase().includes(query.toLocaleLowerCase()));
|
|
240
|
+
: items.filter((item) => item.label.toLocaleLowerCase(code).includes(query.toLocaleLowerCase(code)));
|
|
235
241
|
return {
|
|
236
242
|
selected,
|
|
237
243
|
shown,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { useMendyLocale } from "../locale-context.js";
|
|
2
3
|
import { useLayoutEffect, useRef, useState } from "react";
|
|
3
4
|
import { classifyPaste, initialValues } from "./filter-state.js";
|
|
4
5
|
export function useFilterController(options) {
|
|
6
|
+
const { t, messages } = useMendyLocale();
|
|
5
7
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
6
8
|
const [openField, setOpenField] = useState(null);
|
|
7
9
|
const [editField, edit] = useState(null);
|
|
@@ -15,7 +17,9 @@ export function useFilterController(options) {
|
|
|
15
17
|
continue;
|
|
16
18
|
try {
|
|
17
19
|
const next = entry.field.normalize(value);
|
|
18
|
-
const message = source === "clear" || source === "remove"
|
|
20
|
+
const message = source === "clear" || source === "remove"
|
|
21
|
+
? undefined
|
|
22
|
+
: entry.field.validate(next, messages);
|
|
19
23
|
if (message) {
|
|
20
24
|
setError(message);
|
|
21
25
|
return message;
|
|
@@ -23,7 +27,7 @@ export function useFilterController(options) {
|
|
|
23
27
|
normalized[id] = next;
|
|
24
28
|
}
|
|
25
29
|
catch {
|
|
26
|
-
const message = "
|
|
30
|
+
const message = t("invalidFilter");
|
|
27
31
|
setError(message);
|
|
28
32
|
return message;
|
|
29
33
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { useMendyLocale } from "../locale-context.js";
|
|
2
3
|
import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
3
4
|
import { parseAsString, useQueryStates } from "nuqs";
|
|
4
5
|
import { decodeFilters, encodeFilters, validateDefinitions } from "./filter-state.js";
|
|
@@ -28,6 +29,7 @@ function readSnapshot(key) {
|
|
|
28
29
|
}
|
|
29
30
|
/** Uses nuqs as the applied state owner. Only overflow values live outside the URL. */
|
|
30
31
|
export function useUrlFilters(definitions, options) {
|
|
32
|
+
const { t } = useMendyLocale();
|
|
31
33
|
const searchKey = options.searchKey ?? "q";
|
|
32
34
|
const markerKey = options.markerKey ?? "_filters";
|
|
33
35
|
validateDefinitions(definitions, searchKey, markerKey);
|
|
@@ -69,7 +71,7 @@ export function useUrlFilters(definitions, options) {
|
|
|
69
71
|
return true;
|
|
70
72
|
}
|
|
71
73
|
catch {
|
|
72
|
-
report("
|
|
74
|
+
report(t("persistError"));
|
|
73
75
|
return false;
|
|
74
76
|
}
|
|
75
77
|
}
|
|
@@ -117,13 +119,13 @@ export function useUrlFilters(definitions, options) {
|
|
|
117
119
|
sessionStorage.removeItem(storageKey);
|
|
118
120
|
}
|
|
119
121
|
catch {
|
|
120
|
-
report("
|
|
122
|
+
report(t("clearPersistError"));
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
125
|
else
|
|
124
126
|
writeSnapshot(storageKey, owned.toString());
|
|
125
127
|
}
|
|
126
|
-
void setRaw(next).catch(() => report("
|
|
128
|
+
void setRaw(next).catch(() => report(t("urlError")));
|
|
127
129
|
}
|
|
128
130
|
useEffect(() => {
|
|
129
131
|
setReady(true);
|
|
@@ -157,7 +159,7 @@ export function useUrlFilters(definitions, options) {
|
|
|
157
159
|
...controller,
|
|
158
160
|
ready,
|
|
159
161
|
shareable: !marker,
|
|
160
|
-
persistenceMessage: persistenceNotice(ready, marker, storedQuery, overflow?.marker, message),
|
|
162
|
+
persistenceMessage: persistenceNotice(ready, marker, storedQuery, overflow?.marker, message, t),
|
|
161
163
|
set(key, value) {
|
|
162
164
|
return controller.commit(key, value);
|
|
163
165
|
},
|
|
@@ -176,13 +178,13 @@ export function useUrlFilters(definitions, options) {
|
|
|
176
178
|
},
|
|
177
179
|
};
|
|
178
180
|
}
|
|
179
|
-
function persistenceNotice(ready, marker, storedQuery, memoryMarker, message) {
|
|
181
|
+
function persistenceNotice(ready, marker, storedQuery, memoryMarker, message, t) {
|
|
180
182
|
if (ready && marker && storedQuery === null && memoryMarker !== marker)
|
|
181
|
-
return "
|
|
183
|
+
return t("missingSession");
|
|
182
184
|
if (message)
|
|
183
185
|
return message;
|
|
184
186
|
if (marker)
|
|
185
|
-
return "
|
|
187
|
+
return t("sessionOnly");
|
|
186
188
|
}
|
|
187
189
|
function appliedParams(raw, keys, marker, overflow, stored, scope) {
|
|
188
190
|
if (overflow && overflow.marker === marker && overflow.scope === scope)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const LocaleContext: import("react").Context<{
|
|
2
|
+
configured: boolean;
|
|
3
|
+
t: import("./locale.js").Translate;
|
|
4
|
+
number: {
|
|
5
|
+
(value: number): string;
|
|
6
|
+
(value: number | bigint): string;
|
|
7
|
+
};
|
|
8
|
+
date: (date?: Date | number) => string;
|
|
9
|
+
code: string;
|
|
10
|
+
direction: "ltr" | "rtl";
|
|
11
|
+
messages: import("./locale.js").MendyMessages;
|
|
12
|
+
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
13
|
+
firstWeekContainsDate?: 1 | 4;
|
|
14
|
+
}>;
|
|
15
|
+
/** Use the same translations and formatting in custom editors and table controls. */
|
|
16
|
+
export declare function useMendyLocale(): {
|
|
17
|
+
configured: boolean;
|
|
18
|
+
t: import("./locale.js").Translate;
|
|
19
|
+
number: {
|
|
20
|
+
(value: number): string;
|
|
21
|
+
(value: number | bigint): string;
|
|
22
|
+
};
|
|
23
|
+
date: (date?: Date | number) => string;
|
|
24
|
+
code: string;
|
|
25
|
+
direction: "ltr" | "rtl";
|
|
26
|
+
messages: import("./locale.js").MendyMessages;
|
|
27
|
+
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
28
|
+
firstWeekContainsDate?: 1 | 4;
|
|
29
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
import { createLocale } from "./locale.js";
|
|
4
|
+
export const LocaleContext = createContext({ ...createLocale(), configured: false });
|
|
5
|
+
/** Use the same translations and formatting in custom editors and table controls. */
|
|
6
|
+
export function useMendyLocale() {
|
|
7
|
+
return useContext(LocaleContext);
|
|
8
|
+
}
|
package/dist/locale.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { englishMessages } from "./locales/en.js";
|
|
2
|
+
/** Dictionaries are plain data, including when passed across a server/client boundary. */
|
|
3
|
+
export type MendyMessages = {
|
|
4
|
+
[K in keyof typeof englishMessages]: string;
|
|
5
|
+
};
|
|
6
|
+
export interface MendyLocale {
|
|
7
|
+
/** BCP 47 language tag used by Intl. */
|
|
8
|
+
code: string;
|
|
9
|
+
direction: "ltr" | "rtl";
|
|
10
|
+
messages: MendyMessages;
|
|
11
|
+
/** Gregorian calendar week convention; Sunday by default. */
|
|
12
|
+
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
13
|
+
firstWeekContainsDate?: 1 | 4;
|
|
14
|
+
}
|
|
15
|
+
/** Missing messages fall back to English; private locales need no registration. */
|
|
16
|
+
export declare function defineLocale(locale: Omit<MendyLocale, "messages"> & {
|
|
17
|
+
messages?: Partial<MendyMessages>;
|
|
18
|
+
}): MendyLocale;
|
|
19
|
+
type Placeholders<S extends string> = S extends `${string}{${infer P}}${infer Rest}` ? P | Placeholders<Rest> : never;
|
|
20
|
+
type Params<K extends keyof MendyMessages> = Placeholders<(typeof englishMessages)[K]>;
|
|
21
|
+
export type Translate = <K extends keyof MendyMessages>(key: K, ...args: [Params<K>] extends [never] ? [] : [Record<Params<K>, string | number>]) => string;
|
|
22
|
+
export declare function createLocale(locale?: MendyLocale): {
|
|
23
|
+
t: Translate;
|
|
24
|
+
number: {
|
|
25
|
+
(value: number): string;
|
|
26
|
+
(value: number | bigint): string;
|
|
27
|
+
};
|
|
28
|
+
date: (date?: Date | number) => string;
|
|
29
|
+
/** BCP 47 language tag used by Intl. */
|
|
30
|
+
code: string;
|
|
31
|
+
direction: "ltr" | "rtl";
|
|
32
|
+
messages: MendyMessages;
|
|
33
|
+
/** Gregorian calendar week convention; Sunday by default. */
|
|
34
|
+
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
35
|
+
firstWeekContainsDate?: 1 | 4;
|
|
36
|
+
};
|
|
37
|
+
export {};
|
package/dist/locale.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { en, englishMessages } from "./locales/en.js";
|
|
2
|
+
/** Missing messages fall back to English; private locales need no registration. */
|
|
3
|
+
export function defineLocale(locale) {
|
|
4
|
+
return { ...locale, messages: { ...englishMessages, ...locale.messages } };
|
|
5
|
+
}
|
|
6
|
+
export function createLocale(locale = en) {
|
|
7
|
+
const numbers = new Intl.NumberFormat(locale.code);
|
|
8
|
+
const dates = new Intl.DateTimeFormat(locale.code, {
|
|
9
|
+
calendar: "gregory",
|
|
10
|
+
year: "numeric",
|
|
11
|
+
month: "short",
|
|
12
|
+
day: "numeric",
|
|
13
|
+
});
|
|
14
|
+
const t = (key, ...args) => {
|
|
15
|
+
const params = args[0];
|
|
16
|
+
return (locale.messages[key] ?? englishMessages[key]).replace(/\{(\w+)\}/g, (token, name) => {
|
|
17
|
+
const value = params?.[name];
|
|
18
|
+
return value === undefined
|
|
19
|
+
? token
|
|
20
|
+
: typeof value === "number"
|
|
21
|
+
? numbers.format(value)
|
|
22
|
+
: value;
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
return { ...locale, t, number: numbers.format, date: dates.format };
|
|
26
|
+
}
|