@flamingo-stack/openframe-frontend-core 0.0.702-snapshot.20261005191338 → 0.0.702
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/{chunk-NMGX2ZTU.js → chunk-2V4Z44G6.js} +5 -5
- package/dist/{chunk-RLDAYA2A.cjs → chunk-3G76LRGL.cjs} +9 -9
- package/dist/{chunk-GX3OUTFO.cjs → chunk-3WSFIV5G.cjs} +32 -32
- package/dist/{chunk-BOUPNYUV.js → chunk-446KS5BG.js} +2 -2
- package/dist/{chunk-QGTDWFT6.js → chunk-6JPQDYYY.js} +345 -419
- package/dist/{chunk-67CXGPTW.cjs → chunk-AEQANLBW.cjs} +24 -24
- package/dist/{chunk-NYSYSKSH.cjs → chunk-ASN65P3F.cjs} +9 -9
- package/dist/{chunk-S6V7A5FS.cjs → chunk-B3LZJ6J3.cjs} +12 -12
- package/dist/{chunk-SL2FOFT2.js → chunk-BO5ZHDA4.js} +4 -4
- package/dist/{chunk-RTXTGLGB.cjs → chunk-CXDJUKC6.cjs} +2 -2
- package/dist/{chunk-BMJUKS6S.cjs → chunk-CYYTD5ZK.cjs} +20 -20
- package/dist/{chunk-ENGBNKUJ.js → chunk-CZ34HXDW.js} +157 -12
- package/dist/{chunk-KFDP3DUL.cjs → chunk-E3DH543A.cjs} +44 -44
- package/dist/{chunk-7632TNE7.js → chunk-ETFFEA3J.js} +1 -1
- package/dist/{chunk-MXHJROXJ.cjs → chunk-FD4DBTFO.cjs} +475 -330
- package/dist/{chunk-BSEIEWM5.cjs → chunk-FHM7GD5B.cjs} +2 -2
- package/dist/{chunk-NU724ZVN.cjs → chunk-HIXGAY2L.cjs} +70 -70
- package/dist/{chunk-53AC23ZN.js → chunk-HPJ4XS2L.js} +4 -4
- package/dist/{chunk-2NE3DNXY.js → chunk-HYFLTFFB.js} +1 -1
- package/dist/{chunk-NCTYD7HL.js → chunk-IZKOQVXO.js} +2 -2
- package/dist/{chunk-J4K5JOLI.js → chunk-JOY6HVPV.js} +1 -1
- package/dist/{chunk-YBGEPKC2.cjs → chunk-L7KZA7ZD.cjs} +4 -4
- package/dist/{chunk-5VPIC5MD.cjs → chunk-LZNJH23A.cjs} +3 -3
- package/dist/{chunk-UTW4SQDI.js → chunk-MWUBBBWZ.js} +2 -2
- package/dist/{chunk-G55W76S6.js → chunk-PLK6GHN7.js} +1 -1
- package/dist/{chunk-HYLIQMZH.cjs → chunk-QFW3RUZS.cjs} +21 -21
- package/dist/{chunk-GZYLZ4MB.js → chunk-RS7HWAYX.js} +2 -2
- package/dist/{chunk-HY5YL3U5.cjs → chunk-RTGPIHVO.cjs} +173 -247
- package/dist/{chunk-PJP5SMOU.js → chunk-SAA7B3DA.js} +3 -3
- package/dist/{chunk-DAW6TDZB.js → chunk-UJJWTJ6S.js} +4 -4
- package/dist/{chunk-SFGXIFYO.cjs → chunk-YGRHITPY.cjs} +7 -7
- package/dist/{chunk-HHFPQNP3.js → chunk-Z5HDCZAS.js} +1 -1
- package/dist/components/case-studies/index.cjs +14 -14
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +4 -4
- package/dist/components/faq/index.js +3 -3
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/help-center-pages/index.cjs +71 -71
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/index.cjs +41 -41
- package/dist/components/index.js +13 -13
- package/dist/components/meeting-scheduler/index.cjs +25 -25
- package/dist/components/meeting-scheduler/index.js +2 -2
- package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +6 -6
- package/dist/components/navigation/index.js +5 -5
- package/dist/components/onboarding-guides/index.cjs +6 -6
- package/dist/components/onboarding-guides/index.js +5 -5
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/drawer.d.ts +3 -7
- package/dist/components/ui/drawer.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/index.cjs +7 -7
- package/dist/index.js +6 -6
- package/package.json +1 -1
- package/src/components/chat/embeddable-chat.tsx +9 -11
- package/src/components/navigation/app-layout-drawer.tsx +237 -14
- package/src/components/ui/drawer.tsx +188 -41
- package/dist/components/ui/panel-resize-handle.d.ts +0 -25
- package/dist/components/ui/panel-resize-handle.d.ts.map +0 -1
- package/dist/hooks/ui/use-resizable-panel-size.d.ts +0 -46
- package/dist/hooks/ui/use-resizable-panel-size.d.ts.map +0 -1
- package/src/components/ui/panel-resize-handle.tsx +0 -184
- package/src/hooks/ui/use-resizable-panel-size.ts +0 -114
|
@@ -126,13 +126,13 @@ const HISTORY_RAIL_WIDTH = 320;
|
|
|
126
126
|
const CHAT_BLOCK_MIN_WIDTH = 400;
|
|
127
127
|
const SPLIT_MIN_WIDTH = HISTORY_RAIL_WIDTH + CHAT_BLOCK_MIN_WIDTH;
|
|
128
128
|
|
|
129
|
-
// Desktop drawer
|
|
130
|
-
// Drawer's own min/max)
|
|
131
|
-
// stored (see DRAWER_WIDTH_KEY); the default tracks the window, so changing
|
|
132
|
-
// this policy needs no new key.
|
|
129
|
+
// Desktop drawer opens at this fraction of the viewport width (clamped to the
|
|
130
|
+
// Drawer's own min/max). The user can still resize (persisted per DRAWER_WIDTH_KEY).
|
|
133
131
|
const DRAWER_DEFAULT_WIDTH_RATIO = 0.5;
|
|
132
|
+
// Bump the suffix whenever the default policy changes so previously-persisted
|
|
133
|
+
// widths (e.g. the old fixed 750px / earlier 30% default) reset on next open.
|
|
134
134
|
const DRAWER_WIDTH_KEY = 'mingo-chat-width-v4';
|
|
135
|
-
const DRAWER_DEFAULT_WIDTH_PX = 750; // before the viewport is known
|
|
135
|
+
const DRAWER_DEFAULT_WIDTH_PX = 750; // SSR fallback before the viewport is known
|
|
136
136
|
/**
|
|
137
137
|
* A panel that owns a dialog list opens WIDE ENOUGH TO SPLIT, so the "Current
|
|
138
138
|
* Chats" rail sits beside the conversation instead of replacing it.
|
|
@@ -144,14 +144,12 @@ const DRAWER_DEFAULT_WIDTH_PX = 750; // before the viewport is known (server ren
|
|
|
144
144
|
* viewport, so a genuinely small screen keeps the stacked view.
|
|
145
145
|
*/
|
|
146
146
|
const SPLIT_DEFAULT_WIDTH_ALLOWANCE = 16;
|
|
147
|
-
function drawerDefaultWidth(
|
|
148
|
-
if (
|
|
149
|
-
const ratioWidth = Math.round(
|
|
147
|
+
function drawerDefaultWidth(withHistoryRail: boolean): number {
|
|
148
|
+
if (typeof window === 'undefined') return DRAWER_DEFAULT_WIDTH_PX;
|
|
149
|
+
const ratioWidth = Math.round(window.innerWidth * DRAWER_DEFAULT_WIDTH_RATIO);
|
|
150
150
|
if (!withHistoryRail) return ratioWidth;
|
|
151
151
|
return Math.max(ratioWidth, SPLIT_MIN_WIDTH + SPLIT_DEFAULT_WIDTH_ALLOWANCE);
|
|
152
152
|
}
|
|
153
|
-
const drawerDefaultWidthWithRail = (viewportWidth: number) => drawerDefaultWidth(viewportWidth, true);
|
|
154
|
-
const drawerDefaultWidthWithoutRail = (viewportWidth: number) => drawerDefaultWidth(viewportWidth, false);
|
|
155
153
|
|
|
156
154
|
// =============================================================================
|
|
157
155
|
// Types
|
|
@@ -2812,7 +2810,7 @@ function EmbeddableChatInner({
|
|
|
2812
2810
|
resizable
|
|
2813
2811
|
minSize={480}
|
|
2814
2812
|
maxSize={1600}
|
|
2815
|
-
defaultSize={historyListMode
|
|
2813
|
+
defaultSize={drawerDefaultWidth(historyListMode)}
|
|
2816
2814
|
storageKey={DRAWER_WIDTH_KEY}
|
|
2817
2815
|
resizeAriaLabel="Resize chat panel"
|
|
2818
2816
|
overlayClassName="mingo-chat-overlay"
|
|
@@ -6,6 +6,9 @@ import {
|
|
|
6
6
|
type CSSProperties,
|
|
7
7
|
type ComponentPropsWithoutRef,
|
|
8
8
|
type ComponentRef,
|
|
9
|
+
type KeyboardEvent,
|
|
10
|
+
type PointerEvent,
|
|
11
|
+
type RefObject,
|
|
9
12
|
createContext,
|
|
10
13
|
forwardRef,
|
|
11
14
|
useCallback,
|
|
@@ -15,8 +18,8 @@ import {
|
|
|
15
18
|
useState,
|
|
16
19
|
} from 'react';
|
|
17
20
|
|
|
18
|
-
import { useResizablePanelSize } from '../../hooks/ui/use-resizable-panel-size';
|
|
19
21
|
import { cn } from '../../utils/cn';
|
|
22
|
+
import { clamp } from '../../utils/common';
|
|
20
23
|
import {
|
|
21
24
|
DrawerBody,
|
|
22
25
|
DrawerDescription,
|
|
@@ -26,7 +29,6 @@ import {
|
|
|
26
29
|
OVERLAY_BACKDROP_CLASS,
|
|
27
30
|
type DrawerSide,
|
|
28
31
|
} from '../ui/drawer';
|
|
29
|
-
import { PanelResizeHandle } from '../ui/panel-resize-handle';
|
|
30
32
|
import {
|
|
31
33
|
type AppLayoutDrawerHandle,
|
|
32
34
|
useAppLayoutDrawerContainer,
|
|
@@ -214,8 +216,25 @@ const HORIZONTAL_SIDES: ReadonlySet<DrawerSide> = new Set(['left', 'right']);
|
|
|
214
216
|
*/
|
|
215
217
|
const PERSIST_CLOSED_HOLD = 'data-[state=closed]:fill-mode-forwards';
|
|
216
218
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
+
interface UseContainedResizableSizeArgs {
|
|
220
|
+
enabled: boolean;
|
|
221
|
+
isHorizontal: boolean;
|
|
222
|
+
minSize: number;
|
|
223
|
+
maxSize: number;
|
|
224
|
+
defaultSize: number;
|
|
225
|
+
storageKey?: string;
|
|
226
|
+
container: HTMLElement | null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function useContainedResizableSize({
|
|
230
|
+
enabled,
|
|
231
|
+
isHorizontal,
|
|
232
|
+
minSize,
|
|
233
|
+
maxSize,
|
|
234
|
+
defaultSize,
|
|
235
|
+
storageKey,
|
|
236
|
+
container,
|
|
237
|
+
}: UseContainedResizableSizeArgs) {
|
|
219
238
|
const [available, setAvailable] = useState(0);
|
|
220
239
|
|
|
221
240
|
useEffect(() => {
|
|
@@ -229,7 +248,216 @@ function useContainerExtent(enabled: boolean, isHorizontal: boolean, container:
|
|
|
229
248
|
return () => ro.disconnect();
|
|
230
249
|
}, [enabled, container, isHorizontal]);
|
|
231
250
|
|
|
232
|
-
|
|
251
|
+
const clampToContainer = useCallback(
|
|
252
|
+
(value: number) => {
|
|
253
|
+
// Reserve 40px (the `system-m` outside-edge padding from the wrapper
|
|
254
|
+
// plus a matching gap on the inside edge) so the panel sits symmetrically
|
|
255
|
+
// inside the container. This also keeps the resize grip on-screen at
|
|
256
|
+
// maximum extent.
|
|
257
|
+
const effectiveMax = available > 0 ? Math.min(maxSize, available - 40) : maxSize;
|
|
258
|
+
return clamp(value, minSize, Math.max(minSize, effectiveMax));
|
|
259
|
+
},
|
|
260
|
+
[available, minSize, maxSize],
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const readInitial = useCallback(() => {
|
|
264
|
+
if (!enabled) return defaultSize;
|
|
265
|
+
if (typeof window === 'undefined') return defaultSize;
|
|
266
|
+
if (!storageKey) return defaultSize;
|
|
267
|
+
try {
|
|
268
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
269
|
+
if (!raw) return defaultSize;
|
|
270
|
+
const parsed = parseFloat(raw);
|
|
271
|
+
if (!Number.isFinite(parsed)) return defaultSize;
|
|
272
|
+
return parsed;
|
|
273
|
+
} catch {
|
|
274
|
+
return defaultSize;
|
|
275
|
+
}
|
|
276
|
+
}, [enabled, storageKey, defaultSize]);
|
|
277
|
+
|
|
278
|
+
const [rawSize, setSizeRaw] = useState<number>(readInitial);
|
|
279
|
+
|
|
280
|
+
// Re-clamp the stored size whenever the container resizes so a previously
|
|
281
|
+
// saved size never overflows after the user shrinks the viewport.
|
|
282
|
+
//
|
|
283
|
+
// Clamped where it is READ rather than written back into state from an
|
|
284
|
+
// effect: `available` is already state (the ResizeObserver above owns it), so
|
|
285
|
+
// the container shrinking re-renders regardless — and deriving here means the
|
|
286
|
+
// panel is never painted overflowing its container for the frame between the
|
|
287
|
+
// resize and the corrective commit. Keeping the raw value also means widening
|
|
288
|
+
// the viewport again restores the size the user actually chose, instead of
|
|
289
|
+
// leaving it permanently trimmed to the narrowest width ever seen.
|
|
290
|
+
const size = enabled ? clampToContainer(rawSize) : rawSize;
|
|
291
|
+
|
|
292
|
+
const setSize = useCallback((next: number) => setSizeRaw(clampToContainer(next)), [clampToContainer]);
|
|
293
|
+
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
if (!enabled || !storageKey || typeof window === 'undefined') return;
|
|
296
|
+
try {
|
|
297
|
+
window.localStorage.setItem(storageKey, String(Math.round(size)));
|
|
298
|
+
} catch {
|
|
299
|
+
// ignore quota / disabled-storage
|
|
300
|
+
}
|
|
301
|
+
}, [enabled, size, storageKey]);
|
|
302
|
+
|
|
303
|
+
return { size, setSize, clampSize: clampToContainer };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Ends a drawer resize drag: commits the size it reached and restores the
|
|
307
|
+
* page's cursor and text selection. No-op when no drag is in progress. */
|
|
308
|
+
function finishDrag(
|
|
309
|
+
startRef: RefObject<{ x: number; y: number; size: number } | null>,
|
|
310
|
+
dragSizeRef: RefObject<number | null>,
|
|
311
|
+
commit: (size: number) => void,
|
|
312
|
+
): void {
|
|
313
|
+
if (!startRef.current) return;
|
|
314
|
+
startRef.current = null;
|
|
315
|
+
if (dragSizeRef.current !== null) commit(dragSizeRef.current);
|
|
316
|
+
dragSizeRef.current = null;
|
|
317
|
+
document.body.style.cursor = '';
|
|
318
|
+
document.body.style.userSelect = '';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
interface AppLayoutDrawerResizeHandleProps {
|
|
322
|
+
side: DrawerSide;
|
|
323
|
+
size: number;
|
|
324
|
+
minSize: number;
|
|
325
|
+
maxSize: number;
|
|
326
|
+
onSize: (next: number) => void;
|
|
327
|
+
/** Clamp a candidate size to the min/max and the container. */
|
|
328
|
+
clampSize: (next: number) => number;
|
|
329
|
+
/** The panel the size applies to — written directly while dragging. */
|
|
330
|
+
panelRef: RefObject<HTMLDivElement | null>;
|
|
331
|
+
ariaLabel?: string;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function AppLayoutDrawerResizeHandle({
|
|
335
|
+
side,
|
|
336
|
+
size,
|
|
337
|
+
minSize,
|
|
338
|
+
maxSize,
|
|
339
|
+
onSize,
|
|
340
|
+
clampSize,
|
|
341
|
+
panelRef,
|
|
342
|
+
ariaLabel,
|
|
343
|
+
}: AppLayoutDrawerResizeHandleProps) {
|
|
344
|
+
const isHorizontal = HORIZONTAL_SIDES.has(side);
|
|
345
|
+
const startRef = useRef<{ x: number; y: number; size: number } | null>(null);
|
|
346
|
+
// Size reached by the drag in progress, committed to React state on release.
|
|
347
|
+
const dragSizeRef = useRef<number | null>(null);
|
|
348
|
+
const onSizeRef = useRef(onSize);
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
onSizeRef.current = onSize;
|
|
351
|
+
}, [onSize]);
|
|
352
|
+
// The handle can unmount mid-drag (a persist-mode drawer closing), and then
|
|
353
|
+
// no pointer event ends the drag: end it here, or the panel keeps a width
|
|
354
|
+
// React state never received and the page keeps the resize cursor.
|
|
355
|
+
useEffect(() => () => finishDrag(startRef, dragSizeRef, onSizeRef.current), []);
|
|
356
|
+
|
|
357
|
+
const direction = side === 'right' || side === 'bottom' ? -1 : 1;
|
|
358
|
+
|
|
359
|
+
const handlePointerDown = (e: PointerEvent<HTMLDivElement>) => {
|
|
360
|
+
if (e.button !== 0 && e.pointerType === 'mouse') return;
|
|
361
|
+
e.preventDefault();
|
|
362
|
+
startRef.current = { x: e.clientX, y: e.clientY, size };
|
|
363
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
364
|
+
document.body.style.cursor = isHorizontal ? 'col-resize' : 'row-resize';
|
|
365
|
+
document.body.style.userSelect = 'none';
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// The drag writes the panel's size straight to the DOM and commits it to
|
|
369
|
+
// state once, on release. Committing every pointermove re-rendered the
|
|
370
|
+
// drawer per event — ~16% of a drag's CPU in WebKit on top of the reflow the
|
|
371
|
+
// resize itself costs. ResizeObserver consumers inside the panel still see
|
|
372
|
+
// every change. A re-render of the drawer mid-drag (e.g. a streamed chunk)
|
|
373
|
+
// does not undo the write: React diffs against its previous render, where
|
|
374
|
+
// the size is unchanged, so it leaves the style alone.
|
|
375
|
+
const handlePointerMove = (e: PointerEvent<HTMLDivElement>) => {
|
|
376
|
+
const start = startRef.current;
|
|
377
|
+
const panel = panelRef.current;
|
|
378
|
+
if (!start || !panel) return;
|
|
379
|
+
const delta = isHorizontal ? e.clientX - start.x : e.clientY - start.y;
|
|
380
|
+
const next = clampSize(start.size + delta * direction);
|
|
381
|
+
dragSizeRef.current = next;
|
|
382
|
+
panel.style[isHorizontal ? 'width' : 'height'] = `${next}px`;
|
|
383
|
+
e.currentTarget.setAttribute('aria-valuenow', String(Math.round(next)));
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const endDrag = (e: PointerEvent<HTMLDivElement>) => {
|
|
387
|
+
if (!startRef.current) return;
|
|
388
|
+
finishDrag(startRef, dragSizeRef, onSize);
|
|
389
|
+
try {
|
|
390
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
391
|
+
} catch {
|
|
392
|
+
// ignore — pointer may already be released
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
397
|
+
const step = e.shiftKey ? 40 : 16;
|
|
398
|
+
if (isHorizontal) {
|
|
399
|
+
if (e.key === 'ArrowLeft') {
|
|
400
|
+
e.preventDefault();
|
|
401
|
+
onSize(size + step * (side === 'right' ? 1 : -1));
|
|
402
|
+
} else if (e.key === 'ArrowRight') {
|
|
403
|
+
e.preventDefault();
|
|
404
|
+
onSize(size + step * (side === 'right' ? -1 : 1));
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
if (e.key === 'ArrowUp') {
|
|
408
|
+
e.preventDefault();
|
|
409
|
+
onSize(size + step * (side === 'bottom' ? 1 : -1));
|
|
410
|
+
} else if (e.key === 'ArrowDown') {
|
|
411
|
+
e.preventDefault();
|
|
412
|
+
onSize(size + step * (side === 'bottom' ? -1 : 1));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (e.key === 'Home') {
|
|
416
|
+
e.preventDefault();
|
|
417
|
+
onSize(minSize);
|
|
418
|
+
} else if (e.key === 'End') {
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
onSize(maxSize);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const trackPosition =
|
|
425
|
+
side === 'right'
|
|
426
|
+
? 'right-full top-4 bottom-4 w-6 items-center justify-end pr-1'
|
|
427
|
+
: side === 'left'
|
|
428
|
+
? 'left-full top-4 bottom-4 w-6 items-center justify-start pl-1'
|
|
429
|
+
: side === 'bottom'
|
|
430
|
+
? 'bottom-full left-4 right-4 h-6 justify-center items-end pb-1'
|
|
431
|
+
: 'top-full left-4 right-4 h-6 justify-center items-start pt-1';
|
|
432
|
+
|
|
433
|
+
const cursorClass = isHorizontal ? 'cursor-col-resize' : 'cursor-row-resize';
|
|
434
|
+
const gripClass = isHorizontal ? 'h-10 w-1' : 'w-10 h-1';
|
|
435
|
+
|
|
436
|
+
return (
|
|
437
|
+
<div
|
|
438
|
+
role="separator"
|
|
439
|
+
tabIndex={0}
|
|
440
|
+
aria-orientation={isHorizontal ? 'vertical' : 'horizontal'}
|
|
441
|
+
aria-valuenow={Math.round(size)}
|
|
442
|
+
aria-valuemin={minSize}
|
|
443
|
+
aria-valuemax={maxSize}
|
|
444
|
+
aria-label={ariaLabel ?? (isHorizontal ? 'Resize panel width' : 'Resize panel height')}
|
|
445
|
+
onPointerDown={handlePointerDown}
|
|
446
|
+
onPointerMove={handlePointerMove}
|
|
447
|
+
onPointerUp={endDrag}
|
|
448
|
+
onPointerCancel={endDrag}
|
|
449
|
+
onLostPointerCapture={endDrag}
|
|
450
|
+
onKeyDown={handleKeyDown}
|
|
451
|
+
className={cn(
|
|
452
|
+
'group absolute z-20 flex touch-none select-none',
|
|
453
|
+
'outline-none ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0',
|
|
454
|
+
trackPosition,
|
|
455
|
+
cursorClass,
|
|
456
|
+
)}
|
|
457
|
+
>
|
|
458
|
+
<div aria-hidden className={cn('rounded-full bg-ods-border', gripClass)} />
|
|
459
|
+
</div>
|
|
460
|
+
);
|
|
233
461
|
}
|
|
234
462
|
|
|
235
463
|
export interface AppLayoutDrawerContentProps
|
|
@@ -400,18 +628,14 @@ const AppLayoutDrawerContent = forwardRef<ComponentRef<typeof DialogPrimitive.Co
|
|
|
400
628
|
}, [mobileBreakpoint]);
|
|
401
629
|
|
|
402
630
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
403
|
-
|
|
404
|
-
// matching gap on the inside edge) so the panel sits symmetrically inside
|
|
405
|
-
// the container and the resize grip stays on screen at maximum extent.
|
|
406
|
-
const available = useContainerExtent(resizable, isHorizontal, portalContainer);
|
|
407
|
-
const { size, setSize, clampSize } = useResizablePanelSize({
|
|
631
|
+
const { size, setSize, clampSize } = useContainedResizableSize({
|
|
408
632
|
enabled: resizable,
|
|
633
|
+
isHorizontal,
|
|
409
634
|
minSize,
|
|
410
635
|
maxSize,
|
|
411
636
|
defaultSize: initialSize,
|
|
412
|
-
available,
|
|
413
|
-
reserve: 40,
|
|
414
637
|
storageKey,
|
|
638
|
+
container: portalContainer,
|
|
415
639
|
});
|
|
416
640
|
|
|
417
641
|
const applyInlineSize = resizable && !isMobile;
|
|
@@ -483,8 +707,7 @@ const AppLayoutDrawerContent = forwardRef<ComponentRef<typeof DialogPrimitive.Co
|
|
|
483
707
|
at the container edge. In persist mode it's also non-interactive
|
|
484
708
|
while closed, so drop it entirely until the panel reopens. */}
|
|
485
709
|
{applyInlineSize && (!persist || open) ? (
|
|
486
|
-
<
|
|
487
|
-
variant="inLayout"
|
|
710
|
+
<AppLayoutDrawerResizeHandle
|
|
488
711
|
side={resolvedSide}
|
|
489
712
|
size={size}
|
|
490
713
|
minSize={minSize}
|
|
@@ -8,18 +8,19 @@ import {
|
|
|
8
8
|
type ComponentPropsWithoutRef,
|
|
9
9
|
type ComponentRef,
|
|
10
10
|
type HTMLAttributes,
|
|
11
|
+
type KeyboardEvent,
|
|
12
|
+
type PointerEvent,
|
|
11
13
|
type ReactNode,
|
|
12
14
|
forwardRef,
|
|
15
|
+
useCallback,
|
|
13
16
|
useEffect,
|
|
14
17
|
useRef,
|
|
15
18
|
useState,
|
|
16
|
-
useSyncExternalStore,
|
|
17
19
|
} from 'react';
|
|
18
20
|
|
|
19
21
|
import { useHeaderHeight } from '../../hooks/ui/use-header-height';
|
|
20
|
-
import { type PanelDefaultSize, useResizablePanelSize } from '../../hooks/ui/use-resizable-panel-size';
|
|
21
22
|
import { cn } from '../../utils/cn';
|
|
22
|
-
import {
|
|
23
|
+
import { clamp } from '../../utils/common';
|
|
23
24
|
|
|
24
25
|
/** Unified overlay backdrop — dimmed, no blur. Single source of truth for
|
|
25
26
|
* every full-screen backdrop (Drawer, AppLayoutDrawer, MobileBurgerMenu,
|
|
@@ -137,21 +138,181 @@ type DrawerSide = 'right' | 'left' | 'top' | 'bottom';
|
|
|
137
138
|
|
|
138
139
|
const HORIZONTAL_SIDES: ReadonlySet<DrawerSide> = new Set(['left', 'right']);
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
window
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
141
|
+
function viewportSize(isHorizontal: boolean): number {
|
|
142
|
+
if (typeof window === 'undefined') return 0;
|
|
143
|
+
return isHorizontal ? window.innerWidth : window.innerHeight;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface UseResizableSizeArgs {
|
|
147
|
+
enabled: boolean;
|
|
148
|
+
isHorizontal: boolean;
|
|
149
|
+
minSize: number;
|
|
150
|
+
maxSize: number;
|
|
151
|
+
defaultSize: number;
|
|
152
|
+
storageKey?: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function useResizableSize({ enabled, isHorizontal, minSize, maxSize, defaultSize, storageKey }: UseResizableSizeArgs) {
|
|
156
|
+
const clampToViewport = useCallback(
|
|
157
|
+
(value: number) => {
|
|
158
|
+
const vp = viewportSize(isHorizontal);
|
|
159
|
+
const effectiveMax = vp > 0 ? Math.min(maxSize, vp - 80) : maxSize;
|
|
160
|
+
return clamp(value, minSize, Math.max(minSize, effectiveMax));
|
|
161
|
+
},
|
|
162
|
+
[isHorizontal, minSize, maxSize],
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const readInitial = useCallback(() => {
|
|
166
|
+
if (!enabled) return defaultSize;
|
|
167
|
+
if (typeof window === 'undefined') return defaultSize;
|
|
168
|
+
if (!storageKey) return clampToViewport(defaultSize);
|
|
169
|
+
try {
|
|
170
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
171
|
+
if (!raw) return clampToViewport(defaultSize);
|
|
172
|
+
const parsed = parseFloat(raw);
|
|
173
|
+
if (!Number.isFinite(parsed)) return clampToViewport(defaultSize);
|
|
174
|
+
return clampToViewport(parsed);
|
|
175
|
+
} catch {
|
|
176
|
+
return clampToViewport(defaultSize);
|
|
177
|
+
}
|
|
178
|
+
}, [enabled, storageKey, defaultSize, clampToViewport]);
|
|
179
|
+
|
|
180
|
+
const [size, setSizeRaw] = useState<number>(readInitial);
|
|
181
|
+
|
|
182
|
+
const setSize = useCallback((next: number) => setSizeRaw(clampToViewport(next)), [clampToViewport]);
|
|
183
|
+
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (!enabled || !storageKey || typeof window === 'undefined') return;
|
|
186
|
+
try {
|
|
187
|
+
window.localStorage.setItem(storageKey, String(Math.round(size)));
|
|
188
|
+
} catch {
|
|
189
|
+
// ignore quota / disabled-storage
|
|
190
|
+
}
|
|
191
|
+
}, [enabled, size, storageKey]);
|
|
192
|
+
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (!enabled || typeof window === 'undefined') return undefined;
|
|
195
|
+
const onResize = () => setSizeRaw(prev => clampToViewport(prev));
|
|
196
|
+
window.addEventListener('resize', onResize);
|
|
197
|
+
return () => window.removeEventListener('resize', onResize);
|
|
198
|
+
}, [enabled, clampToViewport]);
|
|
199
|
+
|
|
200
|
+
return { size, setSize };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
interface DrawerResizeHandleProps {
|
|
204
|
+
side: DrawerSide;
|
|
205
|
+
size: number;
|
|
206
|
+
minSize: number;
|
|
207
|
+
maxSize: number;
|
|
208
|
+
onSize: (next: number) => void;
|
|
209
|
+
ariaLabel?: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function DrawerResizeHandle({ side, size, minSize, maxSize, onSize, ariaLabel }: DrawerResizeHandleProps) {
|
|
213
|
+
const isHorizontal = HORIZONTAL_SIDES.has(side);
|
|
214
|
+
const startRef = useRef<{ x: number; y: number; size: number } | null>(null);
|
|
215
|
+
|
|
216
|
+
const direction = side === 'right' || side === 'bottom' ? -1 : 1;
|
|
217
|
+
|
|
218
|
+
const handlePointerDown = (e: PointerEvent<HTMLDivElement>) => {
|
|
219
|
+
if (e.button !== 0 && e.pointerType === 'mouse') return;
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
startRef.current = { x: e.clientX, y: e.clientY, size };
|
|
222
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
223
|
+
document.body.style.cursor = isHorizontal ? 'col-resize' : 'row-resize';
|
|
224
|
+
document.body.style.userSelect = 'none';
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const handlePointerMove = (e: PointerEvent<HTMLDivElement>) => {
|
|
228
|
+
const start = startRef.current;
|
|
229
|
+
if (!start) return;
|
|
230
|
+
const delta = isHorizontal ? e.clientX - start.x : e.clientY - start.y;
|
|
231
|
+
onSize(start.size + delta * direction);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const endDrag = (e: PointerEvent<HTMLDivElement>) => {
|
|
235
|
+
if (!startRef.current) return;
|
|
236
|
+
startRef.current = null;
|
|
237
|
+
try {
|
|
238
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
239
|
+
} catch {
|
|
240
|
+
// ignore — pointer may already be released
|
|
241
|
+
}
|
|
242
|
+
document.body.style.cursor = '';
|
|
243
|
+
document.body.style.userSelect = '';
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
247
|
+
const step = e.shiftKey ? 40 : 16;
|
|
248
|
+
if (isHorizontal) {
|
|
249
|
+
if (e.key === 'ArrowLeft') {
|
|
250
|
+
e.preventDefault();
|
|
251
|
+
onSize(size + step * (side === 'right' ? 1 : -1));
|
|
252
|
+
} else if (e.key === 'ArrowRight') {
|
|
253
|
+
e.preventDefault();
|
|
254
|
+
onSize(size + step * (side === 'right' ? -1 : 1));
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
if (e.key === 'ArrowUp') {
|
|
258
|
+
e.preventDefault();
|
|
259
|
+
onSize(size + step * (side === 'bottom' ? 1 : -1));
|
|
260
|
+
} else if (e.key === 'ArrowDown') {
|
|
261
|
+
e.preventDefault();
|
|
262
|
+
onSize(size + step * (side === 'bottom' ? -1 : 1));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (e.key === 'Home') {
|
|
266
|
+
e.preventDefault();
|
|
267
|
+
onSize(minSize);
|
|
268
|
+
} else if (e.key === 'End') {
|
|
269
|
+
e.preventDefault();
|
|
270
|
+
onSize(maxSize);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Handle is rendered as a SIBLING of the panel (child of DialogPrimitive.Content)
|
|
275
|
+
// so the panel can use `overflow-hidden` without clipping the handle.
|
|
276
|
+
// Wrapper has `py-4 px-4` (or `md:py-4 md:px-4` for flush on desktop)
|
|
277
|
+
// when the handle is visible — handle is desktop-only, where both flush
|
|
278
|
+
// and non-flush wrappers carry that padding — so `top-4 bottom-4` (or
|
|
279
|
+
// `left-4 right-4`) aligns the handle with the panel's bounds.
|
|
280
|
+
const trackPosition =
|
|
281
|
+
side === 'right'
|
|
282
|
+
? 'right-full top-4 bottom-4 w-3 items-center justify-end pr-1'
|
|
283
|
+
: side === 'left'
|
|
284
|
+
? 'left-full top-4 bottom-4 w-3 items-center justify-start pl-1'
|
|
285
|
+
: side === 'bottom'
|
|
286
|
+
? 'bottom-full left-4 right-4 h-3 justify-center items-end pb-1'
|
|
287
|
+
: 'top-full left-4 right-4 h-3 justify-center items-start pt-1';
|
|
288
|
+
|
|
289
|
+
const cursorClass = isHorizontal ? 'cursor-col-resize' : 'cursor-row-resize';
|
|
290
|
+
|
|
291
|
+
const gripClass = isHorizontal ? 'h-10 w-1' : 'w-10 h-1';
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<div
|
|
295
|
+
role="separator"
|
|
296
|
+
tabIndex={0}
|
|
297
|
+
aria-orientation={isHorizontal ? 'vertical' : 'horizontal'}
|
|
298
|
+
aria-valuenow={Math.round(size)}
|
|
299
|
+
aria-valuemin={minSize}
|
|
300
|
+
aria-valuemax={maxSize}
|
|
301
|
+
aria-label={ariaLabel ?? (isHorizontal ? 'Resize panel width' : 'Resize panel height')}
|
|
302
|
+
onPointerDown={handlePointerDown}
|
|
303
|
+
onPointerMove={handlePointerMove}
|
|
304
|
+
onPointerUp={endDrag}
|
|
305
|
+
onPointerCancel={endDrag}
|
|
306
|
+
onKeyDown={handleKeyDown}
|
|
307
|
+
className={cn(
|
|
308
|
+
'group absolute z-20 flex touch-none select-none',
|
|
309
|
+
'outline-none ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0',
|
|
310
|
+
trackPosition,
|
|
311
|
+
cursorClass,
|
|
312
|
+
)}
|
|
313
|
+
>
|
|
314
|
+
<div aria-hidden className={cn('rounded-full bg-ods-bg-surface', gripClass)} />
|
|
315
|
+
</div>
|
|
155
316
|
);
|
|
156
317
|
}
|
|
157
318
|
|
|
@@ -165,12 +326,9 @@ interface DrawerContentBaseProps
|
|
|
165
326
|
minSize?: number;
|
|
166
327
|
/** Maximum allowed size (px) when resizable. Also clamped by viewport. */
|
|
167
328
|
maxSize?: number;
|
|
168
|
-
/**
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
defaultSize?: PanelDefaultSize;
|
|
172
|
-
/** localStorage key for the size the user chose with the handle. Only that
|
|
173
|
-
* choice is stored; see `useResizablePanelSize`. */
|
|
329
|
+
/** Initial size (px) when no localStorage entry exists. */
|
|
330
|
+
defaultSize?: number;
|
|
331
|
+
/** localStorage key for persisting the size across sessions. */
|
|
174
332
|
storageKey?: string;
|
|
175
333
|
/** Pixel breakpoint below which `resizable` is disabled and inline
|
|
176
334
|
* size is not applied (so consumer CSS can render full-viewport).
|
|
@@ -242,6 +400,8 @@ const DrawerContent = forwardRef<ComponentRef<typeof DialogPrimitive.Content>, D
|
|
|
242
400
|
) => {
|
|
243
401
|
const resolvedSide: DrawerSide = side ?? 'right';
|
|
244
402
|
const isHorizontal = HORIZONTAL_SIDES.has(resolvedSide);
|
|
403
|
+
const initialSize = defaultSize ?? (isHorizontal ? 560 : 480);
|
|
404
|
+
|
|
245
405
|
const headerHeight = useHeaderHeight();
|
|
246
406
|
|
|
247
407
|
const [isMobile, setIsMobile] = useState(false);
|
|
@@ -254,21 +414,12 @@ const DrawerContent = forwardRef<ComponentRef<typeof DialogPrimitive.Content>, D
|
|
|
254
414
|
return () => mq.removeEventListener('change', update);
|
|
255
415
|
}, [mobileBreakpoint]);
|
|
256
416
|
|
|
257
|
-
|
|
258
|
-
// resize handle, stay reachable.
|
|
259
|
-
const available = useViewportExtent(resizable, isHorizontal);
|
|
260
|
-
const panelRef = useRef<HTMLDivElement>(null);
|
|
261
|
-
const {
|
|
262
|
-
size: resizedSize,
|
|
263
|
-
setSize,
|
|
264
|
-
clampSize,
|
|
265
|
-
} = useResizablePanelSize({
|
|
417
|
+
const { size: resizedSize, setSize } = useResizableSize({
|
|
266
418
|
enabled: resizable,
|
|
419
|
+
isHorizontal,
|
|
267
420
|
minSize,
|
|
268
421
|
maxSize,
|
|
269
|
-
defaultSize:
|
|
270
|
-
available,
|
|
271
|
-
reserve: 80,
|
|
422
|
+
defaultSize: initialSize,
|
|
272
423
|
storageKey,
|
|
273
424
|
});
|
|
274
425
|
|
|
@@ -292,20 +443,16 @@ const DrawerContent = forwardRef<ComponentRef<typeof DialogPrimitive.Content>, D
|
|
|
292
443
|
`overflow-hidden` so the drag track stays visible while the
|
|
293
444
|
panel cleanly clips children to its rounded corners. */}
|
|
294
445
|
{applyInlineSize ? (
|
|
295
|
-
<
|
|
296
|
-
variant="overlay"
|
|
446
|
+
<DrawerResizeHandle
|
|
297
447
|
side={resolvedSide}
|
|
298
448
|
size={resizedSize}
|
|
299
449
|
minSize={minSize}
|
|
300
450
|
maxSize={maxSize}
|
|
301
451
|
onSize={setSize}
|
|
302
|
-
clampSize={clampSize}
|
|
303
|
-
panelRef={panelRef}
|
|
304
452
|
ariaLabel={resizeAriaLabel}
|
|
305
453
|
/>
|
|
306
454
|
) : null}
|
|
307
455
|
<div
|
|
308
|
-
ref={panelRef}
|
|
309
456
|
className={cn(drawerPanelVariants({ side, flush, size }), className, panelClassName)}
|
|
310
457
|
style={{ ...sizeStyle, ...panelStyle }}
|
|
311
458
|
>
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { type RefObject } from 'react';
|
|
2
|
-
/**
|
|
3
|
-
* The drag-to-resize handle of a side or edge panel, shared by `Drawer` and
|
|
4
|
-
* `AppLayoutDrawer`. A drag writes the panel's size to the DOM and reports it
|
|
5
|
-
* once, on release; arrow keys, Home and End report at once.
|
|
6
|
-
*/
|
|
7
|
-
type PanelSide = 'right' | 'left' | 'top' | 'bottom';
|
|
8
|
-
export interface PanelResizeHandleProps {
|
|
9
|
-
side: PanelSide;
|
|
10
|
-
size: number;
|
|
11
|
-
minSize: number;
|
|
12
|
-
maxSize: number;
|
|
13
|
-
onSize: (next: number) => void;
|
|
14
|
-
/** Clamp a candidate size to the min/max and the container. */
|
|
15
|
-
clampSize: (next: number) => number;
|
|
16
|
-
/** The panel the size applies to — written directly while dragging. */
|
|
17
|
-
panelRef: RefObject<HTMLDivElement | null>;
|
|
18
|
-
/** `overlay` is the body-level `Drawer`'s handle, `inLayout` the wider one of
|
|
19
|
-
* `AppLayoutDrawer`. They differ in track width and grip colour only. */
|
|
20
|
-
variant: 'overlay' | 'inLayout';
|
|
21
|
-
ariaLabel?: string;
|
|
22
|
-
}
|
|
23
|
-
export declare function PanelResizeHandle({ side, size, minSize, maxSize, onSize, clampSize, panelRef, variant, ariaLabel, }: PanelResizeHandleProps): import("react").JSX.Element;
|
|
24
|
-
export {};
|
|
25
|
-
//# sourceMappingURL=panel-resize-handle.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"panel-resize-handle.d.ts","sourceRoot":"","sources":["../../../src/components/ui/panel-resize-handle.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAyC,KAAK,SAAS,EAAqB,MAAM,OAAO,CAAC;AAIjG;;;;GAIG;AAEH,KAAK,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;AAmBrD,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,+DAA+D;IAC/D,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,uEAAuE;IACvE,QAAQ,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAC3C;8EAC0E;IAC1E,OAAO,EAAE,SAAS,GAAG,UAAU,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EACJ,IAAI,EACJ,OAAO,EACP,OAAO,EACP,MAAM,EACN,SAAS,EACT,QAAQ,EACR,OAAO,EACP,SAAS,GACV,EAAE,sBAAsB,+BA8HxB"}
|