@stevederico/skateboard-ui 4.1.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/components/core/Context.js +6 -6
- package/dist/components/core/Utilities.js +18 -14
- package/dist/ui/alert-dialog.js +24 -16
- package/dist/ui/checkbox.d.ts +9 -6
- package/dist/ui/checkbox.js +21 -9
- package/dist/ui/context-menu.js +28 -8
- package/dist/ui/dialog.js +18 -17
- package/dist/ui/drawer.js +28 -16
- package/dist/ui/dropdown-menu.js +44 -14
- package/dist/ui/menubar.js +35 -14
- package/dist/ui/popover.js +24 -5
- package/dist/ui/select.d.ts +1 -1
- package/dist/ui/select.js +27 -6
- package/dist/ui/sheet.js +27 -16
- package/dist/ui/slider.d.ts +2 -1
- package/dist/ui/slider.js +11 -6
- package/dist/ui/tooltip.js +27 -1
- package/dist/ui/use-labelling.d.ts +21 -0
- package/dist/ui/use-labelling.js +43 -0
- package/dist/ui/use-pointer-moved.d.ts +12 -0
- package/dist/ui/use-pointer-moved.js +21 -0
- package/dist/ui/use-scroll-lock.d.ts +6 -0
- package/dist/ui/use-scroll-lock.js +29 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
4.3.0
|
|
4
|
+
|
|
5
|
+
Submenu aria-controls wiring
|
|
6
|
+
Menu labelled by trigger
|
|
7
|
+
Tooltip dismiss on outside tap
|
|
8
|
+
Checkbox indeterminate mixed state
|
|
9
|
+
Replace catch any narrowing
|
|
10
|
+
Match DOMException error names
|
|
11
|
+
Fix relative-time date casts
|
|
12
|
+
Add a11y regression tests
|
|
13
|
+
|
|
14
|
+
4.2.0
|
|
15
|
+
|
|
16
|
+
Restore menu focus on select
|
|
17
|
+
Guard menu pointer focus-steal
|
|
18
|
+
Add menu keyboard focus ring
|
|
19
|
+
Conditional dialog aria labelling
|
|
20
|
+
Reentrant overlay scroll lock
|
|
21
|
+
Name popover dialog
|
|
22
|
+
Slider name and form
|
|
23
|
+
Fix dead slider classes
|
|
24
|
+
Add a11y regression tests
|
|
25
|
+
|
|
3
26
|
4.1.0
|
|
4
27
|
|
|
5
28
|
Add menu/select typeahead
|
|
@@ -43,7 +43,7 @@ function isLocalStorageAvailable() {
|
|
|
43
43
|
return true;
|
|
44
44
|
}
|
|
45
45
|
catch (e) {
|
|
46
|
-
console.warn('localStorage not available:', e.message);
|
|
46
|
+
console.warn('localStorage not available:', e instanceof Error ? e.message : String(e));
|
|
47
47
|
return false;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -58,7 +58,7 @@ function safeLSSetItem(key, value) {
|
|
|
58
58
|
return true;
|
|
59
59
|
}
|
|
60
60
|
catch (error) {
|
|
61
|
-
console.error('localStorage setItem error:', error.message);
|
|
61
|
+
console.error('localStorage setItem error:', error instanceof Error ? error.message : String(error));
|
|
62
62
|
return false;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -69,7 +69,7 @@ function safeLSGetItem(key) {
|
|
|
69
69
|
return localStorage.getItem(key);
|
|
70
70
|
}
|
|
71
71
|
catch (error) {
|
|
72
|
-
console.error('localStorage getItem error:', error.message);
|
|
72
|
+
console.error('localStorage getItem error:', error instanceof Error ? error.message : String(error));
|
|
73
73
|
return null;
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -81,7 +81,7 @@ function safeLSRemoveItem(key) {
|
|
|
81
81
|
return true;
|
|
82
82
|
}
|
|
83
83
|
catch (error) {
|
|
84
|
-
console.error('localStorage removeItem error:', error.message);
|
|
84
|
+
console.error('localStorage removeItem error:', error instanceof Error ? error.message : String(error));
|
|
85
85
|
return false;
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -99,7 +99,7 @@ export function ContextProvider({ children, constants }) {
|
|
|
99
99
|
return JSON.parse(storedUser);
|
|
100
100
|
}
|
|
101
101
|
catch (e) {
|
|
102
|
-
console.error('Error parsing user data:', e.message);
|
|
102
|
+
console.error('Error parsing user data:', e instanceof Error ? e.message : String(e));
|
|
103
103
|
return null;
|
|
104
104
|
}
|
|
105
105
|
};
|
|
@@ -126,7 +126,7 @@ export function ContextProvider({ children, constants }) {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
catch (error) {
|
|
129
|
-
console.error('Error setting user:', error.message);
|
|
129
|
+
console.error('Error setting user:', error instanceof Error ? error.message : String(error));
|
|
130
130
|
}
|
|
131
131
|
return { ...state, user: action.payload };
|
|
132
132
|
}
|
|
@@ -21,7 +21,7 @@ function isLocalStorageAvailable() {
|
|
|
21
21
|
return true;
|
|
22
22
|
}
|
|
23
23
|
catch (e) {
|
|
24
|
-
console.warn('localStorage not available (private mode or quota exceeded):', e.message);
|
|
24
|
+
console.warn('localStorage not available (private mode or quota exceeded):', e instanceof Error ? e.message : String(e));
|
|
25
25
|
_localStorageAvailable = false;
|
|
26
26
|
return false;
|
|
27
27
|
}
|
|
@@ -37,11 +37,13 @@ function safeSetItem(key, value) {
|
|
|
37
37
|
return true;
|
|
38
38
|
}
|
|
39
39
|
catch (error) {
|
|
40
|
-
|
|
40
|
+
// DOMException (browser storage/abort errors) does not extend Error in
|
|
41
|
+
// the browser, so match either to read `.name` safely without a cast.
|
|
42
|
+
if ((error instanceof DOMException || error instanceof Error) && error.name === 'QuotaExceededError') {
|
|
41
43
|
console.error('localStorage quota exceeded - data not persisted');
|
|
42
44
|
}
|
|
43
45
|
else {
|
|
44
|
-
console.error('localStorage setItem error:', error.message);
|
|
46
|
+
console.error('localStorage setItem error:', error instanceof Error ? error.message : String(error));
|
|
45
47
|
}
|
|
46
48
|
return false;
|
|
47
49
|
}
|
|
@@ -53,7 +55,7 @@ function safeGetItem(key) {
|
|
|
53
55
|
return localStorage.getItem(key);
|
|
54
56
|
}
|
|
55
57
|
catch (error) {
|
|
56
|
-
console.error('localStorage getItem error:', error.message);
|
|
58
|
+
console.error('localStorage getItem error:', error instanceof Error ? error.message : String(error));
|
|
57
59
|
return null;
|
|
58
60
|
}
|
|
59
61
|
}
|
|
@@ -65,7 +67,7 @@ function safeRemoveItem(key) {
|
|
|
65
67
|
return true;
|
|
66
68
|
}
|
|
67
69
|
catch (error) {
|
|
68
|
-
console.error('localStorage removeItem error:', error.message);
|
|
70
|
+
console.error('localStorage removeItem error:', error instanceof Error ? error.message : String(error));
|
|
69
71
|
return false;
|
|
70
72
|
}
|
|
71
73
|
}
|
|
@@ -523,7 +525,7 @@ export async function showUpgradeSheet(upgradeSheetRef) {
|
|
|
523
525
|
(!user?.subscription?.expires || user?.subscription?.expires > Math.floor(Date.now() / 1000));
|
|
524
526
|
}
|
|
525
527
|
catch (error) {
|
|
526
|
-
console.error('Error parsing user data from storage:', error.message);
|
|
528
|
+
console.error('Error parsing user data from storage:', error instanceof Error ? error.message : String(error));
|
|
527
529
|
subscriber = false;
|
|
528
530
|
}
|
|
529
531
|
}
|
|
@@ -580,7 +582,7 @@ export function timestampToString(input, format = "DOB") {
|
|
|
580
582
|
return date.toLocaleString();
|
|
581
583
|
case "ago": {
|
|
582
584
|
const now = new Date();
|
|
583
|
-
const diffInSeconds = Math.floor((now - date) / 1000);
|
|
585
|
+
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
584
586
|
const minute = 60, hour = minute * 60, day = hour * 24;
|
|
585
587
|
const week = day * 7, month = day * 30, year = day * 365;
|
|
586
588
|
if (diffInSeconds < minute)
|
|
@@ -703,10 +705,12 @@ export async function apiRequest(endpoint, options = {}) {
|
|
|
703
705
|
});
|
|
704
706
|
}
|
|
705
707
|
catch (error) {
|
|
706
|
-
|
|
708
|
+
// fetch() aborts throw a DOMException, which does not extend Error in
|
|
709
|
+
// the browser — match either so abort detection still works.
|
|
710
|
+
if ((error instanceof DOMException || error instanceof Error) && error.name === 'AbortError') {
|
|
707
711
|
throw error; // Re-throw abort errors for useListData to handle
|
|
708
712
|
}
|
|
709
|
-
throw new Error(`Network error: ${error.message}`);
|
|
713
|
+
throw new Error(`Network error: ${error instanceof Error ? error.message : String(error)}`);
|
|
710
714
|
}
|
|
711
715
|
finally {
|
|
712
716
|
clearTimeout(timeoutId);
|
|
@@ -787,7 +791,7 @@ export async function apiRequest(endpoint, options = {}) {
|
|
|
787
791
|
return await response.json();
|
|
788
792
|
}
|
|
789
793
|
catch (error) {
|
|
790
|
-
throw new Error(`Invalid JSON response from server: ${error.message}`);
|
|
794
|
+
throw new Error(`Invalid JSON response from server: ${error instanceof Error ? error.message : String(error)}`);
|
|
791
795
|
}
|
|
792
796
|
}
|
|
793
797
|
/**
|
|
@@ -861,10 +865,10 @@ export function useListData(endpoint, sortFn = null) {
|
|
|
861
865
|
setError(null);
|
|
862
866
|
}
|
|
863
867
|
catch (err) {
|
|
864
|
-
// Ignore abort errors
|
|
865
|
-
if (err.name === 'AbortError')
|
|
868
|
+
// Ignore abort errors (DOMException in the browser, not an Error)
|
|
869
|
+
if ((err instanceof DOMException || err instanceof Error) && err.name === 'AbortError')
|
|
866
870
|
return;
|
|
867
|
-
setError(err.message);
|
|
871
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
868
872
|
}
|
|
869
873
|
finally {
|
|
870
874
|
setLoading(false);
|
|
@@ -899,7 +903,7 @@ export function useForm(initialValues, onSubmit) {
|
|
|
899
903
|
setValues(initialValues);
|
|
900
904
|
}
|
|
901
905
|
catch (err) {
|
|
902
|
-
setError(err.message);
|
|
906
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
903
907
|
}
|
|
904
908
|
finally {
|
|
905
909
|
setSubmitting(false);
|
package/dist/ui/alert-dialog.js
CHANGED
|
@@ -5,6 +5,8 @@ import { cn } from "../shadcn/lib/utils.js";
|
|
|
5
5
|
import { Button } from "./button.js";
|
|
6
6
|
import { Slot, resolveRender } from "./slot.js";
|
|
7
7
|
import { useControllableState } from "./use-controllable-state.js";
|
|
8
|
+
import { useDialogLabelling } from "./use-labelling.js";
|
|
9
|
+
import { useScrollLock } from "./use-scroll-lock.js";
|
|
8
10
|
const AlertDialogContext = React.createContext(null);
|
|
9
11
|
function useAlertDialog() {
|
|
10
12
|
const ctx = React.useContext(AlertDialogContext);
|
|
@@ -20,9 +22,17 @@ function AlertDialog({ open, defaultOpen = false, onOpenChange, children, }) {
|
|
|
20
22
|
defaultValue: defaultOpen,
|
|
21
23
|
onChange: onOpenChange,
|
|
22
24
|
});
|
|
23
|
-
const titleId =
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const { titleId, descriptionId, registerTitle, registerDescription, labelledBy, describedBy, } = useDialogLabelling();
|
|
26
|
+
return (_jsx(AlertDialogContext.Provider, { value: {
|
|
27
|
+
open: isOpen,
|
|
28
|
+
setOpen,
|
|
29
|
+
titleId,
|
|
30
|
+
descriptionId,
|
|
31
|
+
registerTitle,
|
|
32
|
+
registerDescription,
|
|
33
|
+
labelledBy,
|
|
34
|
+
describedBy,
|
|
35
|
+
}, children: children }));
|
|
26
36
|
}
|
|
27
37
|
function AlertDialogTrigger({ className, asChild = false, render, nativeButton: _nativeButton, children, onClick, ...props }) {
|
|
28
38
|
const { open, setOpen } = useAlertDialog();
|
|
@@ -41,7 +51,7 @@ function AlertDialogOverlay() {
|
|
|
41
51
|
return null;
|
|
42
52
|
}
|
|
43
53
|
function AlertDialogContent({ className, size = "default", children, ...props }) {
|
|
44
|
-
const { open, setOpen,
|
|
54
|
+
const { open, setOpen, labelledBy, describedBy } = useAlertDialog();
|
|
45
55
|
const ref = React.useRef(null);
|
|
46
56
|
React.useEffect(() => {
|
|
47
57
|
const node = ref.current;
|
|
@@ -52,16 +62,10 @@ function AlertDialogContent({ className, size = "default", children, ...props })
|
|
|
52
62
|
else if (!open && node.open)
|
|
53
63
|
node.close();
|
|
54
64
|
}, [open]);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
document.body.style.overflow = "hidden";
|
|
60
|
-
return () => {
|
|
61
|
-
document.body.style.overflow = prev;
|
|
62
|
-
};
|
|
63
|
-
}, [open]);
|
|
64
|
-
return (_jsx("dialog", { ref: ref, role: "alertdialog", "aria-labelledby": titleId, "aria-describedby": descriptionId, "data-slot": "alert-dialog-overlay", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", onCancel: (e) => {
|
|
65
|
+
// Lock background scroll while open via the shared reentrant helper so stacked
|
|
66
|
+
// overlays don't unlock each other when one closes.
|
|
67
|
+
useScrollLock(open);
|
|
68
|
+
return (_jsx("dialog", { ref: ref, role: "alertdialog", "aria-labelledby": labelledBy, "aria-describedby": describedBy, "data-slot": "alert-dialog-overlay", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", onCancel: (e) => {
|
|
65
69
|
e.preventDefault();
|
|
66
70
|
setOpen(false);
|
|
67
71
|
}, onClose: () => setOpen(false), className: "m-0 grid max-h-none max-w-none place-items-center bg-transparent p-4 backdrop:bg-black/10 backdrop:duration-100 supports-backdrop-filter:backdrop:backdrop-blur-xs open:fixed open:inset-0 open:size-full", children: open ? (_jsx("div", { "data-slot": "alert-dialog-content", "data-size": size, "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, className: cn("group/alert-dialog-content relative grid w-full gap-6 rounded-xl bg-background p-6 text-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95", className), ...props, children: children })) : null }));
|
|
@@ -76,11 +80,15 @@ function AlertDialogMedia({ className, ...props }) {
|
|
|
76
80
|
return (_jsx("div", { "data-slot": "alert-dialog-media", className: cn("mb-2 inline-flex size-16 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8", className), ...props }));
|
|
77
81
|
}
|
|
78
82
|
function AlertDialogTitle({ className, ...props }) {
|
|
79
|
-
const { titleId } = useAlertDialog();
|
|
83
|
+
const { titleId, registerTitle } = useAlertDialog();
|
|
84
|
+
// Signal that a title exists so the content wires `aria-labelledby` to it.
|
|
85
|
+
React.useEffect(registerTitle, [registerTitle]);
|
|
80
86
|
return (_jsx("h2", { id: titleId, "data-slot": "alert-dialog-title", className: cn("text-lg font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2", className), ...props }));
|
|
81
87
|
}
|
|
82
88
|
function AlertDialogDescription({ className, ...props }) {
|
|
83
|
-
const { descriptionId } = useAlertDialog();
|
|
89
|
+
const { descriptionId, registerDescription } = useAlertDialog();
|
|
90
|
+
// Signal that a description exists so the content wires `aria-describedby`.
|
|
91
|
+
React.useEffect(registerDescription, [registerDescription]);
|
|
84
92
|
return (_jsx("p", { id: descriptionId, "data-slot": "alert-dialog-description", className: cn("text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground", className), ...props }));
|
|
85
93
|
}
|
|
86
94
|
function AlertDialogAction({ className, onClick, ...props }) {
|
package/dist/ui/checkbox.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/** Checked state of a checkbox: a real boolean, or the tri-state "mixed" value. */
|
|
3
|
+
type CheckedState = boolean | "indeterminate";
|
|
4
|
+
export interface CheckboxProps extends Omit<React.ComponentProps<"button">, "onChange" | "value" | "checked" | "defaultChecked"> {
|
|
5
|
+
checked?: CheckedState;
|
|
6
|
+
defaultChecked?: CheckedState;
|
|
5
7
|
onCheckedChange?: (checked: boolean) => void;
|
|
6
8
|
/** When set, a hidden checkbox of this name mirrors the state for form posts. */
|
|
7
9
|
name?: string;
|
|
@@ -9,9 +11,10 @@ export interface CheckboxProps extends Omit<React.ComponentProps<"button">, "onC
|
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* Self-contained checkbox: a `role="checkbox"` button with controlled/
|
|
12
|
-
* uncontrolled checked state.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* uncontrolled checked state. Supports a tri-state `"indeterminate"` (mixed)
|
|
15
|
+
* value for incoming controlled/default state. Emits `data-checked`/
|
|
16
|
+
* `data-unchecked`/`data-indeterminate`/`data-disabled` so the shared classes
|
|
17
|
+
* apply. Pass `name` to include a hidden checkbox for forms.
|
|
15
18
|
*/
|
|
16
19
|
declare function Checkbox({ className, checked, defaultChecked, onCheckedChange, disabled, name, value, onClick, ...props }: CheckboxProps): React.JSX.Element;
|
|
17
20
|
export { Checkbox };
|
package/dist/ui/checkbox.js
CHANGED
|
@@ -2,24 +2,36 @@
|
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "../shadcn/lib/utils.js";
|
|
5
|
-
import { CheckIcon } from "../icons/index.js";
|
|
5
|
+
import { CheckIcon, MinusIcon } from "../icons/index.js";
|
|
6
6
|
import { useControllableState } from "./use-controllable-state.js";
|
|
7
7
|
/**
|
|
8
8
|
* Self-contained checkbox: a `role="checkbox"` button with controlled/
|
|
9
|
-
* uncontrolled checked state.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* uncontrolled checked state. Supports a tri-state `"indeterminate"` (mixed)
|
|
10
|
+
* value for incoming controlled/default state. Emits `data-checked`/
|
|
11
|
+
* `data-unchecked`/`data-indeterminate`/`data-disabled` so the shared classes
|
|
12
|
+
* apply. Pass `name` to include a hidden checkbox for forms.
|
|
12
13
|
*/
|
|
13
14
|
function Checkbox({ className, checked, defaultChecked = false, onCheckedChange, disabled, name, value = "on", onClick, ...props }) {
|
|
15
|
+
// The hook tracks the full tri-state so an incoming "indeterminate" survives;
|
|
16
|
+
// onCheckedChange is fired separately in onClick where the value is always a
|
|
17
|
+
// boolean, keeping the public callback signature unchanged.
|
|
14
18
|
const [on, setOn] = useControllableState({
|
|
15
19
|
value: checked,
|
|
16
20
|
defaultValue: defaultChecked,
|
|
17
|
-
onChange: onCheckedChange,
|
|
18
21
|
});
|
|
19
|
-
|
|
22
|
+
const isIndeterminate = on === "indeterminate";
|
|
23
|
+
// a11y: a tri-state checkbox must report "mixed" so assistive tech announces
|
|
24
|
+
// the partial selection instead of a plain checked/unchecked state.
|
|
25
|
+
const ariaChecked = isIndeterminate ? "mixed" : on;
|
|
26
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", role: "checkbox", "aria-checked": ariaChecked, disabled: disabled, "data-slot": "checkbox", "data-state": isIndeterminate ? "indeterminate" : on ? "checked" : "unchecked", "data-checked": on === true || isIndeterminate ? "" : undefined, "data-unchecked": on === false ? "" : undefined, "data-indeterminate": isIndeterminate ? "" : undefined, "data-disabled": disabled ? "" : undefined, className: cn("cursor-pointer peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input shadow-xs transition-shadow outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary", className), onClick: (e) => {
|
|
20
27
|
onClick?.(e);
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
if (e.defaultPrevented)
|
|
29
|
+
return;
|
|
30
|
+
// A user click always resolves to a real boolean: indeterminate and
|
|
31
|
+
// unchecked both become checked, checked becomes unchecked.
|
|
32
|
+
const next = on === true ? false : true;
|
|
33
|
+
setOn(next);
|
|
34
|
+
onCheckedChange?.(next);
|
|
35
|
+
}, ...props, children: on === true ? (_jsx("span", { "data-slot": "checkbox-indicator", "data-checked": "", className: "grid place-content-center text-current transition-none [&>svg]:size-3.5", children: _jsx(CheckIcon, {}) })) : isIndeterminate ? (_jsx("span", { "data-slot": "checkbox-indicator", "data-indeterminate": "", className: "grid place-content-center text-current transition-none [&>svg]:size-3.5", children: _jsx(MinusIcon, {}) })) : null }), name ? (_jsx("input", { type: "checkbox", "aria-hidden": true, tabIndex: -1, name: name, value: value, checked: on === true, disabled: disabled, readOnly: true, className: "sr-only" })) : null] }));
|
|
24
36
|
}
|
|
25
37
|
export { Checkbox };
|
package/dist/ui/context-menu.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Portal } from "./portal.js";
|
|
|
7
7
|
import { useFloating } from "./use-floating.js";
|
|
8
8
|
import { usePresence } from "./use-presence.js";
|
|
9
9
|
import { useTypeahead } from "./use-typeahead.js";
|
|
10
|
+
import { usePointerMoved } from "./use-pointer-moved.js";
|
|
10
11
|
import { ChevronRightIcon, CheckIcon } from "../icons/index.js";
|
|
11
12
|
const MenuContext = React.createContext(null);
|
|
12
13
|
function useMenu() {
|
|
@@ -65,6 +66,10 @@ function ContextMenuContent({ className, children, onKeyDown, ...props }) {
|
|
|
65
66
|
const [mounted, presenceRef] = usePresence(open);
|
|
66
67
|
const containerRef = React.useRef(null);
|
|
67
68
|
const { onTypeaheadKeyDown } = useTypeahead();
|
|
69
|
+
// Pointer-move focus follows the cursor, but keyboard arrow nav scrolls the
|
|
70
|
+
// list and fires a synthetic pointermove at the same coordinates — guard it
|
|
71
|
+
// so scrolling doesn't yank focus off the keyboard target.
|
|
72
|
+
const hasPointerMoved = usePointerMoved();
|
|
68
73
|
// Position the menu at the cursor, clamped into the viewport once the
|
|
69
74
|
// popup has measurable dimensions. availableHeight caps the menu so long
|
|
70
75
|
// lists scroll inside the viewport instead of overflowing it.
|
|
@@ -166,6 +171,8 @@ function ContextMenuContent({ className, children, onKeyDown, ...props }) {
|
|
|
166
171
|
["--transform-origin"]: "top left",
|
|
167
172
|
["--available-height"]: pos ? `${pos.availableHeight}px` : undefined,
|
|
168
173
|
}, onPointerMove: (e) => {
|
|
174
|
+
if (!hasPointerMoved(e))
|
|
175
|
+
return;
|
|
169
176
|
const item = e.target.closest('[role^="menuitem"]:not([data-disabled])');
|
|
170
177
|
if (item && item !== document.activeElement)
|
|
171
178
|
item.focus();
|
|
@@ -216,7 +223,10 @@ function ContextMenuLabel({ className, inset, ...props }) {
|
|
|
216
223
|
}
|
|
217
224
|
function ContextMenuItem({ className, inset, variant = "default", disabled, onSelect, onClick, ...props }) {
|
|
218
225
|
const { setOpen } = useMenu();
|
|
219
|
-
return (_jsx("div", { role: "menuitem", tabIndex: -1, "aria-disabled": disabled || undefined, "data-slot": "context-menu-item", "data-inset": inset ? "" : undefined, "data-variant": variant, "data-disabled": disabled ? "" : undefined, className: cn(
|
|
226
|
+
return (_jsx("div", { role: "menuitem", tabIndex: -1, "aria-disabled": disabled || undefined, "data-slot": "context-menu-item", "data-inset": inset ? "" : undefined, "data-variant": variant, "data-disabled": disabled ? "" : undefined, className: cn(
|
|
227
|
+
// focus-visible ring gives keyboard roving focus a visible indicator
|
|
228
|
+
// (outline-hidden removes the native one); pointer focus stays ring-free.
|
|
229
|
+
"group/context-menu-item relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset data-inset:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 focus:*:[svg]:text-accent-foreground data-[variant=destructive]:*:[svg]:text-destructive", className), onClick: (e) => {
|
|
220
230
|
if (disabled)
|
|
221
231
|
return;
|
|
222
232
|
onClick?.(e);
|
|
@@ -228,7 +238,9 @@ function ContextMenuItem({ className, inset, variant = "default", disabled, onSe
|
|
|
228
238
|
}
|
|
229
239
|
const RadioGroupContext = React.createContext({});
|
|
230
240
|
function ContextMenuCheckboxItem({ className, children, checked, onCheckedChange, disabled, inset, onClick, ...props }) {
|
|
231
|
-
return (_jsxs("div", { role: "menuitemcheckbox", tabIndex: -1, "aria-checked": !!checked, "aria-disabled": disabled || undefined, "data-slot": "context-menu-checkbox-item", "data-disabled": disabled ? "" : undefined, "data-inset": inset ? "" : undefined, className: cn(
|
|
241
|
+
return (_jsxs("div", { role: "menuitemcheckbox", tabIndex: -1, "aria-checked": !!checked, "aria-disabled": disabled || undefined, "data-slot": "context-menu-checkbox-item", "data-disabled": disabled ? "" : undefined, "data-inset": inset ? "" : undefined, className: cn(
|
|
242
|
+
// focus-visible ring gives keyboard roving focus a visible indicator.
|
|
243
|
+
"relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), onClick: (e) => {
|
|
232
244
|
if (disabled)
|
|
233
245
|
return;
|
|
234
246
|
onClick?.(e);
|
|
@@ -242,7 +254,9 @@ function ContextMenuRadioGroup({ value, onValueChange, ...props }) {
|
|
|
242
254
|
function ContextMenuRadioItem({ className, children, value, disabled, inset, onClick, ...props }) {
|
|
243
255
|
const { value: groupValue, onValueChange } = React.useContext(RadioGroupContext);
|
|
244
256
|
const checked = groupValue === value;
|
|
245
|
-
return (_jsxs("div", { role: "menuitemradio", tabIndex: -1, "aria-checked": checked, "aria-disabled": disabled || undefined, "data-slot": "context-menu-radio-item", "data-disabled": disabled ? "" : undefined, "data-inset": inset ? "" : undefined, className: cn(
|
|
257
|
+
return (_jsxs("div", { role: "menuitemradio", tabIndex: -1, "aria-checked": checked, "aria-disabled": disabled || undefined, "data-slot": "context-menu-radio-item", "data-disabled": disabled ? "" : undefined, "data-inset": inset ? "" : undefined, className: cn(
|
|
258
|
+
// focus-visible ring gives keyboard roving focus a visible indicator.
|
|
259
|
+
"relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), onClick: (e) => {
|
|
246
260
|
if (disabled)
|
|
247
261
|
return;
|
|
248
262
|
onClick?.(e);
|
|
@@ -266,11 +280,14 @@ function useSub() {
|
|
|
266
280
|
function ContextMenuSub({ defaultOpen = false, children, }) {
|
|
267
281
|
const [open, setOpen] = React.useState(defaultOpen);
|
|
268
282
|
const triggerRef = React.useRef(null);
|
|
269
|
-
|
|
283
|
+
const subContentId = React.useId();
|
|
284
|
+
return (_jsx(SubContext.Provider, { value: { open, setOpen, triggerRef, subContentId }, children: children }));
|
|
270
285
|
}
|
|
271
286
|
function ContextMenuSubTrigger({ className, inset, children, onClick, onKeyDown, onMouseEnter, ...props }) {
|
|
272
|
-
const { open, setOpen, triggerRef } = useSub();
|
|
273
|
-
return (_jsxs("div", { ref: triggerRef, role: "menuitem", tabIndex: -1, "aria-haspopup": "menu", "aria-expanded": open, "data-slot": "context-menu-sub-trigger", "data-inset": inset ? "" : undefined, "data-popup-open": open ? "" : undefined, "data-open": open ? "" : undefined, className: cn(
|
|
287
|
+
const { open, setOpen, triggerRef, subContentId } = useSub();
|
|
288
|
+
return (_jsxs("div", { ref: triggerRef, role: "menuitem", tabIndex: -1, "aria-haspopup": "menu", "aria-expanded": open, "aria-controls": open ? subContentId : undefined, "data-slot": "context-menu-sub-trigger", "data-inset": inset ? "" : undefined, "data-popup-open": open ? "" : undefined, "data-open": open ? "" : undefined, className: cn(
|
|
289
|
+
// focus-visible ring gives keyboard roving focus a visible indicator.
|
|
290
|
+
"flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset data-inset:pl-8 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), onMouseEnter: (e) => {
|
|
274
291
|
onMouseEnter?.(e);
|
|
275
292
|
setOpen(true);
|
|
276
293
|
}, onClick: (e) => {
|
|
@@ -288,7 +305,7 @@ function ContextMenuSubTrigger({ className, inset, children, onClick, onKeyDown,
|
|
|
288
305
|
}, ...props, children: [children, _jsx(ChevronRightIcon, { className: "ml-auto" })] }));
|
|
289
306
|
}
|
|
290
307
|
function ContextMenuSubContent({ className, children, onKeyDown, ...props }) {
|
|
291
|
-
const { open, setOpen, triggerRef } = useSub();
|
|
308
|
+
const { open, setOpen, triggerRef, subContentId } = useSub();
|
|
292
309
|
const { layers } = useMenu();
|
|
293
310
|
const [mounted, presenceRef] = usePresence(open);
|
|
294
311
|
const { floatingRef, pos } = useFloating(triggerRef, mounted, {
|
|
@@ -330,7 +347,10 @@ function ContextMenuSubContent({ className, children, onKeyDown, ...props }) {
|
|
|
330
347
|
}, [open]);
|
|
331
348
|
if (!mounted)
|
|
332
349
|
return null;
|
|
333
|
-
return (_jsx(Portal, { children: _jsx("div", { ref: mergeRefs(floatingRef, presenceRef, containerRef),
|
|
350
|
+
return (_jsx(Portal, { children: _jsx("div", { ref: mergeRefs(floatingRef, presenceRef, containerRef),
|
|
351
|
+
// Matches the SubTrigger's aria-controls target so the trigger->submenu
|
|
352
|
+
// relationship is exposed to assistive tech.
|
|
353
|
+
id: subContentId, role: "menu", tabIndex: -1, "data-slot": "context-menu-sub-content", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", "data-side": pos?.side ?? "right", style: {
|
|
334
354
|
position: "fixed",
|
|
335
355
|
left: pos?.x ?? 0,
|
|
336
356
|
top: pos?.y ?? 0,
|
package/dist/ui/dialog.js
CHANGED
|
@@ -6,6 +6,8 @@ import { Button } from "./button.js";
|
|
|
6
6
|
import { Slot, resolveRender } from "./slot.js";
|
|
7
7
|
import { XIcon } from "../icons/index.js";
|
|
8
8
|
import { useControllableState } from "./use-controllable-state.js";
|
|
9
|
+
import { useDialogLabelling } from "./use-labelling.js";
|
|
10
|
+
import { useScrollLock } from "./use-scroll-lock.js";
|
|
9
11
|
const DialogContext = React.createContext(null);
|
|
10
12
|
function useDialog() {
|
|
11
13
|
const ctx = React.useContext(DialogContext);
|
|
@@ -24,9 +26,10 @@ function Dialog({ open, defaultOpen = false, onOpenChange, children }) {
|
|
|
24
26
|
defaultValue: defaultOpen,
|
|
25
27
|
onChange: onOpenChange,
|
|
26
28
|
});
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
// Track which labelling parts actually render so DialogContent can reference
|
|
30
|
+
// them only when present, avoiding a dangling aria-labelledby/-describedby IDREF.
|
|
31
|
+
const labelling = useDialogLabelling();
|
|
32
|
+
return (_jsx(DialogContext.Provider, { value: { open: isOpen, setOpen, ...labelling }, children: children }));
|
|
30
33
|
}
|
|
31
34
|
function DialogTrigger({ asChild = false, render, nativeButton: _nativeButton, children, onClick, ...props }) {
|
|
32
35
|
const { open, setOpen } = useDialog();
|
|
@@ -57,7 +60,7 @@ function DialogOverlay() {
|
|
|
57
60
|
return null;
|
|
58
61
|
}
|
|
59
62
|
function DialogContent({ className, children, showCloseButton = true, ...props }) {
|
|
60
|
-
const { open, setOpen,
|
|
63
|
+
const { open, setOpen, labelledBy, describedBy } = useDialog();
|
|
61
64
|
const ref = React.useRef(null);
|
|
62
65
|
const pointerDownOnBackdrop = React.useRef(false);
|
|
63
66
|
React.useEffect(() => {
|
|
@@ -69,17 +72,11 @@ function DialogContent({ className, children, showCloseButton = true, ...props }
|
|
|
69
72
|
else if (!open && node.open)
|
|
70
73
|
node.close();
|
|
71
74
|
}, [open]);
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
document.body.style.overflow = "hidden";
|
|
78
|
-
return () => {
|
|
79
|
-
document.body.style.overflow = prev;
|
|
80
|
-
};
|
|
81
|
-
}, [open]);
|
|
82
|
-
return (_jsx("dialog", { ref: ref, "aria-labelledby": titleId, "aria-describedby": descriptionId, "data-slot": "dialog-overlay", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", onCancel: (e) => {
|
|
75
|
+
// Reentrant, reference-counted scroll lock so stacked overlays don't clobber
|
|
76
|
+
// each other's overflow restore (an inner close would otherwise unlock scroll
|
|
77
|
+
// while an outer dialog is still open).
|
|
78
|
+
useScrollLock(open);
|
|
79
|
+
return (_jsx("dialog", { ref: ref, "aria-labelledby": labelledBy, "aria-describedby": describedBy, "data-slot": "dialog-overlay", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", onCancel: (e) => {
|
|
83
80
|
e.preventDefault();
|
|
84
81
|
setOpen(false);
|
|
85
82
|
}, onClose: () => setOpen(false), onPointerDown: (e) => {
|
|
@@ -99,11 +96,15 @@ function DialogFooter({ className, showCloseButton = false, children, ...props }
|
|
|
99
96
|
return (_jsxs("div", { "data-slot": "dialog-footer", className: cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className), ...props, children: [children, showCloseButton && (_jsx(DialogClose, { asChild: true, children: _jsx(Button, { variant: "outline", children: "Close" }) }))] }));
|
|
100
97
|
}
|
|
101
98
|
function DialogTitle({ className, ...props }) {
|
|
102
|
-
const { titleId } = useDialog();
|
|
99
|
+
const { titleId, registerTitle } = useDialog();
|
|
100
|
+
// Register presence so the container wires aria-labelledby only when a title exists.
|
|
101
|
+
React.useEffect(registerTitle, [registerTitle]);
|
|
103
102
|
return (_jsx("h2", { id: titleId, "data-slot": "dialog-title", className: cn("leading-none font-medium", className), ...props }));
|
|
104
103
|
}
|
|
105
104
|
function DialogDescription({ className, ...props }) {
|
|
106
|
-
const { descriptionId } = useDialog();
|
|
105
|
+
const { descriptionId, registerDescription } = useDialog();
|
|
106
|
+
// Register presence so the container wires aria-describedby only when a description exists.
|
|
107
|
+
React.useEffect(registerDescription, [registerDescription]);
|
|
107
108
|
return (_jsx("p", { id: descriptionId, "data-slot": "dialog-description", className: cn("text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground", className), ...props }));
|
|
108
109
|
}
|
|
109
110
|
export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, };
|
package/dist/ui/drawer.js
CHANGED
|
@@ -4,7 +4,9 @@ import * as React from "react";
|
|
|
4
4
|
import { cn } from "../shadcn/lib/utils.js";
|
|
5
5
|
import { Slot, mergeRefs, resolveRender } from "./slot.js";
|
|
6
6
|
import { useControllableState } from "./use-controllable-state.js";
|
|
7
|
+
import { useDialogLabelling } from "./use-labelling.js";
|
|
7
8
|
import { usePresence } from "./use-presence.js";
|
|
9
|
+
import { useScrollLock } from "./use-scroll-lock.js";
|
|
8
10
|
// Drag thresholds and animation timing ported from vaul (MIT — Emil Kowalski).
|
|
9
11
|
// https://github.com/emilkowalski/vaul/blob/main/src/constants.ts
|
|
10
12
|
const VELOCITY_THRESHOLD = 0.4;
|
|
@@ -51,9 +53,17 @@ function Drawer({ open, defaultOpen = false, onOpenChange, children }) {
|
|
|
51
53
|
defaultValue: defaultOpen,
|
|
52
54
|
onChange: onOpenChange,
|
|
53
55
|
});
|
|
54
|
-
const titleId =
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
const { titleId, descriptionId, registerTitle, registerDescription, labelledBy, describedBy } = useDialogLabelling();
|
|
57
|
+
return (_jsx(DrawerContext.Provider, { value: {
|
|
58
|
+
open: isOpen,
|
|
59
|
+
setOpen,
|
|
60
|
+
titleId,
|
|
61
|
+
descriptionId,
|
|
62
|
+
registerTitle,
|
|
63
|
+
registerDescription,
|
|
64
|
+
labelledBy,
|
|
65
|
+
describedBy,
|
|
66
|
+
}, children: children }));
|
|
57
67
|
}
|
|
58
68
|
function DrawerTrigger({ asChild = false, render, nativeButton: _nativeButton, children, onClick, ...props }) {
|
|
59
69
|
const { open, setOpen } = useDrawer();
|
|
@@ -84,7 +94,7 @@ function DrawerOverlay() {
|
|
|
84
94
|
return null;
|
|
85
95
|
}
|
|
86
96
|
function DrawerContent({ className, children, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, ...props }) {
|
|
87
|
-
const { open, setOpen,
|
|
97
|
+
const { open, setOpen, labelledBy, describedBy } = useDrawer();
|
|
88
98
|
const ref = React.useRef(null);
|
|
89
99
|
const pointerDownOnBackdrop = React.useRef(false);
|
|
90
100
|
const [mounted, presenceRef] = usePresence(open);
|
|
@@ -103,15 +113,11 @@ function DrawerContent({ className, children, onPointerDown, onPointerMove, onPo
|
|
|
103
113
|
else if (!mounted && node.open)
|
|
104
114
|
node.close();
|
|
105
115
|
}, [mounted]);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return () => {
|
|
112
|
-
document.body.style.overflow = prev;
|
|
113
|
-
};
|
|
114
|
-
}, [mounted]);
|
|
116
|
+
// Reentrant, reference-counted body scroll lock: when drawers stack with other
|
|
117
|
+
// overlays, the inline overflow effect would let an inner overlay's cleanup
|
|
118
|
+
// unlock scroll while an outer overlay is still open. useScrollLock shares one
|
|
119
|
+
// lock so only the last overlay to close restores the original overflow.
|
|
120
|
+
useScrollLock(mounted);
|
|
115
121
|
// Clear any drag-leftover inline transform whenever we (re)open.
|
|
116
122
|
React.useEffect(() => {
|
|
117
123
|
if (open && ref.current) {
|
|
@@ -207,7 +213,7 @@ function DrawerContent({ className, children, onPointerDown, onPointerMove, onPo
|
|
|
207
213
|
};
|
|
208
214
|
if (!mounted)
|
|
209
215
|
return null;
|
|
210
|
-
return (_jsxs("dialog", { ...props, ref: mergeRefs(ref, presenceRef), "aria-labelledby":
|
|
216
|
+
return (_jsxs("dialog", { ...props, ref: mergeRefs(ref, presenceRef), "aria-labelledby": labelledBy, "aria-describedby": describedBy, "data-slot": "drawer-content", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", onCancel: (e) => {
|
|
211
217
|
e.preventDefault();
|
|
212
218
|
setOpen(false);
|
|
213
219
|
}, onClose: () => setOpen(false), onClick: (e) => {
|
|
@@ -233,11 +239,17 @@ function DrawerFooter({ className, ...props }) {
|
|
|
233
239
|
return (_jsx("div", { "data-slot": "drawer-footer", className: cn("mt-auto flex flex-col gap-2 p-4", className), ...props }));
|
|
234
240
|
}
|
|
235
241
|
function DrawerTitle({ className, ...props }) {
|
|
236
|
-
const { titleId } = useDrawer();
|
|
242
|
+
const { titleId, registerTitle } = useDrawer();
|
|
243
|
+
// Register on mount so the container knows a title exists and can wire
|
|
244
|
+
// aria-labelledby; the returned cleanup unregisters on unmount.
|
|
245
|
+
React.useEffect(registerTitle, [registerTitle]);
|
|
237
246
|
return (_jsx("h2", { id: titleId, "data-slot": "drawer-title", className: cn("font-medium text-foreground", className), ...props }));
|
|
238
247
|
}
|
|
239
248
|
function DrawerDescription({ className, ...props }) {
|
|
240
|
-
const { descriptionId } = useDrawer();
|
|
249
|
+
const { descriptionId, registerDescription } = useDrawer();
|
|
250
|
+
// Register on mount so the container knows a description exists and can wire
|
|
251
|
+
// aria-describedby; the returned cleanup unregisters on unmount.
|
|
252
|
+
React.useEffect(registerDescription, [registerDescription]);
|
|
241
253
|
return (_jsx("p", { id: descriptionId, "data-slot": "drawer-description", className: cn("text-sm text-muted-foreground", className), ...props }));
|
|
242
254
|
}
|
|
243
255
|
export { Drawer, DrawerPortal, DrawerOverlay, DrawerTrigger, DrawerClose, DrawerContent, DrawerHeader, DrawerFooter, DrawerTitle, DrawerDescription, };
|