@sybilion/uilib 1.3.6 → 1.3.8

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.
@@ -1,6 +1,6 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.AppShell_root__ONlNK{display:grid;flex:1;grid-template-areas:\"main\";grid-template-columns:1fr;min-height:0;min-height:100%;width:100%}@media (min-width:768px){.AppShell_root__ONlNK{grid-template-areas:\"sidebar main\";grid-template-columns:var(--sidebar-width) 1fr}[data-slot=sidebar-wrapper][data-state=collapsed] .AppShell_root__ONlNK{grid-template-columns:0 1fr}}.AppShell_mainColumn__Emn1p{display:flex;flex:1;flex-direction:column;grid-area:main;min-height:0;min-width:0}[data-slot=sidebar-wrapper][data-state=collapsed] .AppShell_mainColumn__Emn1p{margin-left:var(--p-3)}.AppShell_mainBody__IoVuy{background-color:var(--page-color);border-radius:var(--p-4);display:flex;flex:1;flex-direction:column;min-width:0;padding-bottom:var(--page-y-padding)}";
3
+ var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.AppShell_root__ONlNK{display:grid;flex:1;grid-template-areas:\"main\";grid-template-columns:1fr;min-height:0;min-height:100%;width:100%}@media (min-width:1100px){.AppShell_root__ONlNK{grid-template-areas:\"sidebar main\";grid-template-columns:var(--sidebar-width) 1fr}[data-slot=sidebar-wrapper][data-state=collapsed] .AppShell_root__ONlNK{grid-template-columns:0 1fr}}.AppShell_mainColumn__Emn1p{display:flex;flex:1;flex-direction:column;grid-area:main;min-height:0;min-width:0}[data-slot=sidebar-wrapper][data-state=collapsed] .AppShell_mainColumn__Emn1p{margin-left:var(--p-3)}.AppShell_mainBody__IoVuy{background-color:var(--page-color);border-radius:var(--p-4);display:flex;flex:1;flex-direction:column;min-width:0;padding-bottom:var(--page-y-padding)}";
4
4
  var S = {"root":"AppShell_root__ONlNK","mainColumn":"AppShell_mainColumn__Emn1p","mainBody":"AppShell_mainBody__IoVuy"};
5
5
  styleInject(css_248z);
6
6
 
@@ -8,7 +8,7 @@ import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '../Too
8
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';
9
9
  import useElemDrag from '../../../hooks/useDragElem.js';
10
10
  import useEvent from '../../../hooks/useEvent.js';
11
- import { useIsMobile } from '../../../hooks/useIsMobile.js';
11
+ import { useIsSidebarSheetLayout } from '../../../hooks/useIsSidebarSheetLayout.js';
12
12
  import { useShellWidthObserver } from '../../../hooks/useShellWidthObserver.js';
13
13
  import { setCookie, getCookie } from '../../../lib/cookie.js';
14
14
  import { getCookiePreferences } from '../../../lib/cookie-consent/cookie-consent.js';
@@ -68,8 +68,8 @@ function SidebarProvider({ defaultOpen = getCookie('isSidebarOpen') ?? true, cla
68
68
  const sidebarLsKey = sidebarWidthStorageKey ?? SIDEBAR_WIDTH_STORAGE_KEY_PX;
69
69
  const allowPersistSidebarWidth = persistSidebarWidthWithoutConsent ||
70
70
  Boolean(getCookiePreferences(userId)?.functional);
71
- const isMobile = useIsMobile();
72
- const [isOpen, setIsOpen] = useState(isMobile ? false : defaultOpen);
71
+ const isSidebarSheetLayout = useIsSidebarSheetLayout();
72
+ const [isOpen, setIsOpen] = useState(isSidebarSheetLayout ? false : defaultOpen);
73
73
  const wrapperRef = useRef(null);
74
74
  const shellWidthRef = useRef(0);
75
75
  const prevShellWidthRef = useRef(0);
@@ -194,14 +194,20 @@ function SidebarProvider({ defaultOpen = getCookie('isSidebarOpen') ?? true, cla
194
194
  allowPersistSidebarWidth,
195
195
  ]);
196
196
  useLayoutEffect(() => {
197
- if (!shellEl || isMobile)
197
+ if (!shellEl || isSidebarSheetLayout)
198
198
  return;
199
199
  fitPanelWidthsToShell();
200
- }, [shellEl, chatPanelOpen, isMobile, isOpen, fitPanelWidthsToShell]);
200
+ }, [
201
+ shellEl,
202
+ chatPanelOpen,
203
+ isSidebarSheetLayout,
204
+ isOpen,
205
+ fitPanelWidthsToShell,
206
+ ]);
201
207
  useShellWidthObserver(shellEl, handleShellResize);
202
208
  const setOpen = useCallback((value, options) => {
203
209
  const useViewTransition = options?.viewTransition !== false &&
204
- !isMobile &&
210
+ !isSidebarSheetLayout &&
205
211
  'startViewTransition' in document &&
206
212
  document.startViewTransition;
207
213
  if (useViewTransition) {
@@ -215,7 +221,7 @@ function SidebarProvider({ defaultOpen = getCookie('isSidebarOpen') ?? true, cla
215
221
  if (getCookiePreferences(userId)?.functional) {
216
222
  setCookie('isSidebarOpen', value.toString(), 60 * 60 * 24 * 7);
217
223
  }
218
- }, [isMobile, userId]);
224
+ }, [isSidebarSheetLayout, userId]);
219
225
  const toggleSidebar = useCallback(() => setOpen(!isOpen), [isOpen, setOpen]);
220
226
  useEffect(() => {
221
227
  const shell = wrapperRef.current;
@@ -223,7 +229,7 @@ function SidebarProvider({ defaultOpen = getCookie('isSidebarOpen') ?? true, cla
223
229
  return;
224
230
  if (chatPanelOpen) {
225
231
  shell.setAttribute('data-chat-open', '');
226
- if (isMobile) {
232
+ if (isSidebarSheetLayout) {
227
233
  shell.style.setProperty('--chat-panel-width', '100%');
228
234
  shell.style.setProperty('--chat-panel-height', '100dvh');
229
235
  }
@@ -237,7 +243,7 @@ function SidebarProvider({ defaultOpen = getCookie('isSidebarOpen') ?? true, cla
237
243
  shell.style.setProperty('--chat-panel-width', '0px');
238
244
  shell.style.removeProperty('--chat-panel-height');
239
245
  }
240
- }, [chatPanelOpen, isMobile, chatWidthPx]);
246
+ }, [chatPanelOpen, isSidebarSheetLayout, chatWidthPx]);
241
247
  useEffect(() => {
242
248
  return () => {
243
249
  const shell = wrapperRef.current;
@@ -291,18 +297,17 @@ function SidebarProvider({ defaultOpen = getCookie('isSidebarOpen') ?? true, cla
291
297
  }, className: cn(SidebarStem.sidebarWrapper, className), ...props, children: [jsx("div", { className: SidebarStem.sidebarMainShell, children: children }), jsx("div", { ref: onChatPanelMount, className: SidebarStem.chatPanelMount, "data-slot": "chat-panel-mount" })] }) }) }));
292
298
  }
293
299
  function Sidebar({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, side = 'left', variant = 'sidebar', collapsible = 'offcanvas', children, fullHeightResizer = false, ...props }) {
294
- const isMobile = useIsMobile();
300
+ const isSidebarSheetLayout = useIsSidebarSheetLayout();
295
301
  const { isOpen, setOpen } = useSidebar();
296
- // Only force-close on mobile. Do not call setOpen(true) on desktop that ran on every
297
- // load and overwrote the isSidebarOpen cookie (collapsed → opened + cookie "true").
302
+ /** Narrow shell: sidebar is a sheet; collapse when crossing into this mode (preserve cookie semantics elsewhere). */
298
303
  useEffect(() => {
299
- if (isMobile)
304
+ if (isSidebarSheetLayout)
300
305
  setOpen(false);
301
- }, [isMobile, setOpen]);
306
+ }, [isSidebarSheetLayout, setOpen]);
302
307
  if (collapsible === 'none') {
303
308
  return (jsx("div", { "data-slot": "sidebar", className: cn(SidebarStem.sidebarNone, className), ...props, children: children }));
304
309
  }
305
- if (isMobile) {
310
+ if (isSidebarSheetLayout) {
306
311
  return (jsx(Sheet, { open: isOpen, onOpenChange: setOpen, ...props, children: jsxs(SheetContent, { "data-sidebar": "sidebar", "data-slot": "sidebar", className: cn(SidebarStem.sheetContentSidebar, className), style: {
307
312
  '--gap-top': '40px',
308
313
  '--sidebar-width': SIDEBAR_WIDTH_MOBILE,
@@ -364,7 +369,7 @@ function PanelResizeHandle({ className, edge, isActive, startWidthPx, getShellWi
364
369
  }
365
370
  function SidebarResizeHandle({ className, side = 'left', ...props }) {
366
371
  const { isOpen, sidebarWidthPx, setSidebarWidthPx, getShellWidth } = useSidebar();
367
- const isMobile = useIsMobile();
372
+ const isSidebarSheetLayout = useIsSidebarSheetLayout();
368
373
  const edge = side === 'left' ? 'trailing' : 'leading';
369
374
  const onDragWidth = useCallback((rawPx) => {
370
375
  setSidebarWidthPx(rawPx, { persist: false });
@@ -372,10 +377,10 @@ function SidebarResizeHandle({ className, side = 'left', ...props }) {
372
377
  const onDragComplete = useCallback((finalRawPx) => {
373
378
  setSidebarWidthPx(finalRawPx, { persist: true });
374
379
  }, [setSidebarWidthPx]);
375
- if (isMobile || !isOpen) {
380
+ if (isSidebarSheetLayout || !isOpen) {
376
381
  return null;
377
382
  }
378
- return (jsx(PanelResizeHandle, { edge: edge, isActive: isOpen && !isMobile, startWidthPx: sidebarWidthPx, getShellWidth: getShellWidth, onDragWidth: onDragWidth, onDragComplete: onDragComplete, "data-sidebar": "resize-handle", "data-slot": "sidebar-resize-handle", "data-side": side, className: cn(SidebarStem.sidebarResizeHandle, className), ...props }));
383
+ return (jsx(PanelResizeHandle, { edge: edge, isActive: isOpen && !isSidebarSheetLayout, startWidthPx: sidebarWidthPx, getShellWidth: getShellWidth, onDragWidth: onDragWidth, onDragComplete: onDragComplete, "data-sidebar": "resize-handle", "data-slot": "sidebar-resize-handle", "data-side": side, className: cn(SidebarStem.sidebarResizeHandle, className), ...props }));
379
384
  }
380
385
  function SidebarFooter({ className, ...props }) {
381
386
  return (jsx("div", { "data-slot": "sidebar-footer", "data-sidebar": "footer", className: cn(SidebarStem.sidebarFooter, className), ...props }));
@@ -421,9 +426,9 @@ function SidebarMenuSubItem({ className, ...props }) {
421
426
  function SidebarMenuSubButton({ asChild = false, size = 'md', isActive = false, tooltip, className, onClick, ...props }) {
422
427
  const Comp = asChild ? Slot : 'a';
423
428
  const { isOpen, setOpen } = useSidebar();
424
- const isMobile = useIsMobile();
429
+ const isSidebarSheetLayout = useIsSidebarSheetLayout();
425
430
  const handleClick = (event) => {
426
- if (isMobile && isOpen)
431
+ if (isSidebarSheetLayout && isOpen)
427
432
  setOpen(false);
428
433
  onClick?.(event);
429
434
  };
@@ -435,7 +440,7 @@ function SidebarMenuSubButton({ asChild = false, size = 'md', isActive = false,
435
440
  children: tooltip,
436
441
  };
437
442
  }
438
- return (jsxs(Tooltip, { children: [jsx(TooltipTrigger, { asChild: true, children: button }), jsx(TooltipContent, { side: "right", align: "center", hidden: !isOpen || isMobile, ...tooltip })] }));
443
+ return (jsxs(Tooltip, { children: [jsx(TooltipTrigger, { asChild: true, children: button }), jsx(TooltipContent, { side: "right", align: "center", hidden: !isOpen || isSidebarSheetLayout, ...tooltip })] }));
439
444
  }
440
445
 
441
446
  export { PanelResizeHandle, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarResizeHandle, SidebarSeparator, SidebarTrigger, useSidebar };
@@ -1,2 +1,3 @@
1
1
  export { useIsMobile } from './useIsMobile.js';
2
+ export { useIsSidebarSheetLayout } from './useIsSidebarSheetLayout.js';
2
3
  export { default as useEvent } from './useEvent.js';
@@ -8,6 +8,10 @@ const CHAT_WIDTH_DEFAULT_PX = 800;
8
8
  const SHELL_MAX_FRACTION = 0.4;
9
9
  const CHAT_WIDTH_ABS_MAX_PX = 800;
10
10
  const CENTRAL_AREA_MIN_PX = 800;
11
+ /** Viewport below this uses sheet sidebar + single-column shell (central + sidebar mins cannot coexist). */
12
+ const SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX = CENTRAL_AREA_MIN_PX + SIDEBAR_WIDTH_MIN_PX;
13
+ /** `max-width` for media queries (`innerWidth < SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX`). */
14
+ const SIDEBAR_SHEET_LAYOUT_MAX_WIDTH_PX = SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX - 1;
11
15
  function maxSidebarWidthPx(shellWidth) {
12
16
  return Math.floor(shellWidth * SHELL_MAX_FRACTION);
13
17
  }
@@ -43,4 +47,4 @@ function defaultChatWidthPx(shellWidth) {
43
47
  return clampChatWidthPx(CHAT_WIDTH_DEFAULT_PX, shellWidth, SIDEBAR_WIDTH_DEFAULT_PX);
44
48
  }
45
49
 
46
- 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_WIDTH_DEFAULT_PX, SIDEBAR_WIDTH_MIN_PX, SIDEBAR_WIDTH_STORAGE_KEY_PCT, SIDEBAR_WIDTH_STORAGE_KEY_PX, clampChatWidthPx, clampSidebarWidthPx, defaultChatWidthPx, maxChatWidthPx, maxSidebarWidthPx };
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 };
@@ -0,0 +1,22 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX } from './panelWidth.js';
3
+
4
+ /** True when viewport cannot fit mains min + sidebar min — use Sheet sidebar + single-column AppShell. */
5
+ function useIsSidebarSheetLayout() {
6
+ const [narrow, setNarrow] = useState(typeof window !== 'undefined'
7
+ ? window.innerWidth < SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX
8
+ : undefined);
9
+ useEffect(() => {
10
+ const maxW = SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX - 1;
11
+ const mql = window.matchMedia(`(max-width: ${maxW}px)`);
12
+ const onChange = () => {
13
+ setNarrow(window.innerWidth < SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX);
14
+ };
15
+ mql.addEventListener('change', onChange);
16
+ setNarrow(window.innerWidth < SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX);
17
+ return () => mql.removeEventListener('change', onChange);
18
+ }, []);
19
+ return !!narrow;
20
+ }
21
+
22
+ export { useIsSidebarSheetLayout };
package/dist/esm/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  export { useIsMobile } from './hooks/useIsMobile.js';
2
+ export { useIsSidebarSheetLayout } from './hooks/useIsSidebarSheetLayout.js';
3
+ export { CENTRAL_AREA_MIN_PX, SIDEBAR_SHEET_LAYOUT_MAX_WIDTH_PX, SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX, SIDEBAR_WIDTH_MIN_PX } from './hooks/panelWidth.js';
2
4
  export { ThemeProvider, useTheme } from './contexts/theme-context.js';
3
5
  export { DEFAULT_THEME_ACTIVE_COLOR } from './docs/lib/theme.js';
4
6
  export { SybilionAuthProvider, createSybilionApiFetch, getSybilionApiOriginFromSdk, sybilionApiFetch, useSybilionApiFetch, useSybilionAuth } from './sybilion-auth/SybilionAuthProvider.js';
@@ -1,2 +1,3 @@
1
1
  export { useIsMobile } from './useIsMobile';
2
+ export { useIsSidebarSheetLayout } from './useIsSidebarSheetLayout';
2
3
  export { default as useEvent, type UseEventParams } from './useEvent';
@@ -8,6 +8,10 @@ export declare const CHAT_WIDTH_DEFAULT_PX = 800;
8
8
  export declare const SHELL_MAX_FRACTION = 0.4;
9
9
  export declare const CHAT_WIDTH_ABS_MAX_PX = 800;
10
10
  export declare const CENTRAL_AREA_MIN_PX = 800;
11
+ /** Viewport below this uses sheet sidebar + single-column shell (central + sidebar mins cannot coexist). */
12
+ export declare const SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX: number;
13
+ /** `max-width` for media queries (`innerWidth < SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX`). */
14
+ export declare const SIDEBAR_SHEET_LAYOUT_MAX_WIDTH_PX: number;
11
15
  export type ClampSidebarWidthOpts = {
12
16
  chatPanelOpen: boolean;
13
17
  chatWidthPx: number;
@@ -0,0 +1,2 @@
1
+ /** True when viewport cannot fit mains min + sidebar min — use Sheet sidebar + single-column AppShell. */
2
+ export declare function useIsSidebarSheetLayout(): boolean;
@@ -1,4 +1,6 @@
1
1
  export { useIsMobile } from './hooks/useIsMobile';
2
+ export { useIsSidebarSheetLayout } from './hooks/useIsSidebarSheetLayout';
3
+ export { CENTRAL_AREA_MIN_PX, SIDEBAR_SHEET_LAYOUT_MAX_WIDTH_PX, SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX, SIDEBAR_WIDTH_MIN_PX, } from './hooks/panelWidth';
2
4
  export * from './contexts/theme-context';
3
5
  export { DEFAULT_THEME_ACTIVE_COLOR } from './docs/lib/theme';
4
6
  export * from './sybilion-auth';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.6",
3
+ "version": "1.3.8",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,5 +1,7 @@
1
1
  @import 'lib/theme.styl'
2
2
 
3
+ SIDEBAR_SHEET_SPLIT = 1100px
4
+
3
5
  .root
4
6
  display grid
5
7
  flex 1
@@ -9,12 +11,12 @@
9
11
  grid-template-columns 1fr
10
12
  grid-template-areas "main"
11
13
 
12
- @media (min-width MOBILE)
14
+ @media (min-width SIDEBAR_SHEET_SPLIT)
13
15
  grid-template-columns var(--sidebar-width) 1fr
14
16
  grid-template-areas "sidebar main"
15
17
 
16
18
  :global([data-slot="sidebar-wrapper"][data-state="collapsed"]) &
17
- @media (min-width MOBILE)
19
+ @media (min-width SIDEBAR_SHEET_SPLIT)
18
20
  grid-template-columns 0 1fr
19
21
 
20
22
  .mainColumn
@@ -37,7 +37,7 @@ import {
37
37
  } from '#uilib/hooks/panelWidth';
38
38
  import useElemDrag, { Delta } from '#uilib/hooks/useDragElem';
39
39
  import useEvent from '#uilib/hooks/useEvent';
40
- import { useIsMobile } from '#uilib/hooks/useIsMobile';
40
+ import { useIsSidebarSheetLayout } from '#uilib/hooks/useIsSidebarSheetLayout';
41
41
  import { useShellWidthObserver } from '#uilib/hooks/useShellWidthObserver';
42
42
  import { getCookie, setCookie } from '#uilib/lib/cookie';
43
43
  import { getCookiePreferences } from '#uilib/lib/cookie-consent/cookie-consent';
@@ -153,8 +153,10 @@ function SidebarProvider({
153
153
  persistSidebarWidthWithoutConsent ||
154
154
  Boolean(getCookiePreferences(userId)?.functional);
155
155
 
156
- const isMobile = useIsMobile();
157
- const [isOpen, setIsOpen] = useState(isMobile ? false : defaultOpen);
156
+ const isSidebarSheetLayout = useIsSidebarSheetLayout();
157
+ const [isOpen, setIsOpen] = useState(
158
+ isSidebarSheetLayout ? false : defaultOpen,
159
+ );
158
160
  const wrapperRef = useRef<HTMLDivElement | null>(null);
159
161
  const shellWidthRef = useRef(0);
160
162
  const prevShellWidthRef = useRef(0);
@@ -297,9 +299,15 @@ function SidebarProvider({
297
299
  ]);
298
300
 
299
301
  useLayoutEffect(() => {
300
- if (!shellEl || isMobile) return;
302
+ if (!shellEl || isSidebarSheetLayout) return;
301
303
  fitPanelWidthsToShell();
302
- }, [shellEl, chatPanelOpen, isMobile, isOpen, fitPanelWidthsToShell]);
304
+ }, [
305
+ shellEl,
306
+ chatPanelOpen,
307
+ isSidebarSheetLayout,
308
+ isOpen,
309
+ fitPanelWidthsToShell,
310
+ ]);
303
311
 
304
312
  useShellWidthObserver(shellEl, handleShellResize);
305
313
 
@@ -307,7 +315,7 @@ function SidebarProvider({
307
315
  (value: boolean, options?: SetSidebarOpenOptions) => {
308
316
  const useViewTransition =
309
317
  options?.viewTransition !== false &&
310
- !isMobile &&
318
+ !isSidebarSheetLayout &&
311
319
  'startViewTransition' in document &&
312
320
  document.startViewTransition;
313
321
 
@@ -323,7 +331,7 @@ function SidebarProvider({
323
331
  setCookie('isSidebarOpen', value.toString(), 60 * 60 * 24 * 7);
324
332
  }
325
333
  },
326
- [isMobile, userId],
334
+ [isSidebarSheetLayout, userId],
327
335
  );
328
336
 
329
337
  const toggleSidebar = useCallback(() => setOpen(!isOpen), [isOpen, setOpen]);
@@ -333,7 +341,7 @@ function SidebarProvider({
333
341
  if (!shell) return;
334
342
  if (chatPanelOpen) {
335
343
  shell.setAttribute('data-chat-open', '');
336
- if (isMobile) {
344
+ if (isSidebarSheetLayout) {
337
345
  shell.style.setProperty('--chat-panel-width', '100%');
338
346
  shell.style.setProperty('--chat-panel-height', '100dvh');
339
347
  } else {
@@ -345,7 +353,7 @@ function SidebarProvider({
345
353
  shell.style.setProperty('--chat-panel-width', '0px');
346
354
  shell.style.removeProperty('--chat-panel-height');
347
355
  }
348
- }, [chatPanelOpen, isMobile, chatWidthPx]);
356
+ }, [chatPanelOpen, isSidebarSheetLayout, chatWidthPx]);
349
357
 
350
358
  useEffect(() => {
351
359
  return () => {
@@ -447,14 +455,13 @@ function Sidebar({
447
455
  fullHeightResizer = false,
448
456
  ...props
449
457
  }: SidebarProps) {
450
- const isMobile = useIsMobile();
458
+ const isSidebarSheetLayout = useIsSidebarSheetLayout();
451
459
  const { isOpen, setOpen } = useSidebar();
452
460
 
453
- // Only force-close on mobile. Do not call setOpen(true) on desktop that ran on every
454
- // load and overwrote the isSidebarOpen cookie (collapsed → opened + cookie "true").
461
+ /** Narrow shell: sidebar is a sheet; collapse when crossing into this mode (preserve cookie semantics elsewhere). */
455
462
  useEffect(() => {
456
- if (isMobile) setOpen(false);
457
- }, [isMobile, setOpen]);
463
+ if (isSidebarSheetLayout) setOpen(false);
464
+ }, [isSidebarSheetLayout, setOpen]);
458
465
 
459
466
  if (collapsible === 'none') {
460
467
  return (
@@ -468,7 +475,7 @@ function Sidebar({
468
475
  );
469
476
  }
470
477
 
471
- if (isMobile) {
478
+ if (isSidebarSheetLayout) {
472
479
  return (
473
480
  <Sheet open={isOpen} onOpenChange={setOpen} {...props}>
474
481
  <SheetContent
@@ -640,7 +647,7 @@ function SidebarResizeHandle({
640
647
  }: ComponentProps<'div'> & { side?: 'left' | 'right' }) {
641
648
  const { isOpen, sidebarWidthPx, setSidebarWidthPx, getShellWidth } =
642
649
  useSidebar();
643
- const isMobile = useIsMobile();
650
+ const isSidebarSheetLayout = useIsSidebarSheetLayout();
644
651
 
645
652
  const edge: 'leading' | 'trailing' = side === 'left' ? 'trailing' : 'leading';
646
653
 
@@ -658,14 +665,14 @@ function SidebarResizeHandle({
658
665
  [setSidebarWidthPx],
659
666
  );
660
667
 
661
- if (isMobile || !isOpen) {
668
+ if (isSidebarSheetLayout || !isOpen) {
662
669
  return null;
663
670
  }
664
671
 
665
672
  return (
666
673
  <PanelResizeHandle
667
674
  edge={edge}
668
- isActive={isOpen && !isMobile}
675
+ isActive={isOpen && !isSidebarSheetLayout}
669
676
  startWidthPx={sidebarWidthPx}
670
677
  getShellWidth={getShellWidth}
671
678
  onDragWidth={onDragWidth}
@@ -877,10 +884,10 @@ function SidebarMenuSubButton({
877
884
  }) {
878
885
  const Comp = asChild ? Slot : 'a';
879
886
  const { isOpen, setOpen } = useSidebar();
880
- const isMobile = useIsMobile();
887
+ const isSidebarSheetLayout = useIsSidebarSheetLayout();
881
888
 
882
889
  const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
883
- if (isMobile && isOpen) setOpen(false);
890
+ if (isSidebarSheetLayout && isOpen) setOpen(false);
884
891
  onClick?.(event);
885
892
  };
886
893
 
@@ -910,7 +917,7 @@ function SidebarMenuSubButton({
910
917
  <TooltipContent
911
918
  side="right"
912
919
  align="center"
913
- hidden={!isOpen || isMobile}
920
+ hidden={!isOpen || isSidebarSheetLayout}
914
921
  {...tooltip}
915
922
  />
916
923
  </Tooltip>
@@ -1,2 +1,3 @@
1
1
  export { useIsMobile } from './useIsMobile';
2
+ export { useIsSidebarSheetLayout } from './useIsSidebarSheetLayout';
2
3
  export { default as useEvent, type UseEventParams } from './useEvent';
@@ -10,6 +10,14 @@ export const SHELL_MAX_FRACTION = 0.4;
10
10
  export const CHAT_WIDTH_ABS_MAX_PX = 800;
11
11
  export const CENTRAL_AREA_MIN_PX = 800;
12
12
 
13
+ /** Viewport below this uses sheet sidebar + single-column shell (central + sidebar mins cannot coexist). */
14
+ export const SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX =
15
+ CENTRAL_AREA_MIN_PX + SIDEBAR_WIDTH_MIN_PX;
16
+
17
+ /** `max-width` for media queries (`innerWidth < SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX`). */
18
+ export const SIDEBAR_SHEET_LAYOUT_MAX_WIDTH_PX =
19
+ SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX - 1;
20
+
13
21
  export type ClampSidebarWidthOpts = {
14
22
  chatPanelOpen: boolean;
15
23
  chatWidthPx: number;
@@ -0,0 +1,25 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ import { SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX } from '#uilib/hooks/panelWidth';
4
+
5
+ /** True when viewport cannot fit mains min + sidebar min — use Sheet sidebar + single-column AppShell. */
6
+ export function useIsSidebarSheetLayout(): boolean {
7
+ const [narrow, setNarrow] = useState<boolean | undefined>(
8
+ typeof window !== 'undefined'
9
+ ? window.innerWidth < SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX
10
+ : undefined,
11
+ );
12
+
13
+ useEffect(() => {
14
+ const maxW = SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX - 1;
15
+ const mql = window.matchMedia(`(max-width: ${maxW}px)`);
16
+ const onChange = () => {
17
+ setNarrow(window.innerWidth < SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX);
18
+ };
19
+ mql.addEventListener('change', onChange);
20
+ setNarrow(window.innerWidth < SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX);
21
+ return () => mql.removeEventListener('change', onChange);
22
+ }, []);
23
+
24
+ return !!narrow;
25
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  export { useIsMobile } from './hooks/useIsMobile';
2
+ export { useIsSidebarSheetLayout } from './hooks/useIsSidebarSheetLayout';
3
+ export {
4
+ CENTRAL_AREA_MIN_PX,
5
+ SIDEBAR_SHEET_LAYOUT_MAX_WIDTH_PX,
6
+ SIDEBAR_SHEET_SPLIT_MIN_VIEWPORT_PX,
7
+ SIDEBAR_WIDTH_MIN_PX,
8
+ } from './hooks/panelWidth';
2
9
  export * from './contexts/theme-context';
3
10
  export { DEFAULT_THEME_ACTIVE_COLOR } from './docs/lib/theme';
4
11
  export * from './sybilion-auth';