@windmorph/react 0.2.3 → 0.2.4
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/dist/components/autocomplete/Autocomplete.d.ts +4 -1
- package/dist/components/autocomplete/Autocomplete.js +2 -4
- package/dist/components/autocomplete/Autocomplete.js.map +1 -1
- package/dist/components/badge/Badge.d.ts +5 -2
- package/dist/components/badge/Badge.js +7 -3
- package/dist/components/badge/Badge.js.map +1 -1
- package/dist/components/button/Button.d.ts +4 -2
- package/dist/components/button/Button.js.map +1 -1
- package/dist/components/button/ButtonContext.d.ts +1 -1
- package/dist/components/button/button-pending.js +2 -4
- package/dist/components/button/button-pending.js.map +1 -1
- package/dist/{composites → components}/button-group/ButtonGroup.d.ts +4 -4
- package/dist/{composites → components}/button-group/ButtonGroup.js +9 -7
- package/dist/components/button-group/ButtonGroup.js.map +1 -0
- package/dist/{composites → components}/button-group/button-group-styles.d.ts +1 -1
- package/dist/{composites → components}/button-group/button-group-styles.js +1 -1
- package/dist/{composites → components}/button-group/button-group-styles.js.map +1 -1
- package/dist/{composites → components}/button-group/lazy-overflow-menu.js +2 -2
- package/dist/components/button-group/lazy-overflow-menu.js.map +1 -0
- package/dist/{composites → components}/button-group/overflow-menu-list.js +2 -2
- package/dist/components/button-group/overflow-menu-list.js.map +1 -0
- package/dist/{composites → components}/button-group/use-overflow.js +1 -1
- package/dist/{composites → components}/button-group/use-overflow.js.map +1 -1
- package/dist/{composites → components}/button-group/use-press-expansion.js +1 -1
- package/dist/{composites → components}/button-group/use-press-expansion.js.map +1 -1
- package/dist/components/carousel/Carousel.d.ts +4 -1
- package/dist/components/carousel/Carousel.js +2 -4
- package/dist/components/carousel/Carousel.js.map +1 -1
- package/dist/components/checkbox/checkbox-styles.d.ts +3 -3
- package/dist/components/checkbox/checkbox-tokens.d.ts +1 -1
- package/dist/components/chip/Chip.d.ts +8 -2
- package/dist/components/chip/Chip.js +3 -8
- package/dist/components/chip/Chip.js.map +1 -1
- package/dist/components/date-picker/DateField.d.ts +4 -1
- package/dist/components/date-picker/DateField.js +3 -1
- package/dist/components/date-picker/DateField.js.map +1 -1
- package/dist/components/date-picker/DatePicker.d.ts +4 -1
- package/dist/components/date-picker/DatePicker.js +5 -2
- package/dist/components/date-picker/DatePicker.js.map +1 -1
- package/dist/components/date-picker/PickerDialog.d.ts +4 -1
- package/dist/components/date-picker/PickerDialog.js +3 -1
- package/dist/components/date-picker/PickerDialog.js.map +1 -1
- package/dist/components/date-picker/date-picker-styles.d.ts +15 -15
- package/dist/components/dialog/Dialog.d.ts +5 -2
- package/dist/components/dialog/Dialog.js +4 -2
- package/dist/components/dialog/Dialog.js.map +1 -1
- package/dist/components/icon-button/IconButton.d.ts +4 -2
- package/dist/components/icon-button/IconButton.js.map +1 -1
- package/dist/components/labels/LabelsProvider.d.ts +35 -0
- package/dist/components/labels/LabelsProvider.js +47 -0
- package/dist/components/labels/LabelsProvider.js.map +1 -0
- package/dist/components/labels/use-labels.d.ts +16 -0
- package/dist/components/labels/use-labels.js +42 -0
- package/dist/components/labels/use-labels.js.map +1 -0
- package/dist/components/menu/Menu.d.ts +4 -1
- package/dist/components/menu/Menu.js +7 -10
- package/dist/components/menu/Menu.js.map +1 -1
- package/dist/components/menu/menu-styles.js +12 -12
- package/dist/components/menu/menu-styles.js.map +1 -1
- package/dist/components/menu/menu-tokens.js.map +1 -1
- package/dist/components/navigation/NavItem.d.ts +1 -1
- package/dist/components/navigation/NavItem.js +9 -6
- package/dist/components/navigation/NavItem.js.map +1 -1
- package/dist/components/pull-to-refresh/PullToRefresh.d.ts +1 -1
- package/dist/components/pull-to-refresh/PullToRefresh.js +2 -4
- package/dist/components/pull-to-refresh/PullToRefresh.js.map +1 -1
- package/dist/components/search/SearchField.d.ts +4 -1
- package/dist/components/search/SearchField.js +2 -4
- package/dist/components/search/SearchField.js.map +1 -1
- package/dist/components/select/Select.d.ts +4 -1
- package/dist/components/select/Select.js +2 -4
- package/dist/components/select/Select.js.map +1 -1
- package/dist/components/sheet/BottomSheet.d.ts +4 -1
- package/dist/components/sheet/BottomSheet.js +2 -4
- package/dist/components/sheet/BottomSheet.js.map +1 -1
- package/dist/components/sheet/SideSheet.d.ts +4 -1
- package/dist/components/sheet/SideSheet.js +2 -4
- package/dist/components/sheet/SideSheet.js.map +1 -1
- package/dist/components/slider/Slider.d.ts +4 -1
- package/dist/components/slider/Slider.js +2 -4
- package/dist/components/slider/Slider.js.map +1 -1
- package/dist/components/snackbar/Snackbar.d.ts +8 -2
- package/dist/components/snackbar/Snackbar.js +2 -4
- package/dist/components/snackbar/Snackbar.js.map +1 -1
- package/dist/components/snackbar/snackbar-tokens.d.ts +1 -1
- package/dist/components/split-button/SplitButton.d.ts +4 -1
- package/dist/components/split-button/SplitButton.js +4 -2
- package/dist/components/split-button/SplitButton.js.map +1 -1
- package/dist/components/switch/switch-styles.d.ts +3 -3
- package/dist/components/text-field/TextField.d.ts +4 -1
- package/dist/components/text-field/TextField.js +2 -4
- package/dist/components/text-field/TextField.js.map +1 -1
- package/dist/components/time-picker/TimeField.d.ts +24 -2
- package/dist/components/time-picker/TimeField.js +40 -12
- package/dist/components/time-picker/TimeField.js.map +1 -1
- package/dist/components/time-picker/TimeFieldList.js +95 -0
- package/dist/components/time-picker/TimeFieldList.js.map +1 -0
- package/dist/components/time-picker/TimePicker.d.ts +9 -5
- package/dist/components/time-picker/TimePicker.js +5 -2
- package/dist/components/time-picker/TimePicker.js.map +1 -1
- package/dist/components/time-picker/time-list.js +33 -0
- package/dist/components/time-picker/time-list.js.map +1 -0
- package/dist/components/tooltip/Tooltip.d.ts +1 -1
- package/dist/index.d.ts +40 -38
- package/dist/index.js +4 -3
- package/dist/primitives.d.ts +7 -4
- package/dist/primitives.js +4 -1
- package/dist/styles.compiled.css +1 -1
- package/dist/utils/use-scroll-keys.d.ts +29 -0
- package/dist/utils/use-scroll-keys.js +66 -0
- package/dist/utils/use-scroll-keys.js.map +1 -0
- package/dist/utils/use-scroll-restoration.d.ts +43 -0
- package/dist/utils/use-scroll-restoration.js +173 -0
- package/dist/utils/use-scroll-restoration.js.map +1 -0
- package/dist/windmorph-labels.d.ts +101 -0
- package/package.json +1 -1
- package/dist/composites/button-group/ButtonGroup.js.map +0 -1
- package/dist/composites/button-group/lazy-overflow-menu.js.map +0 -1
- package/dist/composites/button-group/overflow-menu-list.js.map +0 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { RefObject } from "react";
|
|
2
|
+
//#region src/utils/use-scroll-keys.d.ts
|
|
3
|
+
export interface ScrollKeysResult {
|
|
4
|
+
/**
|
|
5
|
+
* The `tabIndex` for the scrolling element: `0` while it holds nothing that takes focus, so a
|
|
6
|
+
* text-only page can be reached and scrolled from the keyboard, and `-1` otherwise, so every
|
|
7
|
+
* other page has no extra tab stop. Either way the element can take focus.
|
|
8
|
+
*/
|
|
9
|
+
tabIndex: 0 | -1;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Sends the scroll keys to an element that scrolls by itself (an app frame's pane) while nothing
|
|
13
|
+
* has focus. Page Down, Page Up, Home, End, Space and the arrows otherwise go to the window,
|
|
14
|
+
* which has nothing to scroll in such a frame, so the page doesn't move until a click in the
|
|
15
|
+
* pane or a Tab to it. As one of those keys goes down with focus on the body and no Ctrl, Alt
|
|
16
|
+
* or Meta held, the element takes focus (without scrolling), and the browser scrolls it with
|
|
17
|
+
* that same press.
|
|
18
|
+
*
|
|
19
|
+
* Put the returned `tabIndex` on the element: it is a tab stop only while it holds nothing that
|
|
20
|
+
* takes focus (watched as its content changes), as browsers do for a scrolling region.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const pane = useRef<HTMLElement>(null);
|
|
24
|
+
* const { tabIndex } = useScrollKeys(pane);
|
|
25
|
+
* return <main ref={pane} tabIndex={tabIndex} className="overflow-y-auto">…</main>;
|
|
26
|
+
*/
|
|
27
|
+
export declare function useScrollKeys(ref: RefObject<HTMLElement | null>): ScrollKeysResult;
|
|
28
|
+
//#endregion
|
|
29
|
+
//# sourceMappingURL=use-scroll-keys.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
//#region src/utils/use-scroll-keys.ts
|
|
4
|
+
/** The keys that scroll a page. */
|
|
5
|
+
const SCROLL_KEYS = /* @__PURE__ */ new Set([
|
|
6
|
+
"PageDown",
|
|
7
|
+
"PageUp",
|
|
8
|
+
"Home",
|
|
9
|
+
"End",
|
|
10
|
+
" ",
|
|
11
|
+
"ArrowDown",
|
|
12
|
+
"ArrowUp"
|
|
13
|
+
]);
|
|
14
|
+
/** What takes focus with Tab. */
|
|
15
|
+
const TABBABLE = "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
|
|
16
|
+
/**
|
|
17
|
+
* Sends the scroll keys to an element that scrolls by itself (an app frame's pane) while nothing
|
|
18
|
+
* has focus. Page Down, Page Up, Home, End, Space and the arrows otherwise go to the window,
|
|
19
|
+
* which has nothing to scroll in such a frame, so the page doesn't move until a click in the
|
|
20
|
+
* pane or a Tab to it. As one of those keys goes down with focus on the body and no Ctrl, Alt
|
|
21
|
+
* or Meta held, the element takes focus (without scrolling), and the browser scrolls it with
|
|
22
|
+
* that same press.
|
|
23
|
+
*
|
|
24
|
+
* Put the returned `tabIndex` on the element: it is a tab stop only while it holds nothing that
|
|
25
|
+
* takes focus (watched as its content changes), as browsers do for a scrolling region.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const pane = useRef<HTMLElement>(null);
|
|
29
|
+
* const { tabIndex } = useScrollKeys(pane);
|
|
30
|
+
* return <main ref={pane} tabIndex={tabIndex} className="overflow-y-auto">…</main>;
|
|
31
|
+
*/
|
|
32
|
+
function useScrollKeys(ref) {
|
|
33
|
+
const [isTabStop, setIsTabStop] = useState(false);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const element = ref.current;
|
|
36
|
+
if (!element) return;
|
|
37
|
+
const onKeyDown = (event) => {
|
|
38
|
+
if (document.activeElement !== document.body || !SCROLL_KEYS.has(event.key)) return;
|
|
39
|
+
if (event.altKey || event.ctrlKey || event.metaKey) return;
|
|
40
|
+
element.focus({ preventScroll: true });
|
|
41
|
+
};
|
|
42
|
+
const update = () => setIsTabStop(element.querySelector(TABBABLE) === null);
|
|
43
|
+
update();
|
|
44
|
+
const observer = new MutationObserver(update);
|
|
45
|
+
observer.observe(element, {
|
|
46
|
+
childList: true,
|
|
47
|
+
subtree: true,
|
|
48
|
+
attributes: true,
|
|
49
|
+
attributeFilter: [
|
|
50
|
+
"href",
|
|
51
|
+
"disabled",
|
|
52
|
+
"tabindex"
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
56
|
+
return () => {
|
|
57
|
+
observer.disconnect();
|
|
58
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
59
|
+
};
|
|
60
|
+
}, [ref]);
|
|
61
|
+
return { tabIndex: isTabStop ? 0 : -1 };
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { useScrollKeys };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=use-scroll-keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-scroll-keys.js","names":[],"sources":["../../src/utils/use-scroll-keys.ts"],"sourcesContent":["'use client';\n\nimport { useEffect, useState, type RefObject } from 'react';\n\n/** The keys that scroll a page. */\nconst SCROLL_KEYS = new Set(['PageDown', 'PageUp', 'Home', 'End', ' ', 'ArrowDown', 'ArrowUp']);\n\n/** What takes focus with Tab. */\nconst TABBABLE =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\nexport interface ScrollKeysResult {\n /**\n * The `tabIndex` for the scrolling element: `0` while it holds nothing that takes focus, so a\n * text-only page can be reached and scrolled from the keyboard, and `-1` otherwise, so every\n * other page has no extra tab stop. Either way the element can take focus.\n */\n tabIndex: 0 | -1;\n}\n\n/**\n * Sends the scroll keys to an element that scrolls by itself (an app frame's pane) while nothing\n * has focus. Page Down, Page Up, Home, End, Space and the arrows otherwise go to the window,\n * which has nothing to scroll in such a frame, so the page doesn't move until a click in the\n * pane or a Tab to it. As one of those keys goes down with focus on the body and no Ctrl, Alt\n * or Meta held, the element takes focus (without scrolling), and the browser scrolls it with\n * that same press.\n *\n * Put the returned `tabIndex` on the element: it is a tab stop only while it holds nothing that\n * takes focus (watched as its content changes), as browsers do for a scrolling region.\n *\n * @example\n * const pane = useRef<HTMLElement>(null);\n * const { tabIndex } = useScrollKeys(pane);\n * return <main ref={pane} tabIndex={tabIndex} className=\"overflow-y-auto\">…</main>;\n */\nexport function useScrollKeys(ref: RefObject<HTMLElement | null>): ScrollKeysResult {\n const [isTabStop, setIsTabStop] = useState(false);\n\n useEffect(() => {\n const element = ref.current;\n if (!element) return;\n const onKeyDown = (event: KeyboardEvent) => {\n if (document.activeElement !== document.body || !SCROLL_KEYS.has(event.key)) return;\n if (event.altKey || event.ctrlKey || event.metaKey) return;\n element.focus({ preventScroll: true });\n };\n const update = () => setIsTabStop(element.querySelector(TABBABLE) === null);\n update();\n const observer = new MutationObserver(update);\n observer.observe(element, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['href', 'disabled', 'tabindex'],\n });\n document.addEventListener('keydown', onKeyDown, true);\n return () => {\n observer.disconnect();\n document.removeEventListener('keydown', onKeyDown, true);\n };\n }, [ref]);\n\n return { tabIndex: isTabStop ? 0 : -1 };\n}\n"],"mappings":";;;;AAKA,MAAM,8BAAc,IAAI,IAAI;CAAC;CAAY;CAAU;CAAQ;CAAO;CAAK;CAAa;AAAS,CAAC;;AAG9F,MAAM,WACJ;;;;;;;;;;;;;;;;;AA2BF,SAAgB,cAAc,KAAsD;CAClF,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAEhD,gBAAgB;EACd,MAAM,UAAU,IAAI;EACpB,IAAI,CAAC,SAAS;EACd,MAAM,aAAa,UAAyB;GAC1C,IAAI,SAAS,kBAAkB,SAAS,QAAQ,CAAC,YAAY,IAAI,MAAM,GAAG,GAAG;GAC7E,IAAI,MAAM,UAAU,MAAM,WAAW,MAAM,SAAS;GACpD,QAAQ,MAAM,EAAE,eAAe,KAAK,CAAC;EACvC;EACA,MAAM,eAAe,aAAa,QAAQ,cAAc,QAAQ,MAAM,IAAI;EAC1E,OAAO;EACP,MAAM,WAAW,IAAI,iBAAiB,MAAM;EAC5C,SAAS,QAAQ,SAAS;GACxB,WAAW;GACX,SAAS;GACT,YAAY;GACZ,iBAAiB;IAAC;IAAQ;IAAY;GAAU;EAClD,CAAC;EACD,SAAS,iBAAiB,WAAW,WAAW,IAAI;EACpD,aAAa;GACX,SAAS,WAAW;GACpB,SAAS,oBAAoB,WAAW,WAAW,IAAI;EACzD;CACF,GAAG,CAAC,GAAG,CAAC;CAER,OAAO,EAAE,UAAU,YAAY,IAAI,GAAG;AACxC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { RefObject } from "react";
|
|
2
|
+
//#region src/utils/use-scroll-restoration.d.ts
|
|
3
|
+
export interface ScrollRestorationOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Identifies the page shown: the router's path and query, so it works with any router. A
|
|
6
|
+
* position is kept for each key.
|
|
7
|
+
*/
|
|
8
|
+
pageKey: string;
|
|
9
|
+
/**
|
|
10
|
+
* The `sessionStorage` key the positions are kept under, for an app with more than one such
|
|
11
|
+
* element. @default "windmorph-scroll"
|
|
12
|
+
*/
|
|
13
|
+
storageKey?: string;
|
|
14
|
+
/**
|
|
15
|
+
* How long, in ms, a restored position is held while content arrives (images, data): moved
|
|
16
|
+
* by anything but the person in that time (a router, a script), it is put back. The hold
|
|
17
|
+
* ends at once when the person scrolls. @default 1500
|
|
18
|
+
*/
|
|
19
|
+
holdFor?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Keeps and restores the scroll position of an element that scrolls by itself (an app frame's
|
|
23
|
+
* pane), per page. Browsers restore only the window's scroll, so with client-side routing such
|
|
24
|
+
* an element comes back at its top, or wherever it was on the page just left, and a reload
|
|
25
|
+
* loses its position too.
|
|
26
|
+
*
|
|
27
|
+
* - A position is kept for each `pageKey`, in `sessionStorage`, so a reload restores it too.
|
|
28
|
+
* - A page's position is taken when the person sets off: a press, a key, or back and forward.
|
|
29
|
+
* Never from a scroll after the page changed, since a router can scroll the element to the
|
|
30
|
+
* new page's top first.
|
|
31
|
+
* - Going back or forward (the Navigation API's `navigate` event, or `popstate` where it's
|
|
32
|
+
* missing) restores the page's position and holds it for `holdFor` ms while content arrives;
|
|
33
|
+
* the hold ends the moment the person scrolls.
|
|
34
|
+
* - A new page starts at its top.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* const pane = useRef<HTMLElement>(null);
|
|
38
|
+
* useScrollRestoration(pane, { pageKey: `${pathname}?${searchParams}` });
|
|
39
|
+
* return <main ref={pane} className="overflow-y-auto">…</main>;
|
|
40
|
+
*/
|
|
41
|
+
export declare function useScrollRestoration(ref: RefObject<HTMLElement | null>, { pageKey, storageKey, holdFor }: ScrollRestorationOptions): void;
|
|
42
|
+
//#endregion
|
|
43
|
+
//# sourceMappingURL=use-scroll-restoration.d.ts.map
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
3
|
+
//#region src/utils/use-scroll-restoration.ts
|
|
4
|
+
/** The most pages a position is kept for; the oldest go first. */
|
|
5
|
+
const MAX_PAGES = 200;
|
|
6
|
+
/** What a person does to scroll, which ends a hold. */
|
|
7
|
+
const RELEASE_EVENTS = [
|
|
8
|
+
"wheel",
|
|
9
|
+
"touchstart",
|
|
10
|
+
"pointerdown",
|
|
11
|
+
"keydown"
|
|
12
|
+
];
|
|
13
|
+
function readPositions(storageKey) {
|
|
14
|
+
try {
|
|
15
|
+
const stored = JSON.parse(sessionStorage.getItem(storageKey) ?? "{}");
|
|
16
|
+
if (typeof stored !== "object" || stored === null) return /* @__PURE__ */ new Map();
|
|
17
|
+
return new Map(Object.entries(stored).filter((entry) => typeof entry[1] === "number"));
|
|
18
|
+
} catch {
|
|
19
|
+
return /* @__PURE__ */ new Map();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function writePositions(storageKey, positions) {
|
|
23
|
+
try {
|
|
24
|
+
sessionStorage.setItem(storageKey, JSON.stringify(Object.fromEntries(positions)));
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
/** The positions for `storageKey`: read from storage the first time, and again for another key. */
|
|
28
|
+
function keptFor(cache, storageKey) {
|
|
29
|
+
if (cache.current?.storageKey !== storageKey) cache.current = {
|
|
30
|
+
storageKey,
|
|
31
|
+
positions: readPositions(storageKey),
|
|
32
|
+
last: null
|
|
33
|
+
};
|
|
34
|
+
return cache.current;
|
|
35
|
+
}
|
|
36
|
+
/** Whether this page load is a reload or a return through history, where a position comes back. */
|
|
37
|
+
function loadRestores() {
|
|
38
|
+
const [entry] = performance.getEntriesByType("navigation");
|
|
39
|
+
return entry?.type === "reload" || entry?.type === "back_forward";
|
|
40
|
+
}
|
|
41
|
+
/** Moves the element there at once, whatever its `scroll-behavior`. */
|
|
42
|
+
function scrollTo(element, top) {
|
|
43
|
+
if (typeof element.scrollTo === "function") element.scrollTo({
|
|
44
|
+
top,
|
|
45
|
+
behavior: "instant"
|
|
46
|
+
});
|
|
47
|
+
else element.scrollTop = top;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Keeps the element at `top` for `duration` ms: a router can scroll it after the page changes,
|
|
51
|
+
* and content still arriving makes the page tall enough to reach the position only later. Ends
|
|
52
|
+
* the moment the person scrolls. Returns what ends it early.
|
|
53
|
+
*/
|
|
54
|
+
function hold(element, top, duration) {
|
|
55
|
+
let frame = 0;
|
|
56
|
+
const until = performance.now() + duration;
|
|
57
|
+
const stop = () => {
|
|
58
|
+
cancelAnimationFrame(frame);
|
|
59
|
+
for (const type of RELEASE_EVENTS) window.removeEventListener(type, stop, true);
|
|
60
|
+
};
|
|
61
|
+
const apply = () => {
|
|
62
|
+
if (element.scrollTop !== top) scrollTo(element, top);
|
|
63
|
+
if (performance.now() < until) frame = requestAnimationFrame(apply);
|
|
64
|
+
else stop();
|
|
65
|
+
};
|
|
66
|
+
for (const type of RELEASE_EVENTS) window.addEventListener(type, stop, {
|
|
67
|
+
capture: true,
|
|
68
|
+
passive: true
|
|
69
|
+
});
|
|
70
|
+
apply();
|
|
71
|
+
return stop;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Keeps and restores the scroll position of an element that scrolls by itself (an app frame's
|
|
75
|
+
* pane), per page. Browsers restore only the window's scroll, so with client-side routing such
|
|
76
|
+
* an element comes back at its top, or wherever it was on the page just left, and a reload
|
|
77
|
+
* loses its position too.
|
|
78
|
+
*
|
|
79
|
+
* - A position is kept for each `pageKey`, in `sessionStorage`, so a reload restores it too.
|
|
80
|
+
* - A page's position is taken when the person sets off: a press, a key, or back and forward.
|
|
81
|
+
* Never from a scroll after the page changed, since a router can scroll the element to the
|
|
82
|
+
* new page's top first.
|
|
83
|
+
* - Going back or forward (the Navigation API's `navigate` event, or `popstate` where it's
|
|
84
|
+
* missing) restores the page's position and holds it for `holdFor` ms while content arrives;
|
|
85
|
+
* the hold ends the moment the person scrolls.
|
|
86
|
+
* - A new page starts at its top.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* const pane = useRef<HTMLElement>(null);
|
|
90
|
+
* useScrollRestoration(pane, { pageKey: `${pathname}?${searchParams}` });
|
|
91
|
+
* return <main ref={pane} className="overflow-y-auto">…</main>;
|
|
92
|
+
*/
|
|
93
|
+
function useScrollRestoration(ref, { pageKey, storageKey = "windmorph-scroll", holdFor = 1500 }) {
|
|
94
|
+
/** The page the element shows, and whether its position was restored. */
|
|
95
|
+
const shown = useRef(null);
|
|
96
|
+
const kept = useRef(null);
|
|
97
|
+
/** Set by back and forward until the page changes; cleared when the person sets off anew. */
|
|
98
|
+
const traversing = useRef(false);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const keep = () => {
|
|
101
|
+
const element = ref.current;
|
|
102
|
+
if (!element || !shown.current) return;
|
|
103
|
+
const store = keptFor(kept, storageKey);
|
|
104
|
+
const { key } = shown.current;
|
|
105
|
+
const top = element.scrollTop;
|
|
106
|
+
if (store.last?.key === key && store.last.top === top) return;
|
|
107
|
+
store.last = {
|
|
108
|
+
key,
|
|
109
|
+
top
|
|
110
|
+
};
|
|
111
|
+
store.positions.delete(key);
|
|
112
|
+
store.positions.set(key, top);
|
|
113
|
+
if (store.positions.size > MAX_PAGES) store.positions.delete(store.positions.keys().next().value);
|
|
114
|
+
writePositions(storageKey, store.positions);
|
|
115
|
+
};
|
|
116
|
+
const onSetOff = (event) => {
|
|
117
|
+
if (event.repeat) return;
|
|
118
|
+
traversing.current = false;
|
|
119
|
+
keep();
|
|
120
|
+
};
|
|
121
|
+
const onTraverse = () => {
|
|
122
|
+
keep();
|
|
123
|
+
traversing.current = true;
|
|
124
|
+
};
|
|
125
|
+
const onNavigate = (event) => {
|
|
126
|
+
const { navigationType, hashChange } = event;
|
|
127
|
+
if (navigationType === "traverse" && !hashChange) onTraverse();
|
|
128
|
+
};
|
|
129
|
+
const navigation = window.navigation;
|
|
130
|
+
document.addEventListener("pointerdown", onSetOff, true);
|
|
131
|
+
document.addEventListener("keydown", onSetOff, true);
|
|
132
|
+
window.addEventListener("pagehide", keep);
|
|
133
|
+
if (navigation) navigation.addEventListener("navigate", onNavigate);
|
|
134
|
+
else window.addEventListener("popstate", onTraverse, true);
|
|
135
|
+
return () => {
|
|
136
|
+
document.removeEventListener("pointerdown", onSetOff, true);
|
|
137
|
+
document.removeEventListener("keydown", onSetOff, true);
|
|
138
|
+
window.removeEventListener("pagehide", keep);
|
|
139
|
+
if (navigation) navigation.removeEventListener("navigate", onNavigate);
|
|
140
|
+
else window.removeEventListener("popstate", onTraverse, true);
|
|
141
|
+
};
|
|
142
|
+
}, [ref, storageKey]);
|
|
143
|
+
useLayoutEffect(() => {
|
|
144
|
+
const element = ref.current;
|
|
145
|
+
if (!element) return;
|
|
146
|
+
const previous = shown.current;
|
|
147
|
+
const restored = previous?.key === pageKey ? previous.restored : previous === null ? loadRestores() : traversing.current;
|
|
148
|
+
if (previous?.key !== pageKey) traversing.current = false;
|
|
149
|
+
shown.current = {
|
|
150
|
+
key: pageKey,
|
|
151
|
+
restored
|
|
152
|
+
};
|
|
153
|
+
if (!restored) {
|
|
154
|
+
if (previous !== null && previous.key !== pageKey) scrollTo(element, 0);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const top = keptFor(kept, storageKey).positions.get(pageKey);
|
|
158
|
+
if (top === void 0) {
|
|
159
|
+
if (previous !== null && previous.key !== pageKey) scrollTo(element, 0);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
return hold(element, top, holdFor);
|
|
163
|
+
}, [
|
|
164
|
+
ref,
|
|
165
|
+
pageKey,
|
|
166
|
+
storageKey,
|
|
167
|
+
holdFor
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
export { useScrollRestoration };
|
|
172
|
+
|
|
173
|
+
//# sourceMappingURL=use-scroll-restoration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-scroll-restoration.js","names":[],"sources":["../../src/utils/use-scroll-restoration.ts"],"sourcesContent":["'use client';\n\nimport { useEffect, useLayoutEffect, useRef, type RefObject } from 'react';\n\nexport interface ScrollRestorationOptions {\n /**\n * Identifies the page shown: the router's path and query, so it works with any router. A\n * position is kept for each key.\n */\n pageKey: string;\n /**\n * The `sessionStorage` key the positions are kept under, for an app with more than one such\n * element. @default \"windmorph-scroll\"\n */\n storageKey?: string;\n /**\n * How long, in ms, a restored position is held while content arrives (images, data): moved\n * by anything but the person in that time (a router, a script), it is put back. The hold\n * ends at once when the person scrolls. @default 1500\n */\n holdFor?: number;\n}\n\n/** The most pages a position is kept for; the oldest go first. */\nconst MAX_PAGES = 200;\n\n/** What a person does to scroll, which ends a hold. */\nconst RELEASE_EVENTS = ['wheel', 'touchstart', 'pointerdown', 'keydown'] as const;\n\nfunction readPositions(storageKey: string): Map<string, number> {\n try {\n const stored: unknown = JSON.parse(sessionStorage.getItem(storageKey) ?? '{}');\n if (typeof stored !== 'object' || stored === null) return new Map();\n return new Map(\n Object.entries(stored).filter(\n (entry): entry is [string, number] => typeof entry[1] === 'number',\n ),\n );\n } catch {\n // Storage blocked, or not JSON: the positions are kept in memory for the visit.\n return new Map();\n }\n}\n\nfunction writePositions(storageKey: string, positions: Map<string, number>) {\n try {\n sessionStorage.setItem(storageKey, JSON.stringify(Object.fromEntries(positions)));\n } catch {\n // Storage blocked or full: the map in memory still serves back and forward.\n }\n}\n\n/** The positions kept under one storage key, read once. */\ninterface Kept {\n storageKey: string;\n positions: Map<string, number>;\n /** The last page and position written, so a press or key that moved nothing writes nothing. */\n last: { key: string; top: number } | null;\n}\n\n/** The positions for `storageKey`: read from storage the first time, and again for another key. */\nfunction keptFor(cache: { current: Kept | null }, storageKey: string): Kept {\n if (cache.current?.storageKey !== storageKey) {\n cache.current = { storageKey, positions: readPositions(storageKey), last: null };\n }\n return cache.current;\n}\n\n/** Whether this page load is a reload or a return through history, where a position comes back. */\nfunction loadRestores(): boolean {\n const [entry] = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[];\n return entry?.type === 'reload' || entry?.type === 'back_forward';\n}\n\n/** Moves the element there at once, whatever its `scroll-behavior`. */\nfunction scrollTo(element: HTMLElement, top: number) {\n if (typeof element.scrollTo === 'function') element.scrollTo({ top, behavior: 'instant' });\n else element.scrollTop = top;\n}\n\n/**\n * Keeps the element at `top` for `duration` ms: a router can scroll it after the page changes,\n * and content still arriving makes the page tall enough to reach the position only later. Ends\n * the moment the person scrolls. Returns what ends it early.\n */\nfunction hold(element: HTMLElement, top: number, duration: number): () => void {\n let frame = 0;\n const until = performance.now() + duration;\n const stop = () => {\n cancelAnimationFrame(frame);\n for (const type of RELEASE_EVENTS) window.removeEventListener(type, stop, true);\n };\n const apply = () => {\n if (element.scrollTop !== top) scrollTo(element, top);\n if (performance.now() < until) frame = requestAnimationFrame(apply);\n else stop();\n };\n for (const type of RELEASE_EVENTS) {\n window.addEventListener(type, stop, { capture: true, passive: true });\n }\n apply();\n return stop;\n}\n\n/**\n * Keeps and restores the scroll position of an element that scrolls by itself (an app frame's\n * pane), per page. Browsers restore only the window's scroll, so with client-side routing such\n * an element comes back at its top, or wherever it was on the page just left, and a reload\n * loses its position too.\n *\n * - A position is kept for each `pageKey`, in `sessionStorage`, so a reload restores it too.\n * - A page's position is taken when the person sets off: a press, a key, or back and forward.\n * Never from a scroll after the page changed, since a router can scroll the element to the\n * new page's top first.\n * - Going back or forward (the Navigation API's `navigate` event, or `popstate` where it's\n * missing) restores the page's position and holds it for `holdFor` ms while content arrives;\n * the hold ends the moment the person scrolls.\n * - A new page starts at its top.\n *\n * @example\n * const pane = useRef<HTMLElement>(null);\n * useScrollRestoration(pane, { pageKey: `${pathname}?${searchParams}` });\n * return <main ref={pane} className=\"overflow-y-auto\">…</main>;\n */\nexport function useScrollRestoration(\n ref: RefObject<HTMLElement | null>,\n { pageKey, storageKey = 'windmorph-scroll', holdFor = 1500 }: ScrollRestorationOptions,\n): void {\n /** The page the element shows, and whether its position was restored. */\n const shown = useRef<{ key: string; restored: boolean } | null>(null);\n const kept = useRef<Kept | null>(null);\n /** Set by back and forward until the page changes; cleared when the person sets off anew. */\n const traversing = useRef(false);\n\n useEffect(() => {\n const keep = () => {\n const element = ref.current;\n if (!element || !shown.current) return;\n const store = keptFor(kept, storageKey);\n const { key } = shown.current;\n const top = element.scrollTop;\n // Every press and key comes through here: most move nothing, and write nothing.\n if (store.last?.key === key && store.last.top === top) return;\n store.last = { key, top };\n // Deleted first, so the page moves to the newest end of the map.\n store.positions.delete(key);\n store.positions.set(key, top);\n if (store.positions.size > MAX_PAGES) {\n store.positions.delete(store.positions.keys().next().value as string);\n }\n writePositions(storageKey, store.positions);\n };\n const onSetOff = (event: Event) => {\n // A held key repeats after back or forward has begun: that isn't setting off anew.\n if ((event as KeyboardEvent).repeat) return;\n traversing.current = false;\n keep();\n };\n const onTraverse = () => {\n keep();\n traversing.current = true;\n };\n const onNavigate = (event: Event) => {\n const { navigationType, hashChange } = event as Event & {\n navigationType?: string;\n hashChange?: boolean;\n };\n if (navigationType === 'traverse' && !hashChange) onTraverse();\n };\n // In browsers with the Navigation API, `popstate` can arrive after the restored page is on\n // screen; `navigate` comes before the page changes.\n const navigation = (window as { navigation?: EventTarget }).navigation;\n document.addEventListener('pointerdown', onSetOff, true);\n document.addEventListener('keydown', onSetOff, true);\n window.addEventListener('pagehide', keep);\n if (navigation) navigation.addEventListener('navigate', onNavigate);\n else window.addEventListener('popstate', onTraverse, true);\n return () => {\n document.removeEventListener('pointerdown', onSetOff, true);\n document.removeEventListener('keydown', onSetOff, true);\n window.removeEventListener('pagehide', keep);\n if (navigation) navigation.removeEventListener('navigate', onNavigate);\n else window.removeEventListener('popstate', onTraverse, true);\n };\n }, [ref, storageKey]);\n\n // Before paint, so a restored page never shows at its top first.\n useLayoutEffect(() => {\n const element = ref.current;\n if (!element) return;\n const previous = shown.current;\n // The same page again (a development double render) repeats its decision.\n const restored =\n previous?.key === pageKey\n ? previous.restored\n : previous === null\n ? loadRestores()\n : traversing.current;\n if (previous?.key !== pageKey) traversing.current = false;\n shown.current = { key: pageKey, restored };\n if (!restored) {\n // A new page starts at its top; the first page of a visit is left where the browser put it.\n if (previous !== null && previous.key !== pageKey) scrollTo(element, 0);\n return;\n }\n const top = keptFor(kept, storageKey).positions.get(pageKey);\n if (top === undefined) {\n // Nothing kept for it (a page first seen through history): its top, as a new page.\n if (previous !== null && previous.key !== pageKey) scrollTo(element, 0);\n return;\n }\n return hold(element, top, holdFor);\n }, [ref, pageKey, storageKey, holdFor]);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { AutocompleteLabels } from "./components/autocomplete/Autocomplete.js";
|
|
2
|
+
import { BadgeLabels } from "./components/badge/Badge.js";
|
|
3
|
+
import { ButtonLabels } from "./components/button/button-pending.js";
|
|
4
|
+
import { CarouselLabels } from "./components/carousel/Carousel.js";
|
|
5
|
+
import { ChipLabels } from "./components/chip/Chip.js";
|
|
6
|
+
import { TextFieldLabels } from "./components/text-field/TextField.js";
|
|
7
|
+
import { DateFieldLabels } from "./components/date-picker/DateField.js";
|
|
8
|
+
import { DatePickerLabels } from "./components/date-picker/DatePicker.js";
|
|
9
|
+
import { DialogHeaderLabels } from "./components/dialog/Dialog.js";
|
|
10
|
+
import { PickerDialogLabels } from "./components/date-picker/PickerDialog.js";
|
|
11
|
+
import { MenuLabels } from "./components/menu/Menu.js";
|
|
12
|
+
import { PullToRefreshLabels } from "./components/pull-to-refresh/PullToRefresh.js";
|
|
13
|
+
import { SearchFieldLabels } from "./components/search/SearchField.js";
|
|
14
|
+
import { SelectLabels } from "./components/select/Select.js";
|
|
15
|
+
import { BottomSheetLabels } from "./components/sheet/BottomSheet.js";
|
|
16
|
+
import { SideSheetLabels } from "./components/sheet/SideSheet.js";
|
|
17
|
+
import { RangeSliderLabels } from "./components/slider/Slider.js";
|
|
18
|
+
import { SnackbarLabels } from "./components/snackbar/Snackbar.js";
|
|
19
|
+
import { SplitButtonLabels } from "./components/split-button/SplitButton.js";
|
|
20
|
+
import { TimePickerLabels } from "./components/time-picker/TimePicker.js";
|
|
21
|
+
import { TimeFieldLabels } from "./components/time-picker/TimeField.js";
|
|
22
|
+
import { ButtonGroupLabels } from "./components/button-group/ButtonGroup.js";
|
|
23
|
+
//#region src/windmorph-labels.d.ts
|
|
24
|
+
/**
|
|
25
|
+
* The words components show or announce by themselves, keyed by component, for
|
|
26
|
+
* {@link LabelsProvider}. Each key is a labels type's name without `Labels`, and takes the
|
|
27
|
+
* words that component's own `labels` prop takes.
|
|
28
|
+
*
|
|
29
|
+
* Components that share a labels type share a key: `Button` is `Button` and `IconButton`,
|
|
30
|
+
* `Chip` the chips, `DatePicker` both date pickers, `TimePicker` the picker and its mode
|
|
31
|
+
* toggle, `Snackbar` the snackbar and its host. A component built from another reads that
|
|
32
|
+
* one's words too, then its own: `DateField` reads `DatePicker`'s, and `TimeField` reads
|
|
33
|
+
* `TimePicker`'s and `PickerDialog`'s.
|
|
34
|
+
*
|
|
35
|
+
* A package that builds on this one adds its components by declaration merging:
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* declare module '@windmorph/react' {
|
|
39
|
+
* interface WindmorphLabels {
|
|
40
|
+
* Rating?: Partial<RatingLabels>;
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export interface WindmorphLabels {
|
|
46
|
+
/** `Autocomplete`'s words. @default each word's English default */
|
|
47
|
+
Autocomplete?: Partial<AutocompleteLabels>;
|
|
48
|
+
/** `Badge`'s words. @default each word's English default */
|
|
49
|
+
Badge?: Partial<BadgeLabels>;
|
|
50
|
+
/** `BottomSheet`'s words. @default each word's English default */
|
|
51
|
+
BottomSheet?: Partial<BottomSheetLabels>;
|
|
52
|
+
/** `Button`'s and `IconButton`'s words. @default each word's English default */
|
|
53
|
+
Button?: Partial<ButtonLabels>;
|
|
54
|
+
/** `ButtonGroup`'s words. @default each word's English default */
|
|
55
|
+
ButtonGroup?: Partial<ButtonGroupLabels>;
|
|
56
|
+
/** `Carousel`'s words. @default each word's English default */
|
|
57
|
+
Carousel?: Partial<CarouselLabels>;
|
|
58
|
+
/** `FilterChip`'s and `InputChip`'s words. @default each word's English default */
|
|
59
|
+
Chip?: Partial<ChipLabels>;
|
|
60
|
+
/**
|
|
61
|
+
* `DateField`'s words. It reads `DatePicker`'s first, then these.
|
|
62
|
+
* @default each word's English default
|
|
63
|
+
*/
|
|
64
|
+
DateField?: Partial<DateFieldLabels>;
|
|
65
|
+
/** `DatePicker`'s and `DateRangePicker`'s words. @default each word's English default */
|
|
66
|
+
DatePicker?: Partial<DatePickerLabels>;
|
|
67
|
+
/** `DialogHeader`'s words. @default each word's English default */
|
|
68
|
+
DialogHeader?: Partial<DialogHeaderLabels>;
|
|
69
|
+
/** `Menu`'s words. @default each word's English default */
|
|
70
|
+
Menu?: Partial<MenuLabels>;
|
|
71
|
+
/** `PickerDialog`'s words. @default each word's English default */
|
|
72
|
+
PickerDialog?: Partial<PickerDialogLabels>;
|
|
73
|
+
/** `PullToRefresh`'s words. @default each word's English default */
|
|
74
|
+
PullToRefresh?: Partial<PullToRefreshLabels>;
|
|
75
|
+
/** `RangeSlider`'s words. @default each word's English default */
|
|
76
|
+
RangeSlider?: Partial<RangeSliderLabels>;
|
|
77
|
+
/** `SearchField`'s words. @default each word's English default */
|
|
78
|
+
SearchField?: Partial<SearchFieldLabels>;
|
|
79
|
+
/** `Select`'s words. @default each word's English default */
|
|
80
|
+
Select?: Partial<SelectLabels>;
|
|
81
|
+
/** `SideSheet`'s words. @default each word's English default */
|
|
82
|
+
SideSheet?: Partial<SideSheetLabels>;
|
|
83
|
+
/**
|
|
84
|
+
* `Snackbar`'s words, and those of the snackbars a `SnackbarHost` shows.
|
|
85
|
+
* @default each word's English default
|
|
86
|
+
*/
|
|
87
|
+
Snackbar?: Partial<SnackbarLabels>;
|
|
88
|
+
/** `SplitButton`'s words. @default each word's English default */
|
|
89
|
+
SplitButton?: Partial<SplitButtonLabels>;
|
|
90
|
+
/** `TextField`'s words. @default each word's English default */
|
|
91
|
+
TextField?: Partial<TextFieldLabels>;
|
|
92
|
+
/**
|
|
93
|
+
* `TimeField`'s words. It reads `TimePicker`'s and `PickerDialog`'s first, then these.
|
|
94
|
+
* @default each word's English default
|
|
95
|
+
*/
|
|
96
|
+
TimeField?: Partial<TimeFieldLabels>;
|
|
97
|
+
/** `TimePicker`'s and `TimePickerModeToggle`'s words. @default each word's English default */
|
|
98
|
+
TimePicker?: Partial<TimePickerLabels>;
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//# sourceMappingURL=windmorph-labels.d.ts.map
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ButtonGroup.js","names":[],"sources":["../../../src/composites/button-group/ButtonGroup.tsx"],"sourcesContent":["'use client';\n\nimport {\n Children,\n isValidElement,\n useContext,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type HTMLAttributes,\n type Key,\n type ReactElement,\n type ReactNode,\n type Ref,\n type RefCallback,\n type RefObject,\n} from 'react';\nimport { mergeProps, mergeRefs, useObjectRef, useToggleButtonGroup, useToolbar } from 'react-aria';\nimport { useToggleGroupState } from 'react-stately';\nimport { Button } from '../../components/button/Button';\nimport type { ButtonShape, ButtonSize, ButtonVariant } from '../../components/button/button-styles';\nimport {\n ButtonContext,\n ToggleGroupStateContext,\n type ButtonContextValue,\n} from '../../components/button/ButtonContext';\nimport { IconButton } from '../../components/icon-button/IconButton';\nimport type { IconButtonVariant } from '../../components/icon-button/icon-button-styles';\nimport { MenuTrigger } from '../../components/menu/menu-trigger';\nimport { DomDirectionLocale } from '../../primitives/DomDirectionLocale';\nimport { cn } from '../../utils/cn';\nimport {\n focusCheckedRadio,\n notePointerDown,\n radioGroupKeyDown,\n} from '../../utils/radio-group-keys';\nimport { toolbarKeyDown } from '../../utils/toolbar-keys';\nimport type { VariantProps } from '../../utils/tv';\nimport { useWarnOnce } from '../../utils/warn-once';\nimport { buttonGroupStyles } from './button-group-styles';\nimport { LazyOverflowMenuList } from './lazy-overflow-menu';\nimport { useOverflow } from './use-overflow';\nimport { usePressExpansion } from './use-press-expansion';\n\n/** Material Symbols `more_vert` (Apache-2.0), the overflow indicator's icon. */\nfunction MoreVertIcon() {\n return (\n <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\">\n <path d=\"M480-160q-33 0-56.5-23.5T400-240q0-33 23.5-56.5T480-320q33 0 56.5 23.5T560-240q0 33-23.5 56.5T480-160Zm0-240q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm0-240q-33 0-56.5-23.5T400-720q0-33 23.5-56.5T480-800q33 0 56.5 23.5T560-720q0 33-23.5 56.5T480-640Z\" />\n </svg>\n );\n}\n\nexport type ButtonGroupVariant = NonNullable<VariantProps<typeof buttonGroupStyles>['variant']>;\nexport type ButtonGroupSelectionMode = 'none' | 'single' | 'multiple';\n\n/** Text the group adds for assistive tech and its tooltip; translate it for your locale. */\nexport interface ButtonGroupLabels {\n /** The overflow indicator's name and tooltip. @default \"More options\" */\n more: string;\n}\n\nconst DEFAULT_LABELS: ButtonGroupLabels = { more: 'More options' };\n\nexport interface ButtonGroupClassNames {\n /** The group's outer element; `className` is merged here too. */\n root?: string;\n /** The overflow indicator, the \"More options\" icon button (`overflow`). */\n overflowButton?: string;\n /** The overflow indicator's menu of folded buttons (`overflow`). */\n overflowMenu?: string;\n}\n\nexport interface ButtonGroupProps extends Omit<\n ComponentPropsWithRef<'div'>,\n 'onChange' | 'defaultValue'\n> {\n /**\n * `standard` spaces the buttons apart (18 / 12 / 8px by size); `connected` joins them 2px\n * apart with shared inner corners across the container's width (the M3 Expressive\n * replacement for segmented buttons). A standard group grows a pressed button into its\n * neighbours' space; a connected one only changes its shape unless `expandedRatio` is set.\n * @default \"standard\"\n */\n variant?: ButtonGroupVariant;\n /**\n * Whether a `connected` group spans its container (M3's guidance, and the default). `false`\n * sizes the group to its buttons, for one beside other controls; the buttons stay equally\n * wide. Has no effect on a `standard` group or one with `overflow`.\n * @default true\n */\n fullWidth?: boolean;\n /**\n * Size shared by every button and icon button in the group; it also sets the spacing.\n * Unset, the buttons keep their own (or an enclosing toolbar's) size.\n * @default \"sm\"\n */\n size?: ButtonSize;\n /**\n * Shape shared by the buttons. A `square` connected group gives its outer corners the\n * size of its inner ones (4 / 8 / 8 / 16 / 20px). Unset, the buttons keep their own.\n * @default \"round\"\n */\n shape?: ButtonShape;\n /**\n * Variant shared by every `Button` in the group. `filled`, `tonal` and `outlined` reach\n * the `IconButton`s too, so a group never mixes colour styles; a button's own `variant`\n * wins. Unset, the buttons keep their own.\n * @default \"filled\"\n */\n buttonVariant?: ButtonVariant;\n /** Disables every button in the group. @default false */\n disabled?: boolean;\n /**\n * Lets toggle buttons with a `value` act as one selection. `single` behaves like\n * radio buttons, `multiple` like checkboxes. @default \"none\"\n */\n selectionMode?: ButtonGroupSelectionMode;\n /** Controlled selection: the `value`s of the selected toggle buttons. */\n selectedKeys?: Iterable<string>;\n /** Initial selection when uncontrolled. */\n defaultSelectedKeys?: Iterable<string>;\n /** Called with the selected toggle buttons' `value`s when the selection changes. */\n onSelectionChange?: (keys: Set<string>) => void;\n /**\n * The last selected button can be deselected, leaving none. `false` keeps at least one\n * selected. @default true\n */\n deselectable?: boolean;\n /**\n * How much a pressed button grows, as a fraction of its width; its neighbours shrink by the\n * same amount and the group keeps its width. `0` turns it off. Standard groups default to\n * 0.15. Connected groups default to 0 (the spec: they \"don't affect adjacent buttons\", as\n * in Compose's connected sample); set a ratio to opt in, as Compose can by combining a\n * button group's `animateWidth` with the connected shapes. Under reduced motion there's\n * no bump. @default 0.15, or 0 when `variant` is \"connected\"\n */\n expandedRatio?: number;\n /**\n * Folds the trailing buttons that don't fit into a menu behind a trailing \"More options\"\n * icon button (M3: \"Buttons at the trailing edge of the button group can be customized to\n * collapse into an overflow menu at smaller breakpoints\"). The group then fills its\n * container's width and re-measures as it resizes. Each folded button becomes a menu\n * item with its label (an icon button's `aria-label`) and icon; disabled buttons stay\n * disabled, and toggles show their selection. Choosing an item presses the button.\n * @default false\n */\n overflow?: boolean;\n /**\n * Text for assistive tech, e.g. the overflow indicator's name.\n * @default { more: \"More options\" }\n */\n labels?: Partial<ButtonGroupLabels>;\n /** Classes for each part. `classNames.root` is merged with `className`. */\n classNames?: ButtonGroupClassNames;\n children?: ReactNode;\n}\n\n/** The props the group reads from its children for the overflow menu and warnings. */\ninterface ChildProps {\n variant?: string;\n toggle?: boolean;\n value?: string;\n disabled?: boolean;\n children?: ReactNode;\n leadingIcon?: ReactElement;\n selectedLeadingIcon?: ReactElement;\n icon?: ReactElement;\n selectedIcon?: ReactElement;\n 'aria-label'?: string;\n}\n\nconst ICON_BUTTON_VARIANTS: readonly string[] = ['filled', 'tonal', 'outlined'];\n\n/** The icon button variant a group's `buttonVariant` implies, if it exists for icon buttons. */\nfunction iconVariantOf(variant: ButtonVariant | undefined): IconButtonVariant | undefined {\n return variant && ICON_BUTTON_VARIANTS.includes(variant)\n ? (variant as IconButtonVariant)\n : undefined;\n}\n\ntype Position = NonNullable<ButtonContextValue['connected']>;\n\nfunction positionOf(index: number, count: number): Position | undefined {\n if (count < 2) return undefined;\n if (index === 0) return 'leading';\n if (index === count - 1) return 'trailing';\n return 'middle';\n}\n\n/** The group's ref, also reporting its element to `DomDirectionLocale`. */\nfunction useGroupRef(ref: Ref<HTMLDivElement> | undefined, directionRef: RefCallback<HTMLElement>) {\n // Arrow keys follow the laid-out direction, which React Aria reads from its locale.\n const merged = useMemo(\n () => mergeRefs<HTMLDivElement>(ref, directionRef as RefCallback<HTMLDivElement>),\n [ref, directionRef],\n );\n return useObjectRef<HTMLDivElement>(merged);\n}\n\n/**\n * M3 Expressive button group. Shares size, shape, variant and disabled with its `Button`\n * and `IconButton` children. A standard group grows a pressed button while its neighbours\n * shrink; a `connected` group joins the buttons with shared inner corners and, by default,\n * only changes the pressed button's shape (`expandedRatio` opts it into the bump). Tab reaches the group and the arrow keys move\n * between its buttons, wrapping, with Home and End (a `toolbar`). With `selectionMode`, toggle\n * buttons that have a `value` form a single or multiple selection; a single selection is a\n * radio group, whose arrow keys select as they move.\n *\n * Children must be direct `Button` / `IconButton` elements so each gets its position.\n *\n * @example\n * <ButtonGroup variant=\"connected\" selectionMode=\"single\" defaultSelectedKeys={['week']}\n * aria-label=\"Calendar view\">\n * <Button toggle value=\"day\">Day</Button>\n * <Button toggle value=\"week\">Week</Button>\n * <Button toggle value=\"month\">Month</Button>\n * </ButtonGroup>\n */\nexport function ButtonGroup(props: ButtonGroupProps) {\n return (\n <DomDirectionLocale>\n {(directionRef) => <ButtonGroupRoot {...props} directionRef={directionRef} />}\n </DomDirectionLocale>\n );\n}\n\nfunction ButtonGroupRoot({\n variant = 'standard',\n size,\n shape,\n buttonVariant,\n disabled,\n selectionMode = 'none',\n selectedKeys,\n defaultSelectedKeys,\n onSelectionChange,\n deselectable = true,\n expandedRatio,\n overflow = false,\n fullWidth = true,\n labels,\n className,\n classNames,\n children,\n ref,\n directionRef,\n ...props\n}: ButtonGroupProps & { directionRef: RefCallback<HTMLElement> }) {\n const domRef = useGroupRef(ref, directionRef);\n const parent = useContext(ButtonContext);\n const hasSelection = selectionMode !== 'none';\n\n // React Stately reports a change even when the selection is unchanged (e.g. clicking\n // the only selected button of a group that isn't `deselectable`); only real changes reach callers.\n const currentKeys = useRef<ReadonlySet<Key>>(new Set());\n const handleSelectionChange = (keys: Set<Key>) => {\n const previous = currentKeys.current;\n if (keys.size === previous.size && [...keys].every((key) => previous.has(key))) return;\n onSelectionChange?.(keys as Set<string>);\n };\n\n const state = useToggleGroupState({\n selectionMode: hasSelection ? selectionMode : 'single',\n selectedKeys,\n defaultSelectedKeys,\n onSelectionChange: handleSelectionChange,\n disallowEmptySelection: !deselectable,\n isDisabled: disabled,\n });\n const { groupProps } = useToggleButtonGroup(\n { ...props, isDisabled: disabled, orientation: 'horizontal' },\n state,\n domRef,\n );\n // Without a selection the group is a React Aria toolbar: Tab enters and leaves it as one\n // stop, and the arrow keys move between its buttons.\n const { toolbarProps } = useToolbar({ ...props, orientation: 'horizontal' }, domRef);\n const groupAria = withGroupKeys(\n hasSelection ? groupProps : toolbarProps,\n selectionMode === 'single',\n );\n\n useLayoutEffect(() => {\n currentKeys.current = state.selectedKeys;\n });\n\n usePressExpansion(domRef, expandedRatio ?? (variant === 'standard' ? 0.15 : 0));\n\n const groupSize = size ?? parent.size ?? 'sm';\n const iconButtonVariant = iconVariantOf(buttonVariant);\n const shared = useMemo<ButtonContextValue>(\n () => ({\n variant: buttonVariant ?? parent.variant,\n size: size ?? parent.size,\n shape: shape ?? parent.shape,\n disabled: disabled ?? parent.disabled,\n iconButtonVariant: iconButtonVariant ?? parent.iconButtonVariant,\n density: parent.density,\n }),\n [buttonVariant, iconButtonVariant, size, shape, disabled, parent],\n );\n\n const items = Children.toArray(children).filter(isValidElement) as ReactElement<ChildProps>[];\n const visible = useOverflow(domRef, overflow, items.length);\n const overflowing = overflow && visible < items.length;\n\n useGroupWarnings(variant, items, buttonVariant, iconButtonVariant);\n\n // Connected corners follow the buttons that show, the overflow indicator last.\n const shown = overflowing ? visible + 1 : items.length;\n const contextFor = (index: number): ButtonContextValue =>\n variant === 'connected' ? { ...shared, connected: positionOf(index, shown) } : shared;\n const content = items.map((child, index) => (\n <ButtonContext key={child.key ?? index} value={contextFor(Math.min(index, shown - 1))}>\n {child}\n </ButtonContext>\n ));\n if (overflow) {\n content.push(\n <ButtonContext key=\"overflow\" value={contextFor(shown - 1)}>\n <OverflowMenu\n groupRef={domRef}\n hidden={items.slice(visible)}\n offset={visible}\n label={labels?.more ?? DEFAULT_LABELS.more}\n selectionMode={selectionMode}\n selectedKeys={state.selectedKeys}\n disabled={Boolean(disabled ?? parent.disabled)}\n classNames={classNames}\n />\n </ButtonContext>,\n );\n }\n\n return (\n <div\n {...mergeProps(props, groupAria)}\n ref={domRef}\n // Public styling hooks for apps, as on Button: the group's variant and the size it spaces\n // its buttons for (its own, an enclosing toolbar's, or `sm`).\n data-variant={variant}\n data-size={groupSize}\n className={cn(\n buttonGroupStyles({ variant, size: groupSize, overflow, fullWidth }),\n classNames?.root,\n className,\n )}\n >\n {hasSelection ? (\n <ToggleGroupStateContext value={state}>{content}</ToggleGroupStateContext>\n ) : (\n content\n )}\n </div>\n );\n}\n\n/**\n * The group's arrow keys. React Aria's toolbar keys (kept for Tab, which enters and leaves\n * the group as one stop) are replaced: a toolbar's arrows wrap and Home and End reach its\n * ends (WAI-ARIA APG toolbar), and a single selection is a radio group whose arrows also\n * select (APG radio group, as `RadioGroup` does) and which Tab enters at the selected\n * button. A group nested in a toolbar leaves its keys to the toolbar, as React Aria does.\n */\nfunction withGroupKeys(\n aria: HTMLAttributes<HTMLElement>,\n single: boolean,\n): HTMLAttributes<HTMLElement> {\n const ariaKeyDown = aria.onKeyDownCapture;\n if (!ariaKeyDown) return aria;\n return {\n ...aria,\n onKeyDownCapture: (event) => {\n if (event.key === 'Tab') ariaKeyDown(event);\n else if (single) radioGroupKeyDown(event);\n else toolbarKeyDown(event, 'horizontal');\n },\n onFocus: single ? (event) => void focusCheckedRadio(event) : undefined,\n onPointerDownCapture: single ? notePointerDown : undefined,\n };\n}\n\n/**\n * Development warnings, once per group, for what the M3 button group guidelines advise\n * against: standard icon buttons and text buttons in a group (\"they have no container\n * treatment\"), and a connected group with nothing to select (\"Avoid using a connected\n * group when none of the buttons can be toggled\"). Only the variants the group or the\n * buttons set count: a toolbar's standard default doesn't.\n */\nfunction useGroupWarnings(\n variant: ButtonGroupVariant,\n items: ReactElement<ChildProps>[],\n buttonVariant: ButtonVariant | undefined,\n iconButtonVariant: IconButtonVariant | undefined,\n) {\n const containerless = items.some((child) =>\n child.type === IconButton\n ? (child.props.variant ?? iconButtonVariant) === 'standard'\n : child.type === Button && (child.props.variant ?? buttonVariant) === 'text',\n );\n const nothingToSelect =\n variant === 'connected' && items.length > 0 && !items.some((child) => child.props.toggle);\n useWarnOnce(\n containerless,\n '[@windmorph/react] Avoid standard icon buttons and text buttons in a ButtonGroup (M3): they have no container, so the group has nothing to shape.',\n );\n useWarnOnce(\n nothingToSelect,\n '[@windmorph/react] A connected ButtonGroup is for selection (M3): use toggle buttons in it, or a standard group for actions.',\n );\n}\n\ninterface OverflowMenuProps {\n groupRef: RefObject<HTMLDivElement | null>;\n /** The folded buttons, in order. */\n hidden: ReactElement<ChildProps>[];\n /** The index of the first folded button among the group's children. */\n offset: number;\n label: string;\n selectionMode: ButtonGroupSelectionMode;\n selectedKeys: ReadonlySet<Key>;\n disabled: boolean;\n classNames: ButtonGroupClassNames | undefined;\n}\n\n/**\n * The overflow indicator (Compose `ButtonGroupDefaults.OverflowIndicator`: a filled icon\n * button with `more_vert` and a \"More options\" tooltip, here the group's icon button\n * variant) and its menu. Choosing an item clicks the folded button itself, so its own\n * press handler, link, toggle or group selection runs as if it had been pressed. The menu's\n * code loads once a button folds (see `LazyOverflowMenuList`); the indicator is always\n * rendered, so the group can measure it.\n */\nfunction OverflowMenu({\n groupRef,\n hidden,\n offset,\n label,\n selectionMode,\n selectedKeys,\n disabled,\n classNames,\n}: OverflowMenuProps) {\n // Folded standalone toggles keep their state inside them: read it when the menu opens\n // and after each choice.\n const [pressed, setPressed] = useState<ReadonlySet<string>>(new Set());\n const elementAt = (key: string) =>\n groupRef.current?.children[Number(key)] as HTMLElement | undefined;\n const keyOf = (index: number) => String(offset + index);\n const readPressed = () =>\n setPressed(\n new Set(\n hidden\n .map((_, index) => keyOf(index))\n .filter((key) => elementAt(key)?.getAttribute('aria-pressed') === 'true'),\n ),\n );\n\n // A choice made with the mouse closed the menu with focus still on its item, which then\n // left with the menu and dropped focus on <body>. Once the menu has closed, focus still in\n // it (or on the page) goes back to the indicator, as after a keyboard choice (2026-10-15).\n const indicator = useRef<HTMLElement | null>(null);\n const keepFocus = () =>\n requestAnimationFrame(() => {\n const active = document.activeElement;\n if (!active || active === document.body || active.closest('[role=\"menu\"]')) {\n indicator.current?.focus({ preventScroll: true });\n }\n });\n\n const grouped = selectionMode !== 'none';\n const hasToggles = hidden.some((child) => child.props.toggle);\n const menuSelectionMode = grouped ? selectionMode : hasToggles ? 'multiple' : 'none';\n const selected = hidden\n .map((child, index) => ({ child, key: keyOf(index) }))\n .filter(({ child, key }) =>\n grouped\n ? child.props.value !== undefined && selectedKeys.has(child.props.value)\n : pressed.has(key),\n )\n .map(({ key }) => key);\n const disabledKeys = hidden\n .map((child, index) => ({ child, key: keyOf(index) }))\n .filter(({ child }) => disabled || child.props.disabled)\n .map(({ key }) => key);\n\n return (\n <MenuTrigger onOpenChange={(open) => (open ? readPressed() : keepFocus())}>\n <IconButton\n icon={<MoreVertIcon />}\n aria-label={label}\n className={classNames?.overflowButton}\n onPressStart={(event) => {\n indicator.current = event.target as HTMLElement;\n }}\n />\n {hidden.length > 0 && (\n <LazyOverflowMenuList\n items={hidden.map((child, index) => ({\n key: keyOf(index),\n ...describe(child, selected.includes(keyOf(index))),\n // Only a toggle (or a button in the group's selection) is checkable; an action\n // stays a plain item even when the menu is selectable.\n checkable: grouped ? child.props.value !== undefined : Boolean(child.props.toggle),\n }))}\n selectionMode={menuSelectionMode}\n selectedKeys={selected}\n disabledKeys={disabledKeys}\n classNames={{ panel: classNames?.overflowMenu }}\n onAction={(key) => {\n elementAt(String(key))?.click();\n // The button updates once this event is handled.\n setTimeout(readPressed);\n }}\n />\n )}\n </MenuTrigger>\n );\n}\n\n/** A folded button's menu label and icon. */\nfunction describe(child: ReactElement<ChildProps>, isSelected: boolean) {\n const { props } = child;\n if (child.type === IconButton) {\n return {\n label: props['aria-label'],\n icon: (isSelected && props.selectedIcon) || props.icon,\n };\n }\n return {\n label: props.children,\n icon: (isSelected && props.selectedLeadingIcon) || props.leadingIcon,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA+CA,SAAS,eAAe;CACtB,OACE,oBAAC,OAAD;EAAK,SAAQ;EAAiB,MAAK;EACjC,UAAA,oBAAC,QAAD,EAAM,GAAE,iTAAkT,CAAA;CACvT,CAAA;AAET;AAWA,MAAM,iBAAoC,EAAE,MAAM,eAAe;AA8GjE,MAAM,uBAA0C;CAAC;CAAU;CAAS;AAAU;;AAG9E,SAAS,cAAc,SAAmE;CACxF,OAAO,WAAW,qBAAqB,SAAS,OAAO,IAClD,UACD,KAAA;AACN;AAIA,SAAS,WAAW,OAAe,OAAqC;CACtE,IAAI,QAAQ,GAAG,OAAO,KAAA;CACtB,IAAI,UAAU,GAAG,OAAO;CACxB,IAAI,UAAU,QAAQ,GAAG,OAAO;CAChC,OAAO;AACT;;AAGA,SAAS,YAAY,KAAsC,cAAwC;CAEjG,MAAM,SAAS,cACP,UAA0B,KAAK,YAA2C,GAChF,CAAC,KAAK,YAAY,CACpB;CACA,OAAO,aAA6B,MAAM;AAC5C;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,YAAY,OAAyB;CACnD,OACE,oBAAC,oBAAD,EAAA,WACI,iBAAiB,oBAAC,iBAAD;EAAiB,GAAI;EAAqB;CAAe,CAAA,EAC1D,CAAA;AAExB;AAEA,SAAS,gBAAgB,EACvB,UAAU,YACV,MACA,OACA,eACA,UACA,gBAAgB,QAChB,cACA,qBACA,mBACA,eAAe,MACf,eACA,WAAW,OACX,YAAY,MACZ,QACA,WACA,YACA,UACA,KACA,cACA,GAAG,SAC6D;CAChE,MAAM,SAAS,YAAY,KAAK,YAAY;CAC5C,MAAM,SAAS,WAAW,aAAa;CACvC,MAAM,eAAe,kBAAkB;CAIvC,MAAM,cAAc,uBAAyB,IAAI,IAAI,CAAC;CACtD,MAAM,yBAAyB,SAAmB;EAChD,MAAM,WAAW,YAAY;EAC7B,IAAI,KAAK,SAAS,SAAS,QAAQ,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,QAAQ,SAAS,IAAI,GAAG,CAAC,GAAG;EAChF,oBAAoB,IAAmB;CACzC;CAEA,MAAM,QAAQ,oBAAoB;EAChC,eAAe,eAAe,gBAAgB;EAC9C;EACA;EACA,mBAAmB;EACnB,wBAAwB,CAAC;EACzB,YAAY;CACd,CAAC;CACD,MAAM,EAAE,eAAe,qBACrB;EAAE,GAAG;EAAO,YAAY;EAAU,aAAa;CAAa,GAC5D,OACA,MACF;CAGA,MAAM,EAAE,iBAAiB,WAAW;EAAE,GAAG;EAAO,aAAa;CAAa,GAAG,MAAM;CACnF,MAAM,YAAY,cAChB,eAAe,aAAa,cAC5B,kBAAkB,QACpB;CAEA,sBAAsB;EACpB,YAAY,UAAU,MAAM;CAC9B,CAAC;CAED,kBAAkB,QAAQ,kBAAkB,YAAY,aAAa,MAAO,EAAE;CAE9E,MAAM,YAAY,QAAQ,OAAO,QAAQ;CACzC,MAAM,oBAAoB,cAAc,aAAa;CACrD,MAAM,SAAS,eACN;EACL,SAAS,iBAAiB,OAAO;EACjC,MAAM,QAAQ,OAAO;EACrB,OAAO,SAAS,OAAO;EACvB,UAAU,YAAY,OAAO;EAC7B,mBAAmB,qBAAqB,OAAO;EAC/C,SAAS,OAAO;CAClB,IACA;EAAC;EAAe;EAAmB;EAAM;EAAO;EAAU;CAAM,CAClE;CAEA,MAAM,QAAQ,SAAS,QAAQ,QAAQ,CAAC,CAAC,OAAO,cAAc;CAC9D,MAAM,UAAU,YAAY,QAAQ,UAAU,MAAM,MAAM;CAC1D,MAAM,cAAc,YAAY,UAAU,MAAM;CAEhD,iBAAiB,SAAS,OAAO,eAAe,iBAAiB;CAGjE,MAAM,QAAQ,cAAc,UAAU,IAAI,MAAM;CAChD,MAAM,cAAc,UAClB,YAAY,cAAc;EAAE,GAAG;EAAQ,WAAW,WAAW,OAAO,KAAK;CAAE,IAAI;CACjF,MAAM,UAAU,MAAM,KAAK,OAAO,UAChC,oBAAC,eAAD;EAAwC,OAAO,WAAW,KAAK,IAAI,OAAO,QAAQ,CAAC,CAAC;EACjF,UAAA;CACY,GAFK,MAAM,OAAO,KAElB,CAChB;CACD,IAAI,UACF,QAAQ,KACN,oBAAC,eAAD;EAA8B,OAAO,WAAW,QAAQ,CAAC;EACvD,UAAA,oBAAC,cAAD;GACE,UAAU;GACV,QAAQ,MAAM,MAAM,OAAO;GAC3B,QAAQ;GACR,OAAO,QAAQ,QAAQ,eAAe;GACvB;GACf,cAAc,MAAM;GACpB,UAAU,QAAQ,YAAY,OAAO,QAAQ;GACjC;EACb,CAAA;CACY,GAXI,UAWJ,CACjB;CAGF,OACE,oBAAC,OAAD;EACE,GAAI,WAAW,OAAO,SAAS;EAC/B,KAAK;EAGL,gBAAc;EACd,aAAW;EACX,WAAW,GACT,kBAAkB;GAAE;GAAS,MAAM;GAAW;GAAU;EAAU,CAAC,GACnE,YAAY,MACZ,SACF;EAEC,UAAA,eACC,oBAAC,yBAAD;GAAyB,OAAO;GAAQ,UAAA;EAAiC,CAAA,IAEzE;CAEC,CAAA;AAET;;;;;;;;AASA,SAAS,cACP,MACA,QAC6B;CAC7B,MAAM,cAAc,KAAK;CACzB,IAAI,CAAC,aAAa,OAAO;CACzB,OAAO;EACL,GAAG;EACH,mBAAmB,UAAU;GAC3B,IAAI,MAAM,QAAQ,OAAO,YAAY,KAAK;QACrC,IAAI,QAAQ,kBAAkB,KAAK;QACnC,eAAe,OAAO,YAAY;EACzC;EACA,SAAS,UAAU,UAAU,KAAK,kBAAkB,KAAK,IAAI,KAAA;EAC7D,sBAAsB,SAAS,kBAAkB,KAAA;CACnD;AACF;;;;;;;;AASA,SAAS,iBACP,SACA,OACA,eACA,mBACA;CACA,MAAM,gBAAgB,MAAM,MAAM,UAChC,MAAM,SAAS,cACV,MAAM,MAAM,WAAW,uBAAuB,aAC/C,MAAM,SAAS,WAAW,MAAM,MAAM,WAAW,mBAAmB,MAC1E;CACA,MAAM,kBACJ,YAAY,eAAe,MAAM,SAAS,KAAK,CAAC,MAAM,MAAM,UAAU,MAAM,MAAM,MAAM;CAC1F,YACE,eACA,mJACF;CACA,YACE,iBACA,8HACF;AACF;;;;;;;;;AAuBA,SAAS,aAAa,EACpB,UACA,QACA,QACA,OACA,eACA,cACA,UACA,cACoB;CAGpB,MAAM,CAAC,SAAS,cAAc,yBAA8B,IAAI,IAAI,CAAC;CACrE,MAAM,aAAa,QACjB,SAAS,SAAS,SAAS,OAAO,GAAG;CACvC,MAAM,SAAS,UAAkB,OAAO,SAAS,KAAK;CACtD,MAAM,oBACJ,WACE,IAAI,IACF,OACG,KAAK,GAAG,UAAU,MAAM,KAAK,CAAC,CAAC,CAC/B,QAAQ,QAAQ,UAAU,GAAG,CAAC,EAAE,aAAa,cAAc,MAAM,MAAM,CAC5E,CACF;CAKF,MAAM,YAAY,OAA2B,IAAI;CACjD,MAAM,kBACJ,4BAA4B;EAC1B,MAAM,SAAS,SAAS;EACxB,IAAI,CAAC,UAAU,WAAW,SAAS,QAAQ,OAAO,QAAQ,iBAAe,GACvE,UAAU,SAAS,MAAM,EAAE,eAAe,KAAK,CAAC;CAEpD,CAAC;CAEH,MAAM,UAAU,kBAAkB;CAClC,MAAM,aAAa,OAAO,MAAM,UAAU,MAAM,MAAM,MAAM;CAC5D,MAAM,oBAAoB,UAAU,gBAAgB,aAAa,aAAa;CAC9E,MAAM,WAAW,OACd,KAAK,OAAO,WAAW;EAAE;EAAO,KAAK,MAAM,KAAK;CAAE,EAAE,CAAC,CACrD,QAAQ,EAAE,OAAO,UAChB,UACI,MAAM,MAAM,UAAU,KAAA,KAAa,aAAa,IAAI,MAAM,MAAM,KAAK,IACrE,QAAQ,IAAI,GAAG,CACrB,CAAC,CACA,KAAK,EAAE,UAAU,GAAG;CACvB,MAAM,eAAe,OAClB,KAAK,OAAO,WAAW;EAAE;EAAO,KAAK,MAAM,KAAK;CAAE,EAAE,CAAC,CACrD,QAAQ,EAAE,YAAY,YAAY,MAAM,MAAM,QAAQ,CAAC,CACvD,KAAK,EAAE,UAAU,GAAG;CAEvB,OACE,qBAAC,aAAD;EAAa,eAAe,SAAU,OAAO,YAAY,IAAI,UAAU;EAAvE,UAAA,CACE,oBAAC,YAAD;GACE,MAAM,oBAAC,cAAD,CAAe,CAAA;GACrB,cAAY;GACZ,WAAW,YAAY;GACvB,eAAe,UAAU;IACvB,UAAU,UAAU,MAAM;GAC5B;EACD,CAAA,GACA,OAAO,SAAS,KACf,oBAAC,sBAAD;GACE,OAAO,OAAO,KAAK,OAAO,WAAW;IACnC,KAAK,MAAM,KAAK;IAChB,GAAG,SAAS,OAAO,SAAS,SAAS,MAAM,KAAK,CAAC,CAAC;IAGlD,WAAW,UAAU,MAAM,MAAM,UAAU,KAAA,IAAY,QAAQ,MAAM,MAAM,MAAM;GACnF,EAAE;GACF,eAAe;GACf,cAAc;GACA;GACd,YAAY,EAAE,OAAO,YAAY,aAAa;GAC9C,WAAW,QAAQ;IACjB,UAAU,OAAO,GAAG,CAAC,CAAC,EAAE,MAAM;IAE9B,WAAW,WAAW;GACxB;EACD,CAAA,CAEQ;;AAEjB;;AAGA,SAAS,SAAS,OAAiC,YAAqB;CACtE,MAAM,EAAE,UAAU;CAClB,IAAI,MAAM,SAAS,YACjB,OAAO;EACL,OAAO,MAAM;EACb,MAAO,cAAc,MAAM,gBAAiB,MAAM;CACpD;CAEF,OAAO;EACL,OAAO,MAAM;EACb,MAAO,cAAc,MAAM,uBAAwB,MAAM;CAC3D;AACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lazy-overflow-menu.js","names":[],"sources":["../../../src/composites/button-group/lazy-overflow-menu.tsx"],"sourcesContent":["'use client';\n\nimport { useContext, useEffect, useState } from 'react';\nimport { MenuTriggerContext } from '../../components/menu/menu-trigger';\nimport { onDemand } from '../../utils/load-on-demand';\nimport type { OverflowMenuListProps } from './overflow-menu-list';\n\nconst menu = onDemand<OverflowMenuListProps>(() =>\n import('./overflow-menu-list').then((module) => module.OverflowMenuList),\n);\n\n/**\n * Starts loading the overflow menu's module (and `Menu` with it), once; later calls return\n * the same promise.\n */\nexport const preloadOverflowMenu = menu.preload;\n\n/**\n * `ButtonGroup`'s overflow menu, whose code loads only once a button folds: a group without\n * `overflow`, or whose buttons all fit, never carries `Menu`. It sits inside the indicator's\n * `MenuTrigger` and is rendered only while something is folded. Rendering it starts the\n * load, so the menu is usually ready long before the indicator is pressed, and then opens in\n * the same pass as the press (it never suspends, see `onDemand`). It mounts once loaded, or\n * when the menu opens if that comes first, and shows nothing until then. A failed load is\n * tried again when the pointer moves over the indicator, focus enters it, or the menu opens.\n */\nexport function LazyOverflowMenuList(props: OverflowMenuListProps) {\n const trigger = useContext(MenuTriggerContext);\n const isOpen = trigger?.state.isOpen === true;\n const [ready, setReady] = useState(isOpen);\n // Set during render, so an open before the load ends renders the menu in the same pass.\n if (isOpen && !ready) setReady(true);\n\n const triggerRef = trigger?.triggerRef;\n useEffect(() => {\n let mounted = true;\n // A failed preload is left to the next hover, focus or open, which loads again.\n const preload = () =>\n void menu.preload().then(\n () => mounted && setReady(true),\n () => undefined,\n );\n preload();\n const element = triggerRef?.current;\n element?.addEventListener('pointerover', preload);\n element?.addEventListener('focusin', preload);\n return () => {\n mounted = false;\n element?.removeEventListener('pointerover', preload);\n element?.removeEventListener('focusin', preload);\n };\n }, [triggerRef]);\n\n if (!ready) return null;\n return <menu.Component {...props} />;\n}\n"],"mappings":";;;;;;AAOA,MAAM,OAAO,eACX,OAAO,0BAAuB,CAAC,MAAM,WAAW,OAAO,gBAAgB,CACzE;AAMmC,KAAK;;;;;;;;;;AAWxC,SAAgB,qBAAqB,OAA8B;CACjE,MAAM,UAAU,WAAW,kBAAkB;CAC7C,MAAM,SAAS,SAAS,MAAM,WAAW;CACzC,MAAM,CAAC,OAAO,YAAY,SAAS,MAAM;CAEzC,IAAI,UAAU,CAAC,OAAO,SAAS,IAAI;CAEnC,MAAM,aAAa,SAAS;CAC5B,gBAAgB;EACd,IAAI,UAAU;EAEd,MAAM,gBACJ,KAAK,KAAK,QAAQ,CAAC,CAAC,WACZ,WAAW,SAAS,IAAI,SACxB,KAAA,CACR;EACF,QAAQ;EACR,MAAM,UAAU,YAAY;EAC5B,SAAS,iBAAiB,eAAe,OAAO;EAChD,SAAS,iBAAiB,WAAW,OAAO;EAC5C,aAAa;GACX,UAAU;GACV,SAAS,oBAAoB,eAAe,OAAO;GACnD,SAAS,oBAAoB,WAAW,OAAO;EACjD;CACF,GAAG,CAAC,UAAU,CAAC;CAEf,IAAI,CAAC,OAAO,OAAO;CACnB,OAAO,oBAAC,KAAK,WAAN,EAAgB,GAAI,MAAQ,CAAA;AACrC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"overflow-menu-list.js","names":[],"sources":["../../../src/composites/button-group/overflow-menu-list.tsx"],"sourcesContent":["'use client';\n\nimport type { ReactNode } from 'react';\nimport { CheckableMenuItem, Menu, type MenuProps } from '../../components/menu/Menu';\n\n/** A folded button as a menu item. */\nexport interface OverflowMenuItem {\n /** The button's index among the group's children, as a string. */\n key: string;\n label: ReactNode;\n icon: ReactNode;\n /**\n * Whether the item shows and announces a checked state: a toggle button, or a button in\n * the group's selection. Actions stay plain menu items in a selectable menu.\n */\n checkable: boolean;\n}\n\nexport interface OverflowMenuListProps extends Pick<\n MenuProps,\n 'selectionMode' | 'selectedKeys' | 'disabledKeys' | 'onAction' | 'classNames'\n> {\n /** The folded buttons, in order. */\n items: OverflowMenuItem[];\n}\n\n/**\n * The overflow indicator's menu: one item per folded button, opening at the indicator's\n * end. In a selectable menu only toggles (and buttons in the group's selection) are\n * checkable; actions stay plain items. `ButtonGroup` loads this module (and `Menu` with it) only once a button folds,\n * through `LazyOverflowMenuList`.\n */\nexport function OverflowMenuList({ items, ...props }: OverflowMenuListProps) {\n return (\n <Menu {...props} placement=\"bottom-end\">\n {items.map(({ key, label, icon, checkable }) => (\n <CheckableMenuItem\n key={key}\n leadingIcon={icon}\n checkable={checkable}\n textValue={typeof label === 'string' ? label : undefined}\n >\n {label}\n </CheckableMenuItem>\n ))}\n </Menu>\n );\n}\n"],"mappings":";;;;;;;;;;AAgCA,SAAgB,iBAAiB,EAAE,OAAO,GAAG,SAAgC;CAC3E,OACE,oBAAC,MAAD;EAAM,GAAI;EAAO,WAAU;EACxB,UAAA,MAAM,KAAK,EAAE,KAAK,OAAO,MAAM,gBAC9B,oBAAC,mBAAD;GAEE,aAAa;GACF;GACX,WAAW,OAAO,UAAU,WAAW,QAAQ,KAAA;GAE9C,UAAA;EACgB,GANZ,GAMY,CACpB;CACG,CAAA;AAEV"}
|