@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.
Files changed (50) hide show
  1. package/README.md +15 -0
  2. package/dist/calendar-locale.d.ts +7 -0
  3. package/dist/calendar-locale.js +31 -0
  4. package/dist/class-names.js +1 -1
  5. package/dist/customization.d.ts +6 -1
  6. package/dist/customization.js +20 -2
  7. package/dist/filters/filter-bar.d.ts +1 -1
  8. package/dist/filters/filter-bar.js +56 -30
  9. package/dist/filters/filter-collection.js +3 -1
  10. package/dist/filters/filter-date-editor.js +5 -3
  11. package/dist/filters/filter-definition.d.ts +3 -2
  12. package/dist/filters/filter-definition.js +5 -6
  13. package/dist/filters/filter-menu-focus.js +1 -1
  14. package/dist/filters/filter-menu-panel.js +13 -9
  15. package/dist/filters/filter-select-editor.js +6 -4
  16. package/dist/filters/filter-text-editor.js +5 -3
  17. package/dist/filters/filters.js +8 -5
  18. package/dist/filters/index.d.ts +3 -0
  19. package/dist/filters/index.js +2 -0
  20. package/dist/filters/use-filter-options.d.ts +1 -1
  21. package/dist/filters/use-filter-options.js +13 -7
  22. package/dist/filters/use-filters.js +6 -2
  23. package/dist/filters/use-url-filters.js +9 -7
  24. package/dist/locale-context.d.ts +29 -0
  25. package/dist/locale-context.js +8 -0
  26. package/dist/locale.d.ts +37 -0
  27. package/dist/locale.js +26 -0
  28. package/dist/locales/en.d.ts +113 -0
  29. package/dist/locales/en.js +112 -0
  30. package/dist/locales/he.d.ts +116 -0
  31. package/dist/locales/he.js +116 -0
  32. package/dist/primitives/calendar.d.ts +1 -1
  33. package/dist/primitives/calendar.js +15 -10
  34. package/dist/primitives/dropdown-menu.js +7 -5
  35. package/dist/primitives/popover.js +3 -1
  36. package/dist/styles.css +39 -39
  37. package/dist/styles.tailwind3.css +39 -39
  38. package/dist/table/table-column-settings.js +27 -13
  39. package/dist/table/table-controls.js +21 -8
  40. package/dist/table/table-feedback.js +6 -3
  41. package/dist/table/table-frame.js +3 -1
  42. package/dist/table/table-layout.d.ts +1 -1
  43. package/dist/table/table-layout.js +5 -5
  44. package/dist/table/table-loading.js +3 -1
  45. package/dist/table/table-parts.js +15 -11
  46. package/dist/table/table-view.js +6 -4
  47. package/dist/table/use-column-window.js +1 -1
  48. package/dist/table/use-data-table.js +3 -0
  49. package/dist/table/use-table-interaction.js +12 -8
  50. 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
- ? "Minimum must not exceed maximum."
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('[aria-label="Filter types"]') &&
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": "Filters", "aria-labelledby": undefined, "aria-orientation": undefined, align: desktop ? "start" : "end", alignOffset: alignOffset, side: side, style: anchorStyle, sideOffset: 7, collisionPadding: 16, onEscapeKeyDown: (event) => {
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
- 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" }), "Filters"] }), _jsx("span", { "aria-hidden": "true", className: "mui-35f35c41d134", children: "/" })] })), _jsx(MenuHeading, { label: section.label }), section.clear && (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
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": `Clear ${section.label} filter`, className: "mui-8cc46fa87f41 mui-ce1945804664 mui-1da2e8f173c5 mui-3992de70b033 mui-52101fc7d8bb mui-35f35c41d134", children: "Clear" }))] }));
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": "Find a filter", placeholder: "Find a filter\u2026", value: query, className: "mui-b51243872d85", onChange: (event) => setQuery(event.target.value), onKeyDown: (event) => {
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 ? "No matching filters." : "No filters available." })), _jsx(FilterCollection, { items: matches, role: "group", label: "Filter types", 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) => {
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 ? "Filter applied. " : ""}${index + 1} of ${matches.length}`
195
+ ? `${section.active ? t("filterApplied") + ". " : ""}${t("position", { index: index + 1, count: matches.length })}`
192
196
  : section.active
193
- ? "Filter applied"
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 = "No options found.", autoFocus = true, children, }) {
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 = "Apply", commitMode = "enter", placeholder, }) {
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: "Enter to save. Shift+Enter for a new line." }))] }));
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
  }
@@ -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 = `Edit ${label} filter`, removeLabel = `Remove ${label} filter`, contentProps, triggerProps, removeProps, children, ...props }) {
55
- return (_jsx(FilterEditor, { open: open, onOpenChange: onOpenChange, children: _jsxs(FilterChip, { ...props, children: [editor ? (_jsx(FilterEditorTrigger, { disabled: disabled, "aria-label": editLabel, ...triggerProps, onClick: (event) => {
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": `${label} filter`, ...contentProps, children: editor }))] }) }));
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);
@@ -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";
@@ -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 | null | undefined;
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 : "Options could not be loaded.";
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 ?? (matchingResolved ? resolved.error : undefined) ?? source?.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 && !resolved.error
229
- ? `Unavailable (${id})`
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" ? undefined : entry.field.validate(next);
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 = "This filter value is invalid.";
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("Filters could not be saved in this browser. Keep this page open to retain them.");
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("Remembered filters could not be cleared in this browser.");
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("The URL could not be updated. Filters may not survive a reload."));
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 "This link refers to filters saved in another browser session. The full selection is unavailable.";
183
+ return t("missingSession");
182
184
  if (message)
183
185
  return message;
184
186
  if (marker)
185
- return "This selection is saved in this browser session. The URL does not contain the full filters.";
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
+ }
@@ -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
+ }