@sybilion/uilib 1.3.58 → 1.3.60
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/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +63 -28
- package/dist/esm/components/ui/Sidebar/Sidebar.js +42 -3
- package/dist/esm/hooks/panelWidth.js +27 -1
- package/dist/esm/types/src/hooks/panelWidth.d.ts +9 -0
- package/dist/esm/types/src/hooks/panelWidth.test.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +68 -27
- package/src/components/ui/Sidebar/Sidebar.tsx +48 -2
- package/src/hooks/panelWidth.test.ts +108 -0
- package/src/hooks/panelWidth.ts +53 -0
|
@@ -5,6 +5,7 @@ import { isGraphIntakeAssistantStepComplete, matchUserTextToQuickReply, isPreset
|
|
|
5
5
|
import { buildChatSendMessagePayload, displayTextFromSendPayload } from '../buildChatSendMessagePayload.js';
|
|
6
6
|
import { usedPresetIdsFromMessages } from '../chat-preset-utils.js';
|
|
7
7
|
import { useChatsForScopeId, useChat, useChatOutboundPending, useSyncChatPanelBusy, isChatEmpty } from '../../../../contexts/chat-context.js';
|
|
8
|
+
import { shellFitsSidebarsLayout } from '../../../../hooks/panelWidth.js';
|
|
8
9
|
import useEvent from '../../../../hooks/useEvent.js';
|
|
9
10
|
import { useIsMobile } from '../../../../hooks/useIsMobile.js';
|
|
10
11
|
import { useQueryParams } from '../../../../hooks/useQueryParams.js';
|
|
@@ -17,15 +18,13 @@ import { Chat } from '../Chat.js';
|
|
|
17
18
|
const NO_SCOPE_FALLBACK = '__uilib_chat_scope_unset__';
|
|
18
19
|
const SCRIPT_STEP_DELAY_MS = 1200;
|
|
19
20
|
const CHAT_NEW_SHORTCUT_KEY = 'o';
|
|
20
|
-
/** When chat panel is open, collapse left nav below this width so both don't crowd the viewport. */
|
|
21
|
-
const CHAT_NAV_COLLAPSE_BREAKPOINT_PX = 1400;
|
|
22
21
|
const CHAT_QUERY_PARAM = 'chat';
|
|
23
22
|
const CHAT_OPEN_VALUE = 'open';
|
|
24
23
|
const PROMPT_QUERY_PARAM = 'prompt';
|
|
25
24
|
function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, transformSendPayload, }) {
|
|
26
25
|
const effectiveScopeId = scopeId ?? NO_SCOPE_FALLBACK;
|
|
27
26
|
const isMobile = useIsMobile();
|
|
28
|
-
const { chatPanelContainer, isOpen: sidebarNavOpen, setOpen: setSidebarNavOpen, chatWidthPx, setChatWidthPx, getShellWidth, setChatPanelOpen, } = useSidebar();
|
|
27
|
+
const { chatPanelContainer, isOpen: sidebarNavOpen, setOpen: setSidebarNavOpen, sidebarWidthPx, chatWidthPx, setChatWidthPx, getShellWidth, chatPanelOpen: shellChatPanelOpen, setChatPanelOpen, } = useSidebar();
|
|
29
28
|
const [localUiBusy, setLocalUiBusy] = useState(false);
|
|
30
29
|
const { chats, currentChatId, setCurrentChatId, newChat, sendMessage, addMessage, removeMessageById, } = useChatsForScopeId(effectiveScopeId);
|
|
31
30
|
const chat = useChat(effectiveScopeId, currentChatId);
|
|
@@ -124,29 +123,27 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
124
123
|
if (embedAsPage) {
|
|
125
124
|
return;
|
|
126
125
|
}
|
|
127
|
-
const run = () => {
|
|
128
|
-
setIsOpen(open);
|
|
129
|
-
setChatPanelOpen(open);
|
|
130
|
-
};
|
|
131
|
-
if (!isMobile &&
|
|
132
|
-
'startViewTransition' in document &&
|
|
133
|
-
document.startViewTransition) {
|
|
134
|
-
document.startViewTransition(run);
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
run();
|
|
138
|
-
}
|
|
139
126
|
if (open) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// and drop other query keys.
|
|
127
|
+
setIsOpen(true);
|
|
128
|
+
setChatPanelOpen(true);
|
|
143
129
|
if (!searchParams.has(CHAT_QUERY_PARAM)) {
|
|
144
130
|
addSearchParams({ [CHAT_QUERY_PARAM]: CHAT_OPEN_VALUE });
|
|
145
131
|
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const applyClose = () => {
|
|
135
|
+
setIsOpen(false);
|
|
136
|
+
setChatPanelOpen(false);
|
|
137
|
+
};
|
|
138
|
+
if (!isMobile &&
|
|
139
|
+
'startViewTransition' in document &&
|
|
140
|
+
document.startViewTransition) {
|
|
141
|
+
document.startViewTransition(applyClose);
|
|
146
142
|
}
|
|
147
143
|
else {
|
|
148
|
-
|
|
144
|
+
applyClose();
|
|
149
145
|
}
|
|
146
|
+
removeSearchParams(CHAT_QUERY_PARAM);
|
|
150
147
|
};
|
|
151
148
|
const isEmpty = isChatEmpty(chat) && !isLoading;
|
|
152
149
|
const openNewChatWithPrefill = useCallback((prompt) => {
|
|
@@ -631,9 +628,33 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
631
628
|
if (embedAsPage) {
|
|
632
629
|
return;
|
|
633
630
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
631
|
+
if (!chatOpen) {
|
|
632
|
+
setIsOpen(false);
|
|
633
|
+
setChatPanelOpen(false);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (!shellChatPanelOpen && sidebarNavOpen) {
|
|
637
|
+
removeSearchParams(CHAT_QUERY_PARAM);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
setIsOpen(true);
|
|
641
|
+
setChatPanelOpen(true);
|
|
642
|
+
}, [
|
|
643
|
+
embedAsPage,
|
|
644
|
+
chatOpen,
|
|
645
|
+
setChatPanelOpen,
|
|
646
|
+
sidebarNavOpen,
|
|
647
|
+
shellChatPanelOpen,
|
|
648
|
+
removeSearchParams,
|
|
649
|
+
]);
|
|
650
|
+
/** Shell closed chat for space (e.g. nav opened); keep UI and URL in sync. */
|
|
651
|
+
useEffect(() => {
|
|
652
|
+
if (embedAsPage || shellChatPanelOpen || !isOpen) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
setIsOpen(false);
|
|
656
|
+
removeSearchParams(CHAT_QUERY_PARAM);
|
|
657
|
+
}, [embedAsPage, shellChatPanelOpen, isOpen, removeSearchParams]);
|
|
637
658
|
/** When this instance unmounts (e.g. route hides ChatSheet), release shell layout — URL sync alone does not run. */
|
|
638
659
|
useEffect(() => {
|
|
639
660
|
return () => {
|
|
@@ -648,16 +669,30 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
648
669
|
}
|
|
649
670
|
if (!isOpen || !sidebarNavOpen)
|
|
650
671
|
return;
|
|
651
|
-
const
|
|
652
|
-
|
|
672
|
+
const collapseNavIfNoSpace = () => {
|
|
673
|
+
const shellW = getShellWidth();
|
|
674
|
+
if (!shellFitsSidebarsLayout(shellW, {
|
|
675
|
+
mainSidebarOpen: true,
|
|
676
|
+
chatPanelOpen: true,
|
|
677
|
+
sidebarWidthPx,
|
|
678
|
+
chatWidthPx,
|
|
679
|
+
})) {
|
|
653
680
|
// Chat open uses `startViewTransition`; nested transition here aborts first → AbortError overlay.
|
|
654
681
|
setSidebarNavOpen(false, { viewTransition: false });
|
|
655
682
|
}
|
|
656
683
|
};
|
|
657
|
-
|
|
658
|
-
window.addEventListener('resize',
|
|
659
|
-
return () => window.removeEventListener('resize',
|
|
660
|
-
}, [
|
|
684
|
+
collapseNavIfNoSpace();
|
|
685
|
+
window.addEventListener('resize', collapseNavIfNoSpace);
|
|
686
|
+
return () => window.removeEventListener('resize', collapseNavIfNoSpace);
|
|
687
|
+
}, [
|
|
688
|
+
embedAsPage,
|
|
689
|
+
isOpen,
|
|
690
|
+
sidebarNavOpen,
|
|
691
|
+
setSidebarNavOpen,
|
|
692
|
+
getShellWidth,
|
|
693
|
+
sidebarWidthPx,
|
|
694
|
+
chatWidthPx,
|
|
695
|
+
]);
|
|
661
696
|
const renderPresets = (layout = 'fixed') => {
|
|
662
697
|
if (!presetsWithFreeform?.length)
|
|
663
698
|
return null;
|
|
@@ -5,7 +5,7 @@ import { Button } from '../Button/Button.js';
|
|
|
5
5
|
import { Separator } from '../Separator/Separator.js';
|
|
6
6
|
import { Sheet, SheetContent, SheetTitle } from '../Sheet/Sheet.js';
|
|
7
7
|
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip/Tooltip.js';
|
|
8
|
-
import { clampSidebarWidthPx, CHAT_WIDTH_STORAGE_KEY, SIDEBAR_WIDTH_DEFAULT_PX, SIDEBAR_WIDTH_MIN_PX, SIDEBAR_WIDTH_STORAGE_KEY_PX, SIDEBAR_WIDTH_STORAGE_KEY_PCT, CHAT_WIDTH_DEFAULT_PX, CHAT_WIDTH_MIN_PX, defaultChatWidthPx, clampChatWidthPx } from '../../../hooks/panelWidth.js';
|
|
8
|
+
import { clampSidebarWidthPx, CHAT_WIDTH_STORAGE_KEY, shellFitsSidebarsLayout, SIDEBAR_WIDTH_DEFAULT_PX, SIDEBAR_WIDTH_MIN_PX, SIDEBAR_WIDTH_STORAGE_KEY_PX, SIDEBAR_WIDTH_STORAGE_KEY_PCT, CHAT_WIDTH_DEFAULT_PX, CHAT_WIDTH_MIN_PX, defaultChatWidthPx, clampChatWidthPx } from '../../../hooks/panelWidth.js';
|
|
9
9
|
import useElemDrag from '../../../hooks/useDragElem.js';
|
|
10
10
|
import useEvent from '../../../hooks/useEvent.js';
|
|
11
11
|
import { useIsSidebarSheetLayout } from '../../../hooks/useIsSidebarSheetLayout.js';
|
|
@@ -80,7 +80,7 @@ function SidebarProvider({ defaultOpen = getCookie('isSidebarOpen') ?? true, cla
|
|
|
80
80
|
}, []);
|
|
81
81
|
const [sidebarWidthPx, _setSidebarWidthPx] = useState(() => readInitialSidebarWidthPx(sidebarLsKey));
|
|
82
82
|
const [chatWidthPx, _setChatWidthPx] = useState(readInitialChatWidthPx);
|
|
83
|
-
const [chatPanelOpen,
|
|
83
|
+
const [chatPanelOpen, _setChatPanelOpen] = useState(false);
|
|
84
84
|
const sidebarWidthRef = useRef(sidebarWidthPx);
|
|
85
85
|
const chatWidthRef = useRef(chatWidthPx);
|
|
86
86
|
sidebarWidthRef.current = sidebarWidthPx;
|
|
@@ -205,7 +205,46 @@ function SidebarProvider({ defaultOpen = getCookie('isSidebarOpen') ?? true, cla
|
|
|
205
205
|
fitPanelWidthsToShell,
|
|
206
206
|
]);
|
|
207
207
|
useShellWidthObserver(shellEl, handleShellResize);
|
|
208
|
+
const closeOppositeSidebarIfNoSpace = useCallback((openingMain, openingChat) => {
|
|
209
|
+
if (isSidebarSheetLayout)
|
|
210
|
+
return;
|
|
211
|
+
const shellW = getShellWidth();
|
|
212
|
+
const layoutAfterOpen = {
|
|
213
|
+
mainSidebarOpen: openingMain || isOpen,
|
|
214
|
+
chatPanelOpen: openingChat || chatPanelOpen,
|
|
215
|
+
sidebarWidthPx,
|
|
216
|
+
chatWidthPx,
|
|
217
|
+
};
|
|
218
|
+
if (shellFitsSidebarsLayout(shellW, layoutAfterOpen))
|
|
219
|
+
return;
|
|
220
|
+
if (openingMain && chatPanelOpen) {
|
|
221
|
+
_setChatPanelOpen(false);
|
|
222
|
+
}
|
|
223
|
+
else if (openingChat && isOpen) {
|
|
224
|
+
setIsOpen(false);
|
|
225
|
+
if (getCookiePreferences(userId)?.functional) {
|
|
226
|
+
setCookie('isSidebarOpen', 'false', 60 * 60 * 24 * 7);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}, [
|
|
230
|
+
isSidebarSheetLayout,
|
|
231
|
+
getShellWidth,
|
|
232
|
+
isOpen,
|
|
233
|
+
chatPanelOpen,
|
|
234
|
+
sidebarWidthPx,
|
|
235
|
+
chatWidthPx,
|
|
236
|
+
userId,
|
|
237
|
+
]);
|
|
238
|
+
const setChatPanelOpen = useCallback((open) => {
|
|
239
|
+
if (open) {
|
|
240
|
+
closeOppositeSidebarIfNoSpace(false, true);
|
|
241
|
+
}
|
|
242
|
+
_setChatPanelOpen(open);
|
|
243
|
+
}, [closeOppositeSidebarIfNoSpace]);
|
|
208
244
|
const setOpen = useCallback((value, options) => {
|
|
245
|
+
if (value) {
|
|
246
|
+
closeOppositeSidebarIfNoSpace(true, false);
|
|
247
|
+
}
|
|
209
248
|
const useViewTransition = options?.viewTransition !== false &&
|
|
210
249
|
!isSidebarSheetLayout &&
|
|
211
250
|
'startViewTransition' in document &&
|
|
@@ -221,7 +260,7 @@ function SidebarProvider({ defaultOpen = getCookie('isSidebarOpen') ?? true, cla
|
|
|
221
260
|
if (getCookiePreferences(userId)?.functional) {
|
|
222
261
|
setCookie('isSidebarOpen', value.toString(), 60 * 60 * 24 * 7);
|
|
223
262
|
}
|
|
224
|
-
}, [isSidebarSheetLayout, userId]);
|
|
263
|
+
}, [isSidebarSheetLayout, userId, closeOppositeSidebarIfNoSpace]);
|
|
225
264
|
const toggleSidebar = useCallback(() => setOpen(!isOpen), [isOpen, setOpen]);
|
|
226
265
|
useEffect(() => {
|
|
227
266
|
const shell = wrapperRef.current;
|
|
@@ -46,5 +46,31 @@ function clampChatWidthPx(px, shellWidth, effectiveSidebarWidthPx) {
|
|
|
46
46
|
function defaultChatWidthPx(shellWidth) {
|
|
47
47
|
return clampChatWidthPx(CHAT_WIDTH_DEFAULT_PX, shellWidth, SIDEBAR_WIDTH_DEFAULT_PX);
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Stored width can fall below panel min when the shell was narrow (clamp cap < min).
|
|
51
|
+
* For dual-panel fit checks, treat open panels as at least their min width.
|
|
52
|
+
*/
|
|
53
|
+
function effectiveOpenPanelWidthPx(px, minPx, bothPanelsOpen) {
|
|
54
|
+
if (bothPanelsOpen) {
|
|
55
|
+
return Math.max(px, minPx);
|
|
56
|
+
}
|
|
57
|
+
return px > 0 ? px : minPx;
|
|
58
|
+
}
|
|
59
|
+
/** Minimum shell width when nav and/or chat panels are open (main column at least `CENTRAL_AREA_MIN_PX`). */
|
|
60
|
+
function requiredShellWidthForSidebars(widths) {
|
|
61
|
+
const bothPanelsOpen = widths.mainSidebarOpen && widths.chatPanelOpen;
|
|
62
|
+
const sidebarW = widths.mainSidebarOpen
|
|
63
|
+
? effectiveOpenPanelWidthPx(widths.sidebarWidthPx, SIDEBAR_WIDTH_MIN_PX, bothPanelsOpen)
|
|
64
|
+
: 0;
|
|
65
|
+
const chatW = widths.chatPanelOpen
|
|
66
|
+
? effectiveOpenPanelWidthPx(widths.chatWidthPx, CHAT_WIDTH_MIN_PX, bothPanelsOpen)
|
|
67
|
+
: 0;
|
|
68
|
+
return sidebarW + CENTRAL_AREA_MIN_PX + chatW;
|
|
69
|
+
}
|
|
70
|
+
function shellFitsSidebarsLayout(shellWidth, widths) {
|
|
71
|
+
if (shellWidth <= 0)
|
|
72
|
+
return true;
|
|
73
|
+
return shellWidth >= requiredShellWidthForSidebars(widths);
|
|
74
|
+
}
|
|
49
75
|
|
|
50
|
-
export { CENTRAL_AREA_MIN_PX, CHAT_WIDTH_ABS_MAX_PX, CHAT_WIDTH_DEFAULT_PX, CHAT_WIDTH_MIN_PX, CHAT_WIDTH_STORAGE_KEY, SHELL_MAX_FRACTION, SIDEBAR_SHEET_LAYOUT_MAX_WIDTH_PX, SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX, SIDEBAR_WIDTH_DEFAULT_PX, SIDEBAR_WIDTH_MIN_PX, SIDEBAR_WIDTH_STORAGE_KEY_PCT, SIDEBAR_WIDTH_STORAGE_KEY_PX, clampChatWidthPx, clampSidebarWidthPx, defaultChatWidthPx, maxChatWidthPx, maxSidebarWidthPx };
|
|
76
|
+
export { CENTRAL_AREA_MIN_PX, CHAT_WIDTH_ABS_MAX_PX, CHAT_WIDTH_DEFAULT_PX, CHAT_WIDTH_MIN_PX, CHAT_WIDTH_STORAGE_KEY, SHELL_MAX_FRACTION, SIDEBAR_SHEET_LAYOUT_MAX_WIDTH_PX, SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX, SIDEBAR_WIDTH_DEFAULT_PX, SIDEBAR_WIDTH_MIN_PX, SIDEBAR_WIDTH_STORAGE_KEY_PCT, SIDEBAR_WIDTH_STORAGE_KEY_PX, clampChatWidthPx, clampSidebarWidthPx, defaultChatWidthPx, maxChatWidthPx, maxSidebarWidthPx, requiredShellWidthForSidebars, shellFitsSidebarsLayout };
|
|
@@ -22,3 +22,12 @@ export declare function clampSidebarWidthPx(px: number, shellWidth: number, opts
|
|
|
22
22
|
/** Width that reduces the main column when the nav sidebar is expanded; use 0 when collapsed. */
|
|
23
23
|
export declare function clampChatWidthPx(px: number, shellWidth: number, effectiveSidebarWidthPx: number): number;
|
|
24
24
|
export declare function defaultChatWidthPx(shellWidth: number): number;
|
|
25
|
+
export type SidebarsLayoutWidths = {
|
|
26
|
+
mainSidebarOpen: boolean;
|
|
27
|
+
chatPanelOpen: boolean;
|
|
28
|
+
sidebarWidthPx: number;
|
|
29
|
+
chatWidthPx: number;
|
|
30
|
+
};
|
|
31
|
+
/** Minimum shell width when nav and/or chat panels are open (main column at least `CENTRAL_AREA_MIN_PX`). */
|
|
32
|
+
export declare function requiredShellWidthForSidebars(widths: SidebarsLayoutWidths): number;
|
|
33
|
+
export declare function shellFitsSidebarsLayout(shellWidth: number, widths: SidebarsLayoutWidths): boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
useChatsForScopeId,
|
|
32
32
|
useSyncChatPanelBusy,
|
|
33
33
|
} from '#uilib/contexts/chat-context';
|
|
34
|
+
import { shellFitsSidebarsLayout } from '#uilib/hooks/panelWidth';
|
|
34
35
|
import useEvent from '#uilib/hooks/useEvent';
|
|
35
36
|
import { useIsMobile } from '#uilib/hooks/useIsMobile';
|
|
36
37
|
import { useQueryParams } from '#uilib/hooks/useQueryParams';
|
|
@@ -114,8 +115,6 @@ type PresetScriptState = {
|
|
|
114
115
|
|
|
115
116
|
const SCRIPT_STEP_DELAY_MS = 1200;
|
|
116
117
|
const CHAT_NEW_SHORTCUT_KEY = 'o';
|
|
117
|
-
/** When chat panel is open, collapse left nav below this width so both don't crowd the viewport. */
|
|
118
|
-
const CHAT_NAV_COLLAPSE_BREAKPOINT_PX = 1400;
|
|
119
118
|
|
|
120
119
|
const CHAT_QUERY_PARAM = 'chat';
|
|
121
120
|
const CHAT_OPEN_VALUE = 'open';
|
|
@@ -142,9 +141,11 @@ export function useChatPanelChromeModel({
|
|
|
142
141
|
chatPanelContainer,
|
|
143
142
|
isOpen: sidebarNavOpen,
|
|
144
143
|
setOpen: setSidebarNavOpen,
|
|
144
|
+
sidebarWidthPx,
|
|
145
145
|
chatWidthPx,
|
|
146
146
|
setChatWidthPx,
|
|
147
147
|
getShellWidth,
|
|
148
|
+
chatPanelOpen: shellChatPanelOpen,
|
|
148
149
|
setChatPanelOpen,
|
|
149
150
|
} = useSidebar();
|
|
150
151
|
const [localUiBusy, setLocalUiBusy] = useState(false);
|
|
@@ -286,30 +287,29 @@ export function useChatPanelChromeModel({
|
|
|
286
287
|
if (embedAsPage) {
|
|
287
288
|
return;
|
|
288
289
|
}
|
|
289
|
-
|
|
290
|
-
setIsOpen(
|
|
291
|
-
setChatPanelOpen(
|
|
290
|
+
if (open) {
|
|
291
|
+
setIsOpen(true);
|
|
292
|
+
setChatPanelOpen(true);
|
|
293
|
+
if (!searchParams.has(CHAT_QUERY_PARAM)) {
|
|
294
|
+
addSearchParams({ [CHAT_QUERY_PARAM]: CHAT_OPEN_VALUE });
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const applyClose = () => {
|
|
300
|
+
setIsOpen(false);
|
|
301
|
+
setChatPanelOpen(false);
|
|
292
302
|
};
|
|
293
303
|
if (
|
|
294
304
|
!isMobile &&
|
|
295
305
|
'startViewTransition' in document &&
|
|
296
306
|
document.startViewTransition
|
|
297
307
|
) {
|
|
298
|
-
document.startViewTransition(
|
|
308
|
+
document.startViewTransition(applyClose);
|
|
299
309
|
} else {
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (open) {
|
|
304
|
-
// Avoid a second setSearchParams when `chat` is already set (e.g. Reports merges
|
|
305
|
-
// reportId + chat in one update). A redundant add can merge from a stale `prev`
|
|
306
|
-
// and drop other query keys.
|
|
307
|
-
if (!searchParams.has(CHAT_QUERY_PARAM)) {
|
|
308
|
-
addSearchParams({ [CHAT_QUERY_PARAM]: CHAT_OPEN_VALUE });
|
|
309
|
-
}
|
|
310
|
-
} else {
|
|
311
|
-
removeSearchParams(CHAT_QUERY_PARAM);
|
|
310
|
+
applyClose();
|
|
312
311
|
}
|
|
312
|
+
removeSearchParams(CHAT_QUERY_PARAM);
|
|
313
313
|
};
|
|
314
314
|
|
|
315
315
|
const isEmpty = isChatEmpty(chat) && !isLoading;
|
|
@@ -855,9 +855,34 @@ export function useChatPanelChromeModel({
|
|
|
855
855
|
if (embedAsPage) {
|
|
856
856
|
return;
|
|
857
857
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
858
|
+
if (!chatOpen) {
|
|
859
|
+
setIsOpen(false);
|
|
860
|
+
setChatPanelOpen(false);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (!shellChatPanelOpen && sidebarNavOpen) {
|
|
864
|
+
removeSearchParams(CHAT_QUERY_PARAM);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
setIsOpen(true);
|
|
868
|
+
setChatPanelOpen(true);
|
|
869
|
+
}, [
|
|
870
|
+
embedAsPage,
|
|
871
|
+
chatOpen,
|
|
872
|
+
setChatPanelOpen,
|
|
873
|
+
sidebarNavOpen,
|
|
874
|
+
shellChatPanelOpen,
|
|
875
|
+
removeSearchParams,
|
|
876
|
+
]);
|
|
877
|
+
|
|
878
|
+
/** Shell closed chat for space (e.g. nav opened); keep UI and URL in sync. */
|
|
879
|
+
useEffect(() => {
|
|
880
|
+
if (embedAsPage || shellChatPanelOpen || !isOpen) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
setIsOpen(false);
|
|
884
|
+
removeSearchParams(CHAT_QUERY_PARAM);
|
|
885
|
+
}, [embedAsPage, shellChatPanelOpen, isOpen, removeSearchParams]);
|
|
861
886
|
|
|
862
887
|
/** When this instance unmounts (e.g. route hides ChatSheet), release shell layout — URL sync alone does not run. */
|
|
863
888
|
useEffect(() => {
|
|
@@ -874,17 +899,33 @@ export function useChatPanelChromeModel({
|
|
|
874
899
|
}
|
|
875
900
|
if (!isOpen || !sidebarNavOpen) return;
|
|
876
901
|
|
|
877
|
-
const
|
|
878
|
-
|
|
902
|
+
const collapseNavIfNoSpace = () => {
|
|
903
|
+
const shellW = getShellWidth();
|
|
904
|
+
if (
|
|
905
|
+
!shellFitsSidebarsLayout(shellW, {
|
|
906
|
+
mainSidebarOpen: true,
|
|
907
|
+
chatPanelOpen: true,
|
|
908
|
+
sidebarWidthPx,
|
|
909
|
+
chatWidthPx,
|
|
910
|
+
})
|
|
911
|
+
) {
|
|
879
912
|
// Chat open uses `startViewTransition`; nested transition here aborts first → AbortError overlay.
|
|
880
913
|
setSidebarNavOpen(false, { viewTransition: false });
|
|
881
914
|
}
|
|
882
915
|
};
|
|
883
916
|
|
|
884
|
-
|
|
885
|
-
window.addEventListener('resize',
|
|
886
|
-
return () => window.removeEventListener('resize',
|
|
887
|
-
}, [
|
|
917
|
+
collapseNavIfNoSpace();
|
|
918
|
+
window.addEventListener('resize', collapseNavIfNoSpace);
|
|
919
|
+
return () => window.removeEventListener('resize', collapseNavIfNoSpace);
|
|
920
|
+
}, [
|
|
921
|
+
embedAsPage,
|
|
922
|
+
isOpen,
|
|
923
|
+
sidebarNavOpen,
|
|
924
|
+
setSidebarNavOpen,
|
|
925
|
+
getShellWidth,
|
|
926
|
+
sidebarWidthPx,
|
|
927
|
+
chatWidthPx,
|
|
928
|
+
]);
|
|
888
929
|
|
|
889
930
|
const renderPresets = (layout: ChatPresetsLayout = 'fixed') => {
|
|
890
931
|
if (!presetsWithFreeform?.length) return null;
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
clampChatWidthPx,
|
|
35
35
|
clampSidebarWidthPx,
|
|
36
36
|
defaultChatWidthPx,
|
|
37
|
+
shellFitsSidebarsLayout,
|
|
37
38
|
} from '#uilib/hooks/panelWidth';
|
|
38
39
|
import useElemDrag, { Delta } from '#uilib/hooks/useDragElem';
|
|
39
40
|
import useEvent from '#uilib/hooks/useEvent';
|
|
@@ -171,7 +172,7 @@ function SidebarProvider({
|
|
|
171
172
|
readInitialSidebarWidthPx(sidebarLsKey),
|
|
172
173
|
);
|
|
173
174
|
const [chatWidthPx, _setChatWidthPx] = useState(readInitialChatWidthPx);
|
|
174
|
-
const [chatPanelOpen,
|
|
175
|
+
const [chatPanelOpen, _setChatPanelOpen] = useState(false);
|
|
175
176
|
const sidebarWidthRef = useRef(sidebarWidthPx);
|
|
176
177
|
const chatWidthRef = useRef(chatWidthPx);
|
|
177
178
|
sidebarWidthRef.current = sidebarWidthPx;
|
|
@@ -311,8 +312,53 @@ function SidebarProvider({
|
|
|
311
312
|
|
|
312
313
|
useShellWidthObserver(shellEl, handleShellResize);
|
|
313
314
|
|
|
315
|
+
const closeOppositeSidebarIfNoSpace = useCallback(
|
|
316
|
+
(openingMain: boolean, openingChat: boolean) => {
|
|
317
|
+
if (isSidebarSheetLayout) return;
|
|
318
|
+
const shellW = getShellWidth();
|
|
319
|
+
const layoutAfterOpen = {
|
|
320
|
+
mainSidebarOpen: openingMain || isOpen,
|
|
321
|
+
chatPanelOpen: openingChat || chatPanelOpen,
|
|
322
|
+
sidebarWidthPx,
|
|
323
|
+
chatWidthPx,
|
|
324
|
+
};
|
|
325
|
+
if (shellFitsSidebarsLayout(shellW, layoutAfterOpen)) return;
|
|
326
|
+
if (openingMain && chatPanelOpen) {
|
|
327
|
+
_setChatPanelOpen(false);
|
|
328
|
+
} else if (openingChat && isOpen) {
|
|
329
|
+
setIsOpen(false);
|
|
330
|
+
if (getCookiePreferences(userId)?.functional) {
|
|
331
|
+
setCookie('isSidebarOpen', 'false', 60 * 60 * 24 * 7);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
[
|
|
336
|
+
isSidebarSheetLayout,
|
|
337
|
+
getShellWidth,
|
|
338
|
+
isOpen,
|
|
339
|
+
chatPanelOpen,
|
|
340
|
+
sidebarWidthPx,
|
|
341
|
+
chatWidthPx,
|
|
342
|
+
userId,
|
|
343
|
+
],
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const setChatPanelOpen = useCallback(
|
|
347
|
+
(open: boolean) => {
|
|
348
|
+
if (open) {
|
|
349
|
+
closeOppositeSidebarIfNoSpace(false, true);
|
|
350
|
+
}
|
|
351
|
+
_setChatPanelOpen(open);
|
|
352
|
+
},
|
|
353
|
+
[closeOppositeSidebarIfNoSpace],
|
|
354
|
+
);
|
|
355
|
+
|
|
314
356
|
const setOpen = useCallback(
|
|
315
357
|
(value: boolean, options?: SetSidebarOpenOptions) => {
|
|
358
|
+
if (value) {
|
|
359
|
+
closeOppositeSidebarIfNoSpace(true, false);
|
|
360
|
+
}
|
|
361
|
+
|
|
316
362
|
const useViewTransition =
|
|
317
363
|
options?.viewTransition !== false &&
|
|
318
364
|
!isSidebarSheetLayout &&
|
|
@@ -331,7 +377,7 @@ function SidebarProvider({
|
|
|
331
377
|
setCookie('isSidebarOpen', value.toString(), 60 * 60 * 24 * 7);
|
|
332
378
|
}
|
|
333
379
|
},
|
|
334
|
-
[isSidebarSheetLayout, userId],
|
|
380
|
+
[isSidebarSheetLayout, userId, closeOppositeSidebarIfNoSpace],
|
|
335
381
|
);
|
|
336
382
|
|
|
337
383
|
const toggleSidebar = useCallback(() => setOpen(!isOpen), [isOpen, setOpen]);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CENTRAL_AREA_MIN_PX,
|
|
3
|
+
CHAT_WIDTH_MIN_PX,
|
|
4
|
+
SIDEBAR_WIDTH_MIN_PX,
|
|
5
|
+
requiredShellWidthForSidebars,
|
|
6
|
+
shellFitsSidebarsLayout,
|
|
7
|
+
} from './panelWidth';
|
|
8
|
+
|
|
9
|
+
describe('requiredShellWidthForSidebars', () => {
|
|
10
|
+
it('sums only open panels plus central min', () => {
|
|
11
|
+
expect(
|
|
12
|
+
requiredShellWidthForSidebars({
|
|
13
|
+
mainSidebarOpen: true,
|
|
14
|
+
chatPanelOpen: true,
|
|
15
|
+
sidebarWidthPx: 320,
|
|
16
|
+
chatWidthPx: 600,
|
|
17
|
+
}),
|
|
18
|
+
).toBe(320 + CENTRAL_AREA_MIN_PX + 600);
|
|
19
|
+
|
|
20
|
+
expect(
|
|
21
|
+
requiredShellWidthForSidebars({
|
|
22
|
+
mainSidebarOpen: false,
|
|
23
|
+
chatPanelOpen: true,
|
|
24
|
+
sidebarWidthPx: 320,
|
|
25
|
+
chatWidthPx: 600,
|
|
26
|
+
}),
|
|
27
|
+
).toBe(CENTRAL_AREA_MIN_PX + 600);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('falls back to panel mins when width is zero', () => {
|
|
31
|
+
expect(
|
|
32
|
+
requiredShellWidthForSidebars({
|
|
33
|
+
mainSidebarOpen: true,
|
|
34
|
+
chatPanelOpen: true,
|
|
35
|
+
sidebarWidthPx: 0,
|
|
36
|
+
chatWidthPx: 0,
|
|
37
|
+
}),
|
|
38
|
+
).toBe(SIDEBAR_WIDTH_MIN_PX + CENTRAL_AREA_MIN_PX + CHAT_WIDTH_MIN_PX);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('uses panel mins for sub-min stored widths when both panels are open', () => {
|
|
42
|
+
expect(
|
|
43
|
+
requiredShellWidthForSidebars({
|
|
44
|
+
mainSidebarOpen: true,
|
|
45
|
+
chatPanelOpen: true,
|
|
46
|
+
sidebarWidthPx: 300,
|
|
47
|
+
chatWidthPx: 37,
|
|
48
|
+
}),
|
|
49
|
+
).toBe(300 + CENTRAL_AREA_MIN_PX + CHAT_WIDTH_MIN_PX);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('keeps actual chat width when only chat is open', () => {
|
|
53
|
+
expect(
|
|
54
|
+
requiredShellWidthForSidebars({
|
|
55
|
+
mainSidebarOpen: false,
|
|
56
|
+
chatPanelOpen: true,
|
|
57
|
+
sidebarWidthPx: 300,
|
|
58
|
+
chatWidthPx: 337,
|
|
59
|
+
}),
|
|
60
|
+
).toBe(CENTRAL_AREA_MIN_PX + 337);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('shellFitsSidebarsLayout', () => {
|
|
65
|
+
it('returns true when shell meets required width', () => {
|
|
66
|
+
expect(
|
|
67
|
+
shellFitsSidebarsLayout(1600, {
|
|
68
|
+
mainSidebarOpen: true,
|
|
69
|
+
chatPanelOpen: true,
|
|
70
|
+
sidebarWidthPx: 300,
|
|
71
|
+
chatWidthPx: 500,
|
|
72
|
+
}),
|
|
73
|
+
).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns false when shell is too narrow for both panels', () => {
|
|
77
|
+
expect(
|
|
78
|
+
shellFitsSidebarsLayout(1500, {
|
|
79
|
+
mainSidebarOpen: true,
|
|
80
|
+
chatPanelOpen: true,
|
|
81
|
+
sidebarWidthPx: 300,
|
|
82
|
+
chatWidthPx: 500,
|
|
83
|
+
}),
|
|
84
|
+
).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns false when sub-min chat width would falsely fit at shell edge', () => {
|
|
88
|
+
expect(
|
|
89
|
+
shellFitsSidebarsLayout(1137, {
|
|
90
|
+
mainSidebarOpen: true,
|
|
91
|
+
chatPanelOpen: true,
|
|
92
|
+
sidebarWidthPx: 300,
|
|
93
|
+
chatWidthPx: 37,
|
|
94
|
+
}),
|
|
95
|
+
).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('returns true for unknown shell width', () => {
|
|
99
|
+
expect(
|
|
100
|
+
shellFitsSidebarsLayout(0, {
|
|
101
|
+
mainSidebarOpen: true,
|
|
102
|
+
chatPanelOpen: true,
|
|
103
|
+
sidebarWidthPx: 300,
|
|
104
|
+
chatWidthPx: 500,
|
|
105
|
+
}),
|
|
106
|
+
).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
});
|
package/src/hooks/panelWidth.ts
CHANGED
|
@@ -78,3 +78,56 @@ export function defaultChatWidthPx(shellWidth: number): number {
|
|
|
78
78
|
SIDEBAR_WIDTH_DEFAULT_PX,
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
|
+
|
|
82
|
+
export type SidebarsLayoutWidths = {
|
|
83
|
+
mainSidebarOpen: boolean;
|
|
84
|
+
chatPanelOpen: boolean;
|
|
85
|
+
sidebarWidthPx: number;
|
|
86
|
+
chatWidthPx: number;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Stored width can fall below panel min when the shell was narrow (clamp cap < min).
|
|
91
|
+
* For dual-panel fit checks, treat open panels as at least their min width.
|
|
92
|
+
*/
|
|
93
|
+
function effectiveOpenPanelWidthPx(
|
|
94
|
+
px: number,
|
|
95
|
+
minPx: number,
|
|
96
|
+
bothPanelsOpen: boolean,
|
|
97
|
+
): number {
|
|
98
|
+
if (bothPanelsOpen) {
|
|
99
|
+
return Math.max(px, minPx);
|
|
100
|
+
}
|
|
101
|
+
return px > 0 ? px : minPx;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Minimum shell width when nav and/or chat panels are open (main column at least `CENTRAL_AREA_MIN_PX`). */
|
|
105
|
+
export function requiredShellWidthForSidebars(
|
|
106
|
+
widths: SidebarsLayoutWidths,
|
|
107
|
+
): number {
|
|
108
|
+
const bothPanelsOpen =
|
|
109
|
+
widths.mainSidebarOpen && widths.chatPanelOpen;
|
|
110
|
+
const sidebarW = widths.mainSidebarOpen
|
|
111
|
+
? effectiveOpenPanelWidthPx(
|
|
112
|
+
widths.sidebarWidthPx,
|
|
113
|
+
SIDEBAR_WIDTH_MIN_PX,
|
|
114
|
+
bothPanelsOpen,
|
|
115
|
+
)
|
|
116
|
+
: 0;
|
|
117
|
+
const chatW = widths.chatPanelOpen
|
|
118
|
+
? effectiveOpenPanelWidthPx(
|
|
119
|
+
widths.chatWidthPx,
|
|
120
|
+
CHAT_WIDTH_MIN_PX,
|
|
121
|
+
bothPanelsOpen,
|
|
122
|
+
)
|
|
123
|
+
: 0;
|
|
124
|
+
return sidebarW + CENTRAL_AREA_MIN_PX + chatW;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function shellFitsSidebarsLayout(
|
|
128
|
+
shellWidth: number,
|
|
129
|
+
widths: SidebarsLayoutWidths,
|
|
130
|
+
): boolean {
|
|
131
|
+
if (shellWidth <= 0) return true;
|
|
132
|
+
return shellWidth >= requiredShellWidthForSidebars(widths);
|
|
133
|
+
}
|