@marktiderman/genesis-ui 1.1.1 → 1.3.0

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 (65) hide show
  1. package/dist/{chunk-DGRHPJ3M.js → chunk-44TULYGG.js} +3 -3
  2. package/dist/{chunk-DGRHPJ3M.js.map → chunk-44TULYGG.js.map} +1 -1
  3. package/dist/{chunk-AI6CCNWK.js → chunk-6NPEMLBA.js} +2 -2
  4. package/dist/chunk-6NPEMLBA.js.map +1 -0
  5. package/dist/{chunk-UKSBLFEK.js → chunk-6VVVGYKS.js} +4 -4
  6. package/dist/{chunk-UKSBLFEK.js.map → chunk-6VVVGYKS.js.map} +1 -1
  7. package/dist/chunk-A2OXCNNY.js +65 -0
  8. package/dist/chunk-A2OXCNNY.js.map +1 -0
  9. package/dist/{chunk-O4B5ALO7.js → chunk-AUECDWP2.js} +3 -3
  10. package/dist/{chunk-O4B5ALO7.js.map → chunk-AUECDWP2.js.map} +1 -1
  11. package/dist/{chunk-FCGFG4RV.js → chunk-D4JTQYP5.js} +3 -3
  12. package/dist/{chunk-FCGFG4RV.js.map → chunk-D4JTQYP5.js.map} +1 -1
  13. package/dist/chunk-GCQFVMPI.js +56 -0
  14. package/dist/chunk-GCQFVMPI.js.map +1 -0
  15. package/dist/{chunk-X7KMF6NH.js → chunk-IP6TMT3D.js} +3 -3
  16. package/dist/{chunk-X7KMF6NH.js.map → chunk-IP6TMT3D.js.map} +1 -1
  17. package/dist/{chunk-LDPPIBIJ.js → chunk-KB5T5LWM.js} +3 -3
  18. package/dist/{chunk-LDPPIBIJ.js.map → chunk-KB5T5LWM.js.map} +1 -1
  19. package/dist/{chunk-MUQLZFUR.js → chunk-NPNADZHM.js} +11 -5
  20. package/dist/chunk-NPNADZHM.js.map +1 -0
  21. package/dist/{chunk-6V4GJUE3.js → chunk-QSXVL5UB.js} +4 -3
  22. package/dist/chunk-QSXVL5UB.js.map +1 -0
  23. package/dist/{chunk-AIODTBWP.js → chunk-T5VX5DYO.js} +3 -2
  24. package/dist/chunk-T5VX5DYO.js.map +1 -0
  25. package/dist/chunk-YETMISCE.js +23 -0
  26. package/dist/chunk-YETMISCE.js.map +1 -0
  27. package/dist/{chunk-I6JQHP6D.js → chunk-ZJFSZFPI.js} +4 -2
  28. package/dist/chunk-ZJFSZFPI.js.map +1 -0
  29. package/dist/components/data/index.d.ts +186 -20
  30. package/dist/components/data/index.js +1165 -181
  31. package/dist/components/data/index.js.map +1 -1
  32. package/dist/components/ui/badge.js +1 -1
  33. package/dist/components/ui/button.d.ts +3 -1
  34. package/dist/components/ui/button.js +1 -1
  35. package/dist/components/ui/calendar.js +2 -2
  36. package/dist/components/ui/carousel.js +2 -2
  37. package/dist/components/ui/command-palette.d.ts +30 -0
  38. package/dist/components/ui/command-palette.js +6 -0
  39. package/dist/components/ui/command-palette.js.map +1 -0
  40. package/dist/components/ui/date-picker.js +3 -3
  41. package/dist/components/ui/form-field.js +2 -2
  42. package/dist/components/ui/input.js +1 -1
  43. package/dist/components/ui/label.js +1 -1
  44. package/dist/components/ui/pagination.js +2 -2
  45. package/dist/components/ui/textarea.js +1 -1
  46. package/dist/hooks/index.d.ts +4 -2
  47. package/dist/hooks/index.js +8 -3
  48. package/dist/hooks/use-data-filters.d.ts +1 -1
  49. package/dist/hooks/use-data-filters.js +2 -2
  50. package/dist/hooks/use-resource-page.d.ts +2 -1
  51. package/dist/hooks/use-saved-views.d.ts +28 -0
  52. package/dist/hooks/use-saved-views.js +3 -0
  53. package/dist/hooks/use-saved-views.js.map +1 -0
  54. package/dist/index.d.ts +2 -1
  55. package/dist/index.js +24 -23
  56. package/dist/{navigation-qI86Ldug.d.ts → navigation-CpOEyUSb.d.ts} +4 -0
  57. package/dist/{use-resource-page-B-CFcikK.d.ts → use-resource-page-BXBv_Xah.d.ts} +9 -1
  58. package/package.json +5 -1
  59. package/dist/chunk-6V4GJUE3.js.map +0 -1
  60. package/dist/chunk-AI6CCNWK.js.map +0 -1
  61. package/dist/chunk-AIODTBWP.js.map +0 -1
  62. package/dist/chunk-I6JQHP6D.js.map +0 -1
  63. package/dist/chunk-MUQLZFUR.js.map +0 -1
  64. package/dist/chunk-W64GERJW.js +0 -22
  65. package/dist/chunk-W64GERJW.js.map +0 -1
@@ -2,32 +2,36 @@
2
2
  import { Spinner } from '../../chunk-OVU3Z2MT.js';
3
3
  import { Switch } from '../../chunk-2MXMWBDV.js';
4
4
  import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '../../chunk-N2ZFNOOT.js';
5
- import { Textarea } from '../../chunk-W64GERJW.js';
5
+ import { Textarea } from '../../chunk-YETMISCE.js';
6
+ import { Progress } from '../../chunk-W5Y3LG5K.js';
6
7
  import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../../chunk-2B3MFHBQ.js';
7
8
  import { Separator } from '../../chunk-SXSCHWZU.js';
8
9
  import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '../../chunk-SCUT2ZCD.js';
9
10
  import { Skeleton } from '../../chunk-QFM4Z2SS.js';
10
- import { Progress } from '../../chunk-W5Y3LG5K.js';
11
+ import { Input } from '../../chunk-QSXVL5UB.js';
12
+ import { Popover, PopoverTrigger, PopoverContent } from '../../chunk-NJMO3VPA.js';
11
13
  import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuSeparator, DropdownMenuItem } from '../../chunk-SBJV3K4G.js';
12
- import { FormField, FormLabel, FormError, FormDescription } from '../../chunk-X7KMF6NH.js';
13
- import '../../chunk-AIODTBWP.js';
14
- import { Input } from '../../chunk-6V4GJUE3.js';
14
+ import { FormField, FormLabel, FormError, FormDescription } from '../../chunk-IP6TMT3D.js';
15
+ import { Label } from '../../chunk-T5VX5DYO.js';
16
+ import { Card, CardContent, CardHeader, CardTitle } from '../../chunk-FN3FZVUR.js';
15
17
  import { Checkbox } from '../../chunk-WZWR2ELB.js';
18
+ import { Collapsible, CollapsibleContent } from '../../chunk-KBS4BHME.js';
19
+ import { Badge } from '../../chunk-NPNADZHM.js';
20
+ import { Button } from '../../chunk-ZJFSZFPI.js';
21
+ import { titleCase, useResourcePage } from '../../chunk-QLCDAFS7.js';
22
+ export { useSavedViews } from '../../chunk-GCQFVMPI.js';
23
+ import '../../chunk-XISSLIMH.js';
24
+ import { useViewSettings } from '../../chunk-OZ7ZAYCS.js';
25
+ export { CommandPalette, useCommandPaletteHotkey } from '../../chunk-A2OXCNNY.js';
16
26
  import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator } from '../../chunk-7OBL3H5P.js';
17
- import { Popover, PopoverTrigger, PopoverContent } from '../../chunk-NJMO3VPA.js';
18
27
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '../../chunk-YZEA5TGK.js';
19
- import { Badge } from '../../chunk-MUQLZFUR.js';
20
- import { Button } from '../../chunk-I6JQHP6D.js';
21
- import { Card, CardContent, CardHeader, CardTitle } from '../../chunk-FN3FZVUR.js';
22
28
  import { cn } from '../../chunk-NZ5WBA4R.js';
23
- import { titleCase, useResourcePage } from '../../chunk-QLCDAFS7.js';
24
- import '../../chunk-XISSLIMH.js';
25
- import { DefaultLink, defaultNavigate, isPathActive } from '../../chunk-AI6CCNWK.js';
26
- export { DefaultLink, defaultNavigate, isPathActive } from '../../chunk-AI6CCNWK.js';
29
+ import { DefaultLink, defaultNavigate, isPathActive } from '../../chunk-6NPEMLBA.js';
30
+ export { DefaultLink, defaultNavigate, isPathActive } from '../../chunk-6NPEMLBA.js';
27
31
  import { useIsMobile } from '../../chunk-M7AU5DDM.js';
28
- import { Search, LayoutGrid, Table2, List, Settings2, AlertTriangle, RefreshCw, ChevronDown, Check, X, ArrowUpDown, Bookmark, Trash2, BookmarkPlus, Loader2, Columns3, ArrowUp, ArrowDown, ChevronLeft, ChevronRight, ExternalLink, Pencil, ChevronsUpDown, ImageIcon, Upload, AlertCircle, FileIcon, Menu, Sun, Moon, Plus } from 'lucide-react';
32
+ import { Search, LayoutGrid, Table2, List, Settings2, ListFilter, SlidersHorizontal, AlertTriangle, RefreshCw, ChevronDown, Check, X, ArrowUpDown, Bookmark, Trash2, BookmarkPlus, Loader2, Columns3, ArrowUp, ArrowDown, ChevronLeft, ChevronRight, ExternalLink, Pencil, ChevronsUpDown, ImageIcon, Upload, AlertCircle, FileIcon, Menu, PanelLeft, PanelLeftClose, Sun, Moon, Plus } from 'lucide-react';
29
33
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
30
- import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
34
+ import { useId, useState, useEffect, useMemo, useCallback, useRef } from 'react';
31
35
  import { formatDistanceToNow, format } from 'date-fns';
