@kushagradhawan/kookie-ui 0.1.49 → 0.1.51
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 +880 -243
- package/dist/cjs/components/_internal/shell-bottom.d.ts +31 -5
- 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 +23 -5
- 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 +24 -6
- 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/chatbar.d.ts +9 -2
- package/dist/cjs/components/chatbar.d.ts.map +1 -1
- package/dist/cjs/components/chatbar.js +1 -1
- package/dist/cjs/components/chatbar.js.map +3 -3
- package/dist/cjs/components/shell.context.d.ts +88 -0
- package/dist/cjs/components/shell.context.d.ts.map +1 -1
- package/dist/cjs/components/shell.context.js +1 -1
- package/dist/cjs/components/shell.context.js.map +3 -3
- package/dist/cjs/components/shell.d.ts +51 -13
- package/dist/cjs/components/shell.d.ts.map +1 -1
- package/dist/cjs/components/shell.hooks.d.ts +7 -1
- package/dist/cjs/components/shell.hooks.d.ts.map +1 -1
- package/dist/cjs/components/shell.hooks.js +1 -1
- package/dist/cjs/components/shell.hooks.js.map +3 -3
- package/dist/cjs/components/shell.js +1 -1
- package/dist/cjs/components/shell.js.map +3 -3
- package/dist/cjs/components/shell.types.d.ts +1 -0
- package/dist/cjs/components/shell.types.d.ts.map +1 -1
- package/dist/cjs/components/shell.types.js +1 -1
- package/dist/cjs/components/shell.types.js.map +2 -2
- package/dist/cjs/components/sidebar.d.ts +7 -1
- package/dist/cjs/components/sidebar.d.ts.map +1 -1
- package/dist/cjs/components/sidebar.js +1 -1
- package/dist/cjs/components/sidebar.js.map +3 -3
- package/dist/esm/components/_internal/shell-bottom.d.ts +31 -5
- 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 +23 -5
- 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 +24 -6
- 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/chatbar.d.ts +9 -2
- package/dist/esm/components/chatbar.d.ts.map +1 -1
- package/dist/esm/components/chatbar.js +1 -1
- package/dist/esm/components/chatbar.js.map +3 -3
- package/dist/esm/components/shell.context.d.ts +88 -0
- package/dist/esm/components/shell.context.d.ts.map +1 -1
- package/dist/esm/components/shell.context.js +1 -1
- package/dist/esm/components/shell.context.js.map +3 -3
- package/dist/esm/components/shell.d.ts +51 -13
- package/dist/esm/components/shell.d.ts.map +1 -1
- package/dist/esm/components/shell.hooks.d.ts +7 -1
- package/dist/esm/components/shell.hooks.d.ts.map +1 -1
- package/dist/esm/components/shell.hooks.js +1 -1
- package/dist/esm/components/shell.hooks.js.map +3 -3
- package/dist/esm/components/shell.js +1 -1
- package/dist/esm/components/shell.js.map +3 -3
- package/dist/esm/components/shell.types.d.ts +1 -0
- package/dist/esm/components/shell.types.d.ts.map +1 -1
- package/dist/esm/components/shell.types.js.map +2 -2
- package/dist/esm/components/sidebar.d.ts +7 -1
- package/dist/esm/components/sidebar.d.ts.map +1 -1
- package/dist/esm/components/sidebar.js +1 -1
- package/dist/esm/components/sidebar.js.map +3 -3
- package/package.json +14 -3
- 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/base-menu.css +17 -18
- package/src/components/_internal/base-sidebar-menu.css +23 -21
- package/src/components/_internal/base-sidebar.css +20 -0
- package/src/components/_internal/shell-bottom.tsx +176 -49
- package/src/components/_internal/shell-handles.tsx +29 -4
- package/src/components/_internal/shell-inspector.tsx +175 -43
- package/src/components/_internal/shell-sidebar.tsx +176 -69
- package/src/components/chatbar.css +240 -21
- package/src/components/chatbar.tsx +246 -290
- package/src/components/sheet.css +8 -16
- package/src/components/shell.context.tsx +79 -0
- package/src/components/shell.css +28 -2
- package/src/components/shell.hooks.ts +35 -0
- package/src/components/shell.tsx +574 -214
- package/src/components/shell.types.ts +2 -0
- package/src/components/sidebar.css +233 -33
- package/src/components/sidebar.tsx +247 -213
- package/styles.css +841 -204
|
@@ -3,7 +3,7 @@ import classNames from 'classnames';
|
|
|
3
3
|
import * as Sheet from '../sheet.js';
|
|
4
4
|
import { VisuallyHidden } from '../visually-hidden.js';
|
|
5
5
|
import { useShell } from '../shell.context.js';
|
|
6
|
-
import { useResponsivePresentation } from '../shell.hooks.js';
|
|
6
|
+
import { useResponsivePresentation, useResponsiveValue } from '../shell.hooks.js';
|
|
7
7
|
import { PaneResizeContext } from './shell-resize.js';
|
|
8
8
|
import { InspectorHandle, PaneHandle } from './shell-handles.js';
|
|
9
9
|
import { BREAKPOINTS } from '../shell.types.js';
|
|
@@ -11,9 +11,7 @@ import type { Breakpoint, PaneMode, PaneSizePersistence, ResponsivePresentation
|
|
|
11
11
|
|
|
12
12
|
interface PaneProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
13
13
|
presentation?: ResponsivePresentation;
|
|
14
|
-
mode
|
|
15
|
-
defaultMode?: any;
|
|
16
|
-
onModeChange?: (mode: PaneMode) => void;
|
|
14
|
+
// legacy mode removed
|
|
17
15
|
expandedSize?: number;
|
|
18
16
|
minSize?: number;
|
|
19
17
|
maxSize?: number;
|
|
@@ -32,16 +30,29 @@ interface PaneProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
|
32
30
|
persistence?: PaneSizePersistence;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
type
|
|
33
|
+
type InspectorOpenChangeMeta = { reason: 'init' | 'toggle' | 'responsive' };
|
|
34
|
+
type InspectorControlledProps = { open: boolean | Partial<Record<Breakpoint, boolean>>; onOpenChange?: (open: boolean, meta: InspectorOpenChangeMeta) => void; defaultOpen?: never };
|
|
35
|
+
type InspectorUncontrolledProps = { defaultOpen?: boolean | Partial<Record<Breakpoint, boolean>>; onOpenChange?: (open: boolean, meta: InspectorOpenChangeMeta) => void; open?: never };
|
|
36
|
+
type InspectorSizeChangeMeta = { reason: 'init' | 'resize' | 'controlled' };
|
|
37
|
+
type InspectorPublicProps = PaneProps &
|
|
38
|
+
(InspectorControlledProps | InspectorUncontrolledProps) & {
|
|
39
|
+
onSizeChange?: (size: number, meta: InspectorSizeChangeMeta) => void;
|
|
40
|
+
sizeUpdate?: 'throttle' | 'debounce';
|
|
41
|
+
sizeUpdateMs?: number;
|
|
42
|
+
};
|
|
36
43
|
|
|
37
|
-
|
|
44
|
+
type InspectorComponent = React.ForwardRefExoticComponent<InspectorPublicProps & React.RefAttributes<HTMLDivElement>> & { Handle: typeof InspectorHandle };
|
|
45
|
+
|
|
46
|
+
export const Inspector = React.forwardRef<HTMLDivElement, InspectorPublicProps>(
|
|
38
47
|
(
|
|
39
48
|
{
|
|
40
49
|
className,
|
|
41
50
|
presentation = { initial: 'overlay', lg: 'fixed' },
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
// removed legacy props
|
|
52
|
+
// new API
|
|
53
|
+
defaultOpen,
|
|
54
|
+
open,
|
|
55
|
+
onOpenChange,
|
|
45
56
|
expandedSize = 320,
|
|
46
57
|
minSize = 200,
|
|
47
58
|
maxSize = 500,
|
|
@@ -80,47 +91,104 @@ export const Inspector = React.forwardRef<HTMLDivElement, PaneProps>(
|
|
|
80
91
|
const handleChildren = childArray.filter((el: React.ReactElement) => React.isValidElement(el) && el.type === InspectorHandle);
|
|
81
92
|
const contentChildren = childArray.filter((el: React.ReactElement) => !(React.isValidElement(el) && el.type === InspectorHandle));
|
|
82
93
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
// Throttled/debounced emitter for onSizeChange
|
|
95
|
+
const emitSizeChange = React.useMemo(() => {
|
|
96
|
+
const cb = (props as any).onSizeChange as undefined | ((s: number, meta: InspectorSizeChangeMeta) => void);
|
|
97
|
+
const strategy = (props as any).sizeUpdate as undefined | 'throttle' | 'debounce';
|
|
98
|
+
const ms = (props as any).sizeUpdateMs ?? 50;
|
|
99
|
+
if (!cb) return () => {};
|
|
100
|
+
if (strategy === 'debounce') {
|
|
101
|
+
let t: any = null;
|
|
102
|
+
return (s: number, meta: InspectorSizeChangeMeta) => {
|
|
103
|
+
if (t) clearTimeout(t);
|
|
104
|
+
t = setTimeout(() => {
|
|
105
|
+
cb(s, meta);
|
|
106
|
+
}, ms);
|
|
107
|
+
};
|
|
88
108
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
if (strategy === 'throttle') {
|
|
110
|
+
let last = 0;
|
|
111
|
+
return (s: number, meta: InspectorSizeChangeMeta) => {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
if (now - last >= ms) {
|
|
114
|
+
last = now;
|
|
115
|
+
cb(s, meta);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
97
118
|
}
|
|
98
|
-
return
|
|
99
|
-
}, [
|
|
119
|
+
return (s: number, meta: InspectorSizeChangeMeta) => cb(s, meta);
|
|
120
|
+
}, [(props as any).onSizeChange, (props as any).sizeUpdate, (props as any).sizeUpdateMs]);
|
|
100
121
|
|
|
101
|
-
|
|
102
|
-
React.
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const next = resolveResponsiveMode();
|
|
108
|
-
if (next !== shell.inspectorMode) {
|
|
109
|
-
shell.setInspectorMode(next);
|
|
122
|
+
// Dev guards
|
|
123
|
+
const wasControlledRef = React.useRef<boolean | null>(null);
|
|
124
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
125
|
+
if (typeof open !== 'undefined' && typeof defaultOpen !== 'undefined') {
|
|
126
|
+
// eslint-disable-next-line no-console
|
|
127
|
+
console.error('Shell.Inspector: Do not pass both `open` and `defaultOpen`. Choose one.');
|
|
110
128
|
}
|
|
111
|
-
}
|
|
129
|
+
}
|
|
112
130
|
|
|
131
|
+
// Warn on controlled/uncontrolled mode switch
|
|
113
132
|
React.useEffect(() => {
|
|
114
|
-
|
|
115
|
-
|
|
133
|
+
const isControlled = typeof open !== 'undefined';
|
|
134
|
+
if (wasControlledRef.current === null) {
|
|
135
|
+
wasControlledRef.current = isControlled;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (wasControlledRef.current !== isControlled) {
|
|
139
|
+
// eslint-disable-next-line no-console
|
|
140
|
+
console.warn('Shell.Inspector: Switching between controlled and uncontrolled `open` is not supported.');
|
|
141
|
+
wasControlledRef.current = isControlled;
|
|
116
142
|
}
|
|
117
|
-
}, [
|
|
143
|
+
}, [open]);
|
|
144
|
+
|
|
145
|
+
const responsiveNotifiedRef = React.useRef(false);
|
|
146
|
+
const didInitFromDefaultOpenRef = React.useRef(false);
|
|
147
|
+
const resolvedDefaultOpen = useResponsiveValue(defaultOpen);
|
|
148
|
+
React.useEffect(() => {
|
|
149
|
+
if (!shell.currentBreakpointReady) return;
|
|
150
|
+
if (didInitFromDefaultOpenRef.current) return;
|
|
151
|
+
if (typeof open !== 'undefined') return; // controlled ignores default
|
|
152
|
+
if (typeof defaultOpen === 'undefined') return;
|
|
153
|
+
const initialOpen = Boolean(resolvedDefaultOpen);
|
|
154
|
+
shell.setInspectorMode(initialOpen ? 'expanded' : 'collapsed');
|
|
155
|
+
if (initialOpen) onOpenChange?.(true, { reason: 'init' });
|
|
156
|
+
didInitFromDefaultOpenRef.current = true;
|
|
157
|
+
}, [shell.currentBreakpointReady, resolvedDefaultOpen, defaultOpen, open, onOpenChange]);
|
|
118
158
|
|
|
159
|
+
// Controlled responsive open
|
|
160
|
+
const resolvedOpen = useResponsiveValue(open);
|
|
119
161
|
React.useEffect(() => {
|
|
120
|
-
if (
|
|
121
|
-
|
|
162
|
+
if (typeof resolvedOpen === 'undefined') return;
|
|
163
|
+
const shouldExpand = Boolean(resolvedOpen);
|
|
164
|
+
if (shouldExpand && shell.inspectorMode !== 'expanded') shell.setInspectorMode('expanded');
|
|
165
|
+
if (!shouldExpand && shell.inspectorMode !== 'collapsed') shell.setInspectorMode('collapsed');
|
|
166
|
+
}, [resolvedOpen, shell.inspectorMode]);
|
|
167
|
+
|
|
168
|
+
// Removed boolean-only mount init; handled in responsive init effect above
|
|
169
|
+
|
|
170
|
+
// Removed: boolean-only controlled sync. Use responsive-resolved effect below instead.
|
|
171
|
+
|
|
172
|
+
const initNotifiedRef = React.useRef(false);
|
|
173
|
+
const lastInspectorModeRef = React.useRef<PaneMode | null>(null);
|
|
174
|
+
React.useEffect(() => {
|
|
175
|
+
// Notify init open
|
|
176
|
+
if (!initNotifiedRef.current && typeof open === 'undefined' && defaultOpen && shell.inspectorMode === 'expanded') {
|
|
177
|
+
onOpenChange?.(true, { reason: 'init' });
|
|
178
|
+
initNotifiedRef.current = true;
|
|
122
179
|
}
|
|
123
|
-
|
|
180
|
+
|
|
181
|
+
// Notify toggles when uncontrolled (avoid double-notify after responsive change)
|
|
182
|
+
if (typeof open === 'undefined') {
|
|
183
|
+
if (lastInspectorModeRef.current !== null && lastInspectorModeRef.current !== shell.inspectorMode) {
|
|
184
|
+
if (!responsiveNotifiedRef.current) {
|
|
185
|
+
onOpenChange?.(shell.inspectorMode === 'expanded', { reason: 'toggle' });
|
|
186
|
+
}
|
|
187
|
+
responsiveNotifiedRef.current = false;
|
|
188
|
+
}
|
|
189
|
+
lastInspectorModeRef.current = shell.inspectorMode;
|
|
190
|
+
}
|
|
191
|
+
}, [shell.inspectorMode, open, defaultOpen, onOpenChange]);
|
|
124
192
|
|
|
125
193
|
React.useEffect(() => {
|
|
126
194
|
if (shell.inspectorMode === 'expanded') {
|
|
@@ -184,6 +252,7 @@ export const Inspector = React.forwardRef<HTMLDivElement, PaneProps>(
|
|
|
184
252
|
onResizeStart,
|
|
185
253
|
onResizeEnd: (size) => {
|
|
186
254
|
onResizeEnd?.(size);
|
|
255
|
+
emitSizeChange(size, { reason: 'resize' });
|
|
187
256
|
persistenceAdapter?.save?.(size);
|
|
188
257
|
},
|
|
189
258
|
target: 'inspector',
|
|
@@ -199,6 +268,56 @@ export const Inspector = React.forwardRef<HTMLDivElement, PaneProps>(
|
|
|
199
268
|
</PaneResizeContext.Provider>
|
|
200
269
|
) : null;
|
|
201
270
|
|
|
271
|
+
// Normalize CSS lengths to px
|
|
272
|
+
const normalizeToPx = React.useCallback((value: number | string | undefined): number | undefined => {
|
|
273
|
+
if (value == null) return undefined;
|
|
274
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
275
|
+
const str = String(value).trim();
|
|
276
|
+
if (!str) return undefined;
|
|
277
|
+
if (str.endsWith('px')) return Number.parseFloat(str);
|
|
278
|
+
if (str.endsWith('rem')) {
|
|
279
|
+
const rem = Number.parseFloat(getComputedStyle(document.documentElement).fontSize || '16') || 16;
|
|
280
|
+
return Number.parseFloat(str) * rem;
|
|
281
|
+
}
|
|
282
|
+
if (str.endsWith('%')) {
|
|
283
|
+
const pct = Number.parseFloat(str);
|
|
284
|
+
const base = document.documentElement.clientWidth || window.innerWidth || 0;
|
|
285
|
+
return (pct / 100) * base;
|
|
286
|
+
}
|
|
287
|
+
const n = Number.parseFloat(str);
|
|
288
|
+
return Number.isFinite(n) ? n : undefined;
|
|
289
|
+
}, []);
|
|
290
|
+
|
|
291
|
+
// Apply defaultSize on mount when uncontrolled
|
|
292
|
+
React.useEffect(() => {
|
|
293
|
+
if (!localRef.current) return;
|
|
294
|
+
if (typeof (props as any).size === 'undefined' && typeof (props as any).defaultSize !== 'undefined') {
|
|
295
|
+
const px = normalizeToPx((props as any).defaultSize);
|
|
296
|
+
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
297
|
+
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
298
|
+
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
299
|
+
const clamped = Math.min(maxPx ?? px, Math.max(minPx ?? px, px));
|
|
300
|
+
localRef.current.style.setProperty('--inspector-size', `${clamped}px`);
|
|
301
|
+
emitSizeChange(clamped, { reason: 'init' });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
305
|
+
}, []);
|
|
306
|
+
|
|
307
|
+
// Controlled size sync
|
|
308
|
+
React.useEffect(() => {
|
|
309
|
+
if (!localRef.current) return;
|
|
310
|
+
if (typeof (props as any).size === 'undefined') return;
|
|
311
|
+
const px = normalizeToPx((props as any).size);
|
|
312
|
+
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
313
|
+
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
314
|
+
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
315
|
+
const clamped = Math.min(maxPx ?? px, Math.max(minPx ?? px, px));
|
|
316
|
+
localRef.current.style.setProperty('--inspector-size', `${clamped}px`);
|
|
317
|
+
emitSizeChange(clamped, { reason: 'controlled' });
|
|
318
|
+
}
|
|
319
|
+
}, [(props as any).size, minSize, maxSize, normalizeToPx, emitSizeChange]);
|
|
320
|
+
|
|
202
321
|
if (isOverlay) {
|
|
203
322
|
const open = shell.inspectorMode === 'expanded';
|
|
204
323
|
return (
|
|
@@ -213,15 +332,28 @@ export const Inspector = React.forwardRef<HTMLDivElement, PaneProps>(
|
|
|
213
332
|
);
|
|
214
333
|
}
|
|
215
334
|
|
|
335
|
+
// Strip control/size props from DOM spread
|
|
336
|
+
const {
|
|
337
|
+
defaultOpen: _inspectorDefaultOpenIgnored,
|
|
338
|
+
open: _inspectorOpenIgnored,
|
|
339
|
+
onOpenChange: _inspectorOnOpenChangeIgnored,
|
|
340
|
+
size: _sz,
|
|
341
|
+
defaultSize: _dsz,
|
|
342
|
+
onSizeChange: _osc,
|
|
343
|
+
sizeUpdate: _szu,
|
|
344
|
+
sizeUpdateMs: _szums,
|
|
345
|
+
...inspectorDomProps
|
|
346
|
+
} = props as any;
|
|
347
|
+
|
|
216
348
|
return (
|
|
217
349
|
<div
|
|
218
|
-
{...
|
|
350
|
+
{...inspectorDomProps}
|
|
219
351
|
ref={setRef}
|
|
220
352
|
className={classNames('rt-ShellInspector', className)}
|
|
221
353
|
data-mode={shell.inspectorMode}
|
|
222
354
|
data-peek={shell.peekTarget === 'inspector' || undefined}
|
|
223
|
-
data-presentation={resolvedPresentation}
|
|
224
|
-
data-open={(isStacked && isExpanded) || undefined}
|
|
355
|
+
data-presentation={shell.currentBreakpointReady ? resolvedPresentation : undefined}
|
|
356
|
+
data-open={(shell.currentBreakpointReady && isStacked && isExpanded) || undefined}
|
|
225
357
|
style={{
|
|
226
358
|
...style,
|
|
227
359
|
['--inspector-size' as any]: `${expandedSize}px`,
|
|
@@ -3,10 +3,10 @@ import classNames from 'classnames';
|
|
|
3
3
|
import * as Sheet from '../sheet.js';
|
|
4
4
|
import { VisuallyHidden } from '../visually-hidden.js';
|
|
5
5
|
import { useShell } from '../shell.context.js';
|
|
6
|
-
import { useResponsivePresentation } from '../shell.hooks.js';
|
|
6
|
+
import { useResponsivePresentation, useResponsiveValue } from '../shell.hooks.js';
|
|
7
7
|
import { PaneResizeContext } from './shell-resize.js';
|
|
8
8
|
import { SidebarHandle, PaneHandle } from './shell-handles.js';
|
|
9
|
-
import type { Breakpoint, PaneMode, PaneSizePersistence, PresentationValue, ResponsivePresentation, ResponsiveSidebarMode, SidebarMode } from '../shell.types.js';
|
|
9
|
+
import type { Breakpoint, PaneMode, PaneSizePersistence, PresentationValue, ResponsivePresentation, ResponsiveSidebarMode, SidebarMode, Responsive } from '../shell.types.js';
|
|
10
10
|
import { BREAKPOINTS } from '../shell.types.js';
|
|
11
11
|
|
|
12
12
|
interface PaneProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
@@ -32,33 +32,29 @@ interface PaneProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
|
32
32
|
persistence?: PaneSizePersistence;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
type
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
type SidebarStateChangeMeta = { reason: 'init' | 'toggle' | 'responsive' };
|
|
36
|
+
type SidebarControlledProps = { state: Responsive<SidebarMode>; onStateChange?: (state: SidebarMode, meta: SidebarStateChangeMeta) => void; defaultState?: never };
|
|
37
|
+
type SidebarUncontrolledProps = { defaultState?: SidebarMode | Partial<Record<Breakpoint, SidebarMode>>; onStateChange?: (state: SidebarMode, meta: SidebarStateChangeMeta) => void; state?: never };
|
|
38
|
+
type SidebarPublicProps = Omit<PaneProps, 'mode' | 'defaultMode' | 'onModeChange'> & {
|
|
39
|
+
// removed legacy mode props
|
|
40
|
+
thinSize?: number;
|
|
41
|
+
toggleModes?: 'both' | 'single';
|
|
42
|
+
// size API (width when expanded)
|
|
43
|
+
size?: number | string;
|
|
44
|
+
defaultSize?: number | string;
|
|
45
|
+
onSizeChange?: (size: number, meta: { reason: 'init' | 'resize' | 'controlled' }) => void;
|
|
46
|
+
sizeUpdate?: 'throttle' | 'debounce';
|
|
47
|
+
sizeUpdateMs?: number;
|
|
48
|
+
} & (SidebarControlledProps | SidebarUncontrolledProps);
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
mode?: SidebarMode;
|
|
49
|
-
defaultMode?: ResponsiveSidebarMode;
|
|
50
|
-
onModeChange?: (mode: SidebarMode) => void;
|
|
51
|
-
thinSize?: number;
|
|
52
|
-
toggleModes?: 'both' | 'single';
|
|
53
|
-
}
|
|
54
|
-
>(
|
|
50
|
+
type SidebarComponent = React.ForwardRefExoticComponent<SidebarPublicProps & React.RefAttributes<HTMLDivElement>> & { Handle: typeof SidebarHandle };
|
|
51
|
+
|
|
52
|
+
export const Sidebar = React.forwardRef<HTMLDivElement, SidebarPublicProps>(
|
|
55
53
|
(
|
|
56
54
|
{
|
|
57
55
|
className,
|
|
58
56
|
presentation = { initial: 'overlay', md: 'fixed' },
|
|
59
|
-
|
|
60
|
-
defaultMode = 'expanded',
|
|
61
|
-
onModeChange,
|
|
57
|
+
// removed legacy props
|
|
62
58
|
expandedSize = 288,
|
|
63
59
|
minSize = 200,
|
|
64
60
|
maxSize = 400,
|
|
@@ -78,6 +74,10 @@ export const Sidebar = React.forwardRef<
|
|
|
78
74
|
style,
|
|
79
75
|
thinSize = 64,
|
|
80
76
|
toggleModes,
|
|
77
|
+
// new state props (XOR)
|
|
78
|
+
state,
|
|
79
|
+
defaultState,
|
|
80
|
+
onStateChange,
|
|
81
81
|
...props
|
|
82
82
|
},
|
|
83
83
|
ref,
|
|
@@ -86,6 +86,7 @@ export const Sidebar = React.forwardRef<
|
|
|
86
86
|
const resolvedPresentation = useResponsivePresentation(presentation);
|
|
87
87
|
const isOverlay = resolvedPresentation === 'overlay';
|
|
88
88
|
const isStacked = resolvedPresentation === 'stacked';
|
|
89
|
+
// Phase sequencing is now CSS-driven; no JS-managed phase
|
|
89
90
|
const localRef = React.useRef<HTMLDivElement | null>(null);
|
|
90
91
|
const setRef = React.useCallback(
|
|
91
92
|
(node: HTMLDivElement | null) => {
|
|
@@ -99,6 +100,34 @@ export const Sidebar = React.forwardRef<
|
|
|
99
100
|
const handleChildren = childArray.filter((el: React.ReactElement) => React.isValidElement(el) && el.type === SidebarHandle);
|
|
100
101
|
const contentChildren = childArray.filter((el: React.ReactElement) => !(React.isValidElement(el) && el.type === SidebarHandle));
|
|
101
102
|
|
|
103
|
+
// Throttled/debounced emitter for onSizeChange
|
|
104
|
+
const emitSizeChange = React.useMemo(() => {
|
|
105
|
+
const cb = (props as any).onSizeChange as undefined | ((s: number, meta: { reason: 'init' | 'resize' | 'controlled' }) => void);
|
|
106
|
+
const strategy = (props as any).sizeUpdate as undefined | 'throttle' | 'debounce';
|
|
107
|
+
const ms = (props as any).sizeUpdateMs ?? 50;
|
|
108
|
+
if (!cb) return () => {};
|
|
109
|
+
if (strategy === 'debounce') {
|
|
110
|
+
let t: any = null;
|
|
111
|
+
return (s: number, meta: { reason: 'init' | 'resize' | 'controlled' }) => {
|
|
112
|
+
if (t) clearTimeout(t);
|
|
113
|
+
t = setTimeout(() => {
|
|
114
|
+
cb(s, meta);
|
|
115
|
+
}, ms);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (strategy === 'throttle') {
|
|
119
|
+
let last = 0;
|
|
120
|
+
return (s: number, meta: { reason: 'init' | 'resize' | 'controlled' }) => {
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
if (now - last >= ms) {
|
|
123
|
+
last = now;
|
|
124
|
+
cb(s, meta);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return (s: number, meta: { reason: 'init' | 'resize' | 'controlled' }) => cb(s, meta);
|
|
129
|
+
}, [(props as any).onSizeChange, (props as any).sizeUpdate, (props as any).sizeUpdateMs]);
|
|
130
|
+
|
|
102
131
|
// Register with shell
|
|
103
132
|
const sidebarId = React.useId();
|
|
104
133
|
React.useEffect(() => {
|
|
@@ -108,30 +137,78 @@ export const Sidebar = React.forwardRef<
|
|
|
108
137
|
};
|
|
109
138
|
}, [shell, sidebarId]);
|
|
110
139
|
|
|
111
|
-
//
|
|
140
|
+
// Dev guards
|
|
141
|
+
const wasControlledRef = React.useRef<boolean | null>(null);
|
|
142
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
143
|
+
if (typeof state !== 'undefined' && typeof defaultState !== 'undefined') {
|
|
144
|
+
// eslint-disable-next-line no-console
|
|
145
|
+
console.error('Shell.Sidebar: Do not pass both `state` and `defaultState`. Choose one.');
|
|
146
|
+
}
|
|
147
|
+
if (typeof (props as any).size !== 'undefined' && typeof (props as any).defaultSize !== 'undefined') {
|
|
148
|
+
// eslint-disable-next-line no-console
|
|
149
|
+
console.error('Shell.Sidebar: Do not pass both `size` and `defaultSize`. Choose one.');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Warn on mode switch between controlled/uncontrolled
|
|
154
|
+
React.useEffect(() => {
|
|
155
|
+
const isControlled = typeof state !== 'undefined';
|
|
156
|
+
if (wasControlledRef.current === null) {
|
|
157
|
+
wasControlledRef.current = isControlled;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (wasControlledRef.current !== isControlled) {
|
|
161
|
+
// eslint-disable-next-line no-console
|
|
162
|
+
console.warn('Shell.Sidebar: Switching between controlled and uncontrolled `state` is not supported.');
|
|
163
|
+
wasControlledRef.current = isControlled;
|
|
164
|
+
}
|
|
165
|
+
}, [state]);
|
|
166
|
+
|
|
167
|
+
// Resolve responsive controlled state at top level
|
|
168
|
+
const resolvedState = useResponsiveValue(state);
|
|
169
|
+
const resolvedDefaultState = useResponsiveValue(defaultState as any);
|
|
170
|
+
|
|
171
|
+
// Honor state/defaultState on mount when uncontrolled
|
|
112
172
|
const didInitRef = React.useRef(false);
|
|
113
173
|
React.useEffect(() => {
|
|
114
174
|
if (didInitRef.current) return;
|
|
175
|
+
if (!shell.currentBreakpointReady) return;
|
|
115
176
|
didInitRef.current = true;
|
|
116
|
-
|
|
117
|
-
|
|
177
|
+
// Controlled state may be responsive; use resolved value
|
|
178
|
+
if (typeof state !== 'undefined' && resolvedState) {
|
|
179
|
+
if (shell.sidebarMode !== resolvedState) shell.setSidebarMode(resolvedState);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (typeof defaultState !== 'undefined') {
|
|
183
|
+
const initialState = (resolvedDefaultState ?? defaultState) as SidebarMode;
|
|
184
|
+
if (shell.sidebarMode !== initialState) {
|
|
185
|
+
shell.setSidebarMode(initialState);
|
|
186
|
+
}
|
|
187
|
+
onStateChange?.(initialState, { reason: 'init' });
|
|
188
|
+
return;
|
|
118
189
|
}
|
|
119
190
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
120
|
-
}, []);
|
|
191
|
+
}, [shell.currentBreakpointReady, resolvedDefaultState, resolvedState, state, defaultState]);
|
|
121
192
|
|
|
122
|
-
// Sync controlled
|
|
193
|
+
// Sync controlled state (responsive-aware)
|
|
123
194
|
React.useEffect(() => {
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}, [mode, shell]);
|
|
195
|
+
if (resolvedState === undefined) return;
|
|
196
|
+
if (shell.sidebarMode !== resolvedState) shell.setSidebarMode(resolvedState);
|
|
197
|
+
}, [resolvedState, shell.sidebarMode]);
|
|
128
198
|
|
|
129
199
|
// Emit mode changes
|
|
200
|
+
const lastNotifyModeRef = React.useRef<SidebarMode | null>(null);
|
|
130
201
|
React.useEffect(() => {
|
|
131
|
-
|
|
132
|
-
|
|
202
|
+
// notify new API when uncontrolled; skip first run to avoid masking init
|
|
203
|
+
if (typeof state === 'undefined') {
|
|
204
|
+
if (lastNotifyModeRef.current === null) {
|
|
205
|
+
lastNotifyModeRef.current = shell.sidebarMode as SidebarMode;
|
|
206
|
+
} else if (lastNotifyModeRef.current !== shell.sidebarMode) {
|
|
207
|
+
lastNotifyModeRef.current = shell.sidebarMode as SidebarMode;
|
|
208
|
+
onStateChange?.(shell.sidebarMode as SidebarMode, { reason: 'toggle' });
|
|
209
|
+
}
|
|
133
210
|
}
|
|
134
|
-
}, [shell.sidebarMode,
|
|
211
|
+
}, [shell.sidebarMode, state, onStateChange]);
|
|
135
212
|
|
|
136
213
|
// Emit expand/collapse events
|
|
137
214
|
React.useEffect(() => {
|
|
@@ -178,29 +255,12 @@ export const Sidebar = React.forwardRef<
|
|
|
178
255
|
};
|
|
179
256
|
}, [resizable, persistenceAdapter, onResize, isOverlay]);
|
|
180
257
|
|
|
181
|
-
// Always-follow responsive defaultMode for uncontrolled Sidebar (on breakpoint change only)
|
|
182
|
-
const resolveResponsiveMode = React.useCallback((): SidebarMode => {
|
|
183
|
-
if (typeof defaultMode === 'string') return defaultMode as SidebarMode;
|
|
184
|
-
const dm = defaultMode as Partial<Record<Breakpoint, SidebarMode>> | undefined;
|
|
185
|
-
if (dm && dm[shell.currentBreakpoint as Breakpoint]) {
|
|
186
|
-
return dm[shell.currentBreakpoint as Breakpoint] as SidebarMode;
|
|
187
|
-
}
|
|
188
|
-
const bpKeys = Object.keys(BREAKPOINTS) as Array<keyof typeof BREAKPOINTS>;
|
|
189
|
-
const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat('initial' as Breakpoint);
|
|
190
|
-
const startIdx = order.indexOf(shell.currentBreakpoint as Breakpoint);
|
|
191
|
-
for (let i = startIdx + 1; i < order.length; i++) {
|
|
192
|
-
const bp = order[i];
|
|
193
|
-
if (dm && dm[bp]) return dm[bp] as SidebarMode;
|
|
194
|
-
}
|
|
195
|
-
return 'collapsed';
|
|
196
|
-
}, [defaultMode, shell.currentBreakpoint]);
|
|
197
|
-
|
|
198
258
|
// Register custom toggle behavior based on toggleModes (both|single)
|
|
199
259
|
const shellForToggle = useShell();
|
|
200
260
|
const resolveDefaultSidebarMode = React.useCallback((): SidebarMode => {
|
|
201
|
-
const resolved =
|
|
261
|
+
const resolved = defaultState ?? 'expanded';
|
|
202
262
|
return resolved === 'thin' || resolved === 'expanded' ? resolved : 'expanded';
|
|
203
|
-
}, [
|
|
263
|
+
}, [defaultState]);
|
|
204
264
|
|
|
205
265
|
React.useEffect(() => {
|
|
206
266
|
if (!shellForToggle.setSidebarToggleComputer) return;
|
|
@@ -231,15 +291,7 @@ export const Sidebar = React.forwardRef<
|
|
|
231
291
|
}
|
|
232
292
|
}, [shell.sidebarMode, thinSize, expandedSize]);
|
|
233
293
|
|
|
234
|
-
|
|
235
|
-
React.useEffect(() => {
|
|
236
|
-
if (mode !== undefined) return;
|
|
237
|
-
if (!shell.currentBreakpointReady) return;
|
|
238
|
-
if (lastSidebarBpRef.current === shell.currentBreakpoint) return;
|
|
239
|
-
lastSidebarBpRef.current = shell.currentBreakpoint as Breakpoint;
|
|
240
|
-
const next = resolveResponsiveMode();
|
|
241
|
-
if (next !== shell.sidebarMode) shell.setSidebarMode(next);
|
|
242
|
-
}, [mode, shell.currentBreakpoint, shell.currentBreakpointReady, resolveResponsiveMode, shell.sidebarMode, shell.setSidebarMode]);
|
|
294
|
+
// Remove responsive default mode behavior entirely
|
|
243
295
|
|
|
244
296
|
const handleEl =
|
|
245
297
|
resizable && !isOverlay && shell.sidebarMode === 'expanded' ? (
|
|
@@ -261,6 +313,7 @@ export const Sidebar = React.forwardRef<
|
|
|
261
313
|
onResizeStart,
|
|
262
314
|
onResizeEnd: (size) => {
|
|
263
315
|
onResizeEnd?.(size);
|
|
316
|
+
emitSizeChange(size, { reason: 'resize' });
|
|
264
317
|
persistenceAdapter?.save?.(size);
|
|
265
318
|
},
|
|
266
319
|
target: 'sidebar',
|
|
@@ -276,6 +329,61 @@ export const Sidebar = React.forwardRef<
|
|
|
276
329
|
</PaneResizeContext.Provider>
|
|
277
330
|
) : null;
|
|
278
331
|
|
|
332
|
+
// Strip new API props from DOM
|
|
333
|
+
const { state: _s, defaultState: _ds, onStateChange: _osc, size: _sz, defaultSize: _dsz, onSizeChange: _onsc, sizeUpdate: _szu, sizeUpdateMs: _szums, ...domProps } = props as any;
|
|
334
|
+
|
|
335
|
+
// Normalize CSS lengths to px
|
|
336
|
+
const normalizeToPx = React.useCallback((value: number | string | undefined): number | undefined => {
|
|
337
|
+
if (value == null) return undefined;
|
|
338
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
339
|
+
const str = String(value).trim();
|
|
340
|
+
if (!str) return undefined;
|
|
341
|
+
if (str.endsWith('px')) return Number.parseFloat(str);
|
|
342
|
+
if (str.endsWith('rem')) {
|
|
343
|
+
const rem = Number.parseFloat(getComputedStyle(document.documentElement).fontSize || '16') || 16;
|
|
344
|
+
return Number.parseFloat(str) * rem;
|
|
345
|
+
}
|
|
346
|
+
if (str.endsWith('%')) {
|
|
347
|
+
const pct = Number.parseFloat(str);
|
|
348
|
+
const base = document.documentElement.clientWidth || window.innerWidth || 0;
|
|
349
|
+
return (pct / 100) * base;
|
|
350
|
+
}
|
|
351
|
+
const n = Number.parseFloat(str);
|
|
352
|
+
return Number.isFinite(n) ? n : undefined;
|
|
353
|
+
}, []);
|
|
354
|
+
|
|
355
|
+
// Apply defaultSize on mount when uncontrolled
|
|
356
|
+
React.useEffect(() => {
|
|
357
|
+
if (!localRef.current) return;
|
|
358
|
+
const { size, defaultSize } = props as any;
|
|
359
|
+
if (typeof size === 'undefined' && typeof defaultSize !== 'undefined') {
|
|
360
|
+
const px = normalizeToPx(defaultSize);
|
|
361
|
+
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
362
|
+
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
363
|
+
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
364
|
+
const clamped = Math.min(maxPx ?? px, Math.max(minPx ?? px, px));
|
|
365
|
+
localRef.current.style.setProperty('--sidebar-size', `${clamped}px`);
|
|
366
|
+
emitSizeChange(clamped, { reason: 'init' });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
370
|
+
}, []);
|
|
371
|
+
|
|
372
|
+
// Controlled size sync
|
|
373
|
+
React.useEffect(() => {
|
|
374
|
+
if (!localRef.current) return;
|
|
375
|
+
const { size } = props as any;
|
|
376
|
+
if (typeof size === 'undefined') return;
|
|
377
|
+
const px = normalizeToPx(size);
|
|
378
|
+
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
379
|
+
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
380
|
+
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
381
|
+
const clamped = Math.min(maxPx ?? px, Math.max(minPx ?? px, px));
|
|
382
|
+
localRef.current.style.setProperty('--sidebar-size', `${clamped}px`);
|
|
383
|
+
emitSizeChange(clamped, { reason: 'controlled' });
|
|
384
|
+
}
|
|
385
|
+
}, [(props as any).size, minSize, maxSize, normalizeToPx, emitSizeChange]);
|
|
386
|
+
|
|
279
387
|
if (isOverlay) {
|
|
280
388
|
const open = shell.sidebarMode !== 'collapsed';
|
|
281
389
|
return (
|
|
@@ -288,23 +396,22 @@ export const Sidebar = React.forwardRef<
|
|
|
288
396
|
}}
|
|
289
397
|
>
|
|
290
398
|
<VisuallyHidden>
|
|
291
|
-
<Sheet.Title>
|
|
399
|
+
<Sheet.Title>Navigation</Sheet.Title>
|
|
292
400
|
</VisuallyHidden>
|
|
293
401
|
{contentChildren}
|
|
294
402
|
</Sheet.Content>
|
|
295
403
|
</Sheet.Root>
|
|
296
404
|
);
|
|
297
405
|
}
|
|
298
|
-
|
|
299
406
|
return (
|
|
300
407
|
<div
|
|
301
|
-
{...
|
|
408
|
+
{...domProps}
|
|
302
409
|
ref={setRef}
|
|
303
410
|
className={classNames('rt-ShellSidebar', className)}
|
|
304
411
|
data-mode={shell.sidebarMode}
|
|
305
412
|
data-peek={shell.peekTarget === 'sidebar' || undefined}
|
|
306
|
-
data-presentation={resolvedPresentation}
|
|
307
|
-
data-open={(isStacked && isContentVisible) || undefined}
|
|
413
|
+
data-presentation={shell.currentBreakpointReady ? resolvedPresentation : undefined}
|
|
414
|
+
data-open={(shell.currentBreakpointReady && isStacked && isContentVisible) || undefined}
|
|
308
415
|
style={{
|
|
309
416
|
...style,
|
|
310
417
|
['--sidebar-size' as any]: `${expandedSize}px`,
|