@kushagradhawan/kookie-ui 0.1.73 → 0.1.74
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/cjs/components/_internal/shell-bottom.d.ts.map +1 -1
- package/dist/cjs/components/_internal/shell-bottom.js +1 -1
- package/dist/cjs/components/_internal/shell-bottom.js.map +3 -3
- package/dist/cjs/components/_internal/shell-handles.d.ts.map +1 -1
- package/dist/cjs/components/_internal/shell-handles.js +1 -1
- package/dist/cjs/components/_internal/shell-handles.js.map +3 -3
- package/dist/cjs/components/_internal/shell-inspector.d.ts.map +1 -1
- package/dist/cjs/components/_internal/shell-inspector.js +1 -1
- package/dist/cjs/components/_internal/shell-inspector.js.map +3 -3
- package/dist/cjs/components/_internal/shell-sidebar.d.ts.map +1 -1
- package/dist/cjs/components/_internal/shell-sidebar.js +1 -1
- package/dist/cjs/components/_internal/shell-sidebar.js.map +3 -3
- package/dist/cjs/components/shell.d.ts.map +1 -1
- package/dist/cjs/components/shell.js +1 -1
- package/dist/cjs/components/shell.js.map +3 -3
- package/dist/cjs/helpers/index.d.ts +1 -0
- package/dist/cjs/helpers/index.d.ts.map +1 -1
- package/dist/cjs/helpers/index.js +1 -1
- package/dist/cjs/helpers/index.js.map +2 -2
- package/dist/cjs/helpers/normalize-to-px.d.ts +10 -0
- package/dist/cjs/helpers/normalize-to-px.d.ts.map +1 -0
- package/dist/cjs/helpers/normalize-to-px.js +2 -0
- package/dist/cjs/helpers/normalize-to-px.js.map +7 -0
- package/dist/esm/components/_internal/shell-bottom.d.ts.map +1 -1
- package/dist/esm/components/_internal/shell-bottom.js +1 -1
- package/dist/esm/components/_internal/shell-bottom.js.map +3 -3
- package/dist/esm/components/_internal/shell-handles.d.ts.map +1 -1
- package/dist/esm/components/_internal/shell-handles.js +1 -1
- package/dist/esm/components/_internal/shell-handles.js.map +3 -3
- package/dist/esm/components/_internal/shell-inspector.d.ts.map +1 -1
- package/dist/esm/components/_internal/shell-inspector.js +1 -1
- package/dist/esm/components/_internal/shell-inspector.js.map +3 -3
- package/dist/esm/components/_internal/shell-sidebar.d.ts.map +1 -1
- package/dist/esm/components/_internal/shell-sidebar.js +1 -1
- package/dist/esm/components/_internal/shell-sidebar.js.map +3 -3
- package/dist/esm/components/shell.d.ts.map +1 -1
- package/dist/esm/components/shell.js +1 -1
- package/dist/esm/components/shell.js.map +3 -3
- package/dist/esm/helpers/index.d.ts +1 -0
- package/dist/esm/helpers/index.d.ts.map +1 -1
- package/dist/esm/helpers/index.js +1 -1
- package/dist/esm/helpers/index.js.map +2 -2
- package/dist/esm/helpers/normalize-to-px.d.ts +10 -0
- package/dist/esm/helpers/normalize-to-px.d.ts.map +1 -0
- package/dist/esm/helpers/normalize-to-px.js +2 -0
- package/dist/esm/helpers/normalize-to-px.js.map +7 -0
- package/package.json +1 -1
- package/schemas/base-button.json +1 -1
- package/schemas/button.json +1 -1
- package/schemas/icon-button.json +1 -1
- package/schemas/index.json +6 -6
- package/schemas/toggle-button.json +1 -1
- package/schemas/toggle-icon-button.json +1 -1
- package/src/components/_internal/shell-bottom.tsx +32 -28
- package/src/components/_internal/shell-handles.tsx +6 -1
- package/src/components/_internal/shell-inspector.tsx +32 -28
- package/src/components/_internal/shell-sidebar.tsx +31 -28
- package/src/components/shell.tsx +37 -30
- package/src/helpers/index.ts +1 -0
- package/src/helpers/normalize-to-px.ts +42 -0
|
@@ -321,6 +321,6 @@
|
|
|
321
321
|
"title": "Toggle-icon-button Component Props",
|
|
322
322
|
"description": "Props schema for the toggle-icon-button component in Kookie UI",
|
|
323
323
|
"version": "1.0.0",
|
|
324
|
-
"generatedAt": "2025-12-
|
|
324
|
+
"generatedAt": "2025-12-23T12:04:32.722Z",
|
|
325
325
|
"source": "Zod schema"
|
|
326
326
|
}
|
|
@@ -9,6 +9,7 @@ import { BottomHandle, PaneHandle } from './shell-handles.js';
|
|
|
9
9
|
import { _BREAKPOINTS } from '../shell.types.js';
|
|
10
10
|
import type { Breakpoint, PaneMode, PaneSizePersistence, ResponsivePresentation, PaneBaseProps } from '../shell.types.js';
|
|
11
11
|
import { extractPaneDomProps, mapResponsiveBooleanToPaneMode } from './shell-prop-helpers.js';
|
|
12
|
+
import { normalizeToPx } from '../../helpers/normalize-to-px.js';
|
|
12
13
|
|
|
13
14
|
type BottomOpenChangeMeta = { reason: 'init' | 'toggle' | 'responsive' };
|
|
14
15
|
type BottomControlledProps = { open: boolean | Partial<Record<Breakpoint, boolean>>; onOpenChange?: (open: boolean, meta: BottomOpenChangeMeta) => void; defaultOpen?: never };
|
|
@@ -107,17 +108,29 @@ export const Bottom = React.forwardRef<HTMLDivElement, BottomPublicProps>((initi
|
|
|
107
108
|
},
|
|
108
109
|
});
|
|
109
110
|
|
|
111
|
+
// Ref for debounce cleanup
|
|
112
|
+
const debounceTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
113
|
+
// Cleanup debounce timeout on unmount or when dependencies change
|
|
114
|
+
React.useEffect(() => {
|
|
115
|
+
return () => {
|
|
116
|
+
if (debounceTimeoutRef.current) {
|
|
117
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
118
|
+
debounceTimeoutRef.current = null;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}, [onSizeChange, sizeUpdate, sizeUpdateMs]);
|
|
122
|
+
// Throttled/debounced emitter for onSizeChange
|
|
110
123
|
const emitSizeChange = React.useMemo(() => {
|
|
111
124
|
const cb = onSizeChange as undefined | ((s: number, meta: BottomSizeChangeMeta) => void);
|
|
112
125
|
const strategy = sizeUpdate as undefined | 'throttle' | 'debounce';
|
|
113
126
|
const ms = sizeUpdateMs ?? 50;
|
|
114
127
|
if (!cb) return () => {};
|
|
115
128
|
if (strategy === 'debounce') {
|
|
116
|
-
let t: any = null;
|
|
117
129
|
return (s: number, meta: BottomSizeChangeMeta) => {
|
|
118
|
-
if (
|
|
119
|
-
|
|
130
|
+
if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current);
|
|
131
|
+
debounceTimeoutRef.current = setTimeout(() => {
|
|
120
132
|
cb(s, meta);
|
|
133
|
+
debounceTimeoutRef.current = null;
|
|
121
134
|
}, ms);
|
|
122
135
|
};
|
|
123
136
|
}
|
|
@@ -175,6 +188,14 @@ export const Bottom = React.forwardRef<HTMLDivElement, BottomPublicProps>((initi
|
|
|
175
188
|
// Track previous mode to only fire callbacks on actual user-initiated state transitions.
|
|
176
189
|
// We wait for breakpointReady to ensure the initial state sync from useResponsiveInitialState
|
|
177
190
|
// is complete before enabling callbacks. This avoids spurious callbacks during initialization.
|
|
191
|
+
// Use callback refs to avoid re-running effect when inline callbacks change.
|
|
192
|
+
const onExpandRef = React.useRef(onExpand);
|
|
193
|
+
const onCollapseRef = React.useRef(onCollapse);
|
|
194
|
+
React.useLayoutEffect(() => {
|
|
195
|
+
onExpandRef.current = onExpand;
|
|
196
|
+
onCollapseRef.current = onCollapse;
|
|
197
|
+
});
|
|
198
|
+
|
|
178
199
|
const prevBottomModeRef = React.useRef<PaneMode | null>(null);
|
|
179
200
|
const hasInitializedRef = React.useRef(false);
|
|
180
201
|
React.useEffect(() => {
|
|
@@ -198,13 +219,13 @@ export const Bottom = React.forwardRef<HTMLDivElement, BottomPublicProps>((initi
|
|
|
198
219
|
// Only fire on actual state transitions
|
|
199
220
|
if (prevMode !== null && prevMode !== currentMode) {
|
|
200
221
|
if (currentMode === 'expanded') {
|
|
201
|
-
|
|
222
|
+
onExpandRef.current?.();
|
|
202
223
|
} else if (currentMode === 'collapsed') {
|
|
203
|
-
|
|
224
|
+
onCollapseRef.current?.();
|
|
204
225
|
}
|
|
205
226
|
prevBottomModeRef.current = currentMode;
|
|
206
227
|
}
|
|
207
|
-
}, [shell.bottomMode, shell.currentBreakpointReady
|
|
228
|
+
}, [shell.bottomMode, shell.currentBreakpointReady]);
|
|
208
229
|
|
|
209
230
|
const isExpanded = shell.bottomMode === 'expanded';
|
|
210
231
|
|
|
@@ -297,31 +318,14 @@ export const Bottom = React.forwardRef<HTMLDivElement, BottomPublicProps>((initi
|
|
|
297
318
|
) : null;
|
|
298
319
|
|
|
299
320
|
// Strip control/size props from DOM spread (moved above overlay return to keep hook order consistent)
|
|
300
|
-
// Normalize CSS lengths to px
|
|
301
|
-
const
|
|
302
|
-
if (value == null) return undefined;
|
|
303
|
-
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
304
|
-
const str = String(value).trim();
|
|
305
|
-
if (!str) return undefined;
|
|
306
|
-
if (str.endsWith('px')) return Number.parseFloat(str);
|
|
307
|
-
if (str.endsWith('rem')) {
|
|
308
|
-
const rem = Number.parseFloat(getComputedStyle(document.documentElement).fontSize || '16') || 16;
|
|
309
|
-
return Number.parseFloat(str) * rem;
|
|
310
|
-
}
|
|
311
|
-
if (str.endsWith('%')) {
|
|
312
|
-
const pct = Number.parseFloat(str);
|
|
313
|
-
const base = document.documentElement.clientHeight || window.innerHeight || 0;
|
|
314
|
-
return (pct / 100) * base;
|
|
315
|
-
}
|
|
316
|
-
const n = Number.parseFloat(str);
|
|
317
|
-
return Number.isFinite(n) ? n : undefined;
|
|
318
|
-
}, []);
|
|
321
|
+
// Normalize CSS lengths to px helper
|
|
322
|
+
const normalizeSizeToPx = React.useCallback((value: number | string | undefined) => normalizeToPx(value, 'vertical'), []);
|
|
319
323
|
|
|
320
324
|
// Apply defaultSize on mount when uncontrolled (moved above overlay return)
|
|
321
325
|
React.useEffect(() => {
|
|
322
326
|
if (!localRef.current) return;
|
|
323
327
|
if (typeof size === 'undefined' && typeof defaultSize !== 'undefined') {
|
|
324
|
-
const px =
|
|
328
|
+
const px = normalizeSizeToPx(defaultSize);
|
|
325
329
|
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
326
330
|
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
327
331
|
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
@@ -338,7 +342,7 @@ export const Bottom = React.forwardRef<HTMLDivElement, BottomPublicProps>((initi
|
|
|
338
342
|
React.useEffect(() => {
|
|
339
343
|
if (!localRef.current) return;
|
|
340
344
|
if (typeof controlledSize === 'undefined') return;
|
|
341
|
-
const px =
|
|
345
|
+
const px = normalizeSizeToPx(controlledSize);
|
|
342
346
|
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
343
347
|
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
344
348
|
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
@@ -346,7 +350,7 @@ export const Bottom = React.forwardRef<HTMLDivElement, BottomPublicProps>((initi
|
|
|
346
350
|
localRef.current.style.setProperty('--bottom-size', `${clamped}px`);
|
|
347
351
|
emitSizeChange(clamped, { reason: 'controlled' });
|
|
348
352
|
}
|
|
349
|
-
}, [controlledSize, minSize, maxSize,
|
|
353
|
+
}, [controlledSize, minSize, maxSize, normalizeSizeToPx, emitSizeChange]);
|
|
350
354
|
|
|
351
355
|
if (isOverlay) {
|
|
352
356
|
const open = shell.bottomMode === 'expanded';
|
|
@@ -19,7 +19,7 @@ export const PaneHandle = React.forwardRef<HTMLDivElement, React.ComponentPropsW
|
|
|
19
19
|
snapTolerance,
|
|
20
20
|
collapseThreshold,
|
|
21
21
|
collapsible,
|
|
22
|
-
target
|
|
22
|
+
target,
|
|
23
23
|
requestCollapse,
|
|
24
24
|
requestToggle,
|
|
25
25
|
} = usePaneResize();
|
|
@@ -37,6 +37,10 @@ export const PaneHandle = React.forwardRef<HTMLDivElement, React.ComponentPropsW
|
|
|
37
37
|
|
|
38
38
|
const ariaOrientation = orientation;
|
|
39
39
|
|
|
40
|
+
// Generate accessible label from target
|
|
41
|
+
const targetLabel = target.charAt(0).toUpperCase() + target.slice(1);
|
|
42
|
+
const ariaLabel = `Resize ${targetLabel} pane`;
|
|
43
|
+
|
|
40
44
|
return (
|
|
41
45
|
<div
|
|
42
46
|
{...props}
|
|
@@ -45,6 +49,7 @@ export const PaneHandle = React.forwardRef<HTMLDivElement, React.ComponentPropsW
|
|
|
45
49
|
data-orientation={orientation}
|
|
46
50
|
data-edge={edge}
|
|
47
51
|
role="slider"
|
|
52
|
+
aria-label={ariaLabel}
|
|
48
53
|
aria-orientation={ariaOrientation}
|
|
49
54
|
aria-valuemin={minSize}
|
|
50
55
|
aria-valuemax={maxSize}
|
|
@@ -9,6 +9,7 @@ import { InspectorHandle, PaneHandle } from './shell-handles.js';
|
|
|
9
9
|
import { _BREAKPOINTS } from '../shell.types.js';
|
|
10
10
|
import type { Breakpoint, PaneMode, PaneSizePersistence, ResponsivePresentation, PaneBaseProps } from '../shell.types.js';
|
|
11
11
|
import { extractPaneDomProps, mapResponsiveBooleanToPaneMode } from './shell-prop-helpers.js';
|
|
12
|
+
import { normalizeToPx } from '../../helpers/normalize-to-px.js';
|
|
12
13
|
|
|
13
14
|
type InspectorOpenChangeMeta = { reason: 'init' | 'toggle' | 'responsive' };
|
|
14
15
|
type InspectorControlledProps = { open: boolean | Partial<Record<Breakpoint, boolean>>; onOpenChange?: (open: boolean, meta: InspectorOpenChangeMeta) => void; defaultOpen?: never };
|
|
@@ -107,17 +108,29 @@ export const Inspector = React.forwardRef<HTMLDivElement, InspectorPublicProps>(
|
|
|
107
108
|
},
|
|
108
109
|
});
|
|
109
110
|
|
|
111
|
+
// Ref for debounce cleanup
|
|
112
|
+
const debounceTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
113
|
+
// Cleanup debounce timeout on unmount or when dependencies change
|
|
114
|
+
React.useEffect(() => {
|
|
115
|
+
return () => {
|
|
116
|
+
if (debounceTimeoutRef.current) {
|
|
117
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
118
|
+
debounceTimeoutRef.current = null;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}, [onSizeChange, sizeUpdate, sizeUpdateMs]);
|
|
122
|
+
// Throttled/debounced emitter for onSizeChange
|
|
110
123
|
const emitSizeChange = React.useMemo(() => {
|
|
111
124
|
const cb = onSizeChange as undefined | ((s: number, meta: InspectorSizeChangeMeta) => void);
|
|
112
125
|
const strategy = sizeUpdate as undefined | 'throttle' | 'debounce';
|
|
113
126
|
const ms = sizeUpdateMs ?? 50;
|
|
114
127
|
if (!cb) return () => {};
|
|
115
128
|
if (strategy === 'debounce') {
|
|
116
|
-
let t: any = null;
|
|
117
129
|
return (s: number, meta: InspectorSizeChangeMeta) => {
|
|
118
|
-
if (
|
|
119
|
-
|
|
130
|
+
if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current);
|
|
131
|
+
debounceTimeoutRef.current = setTimeout(() => {
|
|
120
132
|
cb(s, meta);
|
|
133
|
+
debounceTimeoutRef.current = null;
|
|
121
134
|
}, ms);
|
|
122
135
|
};
|
|
123
136
|
}
|
|
@@ -176,6 +189,14 @@ export const Inspector = React.forwardRef<HTMLDivElement, InspectorPublicProps>(
|
|
|
176
189
|
// Track previous mode to only fire callbacks on actual user-initiated state transitions.
|
|
177
190
|
// We wait for breakpointReady to ensure the initial state sync from useResponsiveInitialState
|
|
178
191
|
// is complete before enabling callbacks. This avoids spurious callbacks during initialization.
|
|
192
|
+
// Use callback refs to avoid re-running effect when inline callbacks change.
|
|
193
|
+
const onExpandRef = React.useRef(onExpand);
|
|
194
|
+
const onCollapseRef = React.useRef(onCollapse);
|
|
195
|
+
React.useLayoutEffect(() => {
|
|
196
|
+
onExpandRef.current = onExpand;
|
|
197
|
+
onCollapseRef.current = onCollapse;
|
|
198
|
+
});
|
|
199
|
+
|
|
179
200
|
const prevInspectorModeRef = React.useRef<PaneMode | null>(null);
|
|
180
201
|
const hasInitializedRef = React.useRef(false);
|
|
181
202
|
React.useEffect(() => {
|
|
@@ -199,13 +220,13 @@ export const Inspector = React.forwardRef<HTMLDivElement, InspectorPublicProps>(
|
|
|
199
220
|
// Only fire on actual state transitions
|
|
200
221
|
if (prevMode !== null && prevMode !== currentMode) {
|
|
201
222
|
if (currentMode === 'expanded') {
|
|
202
|
-
|
|
223
|
+
onExpandRef.current?.();
|
|
203
224
|
} else if (currentMode === 'collapsed') {
|
|
204
|
-
|
|
225
|
+
onCollapseRef.current?.();
|
|
205
226
|
}
|
|
206
227
|
prevInspectorModeRef.current = currentMode;
|
|
207
228
|
}
|
|
208
|
-
}, [shell.inspectorMode, shell.currentBreakpointReady
|
|
229
|
+
}, [shell.inspectorMode, shell.currentBreakpointReady]);
|
|
209
230
|
|
|
210
231
|
const isExpanded = shell.inspectorMode === 'expanded';
|
|
211
232
|
|
|
@@ -298,31 +319,14 @@ export const Inspector = React.forwardRef<HTMLDivElement, InspectorPublicProps>(
|
|
|
298
319
|
</PaneResizeContext.Provider>
|
|
299
320
|
) : null;
|
|
300
321
|
|
|
301
|
-
// Normalize CSS lengths to px
|
|
302
|
-
const
|
|
303
|
-
if (value == null) return undefined;
|
|
304
|
-
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
305
|
-
const str = String(value).trim();
|
|
306
|
-
if (!str) return undefined;
|
|
307
|
-
if (str.endsWith('px')) return Number.parseFloat(str);
|
|
308
|
-
if (str.endsWith('rem')) {
|
|
309
|
-
const rem = Number.parseFloat(getComputedStyle(document.documentElement).fontSize || '16') || 16;
|
|
310
|
-
return Number.parseFloat(str) * rem;
|
|
311
|
-
}
|
|
312
|
-
if (str.endsWith('%')) {
|
|
313
|
-
const pct = Number.parseFloat(str);
|
|
314
|
-
const base = document.documentElement.clientWidth || window.innerWidth || 0;
|
|
315
|
-
return (pct / 100) * base;
|
|
316
|
-
}
|
|
317
|
-
const n = Number.parseFloat(str);
|
|
318
|
-
return Number.isFinite(n) ? n : undefined;
|
|
319
|
-
}, []);
|
|
322
|
+
// Normalize CSS lengths to px helper
|
|
323
|
+
const normalizeSizeToPx = React.useCallback((value: number | string | undefined) => normalizeToPx(value, 'horizontal'), []);
|
|
320
324
|
|
|
321
325
|
// Apply defaultSize on mount when uncontrolled
|
|
322
326
|
React.useEffect(() => {
|
|
323
327
|
if (!localRef.current) return;
|
|
324
328
|
if (typeof size === 'undefined' && typeof defaultSize !== 'undefined') {
|
|
325
|
-
const px =
|
|
329
|
+
const px = normalizeSizeToPx(defaultSize);
|
|
326
330
|
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
327
331
|
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
328
332
|
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
@@ -339,7 +343,7 @@ export const Inspector = React.forwardRef<HTMLDivElement, InspectorPublicProps>(
|
|
|
339
343
|
React.useEffect(() => {
|
|
340
344
|
if (!localRef.current) return;
|
|
341
345
|
if (typeof controlledSize === 'undefined') return;
|
|
342
|
-
const px =
|
|
346
|
+
const px = normalizeSizeToPx(controlledSize);
|
|
343
347
|
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
344
348
|
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
345
349
|
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
@@ -347,7 +351,7 @@ export const Inspector = React.forwardRef<HTMLDivElement, InspectorPublicProps>(
|
|
|
347
351
|
localRef.current.style.setProperty('--inspector-size', `${clamped}px`);
|
|
348
352
|
emitSizeChange(clamped, { reason: 'controlled' });
|
|
349
353
|
}
|
|
350
|
-
}, [controlledSize, minSize, maxSize,
|
|
354
|
+
}, [controlledSize, minSize, maxSize, normalizeSizeToPx, emitSizeChange]);
|
|
351
355
|
|
|
352
356
|
if (isOverlay) {
|
|
353
357
|
const open = shell.inspectorMode === 'expanded';
|
|
@@ -9,6 +9,7 @@ import { extractPaneDomProps } from './shell-prop-helpers.js';
|
|
|
9
9
|
import { SidebarHandle, PaneHandle } from './shell-handles.js';
|
|
10
10
|
import type { Breakpoint, PaneMode, PaneSizePersistence, ResponsivePresentation, SidebarMode, Responsive, PaneBaseProps } from '../shell.types.js';
|
|
11
11
|
import { _BREAKPOINTS } from '../shell.types.js';
|
|
12
|
+
import { normalizeToPx } from '../../helpers/normalize-to-px.js';
|
|
12
13
|
|
|
13
14
|
type SidebarPaneProps = PaneBaseProps & {
|
|
14
15
|
mode?: PaneMode;
|
|
@@ -100,6 +101,17 @@ export const Sidebar = React.forwardRef<HTMLDivElement, SidebarPublicProps>((ini
|
|
|
100
101
|
const handleChildren = childArray.filter((el: React.ReactElement) => React.isValidElement(el) && el.type === SidebarHandle);
|
|
101
102
|
const contentChildren = childArray.filter((el: React.ReactElement) => !(React.isValidElement(el) && el.type === SidebarHandle));
|
|
102
103
|
|
|
104
|
+
// Ref for debounce cleanup
|
|
105
|
+
const debounceTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
106
|
+
// Cleanup debounce timeout on unmount or when dependencies change
|
|
107
|
+
React.useEffect(() => {
|
|
108
|
+
return () => {
|
|
109
|
+
if (debounceTimeoutRef.current) {
|
|
110
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
111
|
+
debounceTimeoutRef.current = null;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}, [onSizeChange, sizeUpdate, sizeUpdateMs]);
|
|
103
115
|
// Throttled/debounced emitter for onSizeChange
|
|
104
116
|
const emitSizeChange = React.useMemo(() => {
|
|
105
117
|
const cb = onSizeChange as undefined | ((s: number, meta: { reason: 'init' | 'resize' | 'controlled' }) => void);
|
|
@@ -107,11 +119,11 @@ export const Sidebar = React.forwardRef<HTMLDivElement, SidebarPublicProps>((ini
|
|
|
107
119
|
const ms = sizeUpdateMs ?? 50;
|
|
108
120
|
if (!cb) return () => {};
|
|
109
121
|
if (strategy === 'debounce') {
|
|
110
|
-
let t: any = null;
|
|
111
122
|
return (s: number, meta: { reason: 'init' | 'resize' | 'controlled' }) => {
|
|
112
|
-
if (
|
|
113
|
-
|
|
123
|
+
if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current);
|
|
124
|
+
debounceTimeoutRef.current = setTimeout(() => {
|
|
114
125
|
cb(s, meta);
|
|
126
|
+
debounceTimeoutRef.current = null;
|
|
115
127
|
}, ms);
|
|
116
128
|
};
|
|
117
129
|
}
|
|
@@ -191,6 +203,14 @@ export const Sidebar = React.forwardRef<HTMLDivElement, SidebarPublicProps>((ini
|
|
|
191
203
|
// Track previous mode to only fire callbacks on actual user-initiated state transitions.
|
|
192
204
|
// We wait for breakpointReady to ensure the initial state sync from useResponsiveInitialState
|
|
193
205
|
// is complete before enabling callbacks. This avoids spurious callbacks during initialization.
|
|
206
|
+
// Use callback refs to avoid re-running effect when inline callbacks change.
|
|
207
|
+
const onExpandRef = React.useRef(onExpand);
|
|
208
|
+
const onCollapseRef = React.useRef(onCollapse);
|
|
209
|
+
React.useLayoutEffect(() => {
|
|
210
|
+
onExpandRef.current = onExpand;
|
|
211
|
+
onCollapseRef.current = onCollapse;
|
|
212
|
+
});
|
|
213
|
+
|
|
194
214
|
const prevSidebarModeRef = React.useRef<SidebarMode | null>(null);
|
|
195
215
|
const hasInitializedRef = React.useRef(false);
|
|
196
216
|
React.useEffect(() => {
|
|
@@ -215,15 +235,15 @@ export const Sidebar = React.forwardRef<HTMLDivElement, SidebarPublicProps>((ini
|
|
|
215
235
|
if (prevMode !== null && prevMode !== currentMode) {
|
|
216
236
|
// onExpand: when becoming visible (collapsed → thin/expanded)
|
|
217
237
|
if (prevMode === 'collapsed' && currentMode !== 'collapsed') {
|
|
218
|
-
|
|
238
|
+
onExpandRef.current?.();
|
|
219
239
|
}
|
|
220
240
|
// onCollapse: when becoming hidden (any → collapsed)
|
|
221
241
|
else if (currentMode === 'collapsed') {
|
|
222
|
-
|
|
242
|
+
onCollapseRef.current?.();
|
|
223
243
|
}
|
|
224
244
|
prevSidebarModeRef.current = currentMode;
|
|
225
245
|
}
|
|
226
|
-
}, [shell.sidebarMode, shell.currentBreakpointReady
|
|
246
|
+
}, [shell.sidebarMode, shell.currentBreakpointReady]);
|
|
227
247
|
|
|
228
248
|
// Option A: thin is width-only; content remains visible whenever not collapsed
|
|
229
249
|
const isContentVisible = shell.sidebarMode !== 'collapsed';
|
|
@@ -348,31 +368,14 @@ export const Sidebar = React.forwardRef<HTMLDivElement, SidebarPublicProps>((ini
|
|
|
348
368
|
</PaneResizeContext.Provider>
|
|
349
369
|
) : null;
|
|
350
370
|
|
|
351
|
-
// Normalize CSS lengths to px
|
|
352
|
-
const
|
|
353
|
-
if (value == null) return undefined;
|
|
354
|
-
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
355
|
-
const str = String(value).trim();
|
|
356
|
-
if (!str) return undefined;
|
|
357
|
-
if (str.endsWith('px')) return Number.parseFloat(str);
|
|
358
|
-
if (str.endsWith('rem')) {
|
|
359
|
-
const rem = Number.parseFloat(getComputedStyle(document.documentElement).fontSize || '16') || 16;
|
|
360
|
-
return Number.parseFloat(str) * rem;
|
|
361
|
-
}
|
|
362
|
-
if (str.endsWith('%')) {
|
|
363
|
-
const pct = Number.parseFloat(str);
|
|
364
|
-
const base = document.documentElement.clientWidth || window.innerWidth || 0;
|
|
365
|
-
return (pct / 100) * base;
|
|
366
|
-
}
|
|
367
|
-
const n = Number.parseFloat(str);
|
|
368
|
-
return Number.isFinite(n) ? n : undefined;
|
|
369
|
-
}, []);
|
|
371
|
+
// Normalize CSS lengths to px helper
|
|
372
|
+
const normalizeSizeToPx = React.useCallback((value: number | string | undefined) => normalizeToPx(value, 'horizontal'), []);
|
|
370
373
|
|
|
371
374
|
// Apply defaultSize on mount when uncontrolled
|
|
372
375
|
React.useEffect(() => {
|
|
373
376
|
if (!localRef.current) return;
|
|
374
377
|
if (typeof size === 'undefined' && typeof defaultSize !== 'undefined') {
|
|
375
|
-
const px =
|
|
378
|
+
const px = normalizeSizeToPx(defaultSize);
|
|
376
379
|
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
377
380
|
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
378
381
|
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
@@ -389,7 +392,7 @@ export const Sidebar = React.forwardRef<HTMLDivElement, SidebarPublicProps>((ini
|
|
|
389
392
|
React.useEffect(() => {
|
|
390
393
|
if (!localRef.current) return;
|
|
391
394
|
if (typeof controlledSize === 'undefined') return;
|
|
392
|
-
const px =
|
|
395
|
+
const px = normalizeSizeToPx(controlledSize);
|
|
393
396
|
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
394
397
|
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
395
398
|
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
@@ -397,7 +400,7 @@ export const Sidebar = React.forwardRef<HTMLDivElement, SidebarPublicProps>((ini
|
|
|
397
400
|
localRef.current.style.setProperty('--sidebar-size', `${clamped}px`);
|
|
398
401
|
emitSizeChange(clamped, { reason: 'controlled' });
|
|
399
402
|
}
|
|
400
|
-
}, [controlledSize, minSize, maxSize,
|
|
403
|
+
}, [controlledSize, minSize, maxSize, normalizeSizeToPx, emitSizeChange]);
|
|
401
404
|
|
|
402
405
|
if (isOverlay) {
|
|
403
406
|
const open = shell.sidebarMode !== 'collapsed';
|
package/src/components/shell.tsx
CHANGED
|
@@ -38,6 +38,7 @@ import { Bottom } from './_internal/shell-bottom.js';
|
|
|
38
38
|
import { Inspector } from './_internal/shell-inspector.js';
|
|
39
39
|
import type { PresentationValue, ResponsivePresentation, PaneMode, SidebarMode, PaneSizePersistence, Breakpoint, PaneTarget, Responsive, PaneBaseProps } from './shell.types.js';
|
|
40
40
|
import { _BREAKPOINTS } from './shell.types.js';
|
|
41
|
+
import { normalizeToPx } from '../helpers/normalize-to-px.js';
|
|
41
42
|
import {
|
|
42
43
|
ShellProvider,
|
|
43
44
|
useShell,
|
|
@@ -911,15 +912,26 @@ const Panel = assignShellSlot(
|
|
|
911
912
|
sizeUpdateMs = 50,
|
|
912
913
|
} = initialProps;
|
|
913
914
|
const panelDomProps = extractPaneDomProps(initialProps, PANEL_DOM_PROP_KEYS);
|
|
915
|
+
// Ref for debounce cleanup
|
|
916
|
+
const debounceTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
917
|
+
// Cleanup debounce timeout on unmount or when dependencies change
|
|
918
|
+
React.useEffect(() => {
|
|
919
|
+
return () => {
|
|
920
|
+
if (debounceTimeoutRef.current) {
|
|
921
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
922
|
+
debounceTimeoutRef.current = null;
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
}, [onSizeChange, sizeUpdate, sizeUpdateMs]);
|
|
914
926
|
// Throttled/debounced emitter for onSizeChange
|
|
915
927
|
const emitSizeChange = React.useMemo(() => {
|
|
916
928
|
if (!onSizeChange) return () => {};
|
|
917
929
|
if (sizeUpdate === 'debounce') {
|
|
918
|
-
let t: any = null;
|
|
919
930
|
const fn = (s: number, meta: PanelSizeChangeMeta) => {
|
|
920
|
-
if (
|
|
921
|
-
|
|
931
|
+
if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current);
|
|
932
|
+
debounceTimeoutRef.current = setTimeout(() => {
|
|
922
933
|
onSizeChange?.(s, meta);
|
|
934
|
+
debounceTimeoutRef.current = null;
|
|
923
935
|
}, sizeUpdateMs);
|
|
924
936
|
};
|
|
925
937
|
return fn;
|
|
@@ -978,12 +990,16 @@ const Panel = assignShellSlot(
|
|
|
978
990
|
}, [shell, open]);
|
|
979
991
|
|
|
980
992
|
// Dev-only warning if switching controlled/uncontrolled between renders
|
|
993
|
+
const wasControlledRef = React.useRef<boolean | null>(null);
|
|
981
994
|
React.useEffect(() => {
|
|
982
995
|
const isControlled = typeof open !== 'undefined';
|
|
983
|
-
(
|
|
984
|
-
|
|
996
|
+
if (wasControlledRef.current === null) {
|
|
997
|
+
wasControlledRef.current = isControlled;
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (wasControlledRef.current !== isControlled) {
|
|
985
1001
|
console.warn('Shell.Panel: Switching between controlled and uncontrolled `open` is not supported.');
|
|
986
|
-
|
|
1002
|
+
wasControlledRef.current = isControlled;
|
|
987
1003
|
}
|
|
988
1004
|
}, [open]);
|
|
989
1005
|
|
|
@@ -1015,26 +1031,8 @@ const Panel = assignShellSlot(
|
|
|
1015
1031
|
|
|
1016
1032
|
const isOverlay = shell.leftResolvedPresentation === 'overlay';
|
|
1017
1033
|
|
|
1018
|
-
// Normalize CSS lengths to px
|
|
1019
|
-
const
|
|
1020
|
-
if (value == null) return undefined;
|
|
1021
|
-
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
1022
|
-
const str = String(value).trim();
|
|
1023
|
-
if (!str) return undefined;
|
|
1024
|
-
if (str.endsWith('px')) return Number.parseFloat(str);
|
|
1025
|
-
if (str.endsWith('rem')) {
|
|
1026
|
-
const rem = Number.parseFloat(getComputedStyle(document.documentElement).fontSize || '16') || 16;
|
|
1027
|
-
return Number.parseFloat(str) * rem;
|
|
1028
|
-
}
|
|
1029
|
-
if (str.endsWith('%')) {
|
|
1030
|
-
const pct = Number.parseFloat(str);
|
|
1031
|
-
const base = document.documentElement.clientWidth || window.innerWidth || 0;
|
|
1032
|
-
return (pct / 100) * base;
|
|
1033
|
-
}
|
|
1034
|
-
// Bare number-like string
|
|
1035
|
-
const n = Number.parseFloat(str);
|
|
1036
|
-
return Number.isFinite(n) ? n : undefined;
|
|
1037
|
-
}, []);
|
|
1034
|
+
// Normalize CSS lengths to px helper
|
|
1035
|
+
const normalizeSizeToPx = React.useCallback((value: number | string | undefined) => normalizeToPx(value, 'horizontal'), []);
|
|
1038
1036
|
|
|
1039
1037
|
// Derive a default persistence adapter from paneId if none provided
|
|
1040
1038
|
const persistenceAdapter = React.useMemo(() => {
|
|
@@ -1082,7 +1080,7 @@ const Panel = assignShellSlot(
|
|
|
1082
1080
|
React.useEffect(() => {
|
|
1083
1081
|
if (!localRef.current) return;
|
|
1084
1082
|
if (typeof size === 'undefined' && typeof defaultSize !== 'undefined') {
|
|
1085
|
-
const px =
|
|
1083
|
+
const px = normalizeSizeToPx(defaultSize);
|
|
1086
1084
|
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
1087
1085
|
// Clamp to min/max if provided
|
|
1088
1086
|
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
@@ -1099,7 +1097,7 @@ const Panel = assignShellSlot(
|
|
|
1099
1097
|
React.useEffect(() => {
|
|
1100
1098
|
if (!localRef.current) return;
|
|
1101
1099
|
if (typeof size === 'undefined') return;
|
|
1102
|
-
const px =
|
|
1100
|
+
const px = normalizeSizeToPx(size);
|
|
1103
1101
|
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
1104
1102
|
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
1105
1103
|
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
@@ -1107,7 +1105,7 @@ const Panel = assignShellSlot(
|
|
|
1107
1105
|
localRef.current.style.setProperty('--panel-size', `${clamped}px`);
|
|
1108
1106
|
emitSizeChange(clamped, { reason: 'controlled' });
|
|
1109
1107
|
}
|
|
1110
|
-
}, [size, minSize, maxSize,
|
|
1108
|
+
}, [size, minSize, maxSize, normalizeSizeToPx, emitSizeChange]);
|
|
1111
1109
|
|
|
1112
1110
|
// Ensure Left container width is auto whenever Panel is expanded in fixed presentation
|
|
1113
1111
|
React.useEffect(() => {
|
|
@@ -1302,7 +1300,16 @@ const Trigger = React.forwardRef<HTMLButtonElement, TriggerProps>(({ target, act
|
|
|
1302
1300
|
);
|
|
1303
1301
|
|
|
1304
1302
|
return (
|
|
1305
|
-
<button
|
|
1303
|
+
<button
|
|
1304
|
+
{...props}
|
|
1305
|
+
ref={ref}
|
|
1306
|
+
onClick={handleClick}
|
|
1307
|
+
onMouseEnter={handleMouseEnter}
|
|
1308
|
+
onMouseLeave={handleMouseLeave}
|
|
1309
|
+
data-shell-trigger={target}
|
|
1310
|
+
data-shell-action={action}
|
|
1311
|
+
aria-expanded={!isCollapsed}
|
|
1312
|
+
>
|
|
1306
1313
|
{children}
|
|
1307
1314
|
</button>
|
|
1308
1315
|
);
|
package/src/helpers/index.ts
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize CSS length values to pixels.
|
|
3
|
+
* Supports: px, rem, %, and bare numbers.
|
|
4
|
+
*
|
|
5
|
+
* @param value - The value to normalize (number, string, or undefined)
|
|
6
|
+
* @param orientation - 'horizontal' for width-based % or 'vertical' for height-based %
|
|
7
|
+
* @returns The value in pixels, or undefined if invalid
|
|
8
|
+
*/
|
|
9
|
+
export function normalizeToPx(
|
|
10
|
+
value: number | string | undefined,
|
|
11
|
+
orientation: 'horizontal' | 'vertical' = 'horizontal',
|
|
12
|
+
): number | undefined {
|
|
13
|
+
if (value == null) return undefined;
|
|
14
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
15
|
+
|
|
16
|
+
const str = String(value).trim();
|
|
17
|
+
if (!str) return undefined;
|
|
18
|
+
|
|
19
|
+
// px: direct parse
|
|
20
|
+
if (str.endsWith('px')) return Number.parseFloat(str);
|
|
21
|
+
|
|
22
|
+
// rem: multiply by root font size
|
|
23
|
+
if (str.endsWith('rem')) {
|
|
24
|
+
const rem = Number.parseFloat(getComputedStyle(document.documentElement).fontSize || '16') || 16;
|
|
25
|
+
return Number.parseFloat(str) * rem;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// %: calculate based on viewport dimension
|
|
29
|
+
if (str.endsWith('%')) {
|
|
30
|
+
const pct = Number.parseFloat(str);
|
|
31
|
+
const base =
|
|
32
|
+
orientation === 'horizontal'
|
|
33
|
+
? document.documentElement.clientWidth || window.innerWidth || 0
|
|
34
|
+
: document.documentElement.clientHeight || window.innerHeight || 0;
|
|
35
|
+
return (pct / 100) * base;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Bare number-like string
|
|
39
|
+
const n = Number.parseFloat(str);
|
|
40
|
+
return Number.isFinite(n) ? n : undefined;
|
|
41
|
+
}
|
|
42
|
+
|