32
36
  import { useFieldArray, Controller, useWatch } from 'react-hook-form';
33
37
  import { useResource, useWizard, useResourceForm, buildArrayItemDefaults } from '@marktiderman/genesis-core/hooks';
@@ -143,9 +147,330 @@ function ViewSettings({
143
147
  ] })
144
148
  ] });
145
149
  }
146
- function DataPageShell({
150
+
151
+ // src/option-storage.ts
152
+ function readStoredOptions(key) {
153
+ if (typeof window === "undefined") return {};
154
+ try {
155
+ const raw = localStorage.getItem(key);
156
+ if (!raw) return {};
157
+ const parsed = JSON.parse(raw);
158
+ if (!parsed || typeof parsed !== "object") return {};
159
+ return parsed;
160
+ } catch {
161
+ return {};
162
+ }
163
+ }
164
+ function writeStoredOptions(key, value) {
165
+ if (typeof window === "undefined") return;
166
+ try {
167
+ localStorage.setItem(key, JSON.stringify(value));
168
+ } catch {
169
+ }
170
+ }
171
+ function pluralizeEntity(entity, count) {
172
+ if (count === 1) return entity;
173
+ return entity.endsWith("s") ? entity : `${entity}s`;
174
+ }
175
+ var OPTION_DEFAULTS = {
176
+ showIcon: true,
177
+ showSubtitle: true,
178
+ showCount: true,
179
+ showViewToggle: true,
180
+ showViewSettings: true,
181
+ showCreate: true,
182
+ showDetailsToggle: true,
183
+ showDetailsHint: true,
184
+ subtitleInDetails: void 0,
185
+ defaultDetailsOpen: false,
186
+ detailsLabel: "Details",
187
+ configurable: void 0
188
+ };
189
+ var BOOLEAN_OPTION_KEYS = [
190
+ "showIcon",
191
+ "showSubtitle",
192
+ "showCount",
193
+ "showViewToggle",
194
+ "showViewSettings",
195
+ "showCreate",
196
+ "showDetailsToggle",
197
+ "showDetailsHint",
198
+ "subtitleInDetails",
199
+ "defaultDetailsOpen",
200
+ "configurable"
201
+ ];
202
+ var STRING_OPTION_KEYS = ["detailsLabel"];
203
+ function readStoredOptions2(key) {
204
+ const raw = readStoredOptions(key);
205
+ const sanitized = {};
206
+ for (const optKey of BOOLEAN_OPTION_KEYS) {
207
+ const value = raw[optKey];
208
+ if (typeof value === "boolean") {
209
+ sanitized[optKey] = value;
210
+ }
211
+ }
212
+ for (const optKey of STRING_OPTION_KEYS) {
213
+ const value = raw[optKey];
214
+ if (typeof value === "string") {
215
+ sanitized[optKey] = value;
216
+ }
217
+ }
218
+ return sanitized;
219
+ }
220
+ function PageHeader({
147
221
  title,
222
+ subtitle,
148
223
  icon: Icon,
224
+ options: optionsProp,
225
+ optionsStorageKey,
226
+ actions,
227
+ details,
228
+ detailsOpen,
229
+ onDetailsOpenChange,
230
+ detailsActive = false,
231
+ detailsHint,
232
+ count,
233
+ totalCount,
234
+ entityName,
235
+ countActive = false,
236
+ viewMode,
237
+ onViewModeChange,
238
+ pageSize,
239
+ onPageSizeChange,
240
+ density,
241
+ onDensityChange,
242
+ createAction
243
+ }) {
244
+ const detailsRegionId = useId();
245
+ const hasDetails = details != null;
246
+ const hasCount = count !== void 0 && totalCount !== void 0;
247
+ const hasViewToggle = Boolean(viewMode && onViewModeChange);
248
+ const hasViewSettings = Boolean(onPageSizeChange && onDensityChange);
249
+ const hasCreate = createAction != null;
250
+ const [userOverrides, setUserOverrides] = useState(
251
+ {}
252
+ );
253
+ const [hydratedKey, setHydratedKey] = useState(
254
+ optionsStorageKey ? void 0 : ""
255
+ );
256
+ useEffect(() => {
257
+ if (!optionsStorageKey) {
258
+ setUserOverrides({});
259
+ setHydratedKey("");
260
+ return;
261
+ }
262
+ setUserOverrides(readStoredOptions2(optionsStorageKey));
263
+ setHydratedKey(optionsStorageKey);
264
+ }, [optionsStorageKey]);
265
+ useEffect(() => {
266
+ if (!optionsStorageKey || hydratedKey !== optionsStorageKey) return;
267
+ writeStoredOptions(optionsStorageKey, userOverrides);
268
+ }, [optionsStorageKey, userOverrides, hydratedKey]);
269
+ const resolved = useMemo(() => {
270
+ const merged = {
271
+ ...OPTION_DEFAULTS,
272
+ ...optionsProp,
273
+ ...userOverrides
274
+ };
275
+ if (merged.subtitleInDetails === void 0) {
276
+ merged.subtitleInDetails = hasDetails;
277
+ }
278
+ if (merged.configurable === void 0) {
279
+ merged.configurable = hasDetails || hasCount || hasViewSettings || hasViewToggle;
280
+ }
281
+ return merged;
282
+ }, [
283
+ optionsProp,
284
+ userOverrides,
285
+ hasDetails,
286
+ hasCount,
287
+ hasViewSettings,
288
+ hasViewToggle
289
+ ]);
290
+ const patchOption = useCallback((key, value) => {
291
+ setUserOverrides((prev) => ({ ...prev, [key]: value }));
292
+ }, []);
293
+ const moveSubtitle = Boolean(resolved.subtitleInDetails) && hasDetails;
294
+ const showTitleSubtitle = Boolean(subtitle) && Boolean(resolved.showSubtitle) && !moveSubtitle;
295
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(
296
+ () => optionsProp?.defaultDetailsOpen ?? OPTION_DEFAULTS.defaultDetailsOpen
297
+ );
298
+ const isControlled = detailsOpen !== void 0;
299
+ const open = isControlled ? detailsOpen : uncontrolledOpen;
300
+ const setOpen = (next) => {
301
+ if (!isControlled) setUncontrolledOpen(next);
302
+ onDetailsOpenChange?.(next);
303
+ };
304
+ const detailsLabel = resolved.detailsLabel ?? "Details";
305
+ const entity = entityName || "item";
306
+ const toggles = [
307
+ { key: "showIcon", label: "Icon", available: Boolean(Icon) },
308
+ {
309
+ key: "showSubtitle",
310
+ label: "Subtitle",
311
+ available: Boolean(subtitle) && !moveSubtitle
312
+ },
313
+ { key: "showCount", label: "Count", available: hasCount },
314
+ { key: "showViewToggle", label: "View toggle", available: hasViewToggle },
315
+ {
316
+ key: "showViewSettings",
317
+ label: "Display settings",
318
+ available: hasViewSettings
319
+ },
320
+ { key: "showCreate", label: "Create action", available: hasCreate },
321
+ {
322
+ key: "showDetailsToggle",
323
+ label: "Filters / details",
324
+ available: hasDetails
325
+ },
326
+ {
327
+ key: "showDetailsHint",
328
+ label: "Collapsed hint",
329
+ available: hasDetails && detailsHint != null
330
+ }
331
+ ].filter((t) => t.available);
332
+ const showDetailsToggle = hasDetails && Boolean(resolved.showDetailsToggle);
333
+ const showHint = hasDetails && !open && Boolean(resolved.showDetailsHint) && detailsHint != null;
334
+ const actionsCluster = /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 shrink-0", children: [
335
+ showHint ? /* @__PURE__ */ jsx("span", { className: "hidden max-w-[14rem] truncate text-xs text-muted-foreground sm:inline", children: detailsHint }) : null,
336
+ hasCount && resolved.showCount !== false ? /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "text-xs", children: countActive ? `${count} of ${totalCount}` : `${totalCount} ${pluralizeEntity(entity, totalCount)}` }) : null,
337
+ hasViewToggle && resolved.showViewToggle !== false ? /* @__PURE__ */ jsx(ViewToggle, { viewMode, onViewModeChange }) : null,
338
+ showDetailsToggle ? /* @__PURE__ */ jsxs(
339
+ Button,
340
+ {
341
+ type: "button",
342
+ variant: "ghost",
343
+ size: "icon",
344
+ className: cn(
345
+ "relative h-8 w-8",
346
+ (open || detailsActive) && "bg-accent text-accent-foreground"
347
+ ),
348
+ onClick: () => setOpen(!open),
349
+ "data-testid": "page-header-details-toggle",
350
+ title: open ? `Hide ${detailsLabel}` : `Show ${detailsLabel}`,
351
+ "aria-label": open ? `Hide ${detailsLabel}` : `Show ${detailsLabel}`,
352
+ "aria-expanded": open,
353
+ "aria-controls": detailsRegionId,
354
+ testID: "page-header-details-toggle",
355
+ children: [
356
+ /* @__PURE__ */ jsx(ListFilter, { className: "h-4 w-4" }),
357
+ detailsActive ? /* @__PURE__ */ jsx(
358
+ "span",
359
+ {
360
+ className: "absolute right-1.5 top-1.5 size-1.5 rounded-full bg-primary",
361
+ "aria-hidden": true
362
+ }
363
+ ) : null
364
+ ]
365
+ }
366
+ ) : null,
367
+ hasViewSettings && resolved.showViewSettings !== false ? /* @__PURE__ */ jsx(
368
+ ViewSettings,
369
+ {
370
+ pageSize: pageSize ?? 25,
371
+ onPageSizeChange,
372
+ density: density ?? "comfortable",
373
+ onDensityChange
374
+ }
375
+ ) : null,
376
+ hasCreate && resolved.showCreate !== false ? createAction : null,
377
+ actions,
378
+ resolved.configurable && toggles.length > 0 ? /* @__PURE__ */ jsxs(Popover, { children: [
379
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
380
+ Button,
381
+ {
382
+ type: "button",
383
+ variant: "ghost",
384
+ size: "icon",
385
+ className: "h-8 w-8",
386
+ title: "Header options",
387
+ "aria-label": "Header options",
388
+ "data-testid": "page-header-options",
389
+ testID: "page-header-options",
390
+ children: /* @__PURE__ */ jsx(SlidersHorizontal, { className: "h-4 w-4" })
391
+ }
392
+ ) }),
393
+ /* @__PURE__ */ jsxs(PopoverContent, { align: "end", className: "w-64 space-y-3", children: [
394
+ /* @__PURE__ */ jsxs("div", { children: [
395
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Header options" }),
396
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Customize this page header. Saved on this device when a storage key is set." })
397
+ ] }),
398
+ /* @__PURE__ */ jsx("ul", { className: "space-y-3", children: toggles.map((t) => {
399
+ const checked = Boolean(resolved[t.key] ?? true);
400
+ return /* @__PURE__ */ jsxs(
401
+ "li",
402
+ {
403
+ className: "flex items-center justify-between gap-3",
404
+ children: [
405
+ /* @__PURE__ */ jsx(
406
+ Label,
407
+ {
408
+ htmlFor: `page-header-opt-${t.key}`,
409
+ className: "text-sm font-normal",
410
+ children: t.label
411
+ }
412
+ ),
413
+ /* @__PURE__ */ jsx(
414
+ Switch,
415
+ {
416
+ id: `page-header-opt-${t.key}`,
417
+ checked,
418
+ onCheckedChange: (v) => patchOption(t.key, v)
419
+ }
420
+ )
421
+ ]
422
+ },
423
+ t.key
424
+ );
425
+ }) }),
426
+ optionsStorageKey && Object.keys(userOverrides).length > 0 ? /* @__PURE__ */ jsx(
427
+ Button,
428
+ {
429
+ type: "button",
430
+ variant: "ghost",
431
+ size: "sm",
432
+ className: "h-8 w-full text-xs",
433
+ onClick: () => setUserOverrides({}),
434
+ "data-testid": "page-header-options-reset",
435
+ "aria-label": "Reset header options",
436
+ testID: "page-header-options-reset",
437
+ children: "Reset to defaults"
438
+ }
439
+ ) : null
440
+ ] })
441
+ ] }) : null
442
+ ] });
443
+ const titleRow = /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [
444
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [
445
+ Icon && resolved.showIcon !== false ? /* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-primary/10", children: /* @__PURE__ */ jsx(Icon, { className: "h-5 w-5 text-primary" }) }) : null,
446
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
447
+ /* @__PURE__ */ jsx(
448
+ "h1",
449
+ {
450
+ "data-testid": "page-title",
451
+ className: "truncate font-display text-xl font-bold tracking-tight sm:text-2xl",
452
+ children: title
453
+ }
454
+ ),
455
+ showTitleSubtitle ? /* @__PURE__ */ jsx("p", { className: "truncate text-sm text-muted-foreground", children: subtitle }) : null
456
+ ] })
457
+ ] }),
458
+ actionsCluster
459
+ ] });
460
+ if (!hasDetails) {
461
+ return titleRow;
462
+ }
463
+ return /* @__PURE__ */ jsxs(Collapsible, { open, onOpenChange: setOpen, children: [
464
+ titleRow,
465
+ /* @__PURE__ */ jsx(CollapsibleContent, { id: detailsRegionId, className: "mt-3", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 rounded-lg border border-border bg-card/40 p-3 sm:p-4", children: [
466
+ moveSubtitle && subtitle && resolved.showSubtitle !== false ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: subtitle }) : null,
467
+ details
468
+ ] }) })
469
+ ] });
470
+ }
471
+ function DataPageShell({
472
+ title,
473
+ icon,
149
474
  entityName,
150
475
  subtitle,
151
476
  createAction,
@@ -168,44 +493,57 @@ function DataPageShell({
168
493
  filters,
169
494
  hasActiveFilters = false,
170
495
  onClearFilters,
496
+ filtersCollapsible = false,
497
+ defaultFiltersOpen = false,
498
+ filtersOpen,
499
+ onFiltersOpenChange,
500
+ filtersLabel,
501
+ filtersHint,
502
+ headerOptions,
503
+ headerOptionsStorageKey,
171
504
  children
172
505
  }) {
173
506
  const entity = entityName || title.toLowerCase();
174
- const hasViewToggle = viewMode && onViewModeChange;
175
- const hasViewSettings = onPageSizeChange && onDensityChange;
176
- const hasCount = count !== void 0 && totalCount !== void 0;
507
+ const collapseFilters = filtersCollapsible && filters != null;
508
+ const mergedOptions = {
509
+ defaultDetailsOpen: defaultFiltersOpen,
510
+ detailsLabel: filtersLabel,
511
+ subtitleInDetails: collapseFilters ? true : void 0,
512
+ ...headerOptions
513
+ };
514
+ const sharedHeaderProps = {
515
+ title,
516
+ subtitle,
517
+ icon,
518
+ entityName: entity,
519
+ count,
520
+ totalCount,
521
+ countActive: hasActiveFilters,
522
+ viewMode,
523
+ onViewModeChange,
524
+ pageSize,
525
+ onPageSizeChange,
526
+ density,
527
+ onDensityChange,
528
+ createAction,
529
+ optionsStorageKey: headerOptionsStorageKey
530
+ };
177
531
  return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
178
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [
179
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [
180
- Icon && /* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 overflow-hidden", children: /* @__PURE__ */ jsx(Icon, { className: "h-5 w-5 text-primary" }) }),
181
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
182
- /* @__PURE__ */ jsx(
183
- "h1",
184
- {
185
- "data-testid": "page-title",
186
- className: "text-xl sm:text-2xl font-bold font-display tracking-tight truncate",
187
- children: title
188
- }
189
- ),
190
- subtitle && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground truncate", children: subtitle })
191
- ] })
192
- ] }),
193
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
194
- hasCount && /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "text-xs", children: hasActiveFilters ? `${count} of ${totalCount}` : `${totalCount} ${entity.endsWith("s") ? entity : `${entity}s`}` }),
195
- hasViewToggle && /* @__PURE__ */ jsx(ViewToggle, { viewMode, onViewModeChange }),
196
- hasViewSettings && /* @__PURE__ */ jsx(
197
- ViewSettings,
198
- {
199
- pageSize: pageSize ?? 25,
200
- onPageSizeChange,
201
- density: density ?? "comfortable",
202
- onDensityChange
203
- }
204
- ),
205
- createAction
206
- ] })
532
+ collapseFilters ? /* @__PURE__ */ jsx(
533
+ PageHeader,
534
+ {
535
+ ...sharedHeaderProps,
536
+ details: filters,
537
+ detailsOpen: filtersOpen,
538
+ onDetailsOpenChange: onFiltersOpenChange,
539
+ detailsActive: hasActiveFilters,
540
+ detailsHint: filtersHint,
541
+ options: mergedOptions
542
+ }
543
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
544
+ /* @__PURE__ */ jsx(PageHeader, { ...sharedHeaderProps, options: headerOptions }),
545
+ filters
207
546
  ] }),
