@stll/ui 0.0.1-placeholder.0 → 0.2.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/README.md +84 -0
- package/dist/components/accordion.d.ts +8 -0
- package/dist/components/accordion.js +38 -0
- package/dist/components/alert-dialog.d.ts +32 -0
- package/dist/components/alert-dialog.js +75 -0
- package/dist/components/avatar.d.ts +7 -0
- package/dist/components/avatar.js +22 -0
- package/dist/components/bidi-text.d.ts +24 -0
- package/dist/components/bidi-text.js +38 -0
- package/dist/components/brand-icons.d.ts +7 -0
- package/dist/components/brand-icons.js +18 -0
- package/dist/components/breadcrumb.d.ts +12 -0
- package/dist/components/breadcrumb.js +61 -0
- package/dist/components/button-variants.d.ts +17 -0
- package/dist/components/button-variants.js +43 -0
- package/dist/components/button.d.ts +20 -0
- package/dist/components/button.js +53 -0
- package/dist/components/checkbox.d.ts +5 -0
- package/dist/components/checkbox.js +44 -0
- package/dist/components/color-picker.d.ts +47 -0
- package/dist/components/color-picker.js +249 -0
- package/dist/components/combobox.d.ts +40 -0
- package/dist/components/combobox.js +208 -0
- package/dist/components/command.d.ts +34 -0
- package/dist/components/command.js +100 -0
- package/dist/components/date-picker-popover.d.ts +29 -0
- package/dist/components/date-picker-popover.js +548 -0
- package/dist/components/date-picker-popover.logic.d.ts +14 -0
- package/dist/components/date-picker-popover.logic.js +26 -0
- package/dist/components/destructive-action-confirmation.d.ts +21 -0
- package/dist/components/destructive-action-confirmation.js +60 -0
- package/dist/components/destructive-confirm-dialog.d.ts +17 -0
- package/dist/components/destructive-confirm-dialog.js +49 -0
- package/dist/components/dialog.d.ts +37 -0
- package/dist/components/dialog.js +92 -0
- package/dist/components/directional-icon.d.ts +30 -0
- package/dist/components/directional-icon.js +12 -0
- package/dist/components/field.d.ts +11 -0
- package/dist/components/field.js +34 -0
- package/dist/components/form.d.ts +5 -0
- package/dist/components/form.js +12 -0
- package/dist/components/frame.d.ts +10 -0
- package/dist/components/frame.js +35 -0
- package/dist/components/hex-color-picker.d.ts +11 -0
- package/dist/components/hex-color-picker.js +366 -0
- package/dist/components/input-group.d.ts +15 -0
- package/dist/components/input-group.js +53 -0
- package/dist/components/input-otp.d.ts +13 -0
- package/dist/components/input-otp.js +39 -0
- package/dist/components/input.d.ts +12 -0
- package/dist/components/input.js +46 -0
- package/dist/components/label.d.ts +5 -0
- package/dist/components/label.js +18 -0
- package/dist/components/menu.d.ts +44 -0
- package/dist/components/menu.js +152 -0
- package/dist/components/outline-rail.d.ts +39 -0
- package/dist/components/outline-rail.js +405 -0
- package/dist/components/pagination.d.ts +17 -0
- package/dist/components/pagination.js +78 -0
- package/dist/components/popover.d.ts +34 -0
- package/dist/components/popover.js +67 -0
- package/dist/components/preview-card.d.ts +12 -0
- package/dist/components/preview-card.js +25 -0
- package/dist/components/preview-pane.d.ts +23 -0
- package/dist/components/preview-pane.js +33 -0
- package/dist/components/scroll-area.d.ts +19 -0
- package/dist/components/scroll-area.js +38 -0
- package/dist/components/scroll-to-top.d.ts +12 -0
- package/dist/components/scroll-to-top.js +43 -0
- package/dist/components/secret-input.d.ts +9 -0
- package/dist/components/secret-input.js +38 -0
- package/dist/components/segmented-icon-toggle.d.ts +22 -0
- package/dist/components/segmented-icon-toggle.js +30 -0
- package/dist/components/select.d.ts +25 -0
- package/dist/components/select.js +196 -0
- package/dist/components/separator.d.ts +16 -0
- package/dist/components/separator.js +26 -0
- package/dist/components/sheet.d.ts +29 -0
- package/dist/components/sheet.js +87 -0
- package/dist/components/skeleton.d.ts +4 -0
- package/dist/components/skeleton.js +10 -0
- package/dist/components/stella-mark.d.ts +5 -0
- package/dist/components/stella-mark.js +12 -0
- package/dist/components/stella-wordmark.d.ts +5 -0
- package/dist/components/stella-wordmark.js +21 -0
- package/dist/components/table.d.ts +20 -0
- package/dist/components/table.js +80 -0
- package/dist/components/tabs.d.ts +11 -0
- package/dist/components/tabs.js +46 -0
- package/dist/components/textarea.d.ts +9 -0
- package/dist/components/textarea.js +31 -0
- package/dist/components/toast.d.ts +60 -0
- package/dist/components/toast.js +261 -0
- package/dist/components/tooltip-trigger-helper.d.ts +11 -0
- package/dist/components/tooltip-trigger-helper.js +14 -0
- package/dist/components/tooltip.d.ts +15 -0
- package/dist/components/tooltip.js +32 -0
- package/dist/hooks/use-contained-handler.d.ts +55 -0
- package/dist/hooks/use-contained-handler.js +54 -0
- package/dist/hooks/use-content-dir.d.ts +48 -0
- package/dist/hooks/use-content-dir.js +45 -0
- package/dist/hooks/use-mobile.d.ts +4 -0
- package/dist/hooks/use-mobile.js +16 -0
- package/dist/hooks/use-viewport-width.d.ts +5 -0
- package/dist/hooks/use-viewport-width.js +14 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +59 -0
- package/dist/inspector/chrome.d.ts +72 -0
- package/dist/inspector/chrome.js +153 -0
- package/dist/inspector/dock.d.ts +48 -0
- package/dist/inspector/dock.js +36 -0
- package/dist/inspector/index.d.ts +6 -0
- package/dist/inspector/index.js +6 -0
- package/dist/inspector/layout-tokens.d.ts +28 -0
- package/dist/inspector/layout-tokens.js +28 -0
- package/dist/inspector/pane-width.d.ts +64 -0
- package/dist/inspector/pane-width.js +53 -0
- package/dist/inspector/use-pane-width.d.ts +99 -0
- package/dist/inspector/use-pane-width.js +149 -0
- package/dist/lib/button-disposition.d.ts +63 -0
- package/dist/lib/button-disposition.js +52 -0
- package/dist/lib/overlay-layer.d.ts +22 -0
- package/dist/lib/overlay-layer.js +21 -0
- package/dist/lib/tab-order.d.ts +15 -0
- package/dist/lib/tab-order.js +15 -0
- package/dist/lib/tooltip-content.d.ts +13 -0
- package/dist/lib/tooltip-content.js +12 -0
- package/dist/lib/utils.d.ts +20 -0
- package/dist/lib/utils.js +41 -0
- package/dist/lib/week.d.ts +25 -0
- package/dist/lib/week.js +40 -0
- package/dist/styles/theme.css +1209 -0
- package/package.json +506 -2
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//#region src/inspector/pane-width.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Width arithmetic for a docked inspector pane.
|
|
4
|
+
*
|
|
5
|
+
* The pane is a fixed overlay backed by an in-flow spacer, so nothing in
|
|
6
|
+
* the layout pushes back when it grows: whatever the pane takes comes
|
|
7
|
+
* straight out of the content column. Without a clamp against the space
|
|
8
|
+
* actually available, shrinking the window (or expanding the sidebar)
|
|
9
|
+
* leaves the content column a few dozen pixels wide instead of folding.
|
|
10
|
+
*
|
|
11
|
+
* The host application's sidebar width is a parameter rather than an
|
|
12
|
+
* import, so the policy carries no coupling to any particular shell.
|
|
13
|
+
*/
|
|
14
|
+
declare const INSPECTOR_PANE_DEFAULT_WIDTH = 512;
|
|
15
|
+
declare const INSPECTOR_PANE_MIN_WIDTH = 320;
|
|
16
|
+
declare const INSPECTOR_PANE_MAX_WIDTH = 800;
|
|
17
|
+
/**
|
|
18
|
+
* Floor for the content column beside the pane. Below this the content
|
|
19
|
+
* surface stops being usable, so the pane yields space first.
|
|
20
|
+
*/
|
|
21
|
+
declare const INSPECTOR_CONTENT_MIN_WIDTH = 400;
|
|
22
|
+
/** Inline size of the collapsed icon rail, in CSS pixels. */
|
|
23
|
+
declare const INSPECTOR_RAIL_WIDTH = 48;
|
|
24
|
+
type ForceSidebarCollapsedInput = {
|
|
25
|
+
/** Inline size the expanded sidebar takes out of the layout row. */
|
|
26
|
+
expandedSidebarWidth: number;
|
|
27
|
+
inspectorPaneOpen: boolean;
|
|
28
|
+
/** Viewport width in CSS pixels; 0 before the viewport is known. */
|
|
29
|
+
viewportWidth: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The expanded sidebar yields its optional width before either docked pane
|
|
33
|
+
* can violate its minimum. The collapsed rail plus both minimums fit at the
|
|
34
|
+
* desktop breakpoint, so this policy keeps every desktop width usable.
|
|
35
|
+
*/
|
|
36
|
+
declare const shouldForceSidebarCollapsed: ({ expandedSidebarWidth, inspectorPaneOpen, viewportWidth }: ForceSidebarCollapsedInput) => boolean;
|
|
37
|
+
type InspectorPaneWidthInput = {
|
|
38
|
+
/** Width the user dragged the pane to. */
|
|
39
|
+
desiredWidth: number;
|
|
40
|
+
/** Inline size the sidebar takes out of the same layout row. */
|
|
41
|
+
sidebarWidth: number;
|
|
42
|
+
/** Viewport width in CSS pixels; 0 before the viewport is known. */
|
|
43
|
+
viewportWidth: number;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Largest width the pane may take without starving the content column.
|
|
47
|
+
* Never returns less than {@link INSPECTOR_PANE_MIN_WIDTH}: a pane too
|
|
48
|
+
* narrow to read is no better than a content column too narrow to read,
|
|
49
|
+
* and collapsing the sidebar is the escape hatch for a genuinely small
|
|
50
|
+
* viewport.
|
|
51
|
+
*/
|
|
52
|
+
declare const resolveInspectorPaneMaxWidth: ({ sidebarWidth, viewportWidth }: Omit<InspectorPaneWidthInput, "desiredWidth">) => number;
|
|
53
|
+
/** Width the pane renders at, given the width the user asked for. */
|
|
54
|
+
declare const resolveInspectorPaneWidth: ({ desiredWidth, sidebarWidth, viewportWidth }: InspectorPaneWidthInput) => number;
|
|
55
|
+
/**
|
|
56
|
+
* Width the dock reserves in the layout: the full pane while content is
|
|
57
|
+
* shown, the bare rail while it is collapsed or minimized.
|
|
58
|
+
*/
|
|
59
|
+
declare const resolveInspectorDockWidth: ({ paneWidth, showPaneContent }: {
|
|
60
|
+
paneWidth: number;
|
|
61
|
+
showPaneContent: boolean;
|
|
62
|
+
}) => number;
|
|
63
|
+
//#endregion
|
|
64
|
+
export { INSPECTOR_CONTENT_MIN_WIDTH, INSPECTOR_PANE_DEFAULT_WIDTH, INSPECTOR_PANE_MAX_WIDTH, INSPECTOR_PANE_MIN_WIDTH, INSPECTOR_RAIL_WIDTH, resolveInspectorDockWidth, resolveInspectorPaneMaxWidth, resolveInspectorPaneWidth, shouldForceSidebarCollapsed };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//#region src/inspector/pane-width.ts
|
|
2
|
+
/**
|
|
3
|
+
* Width arithmetic for a docked inspector pane.
|
|
4
|
+
*
|
|
5
|
+
* The pane is a fixed overlay backed by an in-flow spacer, so nothing in
|
|
6
|
+
* the layout pushes back when it grows: whatever the pane takes comes
|
|
7
|
+
* straight out of the content column. Without a clamp against the space
|
|
8
|
+
* actually available, shrinking the window (or expanding the sidebar)
|
|
9
|
+
* leaves the content column a few dozen pixels wide instead of folding.
|
|
10
|
+
*
|
|
11
|
+
* The host application's sidebar width is a parameter rather than an
|
|
12
|
+
* import, so the policy carries no coupling to any particular shell.
|
|
13
|
+
*/
|
|
14
|
+
const INSPECTOR_PANE_DEFAULT_WIDTH = 512;
|
|
15
|
+
const INSPECTOR_PANE_MIN_WIDTH = 320;
|
|
16
|
+
const INSPECTOR_PANE_MAX_WIDTH = 800;
|
|
17
|
+
/**
|
|
18
|
+
* Floor for the content column beside the pane. Below this the content
|
|
19
|
+
* surface stops being usable, so the pane yields space first.
|
|
20
|
+
*/
|
|
21
|
+
const INSPECTOR_CONTENT_MIN_WIDTH = 400;
|
|
22
|
+
/** Inline size of the collapsed icon rail, in CSS pixels. */
|
|
23
|
+
const INSPECTOR_RAIL_WIDTH = 48;
|
|
24
|
+
/**
|
|
25
|
+
* The expanded sidebar yields its optional width before either docked pane
|
|
26
|
+
* can violate its minimum. The collapsed rail plus both minimums fit at the
|
|
27
|
+
* desktop breakpoint, so this policy keeps every desktop width usable.
|
|
28
|
+
*/
|
|
29
|
+
const shouldForceSidebarCollapsed = ({ expandedSidebarWidth, inspectorPaneOpen, viewportWidth }) => inspectorPaneOpen && viewportWidth > 0 && viewportWidth < expandedSidebarWidth + 400 + 320;
|
|
30
|
+
/**
|
|
31
|
+
* Largest width the pane may take without starving the content column.
|
|
32
|
+
* Never returns less than {@link INSPECTOR_PANE_MIN_WIDTH}: a pane too
|
|
33
|
+
* narrow to read is no better than a content column too narrow to read,
|
|
34
|
+
* and collapsing the sidebar is the escape hatch for a genuinely small
|
|
35
|
+
* viewport.
|
|
36
|
+
*/
|
|
37
|
+
const resolveInspectorPaneMaxWidth = ({ sidebarWidth, viewportWidth }) => {
|
|
38
|
+
if (viewportWidth <= 0) return 320;
|
|
39
|
+
const available = viewportWidth - sidebarWidth - 400;
|
|
40
|
+
return Math.max(320, Math.min(800, available));
|
|
41
|
+
};
|
|
42
|
+
/** Width the pane renders at, given the width the user asked for. */
|
|
43
|
+
const resolveInspectorPaneWidth = ({ desiredWidth, sidebarWidth, viewportWidth }) => Math.min(Math.max(desiredWidth, 320), resolveInspectorPaneMaxWidth({
|
|
44
|
+
sidebarWidth,
|
|
45
|
+
viewportWidth
|
|
46
|
+
}));
|
|
47
|
+
/**
|
|
48
|
+
* Width the dock reserves in the layout: the full pane while content is
|
|
49
|
+
* shown, the bare rail while it is collapsed or minimized.
|
|
50
|
+
*/
|
|
51
|
+
const resolveInspectorDockWidth = ({ paneWidth, showPaneContent }) => showPaneContent ? paneWidth : 48;
|
|
52
|
+
//#endregion
|
|
53
|
+
export { INSPECTOR_CONTENT_MIN_WIDTH, INSPECTOR_PANE_DEFAULT_WIDTH, INSPECTOR_PANE_MAX_WIDTH, INSPECTOR_PANE_MIN_WIDTH, INSPECTOR_RAIL_WIDTH, resolveInspectorDockWidth, resolveInspectorPaneMaxWidth, resolveInspectorPaneWidth, shouldForceSidebarCollapsed };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { KeyboardEvent, PointerEvent } from "react";
|
|
2
|
+
//#region src/inspector/use-pane-width.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Reads a persisted pane width, rejecting anything that is not a finite
|
|
5
|
+
* number inside the pane's own bounds. A corrupt or stale entry must not
|
|
6
|
+
* be able to render the pane at a width the drag handle cannot recover
|
|
7
|
+
* from, so an unusable value falls back to the default rather than being
|
|
8
|
+
* clamped into something the user never chose.
|
|
9
|
+
*/
|
|
10
|
+
declare const parsePersistedPaneWidth: (raw: string | null) => number;
|
|
11
|
+
/**
|
|
12
|
+
* Width the drag produces for a pane docked to the inline-end edge.
|
|
13
|
+
*
|
|
14
|
+
* That edge is the right in LTR (width = distance from the right) and the
|
|
15
|
+
* left in RTL (width = distance from the left). Without the RTL branch the
|
|
16
|
+
* delta is inverted and the drag oscillates.
|
|
17
|
+
*/
|
|
18
|
+
declare const resolveDragWidth: ({ clientX, isRtl, viewportWidth }: {
|
|
19
|
+
clientX: number;
|
|
20
|
+
isRtl: boolean;
|
|
21
|
+
viewportWidth: number;
|
|
22
|
+
}) => number;
|
|
23
|
+
/** Pixels one arrow press moves the edge; Page/Home/End move further. */
|
|
24
|
+
declare const INSPECTOR_PANE_KEYBOARD_STEP = 16;
|
|
25
|
+
declare const INSPECTOR_PANE_KEYBOARD_PAGE_STEP = 64;
|
|
26
|
+
/**
|
|
27
|
+
* Width a keyboard resize produces. Returns `null` for keys the handle does
|
|
28
|
+
* not own, so the caller can leave the event alone rather than swallowing
|
|
29
|
+
* every keystroke that reaches a focusable separator.
|
|
30
|
+
*
|
|
31
|
+
* Arrow semantics follow the *edge*, not the width: on an inline-end pane
|
|
32
|
+
* the inline-start arrow grows it. Under RTL the physical arrows swap, so
|
|
33
|
+
* the same key keeps meaning "make it bigger" on both sides.
|
|
34
|
+
*/
|
|
35
|
+
declare const resolveKeyboardWidth: ({ currentWidth, isRtl, key }: {
|
|
36
|
+
currentWidth: number;
|
|
37
|
+
isRtl: boolean;
|
|
38
|
+
key: string;
|
|
39
|
+
}) => number | null;
|
|
40
|
+
/**
|
|
41
|
+
* Reads and writes are wrapped because `window.localStorage` is a getter that
|
|
42
|
+
* *throws* where storage is blocked — a sandboxed iframe, a hardened privacy
|
|
43
|
+
* mode, a full quota. The `typeof window` check does not cover that, and a
|
|
44
|
+
* throw in the state initializer takes the whole pane down on mount. A pane
|
|
45
|
+
* that forgets its width is a smaller failure than a pane that does not
|
|
46
|
+
* render, so both directions degrade to in-memory.
|
|
47
|
+
*/
|
|
48
|
+
declare const readStoredWidth: (storageKey: string) => number;
|
|
49
|
+
declare const writeStoredWidth: (storageKey: string, width: number) => void;
|
|
50
|
+
type UseInspectorPaneWidthOptions = {
|
|
51
|
+
/** Inline size the sidebar takes out of the same layout row. */
|
|
52
|
+
sidebarWidth: number;
|
|
53
|
+
/**
|
|
54
|
+
* `localStorage` key the dragged width is remembered under. Omit to keep
|
|
55
|
+
* the width in memory only, which resets the pane on every reload.
|
|
56
|
+
*/
|
|
57
|
+
storageKey?: string | undefined;
|
|
58
|
+
/** Viewport width in CSS pixels; 0 before the viewport is known. */
|
|
59
|
+
viewportWidth: number;
|
|
60
|
+
};
|
|
61
|
+
type UseInspectorPaneWidthResult = {
|
|
62
|
+
/**
|
|
63
|
+
* Pointer + keyboard handlers and ARIA state to spread onto the drag
|
|
64
|
+
* handle. The handle is a focusable `separator`, so it is operable
|
|
65
|
+
* without a pointer.
|
|
66
|
+
*/
|
|
67
|
+
resizeHandleProps: {
|
|
68
|
+
"aria-orientation": "vertical";
|
|
69
|
+
"aria-valuemax": number;
|
|
70
|
+
"aria-valuemin": number;
|
|
71
|
+
"aria-valuenow": number;
|
|
72
|
+
onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
|
|
73
|
+
onLostPointerCapture: (event: PointerEvent<HTMLElement>) => void;
|
|
74
|
+
onPointerCancel: (event: PointerEvent<HTMLElement>) => void;
|
|
75
|
+
onPointerDown: (event: PointerEvent<HTMLElement>) => void;
|
|
76
|
+
onPointerMove: (event: PointerEvent<HTMLElement>) => void;
|
|
77
|
+
onPointerUp: (event: PointerEvent<HTMLElement>) => void;
|
|
78
|
+
tabIndex: number;
|
|
79
|
+
};
|
|
80
|
+
/** Reset the pane to its default width (double-click affordance). */
|
|
81
|
+
resetWidth: () => void;
|
|
82
|
+
/**
|
|
83
|
+
* Width the pane renders at: the dragged width clamped against the room
|
|
84
|
+
* left beside the sidebar.
|
|
85
|
+
*/
|
|
86
|
+
width: number;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Owns the docked pane's width: the width the user dragged to, its
|
|
90
|
+
* persistence, and the clamp against the room actually available.
|
|
91
|
+
*
|
|
92
|
+
* What renders is always the clamped value, so shrinking the window or
|
|
93
|
+
* expanding the sidebar takes space back from the pane instead of crushing
|
|
94
|
+
* the content column. The *desired* width keeps the user's choice, so the
|
|
95
|
+
* pane returns to it once the room comes back.
|
|
96
|
+
*/
|
|
97
|
+
declare const useInspectorPaneWidth: ({ sidebarWidth, storageKey, viewportWidth }: UseInspectorPaneWidthOptions) => UseInspectorPaneWidthResult;
|
|
98
|
+
//#endregion
|
|
99
|
+
export { INSPECTOR_PANE_KEYBOARD_PAGE_STEP, INSPECTOR_PANE_KEYBOARD_STEP, parsePersistedPaneWidth, readStoredWidth, resolveDragWidth, resolveKeyboardWidth, useInspectorPaneWidth, writeStoredWidth };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { resolveInspectorPaneWidth } from "./pane-width.js";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
//#region src/inspector/use-pane-width.ts
|
|
4
|
+
/**
|
|
5
|
+
* Reads a persisted pane width, rejecting anything that is not a finite
|
|
6
|
+
* number inside the pane's own bounds. A corrupt or stale entry must not
|
|
7
|
+
* be able to render the pane at a width the drag handle cannot recover
|
|
8
|
+
* from, so an unusable value falls back to the default rather than being
|
|
9
|
+
* clamped into something the user never chose.
|
|
10
|
+
*/
|
|
11
|
+
const parsePersistedPaneWidth = (raw) => {
|
|
12
|
+
if (raw === null) return 512;
|
|
13
|
+
const parsed = Number.parseInt(raw, 10);
|
|
14
|
+
if (!Number.isFinite(parsed) || parsed < 320 || parsed > 800) return 512;
|
|
15
|
+
return parsed;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Width the drag produces for a pane docked to the inline-end edge.
|
|
19
|
+
*
|
|
20
|
+
* That edge is the right in LTR (width = distance from the right) and the
|
|
21
|
+
* left in RTL (width = distance from the left). Without the RTL branch the
|
|
22
|
+
* delta is inverted and the drag oscillates.
|
|
23
|
+
*/
|
|
24
|
+
const resolveDragWidth = ({ clientX, isRtl, viewportWidth }) => isRtl ? clientX : viewportWidth - clientX;
|
|
25
|
+
/** Pixels one arrow press moves the edge; Page/Home/End move further. */
|
|
26
|
+
const INSPECTOR_PANE_KEYBOARD_STEP = 16;
|
|
27
|
+
const INSPECTOR_PANE_KEYBOARD_PAGE_STEP = 64;
|
|
28
|
+
/**
|
|
29
|
+
* Width a keyboard resize produces. Returns `null` for keys the handle does
|
|
30
|
+
* not own, so the caller can leave the event alone rather than swallowing
|
|
31
|
+
* every keystroke that reaches a focusable separator.
|
|
32
|
+
*
|
|
33
|
+
* Arrow semantics follow the *edge*, not the width: on an inline-end pane
|
|
34
|
+
* the inline-start arrow grows it. Under RTL the physical arrows swap, so
|
|
35
|
+
* the same key keeps meaning "make it bigger" on both sides.
|
|
36
|
+
*/
|
|
37
|
+
const resolveKeyboardWidth = ({ currentWidth, isRtl, key }) => {
|
|
38
|
+
const grow = isRtl ? "ArrowRight" : "ArrowLeft";
|
|
39
|
+
const shrink = isRtl ? "ArrowLeft" : "ArrowRight";
|
|
40
|
+
switch (key) {
|
|
41
|
+
case grow: return currentWidth + 16;
|
|
42
|
+
case shrink: return currentWidth - 16;
|
|
43
|
+
case "PageUp": return currentWidth + 64;
|
|
44
|
+
case "PageDown": return currentWidth - 64;
|
|
45
|
+
case "Home": return 320;
|
|
46
|
+
case "End": return 800;
|
|
47
|
+
case "Enter": return 512;
|
|
48
|
+
default: return null;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Reads and writes are wrapped because `window.localStorage` is a getter that
|
|
53
|
+
* *throws* where storage is blocked — a sandboxed iframe, a hardened privacy
|
|
54
|
+
* mode, a full quota. The `typeof window` check does not cover that, and a
|
|
55
|
+
* throw in the state initializer takes the whole pane down on mount. A pane
|
|
56
|
+
* that forgets its width is a smaller failure than a pane that does not
|
|
57
|
+
* render, so both directions degrade to in-memory.
|
|
58
|
+
*/
|
|
59
|
+
const readStoredWidth = (storageKey) => {
|
|
60
|
+
if (typeof window === "undefined") return 512;
|
|
61
|
+
try {
|
|
62
|
+
return parsePersistedPaneWidth(window.localStorage.getItem(storageKey));
|
|
63
|
+
} catch {
|
|
64
|
+
return 512;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const writeStoredWidth = (storageKey, width) => {
|
|
68
|
+
if (typeof window === "undefined") return;
|
|
69
|
+
try {
|
|
70
|
+
window.localStorage.setItem(storageKey, String(width));
|
|
71
|
+
} catch {}
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Owns the docked pane's width: the width the user dragged to, its
|
|
75
|
+
* persistence, and the clamp against the room actually available.
|
|
76
|
+
*
|
|
77
|
+
* What renders is always the clamped value, so shrinking the window or
|
|
78
|
+
* expanding the sidebar takes space back from the pane instead of crushing
|
|
79
|
+
* the content column. The *desired* width keeps the user's choice, so the
|
|
80
|
+
* pane returns to it once the room comes back.
|
|
81
|
+
*/
|
|
82
|
+
const useInspectorPaneWidth = ({ sidebarWidth, storageKey, viewportWidth }) => {
|
|
83
|
+
const [desiredWidth, setDesiredWidth] = useState(() => storageKey === void 0 ? 512 : readStoredWidth(storageKey));
|
|
84
|
+
const isDragging = useRef(false);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (storageKey === void 0) return;
|
|
87
|
+
writeStoredWidth(storageKey, desiredWidth);
|
|
88
|
+
}, [desiredWidth, storageKey]);
|
|
89
|
+
const handlePointerDown = (event) => {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
isDragging.current = true;
|
|
92
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
93
|
+
};
|
|
94
|
+
const handlePointerMove = (event) => {
|
|
95
|
+
if (!isDragging.current) return;
|
|
96
|
+
setDesiredWidth(resolveDragWidth({
|
|
97
|
+
clientX: event.clientX,
|
|
98
|
+
isRtl: document.documentElement.dir === "rtl",
|
|
99
|
+
viewportWidth: window.innerWidth
|
|
100
|
+
}));
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Ends the drag however the pointer stream ended. `pointerup` is only one of
|
|
104
|
+
* the ways: a touch taken over by scrolling, a context menu, or the handle
|
|
105
|
+
* losing capture mid-drag all end it without one, and a drag flag left set
|
|
106
|
+
* would resize the pane on the next pointer movement with no button held.
|
|
107
|
+
*/
|
|
108
|
+
const endDrag = (event) => {
|
|
109
|
+
isDragging.current = false;
|
|
110
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
|
|
111
|
+
};
|
|
112
|
+
const handleKeyDown = (event) => {
|
|
113
|
+
const next = resolveKeyboardWidth({
|
|
114
|
+
currentWidth: desiredWidth,
|
|
115
|
+
isRtl: document.documentElement.dir === "rtl",
|
|
116
|
+
key: event.key
|
|
117
|
+
});
|
|
118
|
+
if (next === null) return;
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
setDesiredWidth(Math.min(Math.max(next, 320), 800));
|
|
121
|
+
};
|
|
122
|
+
const resetWidth = () => {
|
|
123
|
+
setDesiredWidth(512);
|
|
124
|
+
};
|
|
125
|
+
const width = resolveInspectorPaneWidth({
|
|
126
|
+
desiredWidth,
|
|
127
|
+
sidebarWidth,
|
|
128
|
+
viewportWidth
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
resetWidth,
|
|
132
|
+
resizeHandleProps: {
|
|
133
|
+
"aria-orientation": "vertical",
|
|
134
|
+
"aria-valuemax": 800,
|
|
135
|
+
"aria-valuemin": 320,
|
|
136
|
+
"aria-valuenow": width,
|
|
137
|
+
onKeyDown: handleKeyDown,
|
|
138
|
+
onLostPointerCapture: endDrag,
|
|
139
|
+
onPointerCancel: endDrag,
|
|
140
|
+
onPointerDown: handlePointerDown,
|
|
141
|
+
onPointerMove: handlePointerMove,
|
|
142
|
+
onPointerUp: endDrag,
|
|
143
|
+
tabIndex: 0
|
|
144
|
+
},
|
|
145
|
+
width
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
//#endregion
|
|
149
|
+
export { INSPECTOR_PANE_KEYBOARD_PAGE_STEP, INSPECTOR_PANE_KEYBOARD_STEP, parsePersistedPaneWidth, readStoredWidth, resolveDragWidth, resolveKeyboardWidth, useInspectorPaneWidth, writeStoredWidth };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as React$1 from "react";
|
|
2
|
+
//#region src/lib/button-disposition.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* How a button expresses "unavailable".
|
|
5
|
+
*
|
|
6
|
+
* `buttonVariants` carries `disabled:pointer-events-none`, and the browser
|
|
7
|
+
* suppresses pointer and focus events on a natively disabled control anyway, so
|
|
8
|
+
* a disabled button never receives hover or focus. A tooltip attached to it can
|
|
9
|
+
* therefore never open, which is exactly when the user most needs to be told
|
|
10
|
+
* why the action is unavailable. When there is a tooltip to show, the button
|
|
11
|
+
* switches to the ARIA accessible-disabled pattern instead: it stays in the tab
|
|
12
|
+
* order and keeps receiving pointer events, while every activation path is
|
|
13
|
+
* blocked (see `blockDisabledActivation`).
|
|
14
|
+
*
|
|
15
|
+
* This mirrors Base UI's own `focusableWhenDisabled` disposition
|
|
16
|
+
* (`useFocusableWhenDisabled`): `aria-disabled` instead of `disabled`, focusable,
|
|
17
|
+
* and non-`Tab` key defaults suppressed.
|
|
18
|
+
*/
|
|
19
|
+
declare const BUTTON_DISPOSITION: {
|
|
20
|
+
readonly enabled: "enabled";
|
|
21
|
+
/** Native `disabled`: out of the tab order, inert to pointer and keyboard. */
|
|
22
|
+
readonly nativeDisabled: "native-disabled";
|
|
23
|
+
/** `aria-disabled`: focusable and hoverable so its tooltip can still open. */
|
|
24
|
+
readonly accessibleDisabled: "accessible-disabled";
|
|
25
|
+
};
|
|
26
|
+
type ButtonDisposition = (typeof BUTTON_DISPOSITION)[keyof typeof BUTTON_DISPOSITION];
|
|
27
|
+
type ButtonDispositionInput = {
|
|
28
|
+
disabled: boolean | undefined;
|
|
29
|
+
loading: boolean | undefined;
|
|
30
|
+
/** Resolved tooltip content, i.e. what `renderTooltipTrigger` would mount. */
|
|
31
|
+
tooltip: React$1.ReactNode;
|
|
32
|
+
};
|
|
33
|
+
declare const resolveButtonDisposition: ({ disabled, loading, tooltip }: ButtonDispositionInput) => ButtonDisposition;
|
|
34
|
+
/**
|
|
35
|
+
* The parts of a React synthetic event these blockers touch.
|
|
36
|
+
*
|
|
37
|
+
* `preventBaseUIHandler` is added by Base UI's `mergeProps` to the rightmost
|
|
38
|
+
* handler in a merged chain; calling it stops the handlers merged to its left,
|
|
39
|
+
* which is how a caller's own `onClick` is suppressed.
|
|
40
|
+
*/
|
|
41
|
+
type BlockableEvent = {
|
|
42
|
+
preventDefault: () => void;
|
|
43
|
+
preventBaseUIHandler?: (() => void) | undefined;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Swallow an activation on an `aria-disabled` control.
|
|
47
|
+
*
|
|
48
|
+
* `preventDefault()` is what keeps a `type="submit"` button from submitting its
|
|
49
|
+
* form: the browser's form-submission default runs off the click event, and
|
|
50
|
+
* implicit submission (Enter in a text field) dispatches a click at the form's
|
|
51
|
+
* default button, so both paths funnel through here.
|
|
52
|
+
*/
|
|
53
|
+
declare const blockDisabledActivation: (event: BlockableEvent) => void;
|
|
54
|
+
/**
|
|
55
|
+
* The keyboard counterpart. `Tab` keeps its default so focus can leave the
|
|
56
|
+
* control; suppressing the default on `Enter`/`Space` stops the browser from
|
|
57
|
+
* synthesizing the click that would activate or submit.
|
|
58
|
+
*/
|
|
59
|
+
declare const blockDisabledKeyActivation: (event: BlockableEvent & {
|
|
60
|
+
key: string;
|
|
61
|
+
}) => void;
|
|
62
|
+
//#endregion
|
|
63
|
+
export { BUTTON_DISPOSITION, ButtonDisposition, blockDisabledActivation, blockDisabledKeyActivation, resolveButtonDisposition };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { hasTooltipContent } from "./tooltip-content.js";
|
|
2
|
+
//#region src/lib/button-disposition.ts
|
|
3
|
+
/**
|
|
4
|
+
* How a button expresses "unavailable".
|
|
5
|
+
*
|
|
6
|
+
* `buttonVariants` carries `disabled:pointer-events-none`, and the browser
|
|
7
|
+
* suppresses pointer and focus events on a natively disabled control anyway, so
|
|
8
|
+
* a disabled button never receives hover or focus. A tooltip attached to it can
|
|
9
|
+
* therefore never open, which is exactly when the user most needs to be told
|
|
10
|
+
* why the action is unavailable. When there is a tooltip to show, the button
|
|
11
|
+
* switches to the ARIA accessible-disabled pattern instead: it stays in the tab
|
|
12
|
+
* order and keeps receiving pointer events, while every activation path is
|
|
13
|
+
* blocked (see `blockDisabledActivation`).
|
|
14
|
+
*
|
|
15
|
+
* This mirrors Base UI's own `focusableWhenDisabled` disposition
|
|
16
|
+
* (`useFocusableWhenDisabled`): `aria-disabled` instead of `disabled`, focusable,
|
|
17
|
+
* and non-`Tab` key defaults suppressed.
|
|
18
|
+
*/
|
|
19
|
+
const BUTTON_DISPOSITION = {
|
|
20
|
+
enabled: "enabled",
|
|
21
|
+
/** Native `disabled`: out of the tab order, inert to pointer and keyboard. */
|
|
22
|
+
nativeDisabled: "native-disabled",
|
|
23
|
+
/** `aria-disabled`: focusable and hoverable so its tooltip can still open. */
|
|
24
|
+
accessibleDisabled: "accessible-disabled"
|
|
25
|
+
};
|
|
26
|
+
const resolveButtonDisposition = ({ disabled, loading, tooltip }) => {
|
|
27
|
+
if (disabled !== true && loading !== true) return BUTTON_DISPOSITION.enabled;
|
|
28
|
+
return hasTooltipContent(tooltip) ? BUTTON_DISPOSITION.accessibleDisabled : BUTTON_DISPOSITION.nativeDisabled;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Swallow an activation on an `aria-disabled` control.
|
|
32
|
+
*
|
|
33
|
+
* `preventDefault()` is what keeps a `type="submit"` button from submitting its
|
|
34
|
+
* form: the browser's form-submission default runs off the click event, and
|
|
35
|
+
* implicit submission (Enter in a text field) dispatches a click at the form's
|
|
36
|
+
* default button, so both paths funnel through here.
|
|
37
|
+
*/
|
|
38
|
+
const blockDisabledActivation = (event) => {
|
|
39
|
+
event.preventBaseUIHandler?.();
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The keyboard counterpart. `Tab` keeps its default so focus can leave the
|
|
44
|
+
* control; suppressing the default on `Enter`/`Space` stops the browser from
|
|
45
|
+
* synthesizing the click that would activate or submit.
|
|
46
|
+
*/
|
|
47
|
+
const blockDisabledKeyActivation = (event) => {
|
|
48
|
+
event.preventBaseUIHandler?.();
|
|
49
|
+
if (event.key !== "Tab") event.preventDefault();
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
export { BUTTON_DISPOSITION, blockDisabledActivation, blockDisabledKeyActivation, resolveButtonDisposition };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/lib/overlay-layer.d.ts
|
|
2
|
+
declare const OVERLAY_LAYER_CLASS_NAMES: {
|
|
3
|
+
/** Docked composer, suggestion chips, and other in-pane floating chrome. */
|
|
4
|
+
readonly chrome: "z-[80]";
|
|
5
|
+
/** Dialogs, sheets, popovers, tooltips: above chrome, below toasts. */
|
|
6
|
+
readonly default: "z-[85]";
|
|
7
|
+
/** Search sits a hair above a plain dialog so its own popup wins. */
|
|
8
|
+
readonly search: "z-[86]";
|
|
9
|
+
/**
|
|
10
|
+
* Transient popups — menu, select, combobox, context menus. Above everything
|
|
11
|
+
* including the toast viewport (z-[90]), because any of them can be opened
|
|
12
|
+
* *from* a dialog, a popover or a toast action and must cover its opener.
|
|
13
|
+
* They sat at z-50 alongside dialogs, so which one won was decided by portal
|
|
14
|
+
* mount order: a select inside a filter popover rendered behind it.
|
|
15
|
+
*/
|
|
16
|
+
readonly popup: "z-[100]";
|
|
17
|
+
/** Historical alias for `popup`; Search's own menus use the same band. */
|
|
18
|
+
readonly "search-child": "z-[100]";
|
|
19
|
+
};
|
|
20
|
+
type OverlayLayer = keyof typeof OVERLAY_LAYER_CLASS_NAMES;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { OVERLAY_LAYER_CLASS_NAMES, OverlayLayer };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/lib/overlay-layer.ts
|
|
2
|
+
const OVERLAY_LAYER_CLASS_NAMES = {
|
|
3
|
+
/** Docked composer, suggestion chips, and other in-pane floating chrome. */
|
|
4
|
+
chrome: "z-[80]",
|
|
5
|
+
/** Dialogs, sheets, popovers, tooltips: above chrome, below toasts. */
|
|
6
|
+
default: "z-[85]",
|
|
7
|
+
/** Search sits a hair above a plain dialog so its own popup wins. */
|
|
8
|
+
search: "z-[86]",
|
|
9
|
+
/**
|
|
10
|
+
* Transient popups — menu, select, combobox, context menus. Above everything
|
|
11
|
+
* including the toast viewport (z-[90]), because any of them can be opened
|
|
12
|
+
* *from* a dialog, a popover or a toast action and must cover its opener.
|
|
13
|
+
* They sat at z-50 alongside dialogs, so which one won was decided by portal
|
|
14
|
+
* mount order: a select inside a filter popover rendered behind it.
|
|
15
|
+
*/
|
|
16
|
+
popup: "z-[100]",
|
|
17
|
+
/** Historical alias for `popup`; Search's own menus use the same band. */
|
|
18
|
+
"search-child": "z-[100]"
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
export { OVERLAY_LAYER_CLASS_NAMES };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/lib/tab-order.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether a committed tab list holds different elements, or the same elements
|
|
4
|
+
* in a different order, than the previous commit.
|
|
5
|
+
*
|
|
6
|
+
* Base UI measures the tabs indicator during render, so it reads the DOM as it
|
|
7
|
+
* stood before the commit. Its correction is a `ResizeObserver`, which catches
|
|
8
|
+
* an insertion or a removal (the list's own width changes) but not a reorder:
|
|
9
|
+
* there the tabs swap places without anything changing size. Detecting that is
|
|
10
|
+
* the whole of the workaround, so compare identity rather than count, because
|
|
11
|
+
* React moves the same nodes when it reorders keyed children.
|
|
12
|
+
*/
|
|
13
|
+
declare const hasTabOrderChanged: <T>(previous: readonly T[], next: readonly T[]) => boolean;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { hasTabOrderChanged };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/lib/tab-order.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether a committed tab list holds different elements, or the same elements
|
|
4
|
+
* in a different order, than the previous commit.
|
|
5
|
+
*
|
|
6
|
+
* Base UI measures the tabs indicator during render, so it reads the DOM as it
|
|
7
|
+
* stood before the commit. Its correction is a `ResizeObserver`, which catches
|
|
8
|
+
* an insertion or a removal (the list's own width changes) but not a reorder:
|
|
9
|
+
* there the tabs swap places without anything changing size. Detecting that is
|
|
10
|
+
* the whole of the workaround, so compare identity rather than count, because
|
|
11
|
+
* React moves the same nodes when it reorders keyed children.
|
|
12
|
+
*/
|
|
13
|
+
const hasTabOrderChanged = (previous, next) => previous.length !== next.length || next.some((tab, index) => tab !== previous[index]);
|
|
14
|
+
//#endregion
|
|
15
|
+
export { hasTabOrderChanged };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React$1 from "react";
|
|
2
|
+
//#region src/lib/tooltip-content.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Whether a tooltip value would actually render a popup.
|
|
5
|
+
*
|
|
6
|
+
* Single source of truth for "there is a tooltip": `renderTooltipTrigger`
|
|
7
|
+
* decides whether to mount one, and `resolveButtonDisposition` decides whether
|
|
8
|
+
* a disabled button has to stay hoverable so that popup can open. Two copies of
|
|
9
|
+
* this predicate would drift into a button that suppresses its own tooltip.
|
|
10
|
+
*/
|
|
11
|
+
declare const hasTooltipContent: (content: React$1.ReactNode) => boolean;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { hasTooltipContent };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/lib/tooltip-content.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether a tooltip value would actually render a popup.
|
|
4
|
+
*
|
|
5
|
+
* Single source of truth for "there is a tooltip": `renderTooltipTrigger`
|
|
6
|
+
* decides whether to mount one, and `resolveButtonDisposition` decides whether
|
|
7
|
+
* a disabled button has to stay hoverable so that popup can open. Two copies of
|
|
8
|
+
* this predicate would drift into a button that suppresses its own tooltip.
|
|
9
|
+
*/
|
|
10
|
+
const hasTooltipContent = (content) => content !== void 0 && content !== null && content !== "" && content !== false;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { hasTooltipContent };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ClassValue } from "clsx";
|
|
2
|
+
import * as React$1 from "react";
|
|
3
|
+
//#region src/lib/utils.d.ts
|
|
4
|
+
declare function cn(...inputs: ClassValue[]): string;
|
|
5
|
+
/**
|
|
6
|
+
* Compose multiple refs into a single ref callback.
|
|
7
|
+
* Adapted from `@radix-ui/react-compose-refs` (MIT).
|
|
8
|
+
*
|
|
9
|
+
* Collects cleanup functions returned by React 19 ref
|
|
10
|
+
* callbacks and returns a combined cleanup so React can
|
|
11
|
+
* invoke it on unmount instead of re-calling with `null`.
|
|
12
|
+
*
|
|
13
|
+
* Use this rather than merging refs inline: forwarding a
|
|
14
|
+
* caller's return value verbatim makes React read whatever
|
|
15
|
+
* their callback happened to return as a cleanup, and warn
|
|
16
|
+
* when it is neither a function nor `undefined`.
|
|
17
|
+
*/
|
|
18
|
+
declare const composeRefs: <T>(...refs: (React$1.Ref<T> | undefined)[]) => ((node: T | null) => (() => void) | undefined);
|
|
19
|
+
//#endregion
|
|
20
|
+
export { cn, composeRefs };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { clsx } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
//#region src/lib/utils.ts
|
|
4
|
+
function cn(...inputs) {
|
|
5
|
+
return twMerge(clsx(inputs));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Compose multiple refs into a single ref callback.
|
|
9
|
+
* Adapted from `@radix-ui/react-compose-refs` (MIT).
|
|
10
|
+
*
|
|
11
|
+
* Collects cleanup functions returned by React 19 ref
|
|
12
|
+
* callbacks and returns a combined cleanup so React can
|
|
13
|
+
* invoke it on unmount instead of re-calling with `null`.
|
|
14
|
+
*
|
|
15
|
+
* Use this rather than merging refs inline: forwarding a
|
|
16
|
+
* caller's return value verbatim makes React read whatever
|
|
17
|
+
* their callback happened to return as a cleanup, and warn
|
|
18
|
+
* when it is neither a function nor `undefined`.
|
|
19
|
+
*/
|
|
20
|
+
const composeRefs = (...refs) => (node) => {
|
|
21
|
+
const cleanups = [];
|
|
22
|
+
for (const ref of refs) if (typeof ref === "function") {
|
|
23
|
+
const cleanup = ref(node);
|
|
24
|
+
if (typeof cleanup === "function") cleanups.push(() => {
|
|
25
|
+
cleanup();
|
|
26
|
+
});
|
|
27
|
+
else if (node !== null) cleanups.push(() => {
|
|
28
|
+
ref(null);
|
|
29
|
+
});
|
|
30
|
+
} else if (ref !== void 0 && ref !== null) {
|
|
31
|
+
ref.current = node;
|
|
32
|
+
if (node !== null) cleanups.push(() => {
|
|
33
|
+
ref.current = null;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (cleanups.length > 0) return () => {
|
|
37
|
+
for (const cleanup of cleanups) cleanup();
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
export { cn, composeRefs };
|