@mendylanda/ui 0.1.5 → 0.1.6

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 CHANGED
@@ -61,3 +61,20 @@ Chips enter with a short, staggered Motion spring. Reduced-motion preferences di
61
61
  Filter lists use the search anchor's width by default and shrink when the viewport cannot fit both columns. Override `--mendy-filter-list-width` or `--mendy-filter-menu-width` for a custom layout. In anchored mode, editors touch the list and move upward for lower rows so short editors stay within the list's height.
62
62
 
63
63
  `FilterList` uses the original SimCall Motion variants: a 60ms stagger on initial list entry and the default item transition from y=10/opacity=0 to y=0/opacity=1. New chips enter immediately, without a delay based on their list position. `FilterChipList` provides the same orchestration for custom compositions of `FilterChip`; standalone chips also animate.
64
+
65
+ ### Existing search state
66
+
67
+ `useFilterSearch(value, onChange, delay?)` keeps a responsive input draft while debouncing writes to an existing URL or query hook. It ignores acknowledgements of earlier writes while the user is typing, accepts external navigation, clears immediately, and cancels pending writes on unmount. The default delay is 300ms.
68
+
69
+ ```tsx
70
+ const search = useFilterSearch(params.search, (value) => setParams({ search: value }));
71
+ return <input value={search.value} onChange={(event) => search.setValue(event.target.value)} />;
72
+ ```
73
+
74
+ ### Tailwind 3
75
+
76
+ Import `@mendylanda/ui/styles.tailwind3.css` instead of `@mendylanda/ui/styles.css` in Tailwind 3 projects. It contains the same scoped rules with compatible CSS layers. No package-specific PostCSS plugin is needed. Tailwind 4 projects and projects without Tailwind should use `styles.css`.
77
+
78
+ The package expects complete CSS color values. A host that stores HSL channels still needs to map them to colors, including inside portals and any custom editors that use host styles.
79
+
80
+ Opening the menu shows the category list without choosing a filter. Focusing a category does not open it. Hover previews its editor; click, Enter, Space or the entry arrow opens it and moves focus into the editor.
@@ -26,15 +26,14 @@ function subscribeViewport(listener) {
26
26
  }
27
27
  const isDesktop = () => window.innerWidth >= 40 * parseFloat(getComputedStyle(document.documentElement).fontSize);
28
28
  const serverDesktop = () => false;