208
- filters,
209
547
  isLoading ? /* @__PURE__ */ jsx(
210
548
  "div",
211
549
  {
@@ -222,10 +560,10 @@ function DataPageShell({
222
560
  }
223
561
  ) : hasError ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-16 text-center", children: [
224
562
  /* @__PURE__ */ jsx(AlertTriangle, { className: "h-10 w-10 text-destructive mb-3" }),
225
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground mb-1", children: "Something went wrong" }),
226
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-4", children: errorMessage || `Failed to load ${entity}s. Please try again.` }),
563
+ /* @__PURE__ */ jsx("p", { className: "mb-1 text-sm font-medium text-foreground", children: "Something went wrong" }),
564
+ /* @__PURE__ */ jsx("p", { className: "mb-4 text-sm text-muted-foreground", children: errorMessage || `Failed to load ${entity}s. Please try again.` }),
227
565
  onRetry && /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: onRetry, children: [
228
- /* @__PURE__ */ jsx(RefreshCw, { className: "h-4 w-4 mr-1" }),
566
+ /* @__PURE__ */ jsx(RefreshCw, { className: "mr-1 h-4 w-4" }),
229
567
  " Retry"
230
568
  ] })
231
569
  ] }) : isEmpty ? /* @__PURE__ */ jsx(
@@ -647,6 +985,16 @@ function DataGrid({
647
985
  )
648
986
  ] });
649
987
  }
