@kushagradhawan/kookie-ui 0.1.32 → 0.1.34

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.
Files changed (120) hide show
  1. package/components.css +937 -458
  2. package/dist/cjs/components/_internal/base-button.d.ts.map +1 -1
  3. package/dist/cjs/components/_internal/base-button.js +1 -1
  4. package/dist/cjs/components/_internal/base-button.js.map +3 -3
  5. package/dist/cjs/components/chatbar.d.ts +202 -0
  6. package/dist/cjs/components/chatbar.d.ts.map +1 -0
  7. package/dist/cjs/components/chatbar.js +2 -0
  8. package/dist/cjs/components/chatbar.js.map +7 -0
  9. package/dist/cjs/components/icon-button.d.ts.map +1 -1
  10. package/dist/cjs/components/icon-button.js +2 -2
  11. package/dist/cjs/components/icon-button.js.map +3 -3
  12. package/dist/cjs/components/icons.d.ts +6 -1
  13. package/dist/cjs/components/icons.d.ts.map +1 -1
  14. package/dist/cjs/components/icons.js +1 -1
  15. package/dist/cjs/components/icons.js.map +3 -3
  16. package/dist/cjs/components/index.d.ts +3 -0
  17. package/dist/cjs/components/index.d.ts.map +1 -1
  18. package/dist/cjs/components/index.js +1 -1
  19. package/dist/cjs/components/index.js.map +3 -3
  20. package/dist/cjs/components/popover.d.ts +13 -1
  21. package/dist/cjs/components/popover.d.ts.map +1 -1
  22. package/dist/cjs/components/popover.js +1 -1
  23. package/dist/cjs/components/popover.js.map +3 -3
  24. package/dist/cjs/components/sheet.d.ts +82 -0
  25. package/dist/cjs/components/sheet.d.ts.map +1 -0
  26. package/dist/cjs/components/sheet.js +2 -0
  27. package/dist/cjs/components/sheet.js.map +7 -0
  28. package/dist/cjs/components/shell.d.ts +180 -0
  29. package/dist/cjs/components/shell.d.ts.map +1 -0
  30. package/dist/cjs/components/shell.js +2 -0
  31. package/dist/cjs/components/shell.js.map +7 -0
  32. package/dist/cjs/components/sidebar.d.ts +4 -33
  33. package/dist/cjs/components/sidebar.d.ts.map +1 -1
  34. package/dist/cjs/components/sidebar.js +1 -1
  35. package/dist/cjs/components/sidebar.js.map +3 -3
  36. package/dist/cjs/components/skeleton.d.ts.map +1 -1
  37. package/dist/cjs/components/skeleton.js +1 -1
  38. package/dist/cjs/components/skeleton.js.map +2 -2
  39. package/dist/cjs/helpers/inert.d.ts +1 -1
  40. package/dist/cjs/helpers/inert.d.ts.map +1 -1
  41. package/dist/cjs/helpers/inert.js +1 -1
  42. package/dist/cjs/helpers/inert.js.map +2 -2
  43. package/dist/esm/components/_internal/base-button.d.ts.map +1 -1
  44. package/dist/esm/components/_internal/base-button.js +1 -1
  45. package/dist/esm/components/_internal/base-button.js.map +3 -3
  46. package/dist/esm/components/chatbar.d.ts +202 -0
  47. package/dist/esm/components/chatbar.d.ts.map +1 -0
  48. package/dist/esm/components/chatbar.js +2 -0
  49. package/dist/esm/components/chatbar.js.map +7 -0
  50. package/dist/esm/components/icon-button.d.ts.map +1 -1
  51. package/dist/esm/components/icon-button.js +2 -2
  52. package/dist/esm/components/icon-button.js.map +3 -3
  53. package/dist/esm/components/icons.d.ts +6 -1
  54. package/dist/esm/components/icons.d.ts.map +1 -1
  55. package/dist/esm/components/icons.js +1 -1
  56. package/dist/esm/components/icons.js.map +3 -3
  57. package/dist/esm/components/index.d.ts +3 -0
  58. package/dist/esm/components/index.d.ts.map +1 -1
  59. package/dist/esm/components/index.js +1 -1
  60. package/dist/esm/components/index.js.map +3 -3
  61. package/dist/esm/components/popover.d.ts +13 -1
  62. package/dist/esm/components/popover.d.ts.map +1 -1
  63. package/dist/esm/components/popover.js +1 -1
  64. package/dist/esm/components/popover.js.map +3 -3
  65. package/dist/esm/components/sheet.d.ts +82 -0
  66. package/dist/esm/components/sheet.d.ts.map +1 -0
  67. package/dist/esm/components/sheet.js +2 -0
  68. package/dist/esm/components/sheet.js.map +7 -0
  69. package/dist/esm/components/shell.d.ts +180 -0
  70. package/dist/esm/components/shell.d.ts.map +1 -0
  71. package/dist/esm/components/shell.js +2 -0
  72. package/dist/esm/components/shell.js.map +7 -0
  73. package/dist/esm/components/sidebar.d.ts +4 -33
  74. package/dist/esm/components/sidebar.d.ts.map +1 -1
  75. package/dist/esm/components/sidebar.js +1 -1
  76. package/dist/esm/components/sidebar.js.map +3 -3
  77. package/dist/esm/components/skeleton.d.ts.map +1 -1
  78. package/dist/esm/components/skeleton.js.map +2 -2
  79. package/dist/esm/helpers/inert.d.ts +1 -1
  80. package/dist/esm/helpers/inert.d.ts.map +1 -1
  81. package/dist/esm/helpers/inert.js +1 -1
  82. package/dist/esm/helpers/inert.js.map +2 -2
  83. package/package.json +2 -1
  84. package/src/components/_internal/base-button.tsx +8 -0
  85. package/src/components/_internal/base-card.css +18 -18
  86. package/src/components/_internal/base-dialog.css +11 -49
  87. package/src/components/_internal/base-menu.css +2 -2
  88. package/src/components/_internal/base-sidebar-menu.css +3 -3
  89. package/src/components/accordion.css +6 -6
  90. package/src/components/animations.css +65 -81
  91. package/src/components/callout.css +3 -3
  92. package/src/components/chatbar.css +214 -0
  93. package/src/components/chatbar.tsx +1181 -0
  94. package/src/components/icon-button.tsx +11 -0
  95. package/src/components/icons.tsx +97 -2
  96. package/src/components/image.css +3 -3
  97. package/src/components/index.css +3 -0
  98. package/src/components/index.tsx +3 -0
  99. package/src/components/popover.css +53 -8
  100. package/src/components/popover.tsx +180 -2
  101. package/src/components/scroll-area.css +3 -3
  102. package/src/components/segmented-control.css +3 -3
  103. package/src/components/sheet.css +90 -0
  104. package/src/components/sheet.tsx +247 -0
  105. package/src/components/shell.css +137 -0
  106. package/src/components/shell.tsx +1032 -0
  107. package/src/components/sidebar.css +55 -268
  108. package/src/components/sidebar.tsx +40 -262
  109. package/src/components/skeleton.tsx +1 -2
  110. package/src/components/text-area.css +6 -5
  111. package/src/components/tooltip.css +2 -2
  112. package/src/helpers/inert.ts +3 -3
  113. package/src/styles/tokens/constants.css +6 -3
  114. package/src/styles/tokens/index.css +1 -0
  115. package/src/styles/tokens/radius.css +7 -2
  116. package/src/styles/tokens/space.css +6 -0
  117. package/src/styles/tokens/transition.css +91 -46
  118. package/styles.css +998 -496
  119. package/tokens/base.css +57 -35
  120. package/tokens.css +61 -38