29
- function selectedSection(sections, selectedId, desktop) {
30
- return (sections.find((section) => section.id === selectedId && !section.disabled) ??
31
- (desktop ? sections.find((section) => !section.disabled) : undefined));
29
+ function selectedSection(sections, selectedId) {
30
+ return sections.find((section) => section.id === selectedId && !section.disabled);
32
31
  }
33
32
  /** One dialog contains the filter list and its editor, with a single-panel layout on phones. */
34
33
  export function FilterMenuPanel({ sections, selectedId, onSelect, onClose, anchor, trigger, }) {
35
34
  const { classNames, menuLayout } = useMendyUI();
36
35
  const desktop = useSyncExternalStore(subscribeViewport, isDesktop, serverDesktop);
37
- const selected = selectedSection(sections, selectedId, desktop);
36
+ const selected = selectedSection(sections, selectedId);
38
37
  const content = useRef(null);
39
38
  const editor = useRef(null);
40
39
  const rows = useRef(new Map());
@@ -92,6 +91,7 @@ export function FilterMenuPanel({ sections, selectedId, onSelect, onClose, ancho
92
91
  requestAnimationFrame(() => previous && rows.current.get(previous)?.focus());
93
92
  }
94
93
  const showList = desktop || !selected;
94
+ const sideBySide = desktop && Boolean(selected);
95
95
  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, side: side, style: anchorStyle, sideOffset: 7, collisionPadding: 16, onEscapeKeyDown: (event) => {
96
96
  if (selected && !desktop) {
97
97
  event.preventDefault();
@@ -117,9 +117,9 @@ export function FilterMenuPanel({ sections, selectedId, onSelect, onClose, ancho
117
117
  rows.current.get(selected?.id ?? "")?.focus();
118
118
  else
119
119
  back();
120
- }), className: cn("mui-971625533e56 mui-571ea69568d3 mui-34055e6f8c1b mui-d5111d0e9f48 mui-04760bcd507f mui-94ea94fde25f mui-393df0d154e0 mui-548a450e8e53", "mui-d7d803c609c5", desktop && selected
121
- ? "mui-bd8a502f98ae" : "mui-0b9c84552e40", detached && "mui-0fc84a692dad mui-dad556abfe15 mui-8fca9236d191 mui-89c3006b2722", classNames?.menu), children: _jsxs("div", { className: cn("mui-80baa5d03af7", desktop && selected
122
- ? "mui-0f2a693e93e2 mui-b5985369ee35 mui-a3c6c8c31aed" : "mui-222f930b8752 mui-302c0d124a94", detached && "mui-b9677bd1cb66 mui-e0122edc668b"), children: [showList && (_jsx(FilterMenuList, { sections: sections, selectedId: selected?.id, initialKey: selected?.id ?? lastSelection.current, panelId: panelId, desktop: desktop, detached: detached, rows: rows, choose: choose, keyboardNavigation: keyboardNavigation, onPointerMove: (id, event) => {
120
+ }), className: cn("mui-971625533e56 mui-571ea69568d3 mui-34055e6f8c1b mui-d5111d0e9f48 mui-04760bcd507f mui-94ea94fde25f mui-393df0d154e0 mui-548a450e8e53", "mui-d7d803c609c5", sideBySide
121
+ ? "mui-bd8a502f98ae" : "mui-0b9c84552e40", detached && "mui-0fc84a692dad mui-dad556abfe15 mui-8fca9236d191 mui-89c3006b2722", classNames?.menu), children: _jsxs("div", { className: cn("mui-80baa5d03af7", sideBySide
122
+ ? "mui-0f2a693e93e2 mui-b5985369ee35 mui-a3c6c8c31aed" : "mui-222f930b8752 mui-302c0d124a94", detached && cn("mui-e0122edc668b", selected && "mui-b9677bd1cb66")), children: [showList && (_jsx(FilterMenuList, { sections: sections, selectedId: selected?.id, initialKey: selected?.id ?? lastSelection.current, panelId: panelId, desktop: desktop, detached: detached, rows: rows, choose: choose, keyboardNavigation: keyboardNavigation, onPointerMove: (id, event) => {
123
123
  if (desktop && event.pointerType === "mouse") {
124
124
  setKeyboardNavigation(false);
125
125
  const focused = document.activeElement;
@@ -193,9 +193,6 @@ function FilterMenuList({ sections, selectedId, initialKey, panelId, desktop, de
193
193
  : 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) => {
194
194
  if (!section.disabled)
195
195
  onPointerMove(section.id, event);
196
- }, onFocus: () => {
197
- if (desktop)
198
- choose(section.id);
199
196
  }, onClick: () => choose(section.id, true), onKeyDown: (event) => {
200
197
  if (event.nativeEvent.isComposing)
201
198
  return;
@@ -8,3 +8,4 @@ export * from "./filters.js";
8
8
  export * from "./filter-text-editor.js";
9
9
  export * from "./filter-select-editor.js";
10
10
  export * from "../customization.js";
11
+ export * from "./use-filter-search.js";
@@ -8,3 +8,4 @@ export * from "./filters.js";
8
8
  export * from "./filter-text-editor.js";
9
9
  export * from "./filter-select-editor.js";
10
10
  export * from "../customization.js";
11
+ export * from "./use-filter-search.js";
@@ -0,0 +1,5 @@
1
+ /** Debounce URL-backed queries without replacing newer typing with an older URL acknowledgement. */
2
+ export declare function useFilterSearch(value: string | null | undefined, onChange: (value: string | null) => void, delay?: number): {
3
+ value: string;
4
+ setValue: (next: string) => void;
5
+ };
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ /** Debounce URL-backed queries without replacing newer typing with an older URL acknowledgement. */
4
+ export function useFilterSearch(value, onChange, delay = 300) {
5
+ const committed = value ?? "";
6
+ const [state, setState] = useState({ committed, draft: committed });
7
+ const timer = useRef(null);
8
+ const pending = useRef([]);
9
+ const change = useRef(onChange);
10
+ useLayoutEffect(() => {
11
+ change.current = onChange;
12
+ });
13
+ if (state.committed !== committed) {
14
+ setState({
15
+ committed,
16
+ draft: pending.current.includes(committed) ? state.draft : committed,
17
+ });
18
+ }
19
+ useLayoutEffect(() => {
20
+ const acknowledgement = pending.current.includes(committed);
21
+ pending.current = pending.current.filter((request) => request !== committed);
22
+ if (!acknowledgement && timer.current)
23
+ clearTimeout(timer.current);
24
+ }, [committed]);
25
+ useEffect(() => () => {
26
+ if (timer.current)
27
+ clearTimeout(timer.current);
28
+ }, []);
29
+ function setValue(next) {
30
+ if (timer.current)
31
+ clearTimeout(timer.current);
32
+ setState((current) => ({ ...current, draft: next }));
33
+ function commit() {
34
+ if (next !== committed)
35
+ pending.current.push(next);
36
+ change.current(next || null);
37
+ }
38
+ if (!next)
39
+ commit();
40
+ else
41
+ timer.current = setTimeout(commit, delay);
42
+ }
43
+ return { value: state.draft, setValue };
44
+ }
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { cva } from "class-variance-authority";
4
4
  import { cn } from "../utils.js";
5
- import { Slot } from "radix-ui";
5
+ import * as Slot from "@radix-ui/react-slot";
6
6
  const buttonVariants = cva("mui-123306850fe1 mui-27ead27a81df mui-71556df3b421 mui-a503dd374cca mui-074569488cca mui-3fa8c572949b mui-c74ab393b96d mui-daaac3fbf55e mui-963f6168b7d6 mui-eae178841c68 mui-b30fc56058b6 mui-39be5a15b766 mui-db549d38ff19 mui-8d3c1014b967 mui-4600b77303d6 mui-bcae9d37090c mui-040e748a3fa0 mui-eb5ef2dbc2d4", {
7
7
  variants: {
8
8
  variant: {
@@ -1,4 +1,4 @@
1
1
  import * as React from "react";
2
- import { Checkbox as CheckboxPrimitive } from "radix-ui";
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
3
3
  declare function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>): import("react/jsx-runtime").JSX.Element;
4
4
  export { Checkbox };
@@ -3,7 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { cn } from "../utils.js";
5
5
  import { CheckIcon } from "lucide-react";
6
- import { Checkbox as CheckboxPrimitive } from "radix-ui";
6
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
7
7
  function Checkbox({ className, ...props }) {
8
8
  return (_jsx(CheckboxPrimitive.Root, { "data-mendy-ui": "", "data-slot": "checkbox", className: cn("mui-2ffc1d06387e mui-100c22d5776b mui-27ead27a81df mui-02e603944040 mui-4f1a55de40bc mui-1267095cdeb7 mui-7de07d413c13 mui-2b48746f4314 mui-b30fc56058b6 mui-b906a32b31ee mui-365a4654f7b1 mui-392a26f4c1b0 mui-db567af109b7 mui-4600b77303d6 mui-1fac1d7d1df3 mui-5491fd0b4dc5 mui-20553d55d224 mui-57d6d6401a2f mui-7ca12016214f mui-9daa2369a41b mui-c88f65aa011e mui-bd73d819941e", className), ...props, children: _jsx(CheckboxPrimitive.Indicator, { "data-mendy-ui": "", "data-slot": "checkbox-indicator", className: "mui-0f2a693e93e2 mui-8fd2e2355cbc mui-5eafab9bbefc mui-b4b2fd95f324", children: _jsx(CheckIcon, { className: "mui-61c000c8a98a" }) }) }));
9
9
  }
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
2
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
3
3
  declare function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>): import("react/jsx-runtime").JSX.Element;
4
4
  declare function DropdownMenuPortal({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>): import("react/jsx-runtime").JSX.Element;
5
5
  declare function DropdownMenuTrigger({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>): import("react/jsx-runtime").JSX.Element;
@@ -4,7 +4,7 @@ import * as React from "react";
4
4
  import { useMendyUI } from "../customization.js";
5
5
  import { cn } from "../utils.js";
6
6
  import { CheckIcon, ChevronRightIcon } from "lucide-react";
7
- import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
7
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
8
8
  function DropdownMenu({ ...props }) {
9
9
  return _jsx(DropdownMenuPrimitive.Root, { "data-mendy-ui": "", "data-slot": "dropdown-menu", ...props });
10
10
  }
@@ -1,4 +1,4 @@
1
1
  import * as React from "react";
2
- import { Label as LabelPrimitive } from "radix-ui";
2
+ import * as LabelPrimitive from "@radix-ui/react-label";
3
3
  declare function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>): import("react/jsx-runtime").JSX.Element;
4
4
  export { Label };
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { cn } from "../utils.js";
5
- import { Label as LabelPrimitive } from "radix-ui";
5
+ import * as LabelPrimitive from "@radix-ui/react-label";
6
6
  function Label({ className, ...props }) {
7
7
  return (_jsx(LabelPrimitive.Root, { "data-mendy-ui": "", "data-slot": "label", className: cn("mui-222f930b8752 mui-71556df3b421 mui-074569488cca mui-c74ab393b96d mui-761cdb81bddb mui-daaac3fbf55e mui-78905c9e865c mui-9af9a4182597 mui-272ef88d6a0c mui-f0ee83167878 mui-25d26df52a1d", className), ...props }));
8
8
  }