988
+ var DENSITY_CELL = {
989
+ compact: "py-1 h-8",
990
+ comfortable: "py-2",
991
+ spacious: "py-3"
992
+ };
993
+ var DENSITY_HEAD = {
994
+ compact: "h-8 py-1",
995
+ comfortable: "h-10",
996
+ spacious: "h-12 py-3"
997
+ };
650
998
  function getCellClasses(col) {
651
999
  const hide = col.hideBelow ? col.hideBelow === "sm" ? "hidden sm:table-cell" : col.hideBelow === "md" ? "hidden md:table-cell" : col.hideBelow === "lg" ? "hidden lg:table-cell" : "hidden xl:table-cell" : "";
652
1000
  return cn(col.width, hide);
@@ -764,8 +1112,11 @@ function DataTable({
764
1112
  pageSizeOptions = [10, 25, 50, 100],
765
1113
  stickyHeader = true,
766
1114
  resizeKey,
767
- stickyFirstColumn
1115
+ stickyFirstColumn,
1116
+ density = "comfortable"
768
1117
  }) {
1118
+ const cellDensity = DENSITY_CELL[density];
1119
+ const headDensity = DENSITY_HEAD[density];
769
1120
  const [sorts, setSorts] = useState([]);
770
1121
  const handleSort = useCallback(
771
1122
  (key, shiftKey) => {
@@ -788,15 +1139,6 @@ function DataTable({
788
1139
  );
789
1140
  const [visibleCols, setVisibleCols] = useState(
790
1141
  () => {
791
- if (columnVisibilityKey) {
792
- try {
793
- const saved = localStorage.getItem(
794
- `dt-vis-${columnVisibilityKey}`
795
- );
796
- if (saved) return JSON.parse(saved);
797
- } catch {
798
- }
799
- }
800
1142
  const initial = {};
801
1143
  columns.forEach((c) => {
802
1144
  initial[c.key] = c.defaultVisible !== false;
@@ -804,14 +1146,40 @@ function DataTable({
804
1146
  return initial;
805
1147
  }
806
1148
  );
1149
+ const [visibilityHydrated, setVisibilityHydrated] = useState(false);
807
1150
  useEffect(() => {
808
- if (columnVisibilityKey) {
809
- localStorage.setItem(
810
- `dt-vis-${columnVisibilityKey}`,
811
- JSON.stringify(visibleCols)
812
- );
1151
+ if (!columnVisibilityKey) {
1152
+ setVisibilityHydrated(true);
1153
+ return;
1154
+ }
1155
+ try {
1156
+ const saved = localStorage.getItem(`dt-vis-${columnVisibilityKey}`);
1157
+ if (saved) {
1158
+ const parsed = JSON.parse(saved);
1159
+ setVisibleCols((prev) => {
1160
+ const next = { ...prev };
1161
+ for (const [k, v] of Object.entries(parsed)) {
1162
+ if (k in next || columns.some((c) => c.key === k)) {
1163
+ next[k] = v;
1164
+ }
1165
+ }
1166
+ columns.forEach((c) => {
1167
+ if (!(c.key in next)) next[c.key] = c.defaultVisible !== false;
1168
+ });
1169
+ return next;
1170
+ });
1171
+ }
1172
+ } catch {
813
1173
  }
814
- }, [visibleCols, columnVisibilityKey]);
1174
+ setVisibilityHydrated(true);
1175
+ }, [columnVisibilityKey]);
1176
+ useEffect(() => {
1177
+ if (!visibilityHydrated || !columnVisibilityKey) return;
1178
+ localStorage.setItem(
1179
+ `dt-vis-${columnVisibilityKey}`,
1180
+ JSON.stringify(visibleCols)
1181
+ );
1182
+ }, [visibleCols, columnVisibilityKey, visibilityHydrated]);
815
1183
  const toggleColumnVisibility = useCallback((key) => {
816
1184
  setVisibleCols((prev) => ({ ...prev, [key]: !prev[key] }));
817
1185
  }, []);
@@ -950,7 +1318,7 @@ function DataTable({
950
1318
  {
951
1319
  className: stickyHeader ? "sticky top-0 z-10 bg-background" : "",
952
1320
  children: /* @__PURE__ */ jsxs(TableRow, { children: [
953
- selectable && /* @__PURE__ */ jsx(TableHead, { className: "w-10", children: /* @__PURE__ */ jsx(
1321
+ selectable && /* @__PURE__ */ jsx(TableHead, { className: cn("w-10", headDensity), children: /* @__PURE__ */ jsx(
954
1322
  Checkbox,
955
1323
  {
956
1324
  checked: items.length > 0 && selected?.size === items.length,
@@ -967,6 +1335,7 @@ function DataTable({
967
1335
  {
968
1336
  className: cn(
969
1337
  getCellClasses(col),
1338
+ headDensity,
970
1339
  col.sortable && "cursor-pointer select-none",
971
1340
  resizableColumns && "relative",
972
1341
  isFirstCol && "sticky left-0 bg-background",
@@ -998,7 +1367,7 @@ function DataTable({
998
1367
  col.key
999
1368
  );
1000
1369
  }),
1001
- renderActions && /* @__PURE__ */ jsx(TableHead, { className: "w-10" })
1370
+ renderActions && /* @__PURE__ */ jsx(TableHead, { className: cn("w-10", headDensity) })
1002
1371
  ] })
1003
1372
  }
1004
1373
  ),
@@ -1018,13 +1387,20 @@ function DataTable({
1018
1387
  "data-state": selected?.has(id) ? "selected" : void 0,
1019
1388
  onClick: onRowClick ? () => onRowClick(item) : void 0,
1020
1389
  children: [
1021
- selectable && /* @__PURE__ */ jsx(TableCell, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(
1022
- Checkbox,
1390
+ selectable && /* @__PURE__ */ jsx(
1391
+ TableCell,
1023
1392
  {
1024
- checked: selected?.has(id),
1025
- onCheckedChange: () => onToggleSelect?.(id)
1393
+ className: cellDensity,
1394
+ onClick: (e) => e.stopPropagation(),
1395
+ children: /* @__PURE__ */ jsx(
1396
+ Checkbox,
1397
+ {
1398
+ checked: selected?.has(id),
1399
+ onCheckedChange: () => onToggleSelect?.(id)
1400
+ }
1401
+ )
1026
1402
  }
1027
- ) }),
1403
+ ),
1028
1404
  activeColumns.map((col, colIndex) => {
1029
1405
  const acc = col.accessor || ((it) => it[col.key]);
1030
1406
  const value = acc(item);
@@ -1036,6 +1412,7 @@ function DataTable({
1036
1412
  getCellClasses(
1037
1413
  col
1038
1414
  ),
1415
+ cellDensity,
1039
1416
  isFirstCol && "sticky left-0 z-[1] bg-background"
1040
1417
  ),
1041
1418
  children: col.editable ? /* @__PURE__ */ jsx(
@@ -1050,7 +1427,14 @@ function DataTable({
1050
1427
  col.key
1051
1428
  );
1052
1429
  }),
1053
- renderActions && /* @__PURE__ */ jsx(TableCell, { onClick: (e) => e.stopPropagation(), children: renderActions(item) })
1430
+ renderActions && /* @__PURE__ */ jsx(
1431
+ TableCell,
1432
+ {
1433
+ className: cellDensity,
1434
+ onClick: (e) => e.stopPropagation(),
1435
+ children: renderActions(item)
1436
+ }
1437
+ )
1054
1438
  ]
1055
1439
  },
1056
1440
  id
@@ -2393,9 +2777,12 @@ function ResourcePage(props) {
2393
2777
  emptyState,
2394
2778
  resourceOptions,
2395
2779
  defaultView = "table",
2396
- onNavigate = defaultNavigate
2780
+ onNavigate = defaultNavigate,
2781
+ viewSettingsKey: viewSettingsKeyProp
2397
2782
  } = props;
2398
2783
  const isClientMode = !!resourceName;
2784
+ const viewSettingsKey = viewSettingsKeyProp ?? resourceName ?? title.toLowerCase().replace(/\s+/g, "-");
2785
+ const { settings, updateSetting } = useViewSettings(viewSettingsKey);
2399
2786
  const resourceHook = useResource(
2400
2787
  resourceName ?? "__noop__",
2401
2788
  isClientMode ? resourceOptions : { enabled: false }
@@ -2544,6 +2931,10 @@ function ResourcePage(props) {
2544
2931
  onRetry: isClientMode ? () => resourceHook.list.refetch() : void 0,
2545
2932
  viewMode: page.viewMode,
2546
2933
  onViewModeChange: page.setViewMode,
2934
+ pageSize: settings.pageSize,
2935
+ onPageSizeChange: (size) => updateSetting("pageSize", size),
2936
+ density: settings.density,
2937
+ onDensityChange: (d) => updateSetting("density", d),
2547
2938
  createAction,
2548
2939
  hasActiveFilters: page.hasActiveFilters,
2549
2940
  onClearFilters: page.clearFilters,
@@ -2595,7 +2986,9 @@ function ResourcePage(props) {
2595
2986
  selectable: !!bulkActions && bulkActions.length > 0,
2596
2987
  selected: selectedIds,
2597
2988
  onToggleSelect: handleToggleSelect,
2598
- onToggleAll: handleToggleAll
2989
+ onToggleAll: handleToggleAll,
2990
+ density: settings.density,
2991
+ pageSize: settings.pageSize
2599
2992
  }
2600
2993
  ),
2601
2994
  renderCard: (item) => {
@@ -2743,7 +3136,40 @@ function ResourcePage(props) {
2743
3136
  }
2744
3137
  );
2745
3138
  }
2746
- function ThemeToggle() {
3139
+ var OPTION_DEFAULTS2 = {
3140
+ showLogo: true,
3141
+ showAppName: true,
3142
+ showThemeToggle: true,
3143
+ showCollapse: true,
3144
+ showUserSlot: true,
3145
+ showFooter: true,
3146
+ showSecondaryNav: true,
3147
+ showMobileBottomNav: true,
3148
+ defaultCollapsed: false,
3149
+ configurable: void 0,
3150
+ density: "comfortable",
3151
+ railWidth: "narrow",
3152
+ scrollRoot: "container"
3153
+ };
3154
+ function readStoredCollapsed(key) {
3155
+ if (typeof window === "undefined") return void 0;
3156
+ try {
3157
+ const raw = localStorage.getItem(`${key}:collapsed`);
3158
+ if (raw === "1") return true;
3159
+ if (raw === "0") return false;
3160
+ return void 0;
3161
+ } catch {
3162
+ return void 0;
3163
+ }
3164
+ }
3165
+ function writeStoredCollapsed(key, collapsed) {
3166
+ if (typeof window === "undefined") return;
3167
+ try {
3168
+ localStorage.setItem(`${key}:collapsed`, collapsed ? "1" : "0");
3169
+ } catch {
3170
+ }
3171
+ }
3172
+ function ThemeToggle({ testId }) {
2747
3173
  const [dark, setDark] = useState(false);
2748
3174
  useEffect(() => {
2749
3175
  const stored = localStorage.getItem("theme");
@@ -2760,159 +3186,717 @@ function ThemeToggle() {
2760
3186
  document.documentElement.classList.toggle("dark", next);
2761
3187
  localStorage.setItem("theme", next ? "dark" : "light");
2762
3188
  }
2763
- return /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: toggle, className: "h-8 w-8", children: dark ? /* @__PURE__ */ jsx(Sun, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Moon, { className: "h-4 w-4" }) });
3189
+ return /* @__PURE__ */ jsx(
3190
+ Button,
3191
+ {
3192
+ variant: "ghost",
3193
+ size: "icon",
3194
+ onClick: toggle,
3195
+ className: "h-8 w-8",
3196
+ title: "Toggle theme",
3197
+ "aria-label": "Toggle theme",
3198
+ "data-testid": testId,
3199
+ testID: testId,
3200
+ children: dark ? /* @__PURE__ */ jsx(Sun, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Moon, { className: "h-4 w-4" })
3201
+ }
3202
+ );
2764
3203
  }
2765
- function SidebarNav({
2766
- navItems,
3204
+ function itemIsActive(item, activePath, isActive) {
3205
+ if (item.end) {
3206
+ return Boolean(activePath && activePath === item.to);
3207
+ }
3208
+ if (isActive(item.to, activePath)) return true;
3209
+ return Boolean(
3210
+ item.children?.some((child) => itemIsActive(child, activePath, isActive))
3211
+ );
3212
+ }
3213
+ function NavLinkRow({
3214
+ item,
3215
+ collapsed,
3216
+ density,
2767
3217
  onNavClick,
2768
3218
  linkComponent: LinkComp,
2769
3219
  activePath,
2770
3220
  isActive
2771
3221
  }) {
2772
- return /* @__PURE__ */ jsx("nav", { className: "flex-1 p-3 space-y-1", children: navItems.map((item) => {
2773
- const active = isActive(item.to, activePath);
3222
+ const active = itemIsActive(item, activePath, isActive);
3223
+ const pad = density === "compact" ? "px-2.5 py-1.5" : "px-3 py-2";
3224
+ if (item.disabled) {
2774
3225
  return /* @__PURE__ */ jsxs(
2775
- LinkComp,
3226
+ "span",
2776
3227
  {
2777
- href: item.to,
2778
- onClick: onNavClick,
2779
- "aria-current": active ? "page" : void 0,
3228
+ title: item.label,
2780
3229
  className: cn(
2781
- "flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
2782
- active ? "bg-sidebar-accent text-sidebar-primary" : "text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
3230
+ "flex items-center gap-3 rounded-lg text-sm font-medium opacity-40",
3231
+ pad,
3232
+ collapsed && "justify-center px-2"
2783
3233
  ),
3234
+ "aria-disabled": true,
2784
3235
  children: [
2785
- /* @__PURE__ */ jsx(item.icon, { className: "h-4 w-4" }),
2786
- item.label
3236
+ /* @__PURE__ */ jsx(item.icon, { className: "h-4 w-4 shrink-0" }),
3237
+ !collapsed ? /* @__PURE__ */ jsx("span", { className: "truncate", children: item.label }) : null
2787
3238
  ]
2788
- },
2789
- item.to
3239
+ }
2790
3240
  );
2791
- }) });
3241
+ }
3242
+ return /* @__PURE__ */ jsxs(
3243
+ LinkComp,
3244
+ {
3245
+ href: item.to,
3246
+ onClick: onNavClick,
3247
+ title: collapsed ? item.label : void 0,
3248
+ "aria-label": collapsed ? item.label : void 0,
3249
+ "aria-current": active ? "page" : void 0,
3250
+ className: cn(
3251
+ "flex items-center gap-3 rounded-lg text-sm font-medium transition-colors",
3252
+ pad,
3253
+ collapsed && "justify-center px-2",
3254
+ active ? "bg-sidebar-accent text-sidebar-primary" : "text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
3255
+ ),
3256
+ children: [
3257
+ /* @__PURE__ */ jsx(item.icon, { className: "h-4 w-4 shrink-0" }),
3258
+ !collapsed ? /* @__PURE__ */ jsxs(Fragment, { children: [
3259
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }),
3260
+ item.badge != null ? /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "h-5 shrink-0 px-1.5 text-[10px]", children: item.badge }) : null
3261
+ ] }) : null
3262
+ ]
3263
+ }
3264
+ );
2792
3265
  }
2793
- function SidebarLogo({
3266
+ function NavGroup({
3267
+ item,
3268
+ collapsed,
3269
+ density,
3270
+ onNavClick,
3271
+ linkComponent,
3272
+ activePath,
3273
+ isActive
3274
+ }) {
3275
+ const children = item.children ?? [];
3276
+ const childActive = children.some(
3277
+ (c) => itemIsActive(c, activePath, isActive)
3278
+ );
3279
+ const [open, setOpen] = useState(childActive);
3280
+ useEffect(() => {
3281
+ if (childActive) setOpen(true);
3282
+ }, [childActive]);
3283
+ if (children.length === 0) {
3284
+ return /* @__PURE__ */ jsx(
3285
+ NavLinkRow,
3286
+ {
3287
+ item,
3288
+ collapsed,
3289
+ density,
3290
+ onNavClick,
3291
+ linkComponent,
3292
+ activePath,
3293
+ isActive
3294
+ }
3295
+ );
3296
+ }
3297
+ if (collapsed) {
3298
+ const target = item.to ? { ...item, children: void 0 } : { ...children[0], children: void 0 };
3299
+ return /* @__PURE__ */ jsx(
3300
+ NavLinkRow,
3301
+ {
3302
+ item: target,
3303
+ collapsed: true,
3304
+ density,
3305
+ onNavClick,
3306
+ linkComponent,
3307
+ activePath,
3308
+ isActive
3309
+ }
3310
+ );
3311
+ }
3312
+ const selfExact = Boolean(item.to && activePath && activePath === item.to);
3313
+ const selfActive = item.to ? item.end ? selfExact : isActive(item.to, activePath) : false;
3314
+ const groupHighlight = selfActive || childActive;
3315
+ const pad = density === "compact" ? "px-2.5 py-1.5" : "px-3 py-2";
3316
+ const LinkComp = linkComponent;
3317
+ const parentClass = cn(
3318
+ "flex min-w-0 flex-1 items-center gap-3 rounded-lg text-sm font-medium transition-colors",
3319
+ pad,
3320
+ groupHighlight ? "bg-sidebar-accent/60 text-sidebar-primary" : "text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
3321
+ );
3322
+ return /* @__PURE__ */ jsx(Collapsible, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
3323
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", "data-testid": `nav-group-${item.label}`, children: [
3324
+ item.to && !item.disabled ? /* @__PURE__ */ jsxs(
3325
+ LinkComp,
3326
+ {
3327
+ href: item.to,
3328
+ onClick: onNavClick,
3329
+ "aria-current": selfExact ? "page" : void 0,
3330
+ className: parentClass,
3331
+ children: [
3332
+ /* @__PURE__ */ jsx(item.icon, { className: "h-4 w-4 shrink-0" }),
3333
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: item.label })
3334
+ ]
3335
+ }
3336
+ ) : /* @__PURE__ */ jsxs(
3337
+ "button",
3338
+ {
3339
+ type: "button",
3340
+ className: parentClass,
3341
+ onClick: () => setOpen((o) => !o),
3342
+ "aria-expanded": open,
3343
+ "aria-disabled": item.disabled || void 0,
3344
+ disabled: item.disabled,
3345
+ children: [
3346
+ /* @__PURE__ */ jsx(item.icon, { className: "h-4 w-4 shrink-0" }),
3347
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: item.label })
3348
+ ]
3349
+ }
3350
+ ),
3351
+ /* @__PURE__ */ jsx(
3352
+ "button",
3353
+ {
3354
+ type: "button",
3355
+ className: cn(
3356
+ "shrink-0 rounded-lg p-1.5 text-sidebar-foreground transition-colors hover:bg-sidebar-accent",
3357
+ groupHighlight && "text-sidebar-primary",
3358
+ item.disabled && "pointer-events-none opacity-40"
3359
+ ),
3360
+ onClick: () => setOpen((o) => !o),
3361
+ "aria-expanded": open,
3362
+ "aria-label": open ? `Collapse ${item.label}` : `Expand ${item.label}`,
3363
+ "aria-disabled": item.disabled || void 0,
3364
+ disabled: item.disabled,
3365
+ "data-testid": `nav-group-toggle-${item.label}`,
3366
+ children: /* @__PURE__ */ jsx(
3367
+ ChevronDown,
3368
+ {
3369
+ className: cn(
3370
+ "h-3.5 w-3.5 opacity-60 transition-transform",
3371
+ open && "rotate-180"
3372
+ )
3373
+ }
3374
+ )
3375
+ }
3376
+ )
3377
+ ] }),
3378
+ /* @__PURE__ */ jsx(CollapsibleContent, { children: /* @__PURE__ */ jsx("div", { className: "ml-3 space-y-0.5 border-l border-sidebar-border pl-2", children: children.map((child) => /* @__PURE__ */ jsx(
3379
+ NavLinkRow,
3380
+ {
3381
+ item: child,
3382
+ collapsed: false,
3383
+ density,
3384
+ onNavClick,
3385
+ linkComponent,
3386
+ activePath,
3387
+ isActive
3388
+ },
3389
+ child.to + child.label
3390
+ )) }) })
3391
+ ] }) });
3392
+ }
3393
+ function SidebarNav({
3394
+ navItems,
3395
+ secondaryNavItems,
3396
+ secondaryNavLabel,
3397
+ showSecondary,
3398
+ collapsed,
3399
+ density,
3400
+ onNavClick,
3401
+ linkComponent,
3402
+ activePath,
3403
+ isActive
3404
+ }) {
3405
+ return /* @__PURE__ */ jsxs(
3406
+ "nav",
3407
+ {
3408
+ className: cn(
3409
+ "flex-1 space-y-1 overflow-y-auto",
3410
+ collapsed ? "p-2" : "p-3"
3411
+ ),
3412
+ "aria-label": "Main",
3413
+ children: [
3414
+ navItems.map((item) => /* @__PURE__ */ jsx(
3415
+ NavGroup,
3416
+ {
3417
+ item,
3418
+ collapsed,
3419
+ density,
3420
+ onNavClick,
3421
+ linkComponent,
3422
+ activePath,
3423
+ isActive
3424
+ },
3425
+ item.to + item.label
3426
+ )),
3427
+ showSecondary && secondaryNavItems && secondaryNavItems.length > 0 ? /* @__PURE__ */ jsxs(
3428
+ "div",
3429
+ {
3430
+ className: cn(
3431
+ "mt-4 space-y-1 border-t border-sidebar-border pt-4",
3432
+ collapsed && "mt-2 pt-2"
3433
+ ),
3434
+ children: [
3435
+ !collapsed ? /* @__PURE__ */ jsx("p", { className: "mb-2 px-3 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/40", children: secondaryNavLabel }) : null,
3436
+ secondaryNavItems.map((item) => /* @__PURE__ */ jsx(
3437
+ NavGroup,
3438
+ {
3439
+ item,
3440
+ collapsed,
3441
+ density,
3442
+ onNavClick,
3443
+ linkComponent,
3444
+ activePath,
3445
+ isActive
3446
+ },
3447
+ item.to + item.label
3448
+ ))
3449
+ ]
3450
+ }
3451
+ ) : null
3452
+ ]
3453
+ }
3454
+ );
3455
+ }
3456
+ function SidebarBrand({
2794
3457
  appName,
2795
- appLogo
3458
+ appLogo,
3459
+ logo,
3460
+ showLogo,
3461
+ showAppName,
3462
+ collapsed
2796
3463
  }) {
2797
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2798
- appLogo && /* @__PURE__ */ jsx("div", { className: "h-7 w-7 rounded-lg bg-primary flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "text-primary-foreground text-sm font-bold", children: appLogo }) }),
2799
- /* @__PURE__ */ jsx("span", { className: "font-semibold text-sidebar-foreground", children: appName })
2800
- ] });
3464
+ if (!showLogo && !showAppName) return null;
3465
+ return /* @__PURE__ */ jsxs(
3466
+ "div",
3467
+ {
3468
+ className: cn(
3469
+ "flex min-w-0 items-center gap-2",
3470
+ collapsed && "justify-center"
3471
+ ),
3472
+ children: [
3473
+ showLogo ? logo ? /* @__PURE__ */ jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden", children: logo }) : appLogo ? /* @__PURE__ */ jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-primary", children: /* @__PURE__ */ jsx("span", { className: "text-sm font-bold text-primary-foreground", children: appLogo }) }) : null : null,
3474
+ showAppName && !collapsed ? /* @__PURE__ */ jsx("span", { className: "truncate font-semibold text-sidebar-foreground", children: appName }) : null
3475
+ ]
3476
+ }
3477
+ );
2801
3478
  }
2802
3479
  function AppShell({
2803
3480
  appName,
2804
3481
  appLogo,
3482
+ logo,
2805
3483
  navItems,
3484
+ secondaryNavItems,
3485
+ secondaryNavLabel = "More",
3486
+ mobileNavItems,
3487
+ headerSlot,
3488
+ collapsedHeaderSlot,
3489
+ footer,
3490
+ userSlot,
2806
3491
  children,
2807
- showThemeToggle = true,
3492
+ showThemeToggle: showThemeToggleProp,
3493
+ options: optionsProp,
3494
+ optionsStorageKey,
3495
+ collapsed: collapsedProp,
3496
+ onCollapsedChange,
2808
3497
  linkComponent = DefaultLink,
2809
3498
  activePath,
2810
3499
  isActive = isPathActive
2811
3500
  }) {
2812
3501
  const [mobileOpen, setMobileOpen] = useState(false);
2813
- return /* @__PURE__ */ jsxs("div", { className: "flex h-screen overflow-hidden", children: [
2814
- /* @__PURE__ */ jsxs("aside", { className: "hidden md:flex md:w-60 flex-col border-r border-sidebar-border bg-sidebar", children: [
2815
- /* @__PURE__ */ jsx("div", { className: "flex h-14 items-center gap-2 px-4 border-b border-sidebar-border", children: /* @__PURE__ */ jsx(SidebarLogo, { appName, appLogo }) }),
2816
- /* @__PURE__ */ jsx(
2817
- SidebarNav,
2818
- {
2819
- navItems,
2820
- linkComponent,
2821
- activePath,
2822
- isActive
2823
- }
2824
- ),
2825
- showThemeToggle && /* @__PURE__ */ jsx("div", { className: "border-t border-sidebar-border p-3 flex items-center justify-end", children: /* @__PURE__ */ jsx(ThemeToggle, {}) })
2826
- ] }),
2827
- mobileOpen && /* @__PURE__ */ jsx(
2828
- "div",
3502
+ const [userOverrides, setUserOverrides] = useState(
3503
+ {}
3504
+ );
3505
+ const [hydratedKey, setHydratedKey] = useState(
3506
+ optionsStorageKey ? void 0 : ""
3507
+ );
3508
+ useEffect(() => {
3509
+ setMobileOpen(false);
3510
+ }, [activePath]);
3511
+ const hasSecondary = Boolean(secondaryNavItems && secondaryNavItems.length);
3512
+ const hasUser = userSlot != null;
3513
+ const hasFooter = footer != null;
3514
+ const hasLogo = Boolean(logo || appLogo);
3515
+ useEffect(() => {
3516
+ if (!optionsStorageKey) {
3517
+ setUserOverrides({});
3518
+ setHydratedKey("");
3519
+ return;
3520
+ }
3521
+ setUserOverrides(readStoredOptions(optionsStorageKey));
3522
+ setHydratedKey(optionsStorageKey);
3523
+ }, [optionsStorageKey]);
3524
+ useEffect(() => {
3525
+ if (!optionsStorageKey || hydratedKey !== optionsStorageKey) return;
3526
+ writeStoredOptions(optionsStorageKey, userOverrides);
3527
+ }, [optionsStorageKey, userOverrides, hydratedKey]);
3528
+ const lockedOptions = optionsProp?.lockedOptions;
3529
+ const resolved = useMemo(() => {
3530
+ const merged = {
3531
+ ...OPTION_DEFAULTS2,
3532
+ ...showThemeToggleProp !== void 0 ? { showThemeToggle: showThemeToggleProp } : {},
3533
+ ...optionsProp,
3534
+ ...userOverrides
3535
+ };
3536
+ if (merged.configurable === void 0) {
3537
+ merged.configurable = hasSecondary || hasUser || hasFooter || Boolean(merged.showCollapse) || Boolean(merged.showThemeToggle);
3538
+ }
3539
+ if (lockedOptions?.length) {
3540
+ const defaults = OPTION_DEFAULTS2;
3541
+ const devOptions = optionsProp;
3542
+ const target = merged;
3543
+ for (const key of lockedOptions) {
3544
+ target[key] = devOptions?.[key] ?? defaults[key];
3545
+ }
3546
+ }
3547
+ return merged;
3548
+ }, [
3549
+ optionsProp,
3550
+ userOverrides,
3551
+ showThemeToggleProp,
3552
+ hasSecondary,
3553
+ hasUser,
3554
+ hasFooter,
3555
+ lockedOptions
3556
+ ]);
3557
+ const patchOption = useCallback(
3558
+ (key, value) => {
3559
+ if (lockedOptions?.includes(key)) return;
3560
+ setUserOverrides((prev) => ({ ...prev, [key]: value }));
3561
+ },
3562
+ [lockedOptions]
3563
+ );
3564
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState(
3565
+ () => optionsProp?.defaultCollapsed ?? OPTION_DEFAULTS2.defaultCollapsed
3566
+ );
3567
+ useEffect(() => {
3568
+ if (!optionsStorageKey || collapsedProp !== void 0) return;
3569
+ const stored = readStoredCollapsed(optionsStorageKey);
3570
+ if (stored !== void 0) setUncontrolledCollapsed(stored);
3571
+ }, [optionsStorageKey, collapsedProp]);
3572
+ const isCollapseControlled = collapsedProp !== void 0;
3573
+ const collapsed = isCollapseControlled ? collapsedProp : uncontrolledCollapsed;
3574
+ const setCollapsed = (next) => {
3575
+ if (!isCollapseControlled) {
3576
+ setUncontrolledCollapsed(next);
3577
+ if (optionsStorageKey) writeStoredCollapsed(optionsStorageKey, next);
3578
+ }
3579
+ onCollapsedChange?.(next);
3580
+ };
3581
+ const density = resolved.density ?? "comfortable";
3582
+ const scrollWindow = resolved.scrollRoot === "window";
3583
+ const railExpanded = resolved.railWidth === "wide" ? "md:w-64" : "md:w-52";
3584
+ const railClass = collapsed ? "md:w-14" : railExpanded;
3585
+ const bottomItems = (mobileNavItems ?? navItems.filter((i) => !i.children || i.children.length === 0)).filter((i) => !i.disabled);
3586
+ const toggles = [
3587
+ { key: "showLogo", label: "Logo", available: hasLogo },
3588
+ { key: "showAppName", label: "App name", available: true },
3589
+ {
3590
+ key: "showThemeToggle",
3591
+ label: "Theme toggle",
3592
+ available: true
3593
+ },
3594
+ { key: "showCollapse", label: "Collapse control", available: true },
3595
+ {
3596
+ key: "showSecondaryNav",
3597
+ label: "Secondary nav",
3598
+ available: hasSecondary
3599
+ },
3600
+ { key: "showFooter", label: "Footer", available: hasFooter },
3601
+ { key: "showUserSlot", label: "User block", available: hasUser },
3602
+ {
3603
+ key: "showMobileBottomNav",
3604
+ label: "Mobile bottom nav",
3605
+ available: bottomItems.length > 0
3606
+ }
3607
+ ].filter((t) => t.available && !lockedOptions?.includes(t.key));
3608
+ const showTheme = resolved.showThemeToggle !== false;
3609
+ const showCollapseCtrl = resolved.showCollapse !== false;
3610
+ const showSecondary = hasSecondary && resolved.showSecondaryNav !== false;
3611
+ const showFooterSlot = hasFooter && resolved.showFooter !== false;
3612
+ const showUser = hasUser && resolved.showUserSlot !== false;
3613
+ const showMobileBottom = bottomItems.length > 0 && resolved.showMobileBottomNav !== false;
3614
+ const optionsControl = resolved.configurable && toggles.length > 0 ? /* @__PURE__ */ jsxs(Popover, { children: [
3615
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
3616
+ Button,
2829
3617
  {
2830
- className: "fixed inset-0 z-40 bg-black/50 md:hidden",
2831
- onClick: () => setMobileOpen(false)
3618
+ type: "button",
3619
+ variant: "ghost",
3620
+ size: "icon",
3621
+ className: "h-8 w-8",
3622
+ title: "Sidebar options",
3623
+ "aria-label": "Sidebar options",
3624
+ "data-testid": "app-shell-options",
3625
+ testID: "app-shell-options",
3626
+ children: /* @__PURE__ */ jsx(SlidersHorizontal, { className: "h-4 w-4" })
2832
3627
  }
2833
- ),
3628
+ ) }),
3629
+ /* @__PURE__ */ jsxs(PopoverContent, { align: "start", side: "top", className: "w-64 space-y-3", children: [
3630
+ /* @__PURE__ */ jsxs("div", { children: [
3631
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Sidebar options" }),
3632
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Customize this sidebar. Saved on this device when a storage key is set." })
3633
+ ] }),
3634
+ /* @__PURE__ */ jsx("ul", { className: "space-y-3", children: toggles.map((t) => {
3635
+ const checked = Boolean(resolved[t.key] ?? true);
3636
+ return /* @__PURE__ */ jsxs(
3637
+ "li",
3638
+ {
3639
+ className: "flex items-center justify-between gap-3",
3640
+ children: [
3641
+ /* @__PURE__ */ jsx(
3642
+ Label,
3643
+ {
3644
+ htmlFor: `app-shell-opt-${t.key}`,
3645
+ className: "text-sm font-normal",
3646
+ children: t.label
3647
+ }
3648
+ ),
3649
+ /* @__PURE__ */ jsx(
3650
+ Switch,
3651
+ {
3652
+ id: `app-shell-opt-${t.key}`,
3653
+ checked,
3654
+ onCheckedChange: (v) => patchOption(t.key, v)
3655
+ }
3656
+ )
3657
+ ]
3658
+ },
3659
+ t.key
3660
+ );
3661
+ }) }),
3662
+ optionsStorageKey && Object.keys(userOverrides).length > 0 ? /* @__PURE__ */ jsx(
3663
+ Button,
3664
+ {
3665
+ type: "button",
3666
+ variant: "ghost",
3667
+ size: "sm",
3668
+ className: "h-8 w-full text-xs",
3669
+ onClick: () => setUserOverrides({}),
3670
+ "data-testid": "app-shell-options-reset",
3671
+ "aria-label": "Reset sidebar options",
3672
+ testID: "app-shell-options-reset",
3673
+ children: "Reset to defaults"
3674
+ }
3675
+ ) : null
3676
+ ] })
3677
+ ] }) : null;
3678
+ const sidebarChrome = (opts) => /* @__PURE__ */ jsxs(Fragment, { children: [
2834
3679
  /* @__PURE__ */ jsxs(
2835
- "aside",
3680
+ "div",
2836
3681
  {
2837
3682
  className: cn(
2838
- "fixed inset-y-0 left-0 z-50 w-60 flex flex-col border-r border-sidebar-border bg-sidebar transition-transform md:hidden",
2839
- mobileOpen ? "translate-x-0" : "-translate-x-full"
3683
+ "flex h-14 shrink-0 items-center gap-2 border-b border-sidebar-border",
3684
+ collapsed && !opts.mobile ? "justify-center px-2" : "px-3"
2840
3685
  ),
2841
3686
  children: [
2842
- /* @__PURE__ */ jsxs("div", { className: "flex h-14 items-center justify-between px-4 border-b border-sidebar-border", children: [
2843
- /* @__PURE__ */ jsx(SidebarLogo, { appName, appLogo }),
2844
- /* @__PURE__ */ jsx(
3687
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(
3688
+ SidebarBrand,
3689
+ {
3690
+ appName,
3691
+ appLogo,
3692
+ logo,
3693
+ showLogo: resolved.showLogo !== false,
3694
+ showAppName: resolved.showAppName !== false,
3695
+ collapsed: collapsed && !opts.mobile
3696
+ }
3697
+ ) }),
3698
+ opts.mobile ? /* @__PURE__ */ jsx(
3699
+ Button,
3700
+ {
3701
+ variant: "ghost",
3702
+ size: "icon",
3703
+ onClick: () => setMobileOpen(false),
3704
+ "aria-label": "Close navigation",
3705
+ "data-testid": "app-shell-mobile-close",
3706
+ testID: "app-shell-mobile-close",
3707
+ children: /* @__PURE__ */ jsx(X, { className: "h-5 w-5" })
3708
+ }
3709
+ ) : /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [
3710
+ showCollapseCtrl ? /* @__PURE__ */ jsx(
2845
3711
  Button,
2846
3712
  {
2847
3713
  variant: "ghost",
2848
3714
  size: "icon",
2849
- onClick: () => setMobileOpen(false),
2850
- children: /* @__PURE__ */ jsx(X, { className: "h-5 w-5" })
3715
+ className: "h-8 w-8",
3716
+ onClick: () => setCollapsed(!collapsed),
3717
+ title: collapsed ? "Expand sidebar" : "Collapse sidebar",
3718
+ "aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar",
3719
+ "aria-pressed": collapsed,
3720
+ "data-testid": "app-shell-collapse",
3721
+ testID: "app-shell-collapse",
3722
+ children: collapsed ? /* @__PURE__ */ jsx(PanelLeft, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(PanelLeftClose, { className: "h-4 w-4" })
2851
3723
  }
2852
- )
2853
- ] }),
2854
- /* @__PURE__ */ jsx(
2855
- SidebarNav,
2856
- {
2857
- navItems,
2858
- onNavClick: () => setMobileOpen(false),
2859
- linkComponent,
2860
- activePath,
2861
- isActive
2862
- }
2863
- )
3724
+ ) : null,
3725
+ !collapsed ? optionsControl : null
3726
+ ] })
2864
3727
  ]
2865
3728
  }
2866
3729
  ),
2867
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col overflow-hidden", children: [
2868
- /* @__PURE__ */ jsxs("header", { className: "flex h-14 items-center gap-3 border-b px-4 md:hidden", children: [
3730
+ collapsed && !opts.mobile ? collapsedHeaderSlot ? /* @__PURE__ */ jsx("div", { className: "shrink-0 border-b border-sidebar-border p-2", children: collapsedHeaderSlot }) : null : headerSlot ? /* @__PURE__ */ jsx("div", { className: "shrink-0 border-b border-sidebar-border", children: headerSlot }) : null,
3731
+ /* @__PURE__ */ jsx(
3732
+ SidebarNav,
3733
+ {
3734
+ navItems,
3735
+ secondaryNavItems,
3736
+ secondaryNavLabel,
3737
+ showSecondary,
3738
+ collapsed: collapsed && !opts.mobile,
3739
+ density,
3740
+ onNavClick: opts.mobile ? () => setMobileOpen(false) : void 0,
3741
+ linkComponent,
3742
+ activePath,
3743
+ isActive
3744
+ }
3745
+ ),
3746
+ (showFooterSlot || showUser || showTheme || collapsed && optionsControl) && !opts.mobile ? /* @__PURE__ */ jsxs("div", { className: "mt-auto shrink-0 space-y-2 border-t border-sidebar-border p-2", children: [
3747
+ showFooterSlot && !collapsed ? footer : null,
3748
+ showUser ? /* @__PURE__ */ jsx(
3749
+ "div",
3750
+ {
3751
+ className: cn(
3752
+ collapsed && "flex justify-center [&_[data-shell-user-detail]]:hidden"
3753
+ ),
3754
+ children: userSlot
3755
+ }
3756
+ ) : null,
3757
+ /* @__PURE__ */ jsxs(
3758
+ "div",
3759
+ {
3760
+ className: cn(
3761
+ "flex items-center gap-1",
3762
+ collapsed ? "flex-col" : "justify-end"
3763
+ ),
3764
+ children: [
3765
+ collapsed ? optionsControl : null,
3766
+ showTheme ? /* @__PURE__ */ jsx(ThemeToggle, { testId: "app-shell-theme-toggle-desktop" }) : null
3767
+ ]
3768
+ }
3769
+ )
3770
+ ] }) : null,
3771
+ opts.mobile && (showFooterSlot || showUser || showTheme) ? /* @__PURE__ */ jsxs("div", { className: "mt-auto shrink-0 space-y-2 border-t border-sidebar-border p-3", children: [
3772
+ showFooterSlot ? footer : null,
3773
+ showUser ? userSlot : null,
3774
+ showTheme ? /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(ThemeToggle, { testId: "app-shell-theme-toggle-mobile-drawer" }) }) : null
3775
+ ] }) : null
3776
+ ] });
3777
+ return /* @__PURE__ */ jsxs(
3778
+ "div",
3779
+ {
3780
+ className: cn(
3781
+ "flex",
3782
+ scrollWindow ? "min-h-screen" : "h-screen overflow-hidden"
3783
+ ),
3784
+ children: [
2869
3785
  /* @__PURE__ */ jsx(
2870
- Button,
3786
+ "aside",
2871
3787
  {
2872
- variant: "ghost",
2873
- size: "icon",
2874
- onClick: () => setMobileOpen(true),
2875
- children: /* @__PURE__ */ jsx(Menu, { className: "h-5 w-5" })
3788
+ className: cn(
3789
+ "hidden flex-col border-r border-sidebar-border bg-sidebar transition-[width] duration-200 md:flex",
3790
+ scrollWindow && "sticky top-0 h-screen",
3791
+ railClass
3792
+ ),
3793
+ "data-collapsed": collapsed || void 0,
3794
+ "data-testid": "app-shell-sidebar",
3795
+ children: sidebarChrome({})
2876
3796
  }
2877
3797
  ),
2878
- /* @__PURE__ */ jsx("span", { className: "flex-1 font-semibold", children: appName }),
2879
- showThemeToggle && /* @__PURE__ */ jsx(ThemeToggle, {})
2880
- ] }),
2881
- /* @__PURE__ */ jsx("main", { className: "flex-1 overflow-y-auto pb-16 md:pb-0", children: /* @__PURE__ */ jsx("div", { className: "p-4 sm:p-6 lg:p-8", children }) }),
2882
- /* @__PURE__ */ jsx("nav", { className: "fixed bottom-0 left-0 right-0 z-30 flex border-t bg-background md:hidden", children: navItems.map((item) => {
2883
- const LinkComp = linkComponent;
2884
- const active = isActive(item.to, activePath);
2885
- return /* @__PURE__ */ jsxs(
2886
- LinkComp,
3798
+ mobileOpen ? /* @__PURE__ */ jsx(
3799
+ "div",
3800
+ {
3801
+ className: "fixed inset-0 z-40 bg-black/50 md:hidden",
3802
+ onClick: () => setMobileOpen(false),
3803
+ "aria-hidden": true
3804
+ }
3805
+ ) : null,
3806
+ /* @__PURE__ */ jsx(
3807
+ "aside",
2887
3808
  {
2888
- href: item.to,
2889
- "aria-current": active ? "page" : void 0,
2890
3809
  className: cn(
2891
- "flex flex-1 flex-col items-center gap-1 py-2 text-xs font-medium transition-colors",
2892
- active ? "text-primary" : "text-muted-foreground"
3810
+ "fixed inset-y-0 left-0 z-50 flex w-60 flex-col border-r border-sidebar-border bg-sidebar transition-transform md:hidden",
3811
+ mobileOpen ? "translate-x-0" : "-translate-x-full pointer-events-none"
3812
+ ),
3813
+ id: "app-shell-mobile-nav",
3814
+ "aria-hidden": !mobileOpen,
3815
+ inert: !mobileOpen || void 0,
3816
+ children: sidebarChrome({ mobile: true })
3817
+ }
3818
+ ),
3819
+ /* @__PURE__ */ jsxs(
3820
+ "div",
3821
+ {
3822
+ className: cn(
3823
+ "flex min-w-0 flex-1 flex-col",
3824
+ !scrollWindow && "overflow-hidden"
2893
3825
  ),
2894
3826
  children: [
2895
- /* @__PURE__ */ jsx(item.icon, { className: "h-5 w-5" }),
2896
- item.label
3827
+ /* @__PURE__ */ jsxs(
3828
+ "header",
3829
+ {
3830
+ className: cn(
3831
+ "flex h-14 items-center gap-3 border-b px-4 md:hidden",
3832
+ scrollWindow && "sticky top-0 z-20 bg-background"
3833
+ ),
3834
+ children: [
3835
+ /* @__PURE__ */ jsx(
3836
+ Button,
3837
+ {
3838
+ variant: "ghost",
3839
+ size: "icon",
3840
+ onClick: () => setMobileOpen(true),
3841
+ "aria-label": "Open navigation",
3842
+ "aria-expanded": mobileOpen,
3843
+ "aria-controls": "app-shell-mobile-nav",
3844
+ "data-testid": "app-shell-mobile-open",
3845
+ testID: "app-shell-mobile-open",
3846
+ children: /* @__PURE__ */ jsx(Menu, { className: "h-5 w-5" })
3847
+ }
3848
+ ),
3849
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate font-semibold", children: appName }),
3850
+ showTheme ? /* @__PURE__ */ jsx(ThemeToggle, { testId: "app-shell-theme-toggle-mobile-header" }) : null
3851
+ ]
3852
+ }
3853
+ ),
3854
+ /* @__PURE__ */ jsx(
3855
+ "main",
3856
+ {
3857
+ className: cn(
3858
+ "flex-1",
3859
+ !scrollWindow && "overflow-y-auto",
3860
+ showMobileBottom ? "pb-16 md:pb-0" : "pb-0"
3861
+ ),
3862
+ children: /* @__PURE__ */ jsx("div", { className: "min-w-0 p-4 sm:p-6 lg:p-8", children })
3863
+ }
3864
+ ),
3865
+ showMobileBottom ? /* @__PURE__ */ jsx(
3866
+ "nav",
3867
+ {
3868
+ className: "fixed bottom-0 left-0 right-0 z-30 flex border-t bg-background md:hidden",
3869
+ "aria-label": "Primary",
3870
+ children: bottomItems.map((item) => {
3871
+ const LinkComp = linkComponent;
3872
+ const active = itemIsActive(item, activePath, isActive);
3873
+ return /* @__PURE__ */ jsxs(
3874
+ LinkComp,
3875
+ {
3876
+ href: item.to,
3877
+ title: item.label,
3878
+ "aria-label": item.label,
3879
+ "aria-current": active ? "page" : void 0,
3880
+ className: cn(
3881
+ "flex flex-1 flex-col items-center gap-1 py-2 text-xs font-medium transition-colors",
3882
+ active ? "text-primary" : "text-muted-foreground"
3883
+ ),
3884
+ children: [
3885
+ /* @__PURE__ */ jsx(item.icon, { className: "h-5 w-5" }),
3886
+ /* @__PURE__ */ jsx("span", { className: "max-w-full truncate px-0.5", children: item.label })
3887
+ ]
3888
+ },
3889
+ item.to + item.label
3890
+ );
3891
+ })
3892
+ }
3893
+ ) : null
2897
3894
  ]
2898
- },
2899
- item.to
2900
- );
2901
- }) })
2902
- ] })
2903
- ] });
2904
- }
2905
- function PageHeader({ title, subtitle, icon: Icon, actions }) {
2906
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [
2907
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [
2908
- Icon && /* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 overflow-hidden", children: /* @__PURE__ */ jsx(Icon, { className: "h-5 w-5 text-primary" }) }),
2909
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
2910
- /* @__PURE__ */ jsx("h1", { className: "text-xl sm:text-2xl font-bold font-display tracking-tight truncate", children: title }),
2911
- subtitle && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground truncate", children: subtitle })
2912
- ] })
2913
- ] }),
2914
- actions && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 shrink-0", children: actions })
2915
- ] });
3895
+ }
3896
+ )
3897
+ ]
3898
+ }
3899
+ );
2916
3900
  }
2917
3901
 
2918
3902
  export { AppShell, BadgeCell, CurrencyCell, DataBulkBar, DataFilters, DataGrid, DataList, DataPageShell, DataTable, DateCell, DetailPanel, EmptyState, FieldArrayRenderer, FileUploadField, FilterCombobox, KanbanBoard, LinkCell, NumberCell, PageHeader, RelationField, ResourceForm, ResourcePage, StatCard, ThumbnailCell, ViewSettings, ViewToggle, WizardForm };