@@ -0,0 +1,1032 @@
1
+ /**
2
+ * Shell component
3
+ *
4
+ * High-level application layout primitive with optional global header/footer and
5
+ * one or two sidebars. Each sidebar supports two composition patterns:
6
+ *
7
+ * 1) Split pattern (preferred for interactive rails):
8
+ * <Shell.Sidebar>
9
+ * <Shell.Sidebar.Rail />
10
+ * <Shell.Sidebar.Panel />
11
+ * </Shell.Sidebar>
12
+ *
13
+ * 2) Single-markup morphing (compact):
14
+ * <Shell.Sidebar> ...single content that morphs between rail/panel/collapsed </Shell.Sidebar>
15
+ *
16
+ * The component handles:
17
+ * - RTL/LTR ordering for start/end sidebars
18
+ * - Sticky panel intent in split mode (panel stays requested, visibility depends on rail open state)
19
+ * - Controlled/uncontrolled state for both split (rail value) and single-markup (view)
20
+ * - ARIA relationships and inert for hidden regions
21
+ * - CSS custom properties for sizing with sensible defaults
22
+ *
23
+ * Layout guidance:
24
+ * - Use flex/grid and spacing tokens; avoid margins in layout containers.
25
+ * - Width transitions for rail/panel are CSS-driven; content scroll is confined to Content only.
26
+ */
27
+ 'use client';
28
+
29
+ import * as React from 'react';
30
+ import classNames from 'classnames';
31
+
32
+ import { IconButton } from './icon-button.js';
33
+ import { ChevronDownIcon } from './icons.js';
34
+ import { inert } from '../helpers/inert.js';
35
+ import * as Sheet from './sheet.js';
36
+ import { VisuallyHidden } from './visually-hidden.js';
37
+
38
+ /** Logical document direction. Derived from document root unless `rtl` is passed. */
39
+ type ShellDirection = 'ltr' | 'rtl';
40
+ /** Logical side, independent of physical left/right and aware of RTL. */
41
+ type ShellSide = 'start' | 'end';
42
+ /** Section slot identifiers inside a sidebar. */
43
+ type SidebarSection = 'rail' | 'panel';
44
+
45
+ /** Split-mode rail state. */
46
+ type RailValue = 'open' | 'collapsed';
47
+ /** Single-markup view for morphing sidebars. */
48
+ type SingleView = 'panel' | 'rail' | 'collapsed';
49
+
50
+ /**
51
+ * Shared shell state across all subcomponents.
52
+ *
53
+ * We centralize both split-mode and single-markup state to allow triggers,
54
+ * meters, and other UI to observe/modify layout consistently.
55
+ */
56
+ type ShellContextValue = {
57
+ dir: ShellDirection;
58
+ headerHeight: string;
59
+ zHeader?: number;
60
+ // Split pattern state (rail + panel)
61
+ railBySide: Record<ShellSide, RailValue>;
62
+ setRailBySide: (side: ShellSide, value: RailValue) => void;
63
+ toggleRail: (side: ShellSide) => void;
64
+ panelRequestedBySide: Record<ShellSide, boolean>;
65
+ setPanelRequestedBySide: (side: ShellSide, requested: boolean) => void;
66
+ // Pattern detection per side so triggers behave correctly
67
+ patternBySide: Record<ShellSide, 'single' | 'split'>;
68
+ setPatternForSide: (side: ShellSide, pattern: 'single' | 'split') => void;
69
+ // Single-markup morph state (panel/rail/collapsed)
70
+ singleViewBySide: Record<ShellSide, SingleView>;
71
+ setSingleViewBySide: (side: ShellSide, view: SingleView) => void;
72
+ cycleSingleView: (side: ShellSide) => void;
73
+ // Active tool coordination state
74
+ activeToolBySide: Record<ShellSide, string | null>;
75
+ setActiveTool: (side: ShellSide, tool: string | null) => void;
76
+ onItemSelected: (side: ShellSide, item: string) => void;
77
+ // Context (panel-level) coordination state
78
+ activeContextBySide: Record<ShellSide, string | null>;
79
+ setActiveContext: (side: ShellSide, context: string | null) => void;
80
+ getRegionId: (side: ShellSide) => string;
81
+ getPanelId: (side: ShellSide) => string;
82
+ getRailId: (side: ShellSide) => string;
83
+ };
84
+
85
+ const ShellContext = React.createContext<ShellContextValue | null>(null);
86
+
87
+ /** Access the shell context (internal wiring for subcomponents). */
88
+ function useShell() {
89
+ const ctx = React.useContext(ShellContext);
90
+ if (!ctx) throw new Error('Shell components must be used within <Shell.Root>.');
91
+ return ctx;
92
+ }
93
+
94
+ // Local context to communicate section (rail/panel) and side to presentational children
95
+ type SidebarSectionContextValue = {
96
+ side: ShellSide;
97
+ section: SidebarSection;
98
+ };
99
+ const SidebarSectionContext = React.createContext<SidebarSectionContextValue | null>(null);
100
+ function useSidebarSection() {
101
+ return React.useContext(SidebarSectionContext);
102
+ }
103
+
104
+ // Rail context for event emission pattern
105
+ type RailContextValue = {
106
+ onItemSelected: (item: string) => void;
107
+ };
108
+ const RailContext = React.createContext<RailContextValue | null>(null);
109
+
110
+ /** Hook to emit rail selection events. Use this in Rail children to emit item selection. */
111
+ function useRailEvents() {
112
+ const ctx = React.useContext(RailContext);
113
+ if (!ctx) throw new Error('useRailEvents must be used within <Shell.Sidebar.Rail>');
114
+ return ctx;
115
+ }
116
+
117
+ // Panel context for active tool consumption
118
+ type PanelContextValue = {
119
+ activeTool: string | null;
120
+ activeContext: string | null;
121
+ };
122
+ const PanelContext = React.createContext<PanelContextValue | null>(null);
123
+
124
+ /** Hook to access active tool state. Use this in Panel children to render based on active tool. */
125
+ function usePanelState() {
126
+ const ctx = React.useContext(PanelContext);
127
+ if (!ctx) throw new Error('usePanelState must be used within <Shell.Sidebar.Panel>');
128
+ return ctx;
129
+ }
130
+
131
+ // Utilities
132
+ /**
133
+ * Read the document `dir` attribute. Defaults to `ltr` on server.
134
+ */
135
+ function getDocumentDirection(): ShellDirection {
136
+ if (typeof document === 'undefined') return 'ltr';
137
+ const dir = document.documentElement.getAttribute('dir');
138
+ return dir === 'rtl' ? 'rtl' : 'ltr';
139
+ }
140
+
141
+ // Root
142
+ /**
143
+ * Props for `Shell.Root`.
144
+ *
145
+ * - `minContentWidth`: CSS length to enforce a minimum inline-size for content
146
+ * area. This prevents content from collapsing too far when sidebars are open.
147
+ * - `rtl`: Force RTL/LTR independent of document; otherwise derived from root.
148
+ * - `headerHeight`: Sticky header block-size.
149
+ * - `zHeader`: z-index for sticky header stacking.
150
+ */
151
+ interface ShellRootProps extends React.ComponentPropsWithoutRef<'div'> {
152
+ minContentWidth?: string;
153
+ rtl?: boolean;
154
+ headerHeight?: string;
155
+ zHeader?: number;
156
+ cascadeSide?: ShellSide;
157
+ activeTool?: string | null;
158
+ onToolChange?: (id: string | null) => void;
159
+ activeContext?: string | null;
160
+ onContextChange?: (id: string | null) => void;
161
+ }
162
+
163
+ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(
164
+ (
165
+ {
166
+ minContentWidth = '640px',
167
+ rtl,
168
+ headerHeight = '64px',
169
+ zHeader,
170
+ cascadeSide = 'start',
171
+ activeTool: activeToolProp,
172
+ onToolChange,
173
+ activeContext: activeContextProp,
174
+ onContextChange,
175
+ className,
176
+ style,
177
+ children,
178
+ ...props
179
+ },
180
+ ref,
181
+ ) => {
182
+ // Determine logical direction once, overriding document if `rtl` provided
183
+ const computedDir = React.useMemo<ShellDirection>(() => {
184
+ if (typeof rtl === 'boolean') return rtl ? 'rtl' : 'ltr';
185
+ return getDocumentDirection();
186
+ }, [rtl]);
187
+
188
+ // === Split-mode and single-markup state model ===
189
+ // In split-mode, a sidebar has both Rail and Panel slots. We track:
190
+ // - rail open/collapsed state per side
191
+ // - a sticky panel intent per side (requested or not)
192
+ // In single-markup, we track a single `view` morphing between panel/rail/collapsed
193
+ const [railBySide, setRailBySideState] = React.useState<Record<ShellSide, RailValue>>({
194
+ start: 'open',
195
+ end: 'collapsed',
196
+ });
197
+ const setRailBySide = React.useCallback((side: ShellSide, value: RailValue) => {
198
+ setRailBySideState((prev) => (prev[side] === value ? prev : { ...prev, [side]: value }));
199
+ }, []);
200
+ const toggleRail = React.useCallback((side: ShellSide) => {
201
+ setRailBySideState((prev) => {
202
+ const next = prev[side] === 'open' ? 'collapsed' : 'open';
203
+ return { ...prev, [side]: next };
204
+ });
205
+ // Keep panelRequested sticky across rail collapse/expand
206
+ }, []);
207
+ const [panelRequestedBySide, setPanelRequestedBySideState] = React.useState<
208
+ Record<ShellSide, boolean>
209
+ >({ start: false, end: false });
210
+ const setPanelRequestedBySide = React.useCallback((side: ShellSide, requested: boolean) => {
211
+ setPanelRequestedBySideState((prev) =>
212
+ prev[side] === requested ? prev : { ...prev, [side]: requested },
213
+ );
214
+ }, []);
215
+ // Pattern per side and single-markup state
216
+ // We detect pattern per side based on presence of slots (registered by Sidebar)
217
+ const [patternBySide, setPatternBySide] = React.useState<Record<ShellSide, 'single' | 'split'>>(
218
+ {
219
+ start: 'single',
220
+ end: 'single',
221
+ },
222
+ );
223
+ const setPatternForSide = React.useCallback((side: ShellSide, pattern: 'single' | 'split') => {
224
+ setPatternBySide((prev) => (prev[side] === pattern ? prev : { ...prev, [side]: pattern }));
225
+ }, []);
226
+ const [singleViewBySide, setSingleViewBySideState] = React.useState<
227
+ Record<ShellSide, SingleView>
228
+ >({
229
+ start: 'panel',
230
+ end: 'collapsed',
231
+ });
232
+ const setSingleViewBySide = React.useCallback((side: ShellSide, view: SingleView) => {
233
+ setSingleViewBySideState((prev) => (prev[side] === view ? prev : { ...prev, [side]: view }));
234
+ }, []);
235
+ const cycleSingleView = React.useCallback(
236
+ (side: ShellSide) => {
237
+ const order: SingleView[] = ['panel', 'rail', 'collapsed'];
238
+ const current = singleViewBySide[side];
239
+ const idx = order.indexOf(current);
240
+ const next = order[(idx + 1) % order.length];
241
+ setSingleViewBySide(side, next);
242
+ },
243
+ [singleViewBySide, setSingleViewBySide],
244
+ );
245
+
246
+ // === Active tool coordination state ===
247
+ // Track which tool/mode is active per side for coordinated rail/panel communication
248
+ const [activeToolBySide, setActiveToolBySideState] = React.useState<
249
+ Record<ShellSide, string | null>
250
+ >({
251
+ start: null,
252
+ end: null,
253
+ });
254
+ const setActiveTool = React.useCallback(
255
+ (side: ShellSide, tool: string | null) => {
256
+ setActiveToolBySideState((prev) =>
257
+ prev[side] === tool ? prev : { ...prev, [side]: tool },
258
+ );
259
+ // Auto-hide panel when no tool is active
260
+ if (tool === null) {
261
+ setPanelRequestedBySide(side, false);
262
+ }
263
+ },
264
+ [setPanelRequestedBySide],
265
+ );
266
+ const onItemSelected = React.useCallback(
267
+ (side: ShellSide, item: string) => {
268
+ setActiveTool(side, item);
269
+ // Auto-show panel when item is selected
270
+ setPanelRequestedBySide(side, true);
271
+ },
272
+ [setActiveTool, setPanelRequestedBySide],
273
+ );
274
+
275
+ // === Active context coordination state ===
276
+ const [activeContextBySide, setActiveContextBySideState] = React.useState<
277
+ Record<ShellSide, string | null>
278
+ >({ start: null, end: null });
279
+ const setActiveContext = React.useCallback(
280
+ (side: ShellSide, context: string | null) => {
281
+ setActiveContextBySideState((prev) =>
282
+ prev[side] === context ? prev : { ...prev, [side]: context },
283
+ );
284
+ if (side === cascadeSide) onContextChange?.(context);
285
+ },
286
+ [cascadeSide, onContextChange],
287
+ );
288
+
289
+ // === Controlled prop sync (cascade side) ===
290
+ React.useEffect(() => {
291
+ if (activeToolProp !== undefined && activeToolBySide[cascadeSide] !== activeToolProp) {
292
+ setActiveToolBySideState((prev) => ({ ...prev, [cascadeSide]: activeToolProp }));
293
+ }
294
+ }, [activeToolProp, cascadeSide, activeToolBySide]);
295
+ React.useEffect(() => {
296
+ if (
297
+ activeContextProp !== undefined &&
298
+ activeContextBySide[cascadeSide] !== activeContextProp
299
+ ) {
300
+ setActiveContextBySideState((prev) => ({ ...prev, [cascadeSide]: activeContextProp }));
301
+ }
302
+ }, [activeContextProp, cascadeSide, activeContextBySide]);
303
+
304
+ // === Stable ids per side ===
305
+ // These IDs are used to wire aria-controls and aria-expanded for triggers,
306
+ // and to scope region/panel/rail DOM nodes for measurement.
307
+ const startRegionId = React.useId();
308
+ const endRegionId = React.useId();
309
+ const startPanelId = React.useId();
310
+ const endPanelId = React.useId();
311
+ const startRailId = React.useId();
312
+ const endRailId = React.useId();
313
+ const getRegionId = React.useCallback(
314
+ (side: ShellSide) =>
315
+ side === 'start' ? `kui-shell-region-${startRegionId}` : `kui-shell-region-${endRegionId}`,
316
+ [startRegionId, endRegionId],
317
+ );
318
+ const getPanelId = React.useCallback(
319
+ (side: ShellSide) =>
320
+ side === 'start' ? `kui-shell-panel-${startPanelId}` : `kui-shell-panel-${endPanelId}`,
321
+ [startPanelId, endPanelId],
322
+ );
323
+ const getRailId = React.useCallback(
324
+ (side: ShellSide) =>
325
+ side === 'start' ? `kui-shell-rail-${startRailId}` : `kui-shell-rail-${endRailId}`,
326
+ [startRailId, endRailId],
327
+ );
328
+
329
+ const ctx = React.useMemo<ShellContextValue>(
330
+ () => ({
331
+ dir: computedDir,
332
+ headerHeight,
333
+ zHeader,
334
+ railBySide,
335
+ setRailBySide,
336
+ toggleRail,
337
+ panelRequestedBySide,
338
+ setPanelRequestedBySide,
339
+ patternBySide,
340
+ setPatternForSide,
341
+ singleViewBySide,
342
+ setSingleViewBySide,
343
+ cycleSingleView,
344
+ activeToolBySide,
345
+ setActiveTool,
346
+ onItemSelected,
347
+ activeContextBySide,
348
+ setActiveContext,
349
+ getRegionId,
350
+ getPanelId,
351
+ getRailId,
352
+ }),
353
+ [
354
+ computedDir,
355
+ headerHeight,
356
+ zHeader,
357
+ railBySide,
358
+ setRailBySide,
359
+ toggleRail,
360
+ panelRequestedBySide,
361
+ setPanelRequestedBySide,
362
+ patternBySide,
363
+ setPatternForSide,
364
+ singleViewBySide,
365
+ setSingleViewBySide,
366
+ cycleSingleView,
367
+ activeToolBySide,
368
+ setActiveTool,
369
+ onItemSelected,
370
+ activeContextBySide,
371
+ setActiveContext,
372
+ getRegionId,
373
+ getPanelId,
374
+ getRailId,
375
+ ],
376
+ );
377
+
378
+ // === Composition: order children based on direction ===
379
+ const childArray = React.Children.toArray(children) as React.ReactElement[];
380
+ const isType = (el: React.ReactElement, comp: any) =>
381
+ React.isValidElement(el) && el.type === comp;
382
+ const headerEls = childArray.filter((el) => isType(el, Header));
383
+ const footerEls = childArray.filter((el) => isType(el, Footer));
384
+ const contentEls = childArray.filter((el) => isType(el, Content));
385
+ const sidebarEls = childArray.filter((el) => isType(el, Sidebar));
386
+
387
+ // Partition sidebars by side
388
+ const startSidebars = sidebarEls.filter((el) => (el.props as any).side === 'start');
389
+ const endSidebars = sidebarEls.filter((el) => (el.props as any).side === 'end');
390
+
391
+ const bodyChildren =
392
+ computedDir === 'rtl'
393
+ ? [...endSidebars, ...contentEls, ...startSidebars]
394
+ : [...startSidebars, ...contentEls, ...endSidebars];
395
+
396
+ return (
397
+ <div
398
+ {...props}
399
+ ref={ref}
400
+ className={classNames('rt-ShellRoot', className)}
401
+ style={{
402
+ ...style,
403
+ // Internal CSS custom props for sizing
404
+ ['--shell-min-content-width' as any]: minContentWidth,
405
+ ['--shell-header-height' as any]: headerHeight,
406
+ }}
407
+ dir={computedDir}
408
+ >
409
+ <ShellContext.Provider value={ctx}>
410
+ {headerEls}
411
+ <div className="rt-ShellBody">{bodyChildren}</div>
412
+ {footerEls}
413
+ </ShellContext.Provider>
414
+ </div>
415
+ );
416
+ },
417
+ );
418
+ Root.displayName = 'Shell.Root';
419
+
420
+ // Global Header
421
+ /** Props for `Shell.Header`. Sticky by default and respects `--shell-header-height`. */
422
+ interface ShellHeaderProps extends React.ComponentPropsWithoutRef<'header'> {}
423
+ const Header = React.forwardRef<HTMLElement, ShellHeaderProps>(
424
+ ({ className, style, ...props }, ref) => {
425
+ const { headerHeight, zHeader } = useShell();
426
+ return (
427
+ <header
428
+ {...props}
429
+ ref={ref}
430
+ role="banner"
431
+ className={classNames('rt-ShellHeader', className)}
432
+ style={{
433
+ ['--shell-header-height' as any]: headerHeight,
434
+ ['--shell-z-header' as any]: zHeader,
435
+ ...style,
436
+ }}
437
+ />
438
+ );
439
+ },
440
+ );
441
+ Header.displayName = 'Shell.Header';
442
+
443
+ // Global Footer
444
+ /** Props for `Shell.Footer`. Rendered after body and outside the content scroll. */
445
+ interface ShellFooterProps extends React.ComponentPropsWithoutRef<'footer'> {}
446
+ const Footer = React.forwardRef<HTMLElement, ShellFooterProps>(({ className, ...props }, ref) => (
447
+ <footer
448
+ {...props}
449
+ ref={ref}
450
+ role="contentinfo"
451
+ className={classNames('rt-ShellFooter', className)}
452
+ />
453
+ ));
454
+ Footer.displayName = 'Shell.Footer';
455
+
456
+ // Content
457
+ /** Props for `Shell.Content`. The only scrollable area of the Shell. */
458
+ interface ShellContentProps extends React.ComponentPropsWithoutRef<'main'> {}
459
+ const ContentBase = React.forwardRef<HTMLElement, ShellContentProps>(
460
+ ({ className, ...props }, ref) => (
461
+ <main {...props} ref={ref} className={classNames('rt-ShellContent', className)} />
462
+ ),
463
+ );
464
+ ContentBase.displayName = 'Shell.Content';
465
+
466
+ const Content = ContentBase;
467
+
468
+ // Sidebar (stateful owner)
469
+ /**
470
+ * `Shell.Sidebar` controls one logical side. It supports two patterns:
471
+ * - Split pattern by providing both `Sidebar.Rail` and `Sidebar.Panel` children
472
+ * - Single-markup morphing when no slots are provided
473
+ *
474
+ * Controlled/uncontrolled:
475
+ * - Split: `value`/`defaultValue` reflect rail `open|collapsed`
476
+ * - Single: `view`/`defaultView` reflect `panel|rail|collapsed`
477
+ */
478
+ interface ShellSidebarProps extends React.ComponentPropsWithoutRef<'div'> {
479
+ side: ShellSide;
480
+ // Overlay: render as a top sheet rather than inline. Responsiveness handled separately.
481
+ overlay?: boolean | Partial<Record<'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl', boolean>>;
482
+ overlaySide?: 'start' | 'end' | 'top' | 'bottom';
483
+ // Split: rail control
484
+ value?: RailValue;
485
+ defaultValue?: RailValue;
486
+ onValueChange?: (value: RailValue) => void;
487
+ // Single-markup view control
488
+ view?: SingleView;
489
+ defaultView?: SingleView;
490
+ onViewChange?: (view: SingleView) => void;
491
+ as?: 'nav' | 'aside' | 'div';
492
+ 'aria-label'?: string;
493
+ }
494
+
495
+ // Rail
496
+ /** Rail component that provides event emission context for stateless navigation. */
497
+ interface RailProps extends React.ComponentPropsWithoutRef<'div'> {}
498
+ const Rail = Object.assign(
499
+ React.forwardRef<HTMLDivElement, RailProps>(({ children }, _ref) => {
500
+ const sidebarSection = useSidebarSection();
501
+ const shell = useShell();
502
+ const side = sidebarSection?.side ?? 'start';
503
+
504
+ const railContext = React.useMemo<RailContextValue>(
505
+ () => ({
506
+ onItemSelected: (item: string) => shell.onItemSelected(side, item),
507
+ }),
508
+ [shell, side],
509
+ );
510
+
511
+ return <RailContext.Provider value={railContext}>{children}</RailContext.Provider>;
512
+ }),
513
+ { displayName: 'Shell.Sidebar.Rail', __shellSlot: 'rail' as const },
514
+ );
515
+
516
+ // Panel
517
+ /** Panel component that provides active tool context for stateless content rendering. */
518
+ interface PanelProps extends React.ComponentPropsWithoutRef<'div'> {}
519
+ const Panel = Object.assign(
520
+ React.forwardRef<HTMLDivElement, PanelProps>(({ children }, _ref) => {
521
+ const sidebarSection = useSidebarSection();
522
+ const shell = useShell();
523
+ const side = sidebarSection?.side ?? 'start';
524
+
525
+ const panelContext = React.useMemo<PanelContextValue>(
526
+ () => ({
527
+ activeTool: shell.activeToolBySide[side],
528
+ activeContext: shell.activeContextBySide[side],
529
+ }),
530
+ [shell.activeToolBySide, shell.activeContextBySide, side],
531
+ );
532
+
533
+ return <PanelContext.Provider value={panelContext}>{children}</PanelContext.Provider>;
534
+ }),
535
+ { displayName: 'Shell.Sidebar.Panel', __shellSlot: 'panel' as const },
536
+ );
537
+
538
+ type ShellSidebarComponent = React.ForwardRefExoticComponent<
539
+ ShellSidebarProps & React.RefAttributes<HTMLDivElement>
540
+ > & {
541
+ Rail: typeof Rail;
542
+ Panel: typeof Panel;
543
+ Trigger: typeof LocalTrigger;
544
+ };
545
+
546
+ const SidebarInner = (
547
+ {
548
+ side,
549
+ overlay,
550
+ overlaySide,
551
+ value,
552
+ defaultValue,
553
+ onValueChange,
554
+ view,
555
+ defaultView,
556
+ onViewChange,
557
+ as = 'nav',
558
+ className,
559
+ style,
560
+ children,
561
+ ...props
562
+ }: ShellSidebarProps,
563
+ ref: React.ForwardedRef<HTMLDivElement>,
564
+ ) => {
565
+ const shell = useShell();
566
+ const Comp = as as any;
567
+
568
+ const regionId = shell.getRegionId(side);
569
+ const panelId = shell.getPanelId(side);
570
+ const railId = shell.getRailId(side);
571
+
572
+ const railChildren = childrenArrayOf(children, 'rail');
573
+ const panelChildren = childrenArrayOf(children, 'panel');
574
+ const hasRail = railChildren.length > 0;
575
+ const hasPanel = panelChildren.length > 0;
576
+ const hasSlots = hasRail || hasPanel;
577
+
578
+ // Pattern registration per side
579
+ React.useEffect(() => {
580
+ shell.setPatternForSide(side, hasSlots && hasRail && hasPanel ? 'split' : 'single');
581
+ }, [shell, side, hasSlots, hasRail, hasPanel]);
582
+
583
+ // Initialize defaults (run once)
584
+ const didInitRef = React.useRef(false);
585
+ React.useEffect(() => {
586
+ if (didInitRef.current) return;
587
+ didInitRef.current = true;
588
+ if (hasSlots) {
589
+ // split: rail value
590
+ const initial = value ?? defaultValue ?? (overlay ? 'collapsed' : 'open');
591
+ shell.setRailBySide(side, initial);
592
+ if (overlay) shell.setPanelRequestedBySide(side, false);
593
+ } else {
594
+ // single: view
595
+ const initialView = view ?? defaultView ?? (overlay ? 'collapsed' : 'panel');
596
+ shell.setSingleViewBySide(side, initialView);
597
+ }
598
+ }, [hasSlots, value, defaultValue, view, defaultView, shell, side, overlay]);
599
+
600
+ // Keep context in sync for controlled
601
+ React.useEffect(() => {
602
+ if (!hasSlots) return;
603
+ if (value !== undefined && shell.railBySide[side] !== value) {
604
+ shell.setRailBySide(side, value);
605
+ }
606
+ }, [value, hasSlots, shell, side]);
607
+ React.useEffect(() => {
608
+ if (hasSlots) return;
609
+ if (view !== undefined && shell.singleViewBySide[side] !== view) {
610
+ shell.setSingleViewBySide(side, view);
611
+ }
612
+ }, [view, hasSlots, shell, side]);
613
+
614
+ const railValue = shell.railBySide[side];
615
+ const panelRequested = shell.panelRequestedBySide[side];
616
+ const singleView = shell.singleViewBySide[side];
617
+ const activeTool = shell.activeToolBySide[side];
618
+
619
+ // Emit changes for uncontrolled
620
+ const prevRailRef = React.useRef<RailValue | null>(null);
621
+ React.useEffect(() => {
622
+ if (!hasSlots) return;
623
+ if (value !== undefined) return;
624
+ if (prevRailRef.current !== railValue) {
625
+ prevRailRef.current = railValue;
626
+ onValueChange?.(railValue);
627
+ }
628
+ }, [hasSlots, railValue, value, onValueChange]);
629
+ const prevViewRef = React.useRef<SingleView | null>(null);
630
+ React.useEffect(() => {
631
+ if (hasSlots) return;
632
+ if (view !== undefined) return;
633
+ if (prevViewRef.current !== singleView) {
634
+ prevViewRef.current = singleView;
635
+ onViewChange?.(singleView);
636
+ }
637
+ }, [hasSlots, singleView, view, onViewChange]);
638
+
639
+ // Derived visibility:
640
+ // - Split (rail+panel): panel shows when rail is open, panel is requested, and a tool is active
641
+ // - Panel-only (no rail): panel shows unconditionally (no tool gating)
642
+ // - Single-markup: shows when view === 'panel' and a tool is active
643
+ const railVisible = hasSlots ? hasRail && railValue === 'open' : singleView === 'rail';
644
+ const panelVisible = hasSlots
645
+ ? hasPanel && (hasRail ? railValue === 'open' && panelRequested && activeTool !== null : true)
646
+ : singleView === 'panel';
647
+
648
+ // Overlay behavior (non-inline): mount as a Sheet from the top (default)
649
+ // Resolve overlay responsively: built-in defaults overruled by prop
650
+ // Defaults: overlay on initial/xs/sm; inline on md+
651
+ const defaultOverlay = { initial: true, xs: true, sm: true, md: false, lg: false, xl: false };
652
+ const mergedOverlay = React.useMemo(() => {
653
+ if (typeof overlay === 'boolean') {
654
+ return { initial: overlay } as Partial<
655
+ Record<'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl', boolean>
656
+ >;
657
+ }
658
+ return { ...defaultOverlay, ...(overlay || {}) } as Partial<
659
+ Record<'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl', boolean>
660
+ >;
661
+ }, [overlay, defaultOverlay]);
662
+
663
+ const [currentBp, setCurrentBp] = React.useState<'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'>(
664
+ 'initial',
665
+ );
666
+
667
+ React.useEffect(() => {
668
+ if (typeof window === 'undefined') return;
669
+ const queries: [key: 'xs' | 'sm' | 'md' | 'lg' | 'xl', query: string][] = [
670
+ ['xs', '(min-width: 520px)'],
671
+ ['sm', '(min-width: 768px)'],
672
+ ['md', '(min-width: 1024px)'],
673
+ ['lg', '(min-width: 1280px)'],
674
+ ['xl', '(min-width: 1640px)'],
675
+ ];
676
+ const mqls = queries.map(([k, q]) => [k, window.matchMedia(q)] as const);
677
+ const compute = () => {
678
+ // Highest matched wins
679
+ const matched = mqls.filter(([, m]) => m.matches).map(([k]) => k);
680
+ const next = (matched[matched.length - 1] as typeof currentBp | undefined) ?? 'initial';
681
+ setCurrentBp(next);
682
+ };
683
+ compute();
684
+ mqls.forEach(([, m]) => m.addEventListener('change', compute));
685
+ return () => {
686
+ mqls.forEach(([, m]) => m.removeEventListener('change', compute));
687
+ };
688
+ }, []);
689
+
690
+ const isOverlay = (() => {
691
+ const val = mergedOverlay[currentBp];
692
+ if (typeof val === 'boolean') return val;
693
+ // Fallback cascade: if current not set, try smaller breakpoints, then initial
694
+ const order: (typeof currentBp)[] = ['xl', 'lg', 'md', 'sm', 'xs', 'initial'];
695
+ const idx = order.indexOf(currentBp);
696
+ for (let i = idx + 1; i < order.length; i++) {
697
+ const v = mergedOverlay[order[i]];
698
+ if (typeof v === 'boolean') return v;
699
+ }
700
+ return mergedOverlay.initial ?? false;
701
+ })();
702
+
703
+ if (isOverlay) {
704
+ const open = hasSlots ? railValue === 'open' || panelRequested : singleView !== 'collapsed';
705
+ const onOpenChange = (next: boolean) => {
706
+ if (hasSlots) {
707
+ if (!next) {
708
+ shell.setRailBySide(side, 'collapsed');
709
+ shell.setPanelRequestedBySide(side, false);
710
+ } else {
711
+ shell.setRailBySide(side, 'open');
712
+ }
713
+ } else {
714
+ if (!next) shell.setSingleViewBySide(side, 'collapsed');
715
+ else shell.setSingleViewBySide(side, 'panel');
716
+ }
717
+ };
718
+
719
+ const sheetSide = overlaySide ?? side;
720
+
721
+ // Choose what to render in overlay
722
+ // Split pattern: default to combined (rail + panel). Panel visibility follows panel intent and active tool.
723
+ // Single-markup: render the provided children as-is.
724
+ const overlayPanelVisible = hasSlots
725
+ ? hasPanel && (hasRail ? panelRequested && activeTool !== null : true)
726
+ : false;
727
+ const overlayContent = hasSlots ? (
728
+ <div style={{ display: 'flex', height: '100%', minBlockSize: 0 }}>
729
+ {hasRail ? (
730
+ <div
731
+ id={railId}
732
+ className="rt-ShellSidebarRail"
733
+ data-section="rail"
734
+ data-visible
735
+ style={{ inlineSize: 'var(--shell-sidebar-rail-width, 64px)', overflow: 'hidden' }}
736
+ >
737
+ {railChildren}
738
+ </div>
739
+ ) : null}
740
+ {hasPanel ? (
741
+ <div
742
+ id={panelId}
743
+ className="rt-ShellSidebarPanel"
744
+ data-section="panel"
745
+ data-visible={overlayPanelVisible || undefined}
746
+ aria-hidden={overlayPanelVisible ? undefined : true}
747
+ {...(overlayPanelVisible ? {} : { inert })}
748
+ style={{
749
+ inlineSize: overlayPanelVisible ? 'var(--shell-sidebar-panel-width, 288px)' : '0px',
750
+ overflow: 'hidden',
751
+ }}
752
+ >
753
+ {panelChildren}
754
+ </div>
755
+ ) : null}
756
+ </div>
757
+ ) : (
758
+ children
759
+ );
760
+
761
+ // Compute sheet width based on split/single pattern using CSS tokens
762
+ const computedWidth =
763
+ sheetSide === 'start' || sheetSide === 'end'
764
+ ? hasSlots
765
+ ? overlayPanelVisible
766
+ ? 'var(--shell-sidebar-combined-width)'
767
+ : 'var(--shell-sidebar-rail-width)'
768
+ : singleView === 'rail'
769
+ ? 'var(--shell-sidebar-rail-width)'
770
+ : 'var(--shell-sidebar-panel-width)'
771
+ : '100vw';
772
+
773
+ return (
774
+ <>
775
+ <Sheet.Root open={open} onOpenChange={onOpenChange}>
776
+ <Sheet.Content
777
+ id={regionId}
778
+ side={sheetSide}
779
+ height={{ initial: '100vh' }}
780
+ width={{ initial: computedWidth }}
781
+ style={{ ['--dialog-content-padding' as any]: '0px' }}
782
+ >
783
+ <Sheet.Title>
784
+ <VisuallyHidden>Sidebar</VisuallyHidden>
785
+ </Sheet.Title>
786
+ {overlayContent}
787
+ </Sheet.Content>
788
+ </Sheet.Root>
789
+ </>
790
+ );
791
+ }
792
+
793
+ return (
794
+ <Comp
795
+ {...props}
796
+ ref={ref}
797
+ id={regionId}
798
+ className={classNames('rt-ShellSidebar', className)}
799
+ data-side={side}
800
+ data-rail={hasSlots ? railValue : undefined}
801
+ data-panel={hasSlots ? (panelVisible ? 'visible' : 'hidden') : undefined}
802
+ data-state={!hasSlots ? singleView : undefined}
803
+ aria-label={props['aria-label']}
804
+ style={{ ...style }}
805
+ >
806
+ {hasSlots ? (
807
+ <>
808
+ <SidebarSectionContext.Provider value={{ side, section: 'rail' }}>
809
+ {hasRail ? (
810
+ <div
811
+ id={railId}
812
+ className="rt-ShellSidebarRail"
813
+ data-section="rail"
814
+ data-visible={railVisible || undefined}
815
+ aria-hidden={railVisible ? undefined : true}
816
+ {...(railVisible ? {} : { inert })}
817
+ style={{
818
+ inlineSize: railVisible ? 'var(--shell-sidebar-rail-width, 64px)' : '0px',
819
+ overflow: 'hidden',
820
+ }}
821
+ >
822
+ {railChildren}
823
+ </div>
824
+ ) : null}
825
+ </SidebarSectionContext.Provider>
826
+ <SidebarSectionContext.Provider value={{ side, section: 'panel' }}>
827
+ {hasPanel ? (
828
+ <div
829
+ id={panelId}
830
+ className="rt-ShellSidebarPanel"
831
+ data-section="panel"
832
+ data-visible={panelVisible || undefined}
833
+ aria-hidden={panelVisible ? undefined : true}
834
+ {...(panelVisible ? {} : { inert })}
835
+ style={{
836
+ inlineSize: panelVisible ? 'var(--shell-sidebar-panel-width, 288px)' : '0px',
837
+ overflow: 'hidden',
838
+ }}
839
+ >
840
+ {panelChildren}
841
+ </div>
842
+ ) : null}
843
+ </SidebarSectionContext.Provider>
844
+ </>
845
+ ) : (
846
+ // Single-markup morphing
847
+ <div
848
+ className="rt-ShellSidebarSingle"
849
+ data-visible={singleView !== 'collapsed' || undefined}
850
+ aria-hidden={singleView === 'collapsed' ? true : undefined}
851
+ style={{
852
+ inlineSize:
853
+ singleView === 'collapsed'
854
+ ? '0px'
855
+ : singleView === 'rail'
856
+ ? 'var(--shell-sidebar-rail-width, 64px)'
857
+ : 'var(--shell-sidebar-panel-width, 288px)',
858
+ overflow: 'hidden',
859
+ }}
860
+ >
861
+ {children}
862
+ </div>
863
+ )}
864
+ </Comp>
865
+ );
866
+ };
867
+
868
+ const Sidebar = React.forwardRef<HTMLDivElement, ShellSidebarProps>(
869
+ SidebarInner,
870
+ ) as ShellSidebarComponent;
871
+ Sidebar.displayName = 'Shell.Sidebar';
872
+
873
+ // Helper to pick rail/panel children by marker components
874
+ function childrenArrayOf(children: React.ReactNode, slot: 'rail' | 'panel') {
875
+ const arr = React.Children.toArray(children) as React.ReactElement[];
876
+ return arr.filter((el) => React.isValidElement(el) && (el.type as any)?.['__shellSlot'] === slot);
877
+ }
878
+
879
+ Sidebar.Rail = Rail;
880
+ Sidebar.Panel = Panel;
881
+
882
+ // Local Trigger (inside a sidebar)
883
+ import type { IconButtonProps } from './icon-button.js';
884
+ type LocalTriggerProps = IconButtonProps<'button'>;
885
+ /**
886
+ * `Shell.Sidebar.Trigger` toggles the sidebar where it is rendered.
887
+ * - In split-mode: toggles rail open/collapsed
888
+ * - In single-markup: cycles through panel → rail → collapsed
889
+ */
890
+ const LocalTrigger = React.forwardRef<React.ElementRef<typeof IconButton>, LocalTriggerProps>(
891
+ ({ onClick, children, ...props }, ref) => {
892
+ const section = useSidebarSection();
893
+ const shell = useShell();
894
+ const side = section?.side ?? 'start';
895
+ const controlsId = shell.getRegionId(side);
896
+ const expanded =
897
+ shell.patternBySide[side] === 'split'
898
+ ? shell.railBySide[side] === 'open'
899
+ : shell.singleViewBySide[side] !== 'collapsed';
900
+ return (
901
+ <IconButton
902
+ {...props}
903
+ ref={ref}
904
+ variant="soft"
905
+ aria-controls={controlsId}
906
+ aria-expanded={expanded}
907
+ onClick={(e) => {
908
+ onClick?.(e);
909
+ if (shell.patternBySide[side] === 'split') shell.toggleRail(side);
910
+ else shell.cycleSingleView(side);
911
+ }}
912
+ >
913
+ {children || <ChevronDownIcon />}
914
+ </IconButton>
915
+ );
916
+ },
917
+ );
918
+ LocalTrigger.displayName = 'Shell.Sidebar.Trigger';
919
+
920
+ // Global Trigger
921
+ type GlobalTriggerProps = IconButtonProps<'button'> & { side: ShellSide };
922
+ /**
923
+ * `Shell.Trigger` controls a specific `side` from anywhere inside `Shell.Root`.
924
+ * Mirrors behavior of the local trigger, but with explicit side.
925
+ */
926
+ const Trigger = React.forwardRef<React.ElementRef<typeof IconButton>, GlobalTriggerProps>(
927
+ ({ side, onClick, children, ...props }, ref) => {
928
+ const shell = useShell();
929
+ const controlsId = shell.getRegionId(side);
930
+ const expanded =
931
+ shell.patternBySide[side] === 'split'
932
+ ? shell.railBySide[side] === 'open'
933
+ : shell.singleViewBySide[side] !== 'collapsed';
934
+ return (
935
+ <IconButton
936
+ {...props}
937
+ ref={ref}
938
+ variant="soft"
939
+ aria-controls={controlsId}
940
+ aria-expanded={expanded}
941
+ onClick={(e) => {
942
+ onClick?.(e);
943
+ if (shell.patternBySide[side] === 'split') shell.toggleRail(side);
944
+ else shell.cycleSingleView(side);
945
+ }}
946
+ >
947
+ {children || <ChevronDownIcon />}
948
+ </IconButton>
949
+ );
950
+ },
951
+ );
952
+ Trigger.displayName = 'Shell.Trigger';
953
+
954
+ // Attach slots to Sidebar for namespaced API
955
+ (Sidebar as any).Rail = Rail;
956
+ (Sidebar as any).Panel = Panel;
957
+ (Sidebar as any).Trigger = LocalTrigger;
958
+
959
+ export { Root, Header, Footer, Content, Sidebar, Trigger, useShell, useRailEvents, usePanelState };
960
+ // Convenience per-side API
961
+ /**
962
+ * Convenience hook to interrogate and control one sidebar.
963
+ * - `rail`: open/collapsed helpers for split pattern
964
+ * - `panel`: show/hide helpers; visibility is conditional on rail open in split pattern
965
+ * - `single`: view control for single-markup pattern
966
+ * - `activeTool`: active tool coordination state
967
+ */
968
+ function useSidebar(side: ShellSide): {
969
+ side: ShellSide;
970
+ isSplit: boolean;
971
+ rail: {
972
+ value: RailValue;
973
+ isOpen: boolean;
974
+ open: () => void;
975
+ close: () => void;
976
+ toggle: () => void;
977
+ onItemSelected: (item: string) => void;
978
+ };
979
+ panel: {
980
+ isVisible: boolean;
981
+ show: () => void;
982
+ hide: () => void;
983
+ activeTool: string | null;
984
+ activeContext: string | null;
985
+ };
986
+ single: { view: SingleView; setView: (view: SingleView) => void; cycle: () => void };
987
+ activeTool: string | null;
988
+ setActiveTool: (tool: string | null) => void;
989
+ activeContext: string | null;
990
+ setActiveContext: (context: string | null) => void;
991
+ } {
992
+ const shell = useShell();
993
+ const isSplit = shell.patternBySide[side] === 'split';
994
+ const railValue = shell.railBySide[side];
995
+ const activeTool = shell.activeToolBySide[side];
996
+
997
+ const panelVisible =
998
+ shell.panelRequestedBySide[side] && railValue === 'open' && activeTool !== null;
999
+ return {
1000
+ side,
1001
+ isSplit,
1002
+ rail: {
1003
+ value: railValue,
1004
+ isOpen: railValue === 'open',
1005
+ open: () => shell.setRailBySide(side, 'open'),
1006
+ close: () => {
1007
+ shell.setRailBySide(side, 'collapsed');
1008
+ shell.setPanelRequestedBySide(side, false);
1009
+ },
1010
+ toggle: () => shell.toggleRail(side),
1011
+ onItemSelected: (item: string) => shell.onItemSelected(side, item),
1012
+ },
1013
+ panel: {
1014
+ isVisible: panelVisible,
1015
+ show: () => shell.setPanelRequestedBySide(side, true),
1016
+ hide: () => shell.setPanelRequestedBySide(side, false),
1017
+ activeTool: shell.activeToolBySide[side],
1018
+ activeContext: shell.activeContextBySide[side],
1019
+ },
1020
+ single: {
1021
+ view: shell.singleViewBySide[side],
1022
+ setView: (view: SingleView) => shell.setSingleViewBySide(side, view),
1023
+ cycle: () => shell.cycleSingleView(side),
1024
+ },
1025
+ activeTool: shell.activeToolBySide[side],
1026
+ setActiveTool: (tool: string | null) => shell.setActiveTool(side, tool),
1027
+ activeContext: shell.activeContextBySide[side],
1028
+ setActiveContext: (context: string | null) => shell.setActiveContext(side, context),
1029
+ };
1030
+ }
1031
+
1032
+ export { useSidebar };