@kushagradhawan/kookie-ui 0.1.72 → 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/components.css +6 -2
- 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 +60 -30
- package/src/components/_internal/shell-handles.tsx +6 -1
- package/src/components/_internal/shell-inspector.tsx +60 -30
- package/src/components/_internal/shell-sidebar.tsx +62 -31
- package/src/components/shell.css +10 -11
- package/src/components/shell.tsx +83 -32
- package/src/helpers/index.ts +1 -0
- package/src/helpers/normalize-to-px.ts +42 -0
- package/styles.css +6 -2
|
@@ -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
|
}
|
|
@@ -172,13 +185,47 @@ export const Bottom = React.forwardRef<HTMLDivElement, BottomPublicProps>((initi
|
|
|
172
185
|
}
|
|
173
186
|
}, [shell.bottomMode, open, defaultOpen, onOpenChange]);
|
|
174
187
|
|
|
188
|
+
// Track previous mode to only fire callbacks on actual user-initiated state transitions.
|
|
189
|
+
// We wait for breakpointReady to ensure the initial state sync from useResponsiveInitialState
|
|
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
|
+
|
|
199
|
+
const prevBottomModeRef = React.useRef<PaneMode | null>(null);
|
|
200
|
+
const hasInitializedRef = React.useRef(false);
|
|
175
201
|
React.useEffect(() => {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
202
|
+
const currentMode = shell.bottomMode;
|
|
203
|
+
|
|
204
|
+
// Wait for breakpoint to be ready before enabling callbacks
|
|
205
|
+
if (!shell.currentBreakpointReady) {
|
|
206
|
+
prevBottomModeRef.current = currentMode;
|
|
207
|
+
return;
|
|
180
208
|
}
|
|
181
|
-
|
|
209
|
+
|
|
210
|
+
// Skip the first run after breakpoint is ready - this captures the post-sync state
|
|
211
|
+
if (!hasInitializedRef.current) {
|
|
212
|
+
hasInitializedRef.current = true;
|
|
213
|
+
prevBottomModeRef.current = currentMode;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const prevMode = prevBottomModeRef.current;
|
|
218
|
+
|
|
219
|
+
// Only fire on actual state transitions
|
|
220
|
+
if (prevMode !== null && prevMode !== currentMode) {
|
|
221
|
+
if (currentMode === 'expanded') {
|
|
222
|
+
onExpandRef.current?.();
|
|
223
|
+
} else if (currentMode === 'collapsed') {
|
|
224
|
+
onCollapseRef.current?.();
|
|
225
|
+
}
|
|
226
|
+
prevBottomModeRef.current = currentMode;
|
|
227
|
+
}
|
|
228
|
+
}, [shell.bottomMode, shell.currentBreakpointReady]);
|
|
182
229
|
|
|
183
230
|
const isExpanded = shell.bottomMode === 'expanded';
|
|
184
231
|
|
|
@@ -271,31 +318,14 @@ export const Bottom = React.forwardRef<HTMLDivElement, BottomPublicProps>((initi
|
|
|
271
318
|
) : null;
|
|
272
319
|
|
|
273
320
|
// Strip control/size props from DOM spread (moved above overlay return to keep hook order consistent)
|
|
274
|
-
// Normalize CSS lengths to px
|
|
275
|
-
const
|
|
276
|
-
if (value == null) return undefined;
|
|
277
|
-
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
278
|
-
const str = String(value).trim();
|
|
279
|
-
if (!str) return undefined;
|
|
280
|
-
if (str.endsWith('px')) return Number.parseFloat(str);
|
|
281
|
-
if (str.endsWith('rem')) {
|
|
282
|
-
const rem = Number.parseFloat(getComputedStyle(document.documentElement).fontSize || '16') || 16;
|
|
283
|
-
return Number.parseFloat(str) * rem;
|
|
284
|
-
}
|
|
285
|
-
if (str.endsWith('%')) {
|
|
286
|
-
const pct = Number.parseFloat(str);
|
|
287
|
-
const base = document.documentElement.clientHeight || window.innerHeight || 0;
|
|
288
|
-
return (pct / 100) * base;
|
|
289
|
-
}
|
|
290
|
-
const n = Number.parseFloat(str);
|
|
291
|
-
return Number.isFinite(n) ? n : undefined;
|
|
292
|
-
}, []);
|
|
321
|
+
// Normalize CSS lengths to px helper
|
|
322
|
+
const normalizeSizeToPx = React.useCallback((value: number | string | undefined) => normalizeToPx(value, 'vertical'), []);
|
|
293
323
|
|
|
294
324
|
// Apply defaultSize on mount when uncontrolled (moved above overlay return)
|
|
295
325
|
React.useEffect(() => {
|
|
296
326
|
if (!localRef.current) return;
|
|
297
327
|
if (typeof size === 'undefined' && typeof defaultSize !== 'undefined') {
|
|
298
|
-
const px =
|
|
328
|
+
const px = normalizeSizeToPx(defaultSize);
|
|
299
329
|
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
300
330
|
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
301
331
|
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
@@ -312,7 +342,7 @@ export const Bottom = React.forwardRef<HTMLDivElement, BottomPublicProps>((initi
|
|
|
312
342
|
React.useEffect(() => {
|
|
313
343
|
if (!localRef.current) return;
|
|
314
344
|
if (typeof controlledSize === 'undefined') return;
|
|
315
|
-
const px =
|
|
345
|
+
const px = normalizeSizeToPx(controlledSize);
|
|
316
346
|
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
317
347
|
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
318
348
|
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
@@ -320,7 +350,7 @@ export const Bottom = React.forwardRef<HTMLDivElement, BottomPublicProps>((initi
|
|
|
320
350
|
localRef.current.style.setProperty('--bottom-size', `${clamped}px`);
|
|
321
351
|
emitSizeChange(clamped, { reason: 'controlled' });
|
|
322
352
|
}
|
|
323
|
-
}, [controlledSize, minSize, maxSize,
|
|
353
|
+
}, [controlledSize, minSize, maxSize, normalizeSizeToPx, emitSizeChange]);
|
|
324
354
|
|
|
325
355
|
if (isOverlay) {
|
|
326
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
|
}
|
|
@@ -173,13 +186,47 @@ export const Inspector = React.forwardRef<HTMLDivElement, InspectorPublicProps>(
|
|
|
173
186
|
}
|
|
174
187
|
}, [shell.inspectorMode, open, defaultOpen, onOpenChange]);
|
|
175
188
|
|
|
189
|
+
// Track previous mode to only fire callbacks on actual user-initiated state transitions.
|
|
190
|
+
// We wait for breakpointReady to ensure the initial state sync from useResponsiveInitialState
|
|
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
|
+
|
|
200
|
+
const prevInspectorModeRef = React.useRef<PaneMode | null>(null);
|
|
201
|
+
const hasInitializedRef = React.useRef(false);
|
|
176
202
|
React.useEffect(() => {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
203
|
+
const currentMode = shell.inspectorMode;
|
|
204
|
+
|
|
205
|
+
// Wait for breakpoint to be ready before enabling callbacks
|
|
206
|
+
if (!shell.currentBreakpointReady) {
|
|
207
|
+
prevInspectorModeRef.current = currentMode;
|
|
208
|
+
return;
|
|
181
209
|
}
|
|
182
|
-
|
|
210
|
+
|
|
211
|
+
// Skip the first run after breakpoint is ready - this captures the post-sync state
|
|
212
|
+
if (!hasInitializedRef.current) {
|
|
213
|
+
hasInitializedRef.current = true;
|
|
214
|
+
prevInspectorModeRef.current = currentMode;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const prevMode = prevInspectorModeRef.current;
|
|
219
|
+
|
|
220
|
+
// Only fire on actual state transitions
|
|
221
|
+
if (prevMode !== null && prevMode !== currentMode) {
|
|
222
|
+
if (currentMode === 'expanded') {
|
|
223
|
+
onExpandRef.current?.();
|
|
224
|
+
} else if (currentMode === 'collapsed') {
|
|
225
|
+
onCollapseRef.current?.();
|
|
226
|
+
}
|
|
227
|
+
prevInspectorModeRef.current = currentMode;
|
|
228
|
+
}
|
|
229
|
+
}, [shell.inspectorMode, shell.currentBreakpointReady]);
|
|
183
230
|
|
|
184
231
|
const isExpanded = shell.inspectorMode === 'expanded';
|
|
185
232
|
|
|
@@ -272,31 +319,14 @@ export const Inspector = React.forwardRef<HTMLDivElement, InspectorPublicProps>(
|
|
|
272
319
|
</PaneResizeContext.Provider>
|
|
273
320
|
) : null;
|
|
274
321
|
|
|
275
|
-
// Normalize CSS lengths to px
|
|
276
|
-
const
|
|
277
|
-
if (value == null) return undefined;
|
|
278
|
-
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
279
|
-
const str = String(value).trim();
|
|
280
|
-
if (!str) return undefined;
|
|
281
|
-
if (str.endsWith('px')) return Number.parseFloat(str);
|
|
282
|
-
if (str.endsWith('rem')) {
|
|
283
|
-
const rem = Number.parseFloat(getComputedStyle(document.documentElement).fontSize || '16') || 16;
|
|
284
|
-
return Number.parseFloat(str) * rem;
|
|
285
|
-
}
|
|
286
|
-
if (str.endsWith('%')) {
|
|
287
|
-
const pct = Number.parseFloat(str);
|
|
288
|
-
const base = document.documentElement.clientWidth || window.innerWidth || 0;
|
|
289
|
-
return (pct / 100) * base;
|
|
290
|
-
}
|
|
291
|
-
const n = Number.parseFloat(str);
|
|
292
|
-
return Number.isFinite(n) ? n : undefined;
|
|
293
|
-
}, []);
|
|
322
|
+
// Normalize CSS lengths to px helper
|
|
323
|
+
const normalizeSizeToPx = React.useCallback((value: number | string | undefined) => normalizeToPx(value, 'horizontal'), []);
|
|
294
324
|
|
|
295
325
|
// Apply defaultSize on mount when uncontrolled
|
|
296
326
|
React.useEffect(() => {
|
|
297
327
|
if (!localRef.current) return;
|
|
298
328
|
if (typeof size === 'undefined' && typeof defaultSize !== 'undefined') {
|
|
299
|
-
const px =
|
|
329
|
+
const px = normalizeSizeToPx(defaultSize);
|
|
300
330
|
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
301
331
|
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
302
332
|
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
@@ -313,7 +343,7 @@ export const Inspector = React.forwardRef<HTMLDivElement, InspectorPublicProps>(
|
|
|
313
343
|
React.useEffect(() => {
|
|
314
344
|
if (!localRef.current) return;
|
|
315
345
|
if (typeof controlledSize === 'undefined') return;
|
|
316
|
-
const px =
|
|
346
|
+
const px = normalizeSizeToPx(controlledSize);
|
|
317
347
|
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
318
348
|
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
319
349
|
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
@@ -321,7 +351,7 @@ export const Inspector = React.forwardRef<HTMLDivElement, InspectorPublicProps>(
|
|
|
321
351
|
localRef.current.style.setProperty('--inspector-size', `${clamped}px`);
|
|
322
352
|
emitSizeChange(clamped, { reason: 'controlled' });
|
|
323
353
|
}
|
|
324
|
-
}, [controlledSize, minSize, maxSize,
|
|
354
|
+
}, [controlledSize, minSize, maxSize, normalizeSizeToPx, emitSizeChange]);
|
|
325
355
|
|
|
326
356
|
if (isOverlay) {
|
|
327
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
|
}
|
|
@@ -188,14 +200,50 @@ export const Sidebar = React.forwardRef<HTMLDivElement, SidebarPublicProps>((ini
|
|
|
188
200
|
}
|
|
189
201
|
}, [shell.sidebarMode, state, onStateChange]);
|
|
190
202
|
|
|
191
|
-
//
|
|
203
|
+
// Track previous mode to only fire callbacks on actual user-initiated state transitions.
|
|
204
|
+
// We wait for breakpointReady to ensure the initial state sync from useResponsiveInitialState
|
|
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
|
+
|
|
214
|
+
const prevSidebarModeRef = React.useRef<SidebarMode | null>(null);
|
|
215
|
+
const hasInitializedRef = React.useRef(false);
|
|
192
216
|
React.useEffect(() => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
217
|
+
const currentMode = shell.sidebarMode as SidebarMode;
|
|
218
|
+
|
|
219
|
+
// Wait for breakpoint to be ready before enabling callbacks
|
|
220
|
+
if (!shell.currentBreakpointReady) {
|
|
221
|
+
prevSidebarModeRef.current = currentMode;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Skip the first run after breakpoint is ready - this captures the post-sync state
|
|
226
|
+
if (!hasInitializedRef.current) {
|
|
227
|
+
hasInitializedRef.current = true;
|
|
228
|
+
prevSidebarModeRef.current = currentMode;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const prevMode = prevSidebarModeRef.current;
|
|
233
|
+
|
|
234
|
+
// Only fire on actual state transitions
|
|
235
|
+
if (prevMode !== null && prevMode !== currentMode) {
|
|
236
|
+
// onExpand: when becoming visible (collapsed → thin/expanded)
|
|
237
|
+
if (prevMode === 'collapsed' && currentMode !== 'collapsed') {
|
|
238
|
+
onExpandRef.current?.();
|
|
239
|
+
}
|
|
240
|
+
// onCollapse: when becoming hidden (any → collapsed)
|
|
241
|
+
else if (currentMode === 'collapsed') {
|
|
242
|
+
onCollapseRef.current?.();
|
|
243
|
+
}
|
|
244
|
+
prevSidebarModeRef.current = currentMode;
|
|
197
245
|
}
|
|
198
|
-
}, [shell.sidebarMode,
|
|
246
|
+
}, [shell.sidebarMode, shell.currentBreakpointReady]);
|
|
199
247
|
|
|
200
248
|
// Option A: thin is width-only; content remains visible whenever not collapsed
|
|
201
249
|
const isContentVisible = shell.sidebarMode !== 'collapsed';
|
|
@@ -320,31 +368,14 @@ export const Sidebar = React.forwardRef<HTMLDivElement, SidebarPublicProps>((ini
|
|
|
320
368
|
</PaneResizeContext.Provider>
|
|
321
369
|
) : null;
|
|
322
370
|
|
|
323
|
-
// Normalize CSS lengths to px
|
|
324
|
-
const
|
|
325
|
-
if (value == null) return undefined;
|
|
326
|
-
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
327
|
-
const str = String(value).trim();
|
|
328
|
-
if (!str) return undefined;
|
|
329
|
-
if (str.endsWith('px')) return Number.parseFloat(str);
|
|
330
|
-
if (str.endsWith('rem')) {
|
|
331
|
-
const rem = Number.parseFloat(getComputedStyle(document.documentElement).fontSize || '16') || 16;
|
|
332
|
-
return Number.parseFloat(str) * rem;
|
|
333
|
-
}
|
|
334
|
-
if (str.endsWith('%')) {
|
|
335
|
-
const pct = Number.parseFloat(str);
|
|
336
|
-
const base = document.documentElement.clientWidth || window.innerWidth || 0;
|
|
337
|
-
return (pct / 100) * base;
|
|
338
|
-
}
|
|
339
|
-
const n = Number.parseFloat(str);
|
|
340
|
-
return Number.isFinite(n) ? n : undefined;
|
|
341
|
-
}, []);
|
|
371
|
+
// Normalize CSS lengths to px helper
|
|
372
|
+
const normalizeSizeToPx = React.useCallback((value: number | string | undefined) => normalizeToPx(value, 'horizontal'), []);
|
|
342
373
|
|
|
343
374
|
// Apply defaultSize on mount when uncontrolled
|
|
344
375
|
React.useEffect(() => {
|
|
345
376
|
if (!localRef.current) return;
|
|
346
377
|
if (typeof size === 'undefined' && typeof defaultSize !== 'undefined') {
|
|
347
|
-
const px =
|
|
378
|
+
const px = normalizeSizeToPx(defaultSize);
|
|
348
379
|
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
349
380
|
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
350
381
|
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
@@ -361,7 +392,7 @@ export const Sidebar = React.forwardRef<HTMLDivElement, SidebarPublicProps>((ini
|
|
|
361
392
|
React.useEffect(() => {
|
|
362
393
|
if (!localRef.current) return;
|
|
363
394
|
if (typeof controlledSize === 'undefined') return;
|
|
364
|
-
const px =
|
|
395
|
+
const px = normalizeSizeToPx(controlledSize);
|
|
365
396
|
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
366
397
|
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
367
398
|
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
@@ -369,7 +400,7 @@ export const Sidebar = React.forwardRef<HTMLDivElement, SidebarPublicProps>((ini
|
|
|
369
400
|
localRef.current.style.setProperty('--sidebar-size', `${clamped}px`);
|
|
370
401
|
emitSizeChange(clamped, { reason: 'controlled' });
|
|
371
402
|
}
|
|
372
|
-
}, [controlledSize, minSize, maxSize,
|
|
403
|
+
}, [controlledSize, minSize, maxSize, normalizeSizeToPx, emitSizeChange]);
|
|
373
404
|
|
|
374
405
|
if (isOverlay) {
|
|
375
406
|
const open = shell.sidebarMode !== 'collapsed';
|
package/src/components/shell.css
CHANGED
|
@@ -189,17 +189,16 @@
|
|
|
189
189
|
width: var(--sidebar-thin-size, 64px);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
.rt-ShellSidebar[data-mode='collapsed'] {
|
|
193
|
-
width: 0px;
|
|
194
|
-
/* Delay container collapse until content fade completes */
|
|
195
|
-
transition-delay: var(--motion-duration-small);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
192
|
/* Keep collapsed sidebar out of flow to avoid layout blips when exiting peek */
|
|
199
193
|
.rt-ShellSidebar[data-mode='collapsed'] {
|
|
194
|
+
width: 0px;
|
|
200
195
|
position: absolute;
|
|
201
196
|
inset-block: 0;
|
|
202
197
|
inset-inline-start: 0;
|
|
198
|
+
flex-shrink: 0;
|
|
199
|
+
flex-basis: 0;
|
|
200
|
+
/* Delay container collapse until content fade completes */
|
|
201
|
+
transition-delay: var(--motion-duration-small);
|
|
203
202
|
}
|
|
204
203
|
|
|
205
204
|
.rt-ShellSidebarContent {
|
|
@@ -293,16 +292,16 @@
|
|
|
293
292
|
width: var(--inspector-size, 320px);
|
|
294
293
|
}
|
|
295
294
|
|
|
295
|
+
/* Keep collapsed inspector out of flow to avoid layout issues */
|
|
296
296
|
.rt-ShellInspector[data-mode='collapsed'] {
|
|
297
297
|
width: 0px;
|
|
298
|
-
/* Delay container collapse until content fade completes */
|
|
299
|
-
transition-delay: var(--motion-duration-small);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
.rt-ShellInspector[data-mode='collapsed'] {
|
|
303
298
|
position: absolute;
|
|
304
299
|
inset-block: 0;
|
|
305
300
|
inset-inline-end: 0;
|
|
301
|
+
flex-shrink: 0;
|
|
302
|
+
flex-basis: 0;
|
|
303
|
+
/* Delay container collapse until content fade completes */
|
|
304
|
+
transition-delay: var(--motion-duration-small);
|
|
306
305
|
}
|
|
307
306
|
|
|
308
307
|
.rt-ShellInspectorContent {
|