@petrarca/sonnet-shell 0.7.0 → 0.9.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.
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  // src/AppShell.tsx
2
2
  import {
3
- useState as useState2,
4
- useEffect as useEffect3,
5
- useCallback as useCallback2,
3
+ useState as useState4,
4
+ useEffect as useEffect6,
5
+ useCallback as useCallback3,
6
6
  useRef as useRef2,
7
- useMemo as useMemo5
7
+ useMemo as useMemo4
8
8
  } from "react";
9
- import { useResizablePanel as useResizablePanel2 } from "@petrarca/sonnet-core/hooks";
9
+ import { useResizablePanel as useResizablePanel2, useMediaQuery } from "@petrarca/sonnet-core/hooks";
10
10
  import { Outlet, useLocation as useLocation2, useNavigate as useNavigate2 } from "react-router-dom";
11
11
  import { TooltipProvider } from "@petrarca/sonnet-ui";
12
12
  import { ScrollArea as ScrollArea2 } from "@petrarca/sonnet-ui";
@@ -20,8 +20,9 @@ import {
20
20
  } from "@petrarca/sonnet-ui";
21
21
 
22
22
  // src/ConfirmDialog.tsx
23
+ import { useState } from "react";
23
24
  import { AlertTriangle } from "lucide-react";
24
- import { Button } from "@petrarca/sonnet-ui";
25
+ import { Button, Input } from "@petrarca/sonnet-ui";
25
26
  import {
26
27
  Dialog,
27
28
  DialogContent,
@@ -38,9 +39,12 @@ function ConfirmDialog({
38
39
  confirmLabel = "Confirm",
39
40
  cancelLabel = "Cancel",
40
41
  variant = "default",
42
+ confirmText,
41
43
  onConfirm,
42
44
  onCancel
43
45
  }) {
46
+ const [typed, setTyped] = useState("");
47
+ const confirmed = confirmText == null || typed === confirmText;
44
48
  return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (isOpen) => !isOpen && onCancel(), children: /* @__PURE__ */ jsxs(DialogContent, { children: [
45
49
  /* @__PURE__ */ jsxs(DialogHeader, { children: [
46
50
  /* @__PURE__ */ jsx(DialogTitle, { children: title }),
@@ -56,9 +60,28 @@ function ConfirmDialog({
56
60
  ),
57
61
  /* @__PURE__ */ jsx("p", { className: "flex-1 text-sm text-muted-foreground", children: "This action cannot be undone." })
58
62
  ] }),
63
+ confirmText != null && /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5 text-sm", children: [
64
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
65
+ "Type",
66
+ " ",
67
+ /* @__PURE__ */ jsx("span", { className: "font-mono font-medium text-foreground", children: confirmText }),
68
+ " ",
69
+ "to confirm"
70
+ ] }),
71
+ /* @__PURE__ */ jsx(
72
+ Input,
73
+ {
74
+ value: typed,
75
+ onChange: (e) => setTyped(e.target.value),
76
+ autoComplete: "off",
77
+ spellCheck: false,
78
+ "aria-label": `Type ${confirmText} to confirm`
79
+ }
80
+ )
81
+ ] }),
59
82
  /* @__PURE__ */ jsxs(DialogFooter, { children: [
60
83
  /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onCancel, children: cancelLabel }),
61
- /* @__PURE__ */ jsx(Button, { variant, onClick: onConfirm, children: confirmLabel })
84
+ /* @__PURE__ */ jsx(Button, { variant, onClick: onConfirm, disabled: !confirmed, children: confirmLabel })
62
85
  ] })
63
86
  ] }) });
64
87
  }
@@ -76,7 +99,7 @@ function ShellFooter({ children }) {
76
99
  }
77
100
 
78
101
  // src/ShellRail.tsx
79
- import { useMemo as useMemo3 } from "react";
102
+ import { useMemo as useMemo2 } from "react";
80
103
 
81
104
  // src/IconRail.tsx
82
105
  import { cn } from "@petrarca/sonnet-core";
@@ -132,7 +155,6 @@ function RailSeparator() {
132
155
  }
133
156
 
134
157
  // src/SubNavPanel.tsx
135
- import { useMemo as useMemo2 } from "react";
136
158
  import { ChevronsLeft, ChevronsRight } from "lucide-react";
137
159
  import { ScrollArea } from "@petrarca/sonnet-ui";
138
160
  import { Tooltip as Tooltip2, TooltipContent as TooltipContent2, TooltipTrigger as TooltipTrigger2 } from "@petrarca/sonnet-ui";
@@ -185,18 +207,25 @@ function emit(key, payload) {
185
207
  }
186
208
 
187
209
  // src/api.ts
210
+ var toastOptions = (options) => options && {
211
+ description: options.description,
212
+ action: options.action && {
213
+ label: options.action.label,
214
+ onClick: () => options.action?.onClick()
215
+ }
216
+ };
188
217
  var notification = {
189
- success(message) {
190
- toast.success(message);
218
+ success(message, options) {
219
+ toast.success(message, toastOptions(options));
191
220
  },
192
- error(message) {
193
- toast.error(message);
221
+ error(message, options) {
222
+ toast.error(message, toastOptions(options));
194
223
  },
195
- warning(message) {
196
- toast.warning(message);
224
+ warning(message, options) {
225
+ toast.warning(message, toastOptions(options));
197
226
  },
198
- info(message) {
199
- toast.info(message);
227
+ info(message, options) {
228
+ toast.info(message, toastOptions(options));
200
229
  },
201
230
  promise(promise, opts) {
202
231
  return toast.promise(promise, opts);
@@ -419,6 +448,33 @@ function useExtensionPoint(name) {
419
448
  const { getExtensionPoint } = useShellModules();
420
449
  return useMemo(() => getExtensionPoint(name), [getExtensionPoint, name]);
421
450
  }
451
+ function useContributedComponent(extensionPoint, path) {
452
+ const contributions = useExtensionPoint(extensionPoint);
453
+ return useMemo(() => {
454
+ const found = contributions.find((c) => c.path === path && c.component);
455
+ return found?.component;
456
+ }, [contributions, path]);
457
+ }
458
+ function noRuntimeNavigation() {
459
+ return [];
460
+ }
461
+ function useModuleNavigation(module) {
462
+ const runtime = (module.useNavigation ?? noRuntimeNavigation)();
463
+ const contributions = useExtensionPoint(`${module.id}.nav`);
464
+ return useMemo(() => {
465
+ const groups = [...module.navigation, ...runtime];
466
+ if (contributions.length === 0) return groups;
467
+ const contributed = {
468
+ id: `${module.id}-contributed`,
469
+ links: contributions.map((c) => ({
470
+ id: c.extensionPoint + "." + c.label.toLowerCase().replace(/\s+/g, "-"),
471
+ label: c.label,
472
+ path: c.path
473
+ }))
474
+ };
475
+ return [...groups, contributed];
476
+ }, [module, runtime, contributions]);
477
+ }
422
478
  function useMainModules() {
423
479
  const { mainModules } = useShellModules();
424
480
  return mainModules;
@@ -432,7 +488,7 @@ function useShellEvent(key, handler) {
432
488
  }
433
489
 
434
490
  // src/SidebarGroup.tsx
435
- import { useState } from "react";
491
+ import { useState as useState2 } from "react";
436
492
  import { ChevronDown } from "lucide-react";
437
493
  import { cn as cn2 } from "@petrarca/sonnet-core";
438
494
  import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -448,9 +504,10 @@ function GroupHeading({
448
504
  type: "button",
449
505
  onClick: canCollapse ? onToggle : void 0,
450
506
  className: cn2(
451
- "flex w-full items-center gap-1 px-2 py-1.5",
507
+ "flex w-full items-center gap-1 rounded px-2 py-1.5",
452
508
  "text-[11px] font-semibold uppercase tracking-widest text-muted-foreground/70",
453
- canCollapse ? "hover:text-foreground cursor-pointer" : "cursor-default"
509
+ canCollapse ? "hover:text-foreground cursor-pointer" : "cursor-default",
510
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
454
511
  ),
455
512
  children: [
456
513
  canCollapse && /* @__PURE__ */ jsx5(
@@ -471,7 +528,7 @@ function SidebarGroup({
471
528
  separator = false,
472
529
  children
473
530
  }) {
474
- const [open, setOpen] = useState(defaultOpen);
531
+ const [open, setOpen] = useState2(defaultOpen);
475
532
  const canCollapse = collapsible && !!heading;
476
533
  return /* @__PURE__ */ jsxs3("div", { className: cn2(separator && "border-b border-border pb-2"), children: [
477
534
  heading && /* @__PURE__ */ jsx5(
@@ -511,8 +568,9 @@ function ItemTrailing({
511
568
  "button",
512
569
  {
513
570
  type: "button",
571
+ "aria-label": expanded ? "Collapse" : "Expand",
514
572
  onClick: handleClick,
515
- className: "p-0.5 text-muted-foreground hover:text-foreground",
573
+ className: "rounded p-0.5 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
516
574
  children: /* @__PURE__ */ jsx6(
517
575
  ChevronRight,
518
576
  {
@@ -562,6 +620,7 @@ function SidebarItem({
562
620
  style,
563
621
  className: cn3(
564
622
  "flex items-center gap-2 rounded px-2 py-1.5 text-[13px] font-medium transition-colors select-none",
623
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
565
624
  itemStateClass(disabled, isActive)
566
625
  ),
567
626
  children: [
@@ -583,34 +642,23 @@ function SidebarItem({
583
642
 
584
643
  // src/SubNavPanel.tsx
585
644
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
586
- function useMergedNavigation(service) {
587
- const contributions = useExtensionPoint(`${service.id}.nav`);
588
- return useMemo2(() => {
589
- if (contributions.length === 0) return service.navigation;
590
- const contributed = {
591
- id: `${service.id}-contributed`,
592
- links: contributions.map((c) => ({
593
- id: c.extensionPoint + "." + c.label.toLowerCase().replace(/\s+/g, "-"),
594
- label: c.label,
595
- path: c.path
596
- }))
597
- };
598
- return [...service.navigation, contributed];
599
- }, [service, contributions]);
645
+ function SubNavPanel(props) {
646
+ return /* @__PURE__ */ jsx7(ModuleSubNav, { ...props }, props.service.id);
600
647
  }
601
- function SubNavPanel({
648
+ function ModuleSubNav({
602
649
  service,
603
650
  collapsed,
604
651
  onToggleCollapse
605
652
  }) {
606
- const mergedNavigation = useMergedNavigation(service);
653
+ const mergedNavigation = useModuleNavigation(service);
607
654
  if (collapsed) {
608
655
  return /* @__PURE__ */ jsx7("aside", { className: "shrink-0 border-r bg-background flex flex-col items-center pt-2.5", children: /* @__PURE__ */ jsxs5(Tooltip2, { children: [
609
656
  /* @__PURE__ */ jsx7(TooltipTrigger2, { asChild: true, children: /* @__PURE__ */ jsx7(
610
657
  "button",
611
658
  {
612
659
  type: "button",
613
- className: "text-muted-foreground/50 hover:text-muted-foreground transition-colors p-1",
660
+ "aria-label": "Expand navigation",
661
+ className: "rounded text-muted-foreground/50 hover:text-muted-foreground transition-colors p-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
614
662
  onClick: onToggleCollapse,
615
663
  children: /* @__PURE__ */ jsx7(ChevronsRight, { className: "h-4 w-4" })
616
664
  }
@@ -626,7 +674,8 @@ function SubNavPanel({
626
674
  "button",
627
675
  {
628
676
  type: "button",
629
- className: "text-muted-foreground/40 hover:text-muted-foreground transition-colors p-0.5",
677
+ "aria-label": "Collapse navigation",
678
+ className: "rounded text-muted-foreground/40 hover:text-muted-foreground transition-colors p-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
630
679
  onClick: onToggleCollapse,
631
680
  children: /* @__PURE__ */ jsx7(ChevronsLeft, { className: "h-3.5 w-3.5" })
632
681
  }
@@ -655,7 +704,7 @@ function SubNavPanel({
655
704
  }
656
705
 
657
706
  // src/shellNavigation.ts
658
- import { createContext as createContext2, useContext as useContext2 } from "react";
707
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect2 } from "react";
659
708
  var ShellNavigationContext = createContext2(null);
660
709
  function useShellNavigation() {
661
710
  const ctx = useContext2(ShellNavigationContext);
@@ -664,6 +713,13 @@ function useShellNavigation() {
664
713
  }
665
714
  return ctx;
666
715
  }
716
+ function useCollapsedSubNav(active = true) {
717
+ const request = useContext2(ShellNavigationContext)?.requestSubNavCollapse;
718
+ useEffect2(
719
+ () => active && request ? request() : void 0,
720
+ [active, request]
721
+ );
722
+ }
667
723
 
668
724
  // src/shellConfig.ts
669
725
  import { createContext as createContext3, useContext as useContext3 } from "react";
@@ -749,7 +805,7 @@ function ShellRail() {
749
805
  onServiceSelect
750
806
  } = useShellNavigation();
751
807
  const isActive = (id) => activeService?.id === id || sidePaneModuleId === id;
752
- const policy = useMemo3(
808
+ const policy = useMemo2(
753
809
  () => resolveCapabilityPolicy(capabilityPolicy),
754
810
  [capabilityPolicy]
755
811
  );
@@ -896,7 +952,7 @@ function SidePaneOpen({
896
952
  }
897
953
 
898
954
  // src/CommandMenu.tsx
899
- import { useEffect as useEffect2, useCallback, useMemo as useMemo4 } from "react";
955
+ import { useEffect as useEffect3, useCallback, useMemo as useMemo3 } from "react";
900
956
  import { useNavigate } from "react-router-dom";
901
957
  import {
902
958
  CommandDialog,
@@ -912,7 +968,7 @@ function CommandMenu({ open, onOpenChange }) {
912
968
  const navigate = useNavigate();
913
969
  const { mainModules, bottomModules } = useShellModules();
914
970
  const { resolveCapability, capabilityPolicy } = useShellConfig();
915
- useEffect2(() => {
971
+ useEffect3(() => {
916
972
  const handleKeyDown = (e) => {
917
973
  if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
918
974
  e.preventDefault();
@@ -922,7 +978,7 @@ function CommandMenu({ open, onOpenChange }) {
922
978
  document.addEventListener("keydown", handleKeyDown);
923
979
  return () => document.removeEventListener("keydown", handleKeyDown);
924
980
  }, [open, onOpenChange]);
925
- const policy = useMemo4(
981
+ const policy = useMemo3(
926
982
  () => resolveCapabilityPolicy(capabilityPolicy),
927
983
  [capabilityPolicy]
928
984
  );
@@ -935,27 +991,18 @@ function CommandMenu({ open, onOpenChange }) {
935
991
  [navigate, onOpenChange]
936
992
  );
937
993
  const renderNavGroup = (service) => {
938
- if (service.navigation.length === 0) return null;
939
994
  const { effect, reason } = moduleEffect(service);
940
995
  if (effect === "hidden" || effect === "redirect") return null;
941
- const disabled = effect === "disabled";
942
- const Icon = service.icon;
943
- return /* @__PURE__ */ jsx11(CommandGroup, { heading: service.label, children: service.navigation.flatMap(
944
- (group) => group.links.map((link) => /* @__PURE__ */ jsxs8(
945
- CommandItem,
946
- {
947
- value: `${service.label} ${group.heading ?? ""} ${link.label}`,
948
- disabled,
949
- onSelect: disabled ? void 0 : () => handleSelect(link.path),
950
- children: [
951
- /* @__PURE__ */ jsx11(Icon, { className: "mr-2 h-4 w-4 text-muted-foreground" }),
952
- /* @__PURE__ */ jsx11("span", { children: link.label }),
953
- disabled && reason ? /* @__PURE__ */ jsx11("span", { className: "ml-auto text-xs text-muted-foreground", children: reason }) : group.heading && /* @__PURE__ */ jsx11("span", { className: "ml-auto text-xs text-muted-foreground", children: group.heading })
954
- ]
955
- },
956
- link.path
957
- ))
958
- ) }, `nav-${service.id}`);
996
+ return /* @__PURE__ */ jsx11(
997
+ ModuleNavCommands,
998
+ {
999
+ service,
1000
+ disabled: effect === "disabled",
1001
+ reason,
1002
+ onSelect: handleSelect
1003
+ },
1004
+ `nav-${service.id}`
1005
+ );
959
1006
  };
960
1007
  const renderCommandGroup = (service) => {
961
1008
  if (!service.commands?.length) return null;
@@ -1012,12 +1059,38 @@ function CommandMenu({ open, onOpenChange }) {
1012
1059
  ] })
1013
1060
  ] });
1014
1061
  }
1062
+ function ModuleNavCommands({
1063
+ service,
1064
+ disabled,
1065
+ reason,
1066
+ onSelect
1067
+ }) {
1068
+ const navigation2 = useModuleNavigation(service);
1069
+ if (navigation2.length === 0) return null;
1070
+ const Icon = service.icon;
1071
+ return /* @__PURE__ */ jsx11(CommandGroup, { heading: service.label, children: navigation2.flatMap(
1072
+ (group) => group.links.map((link) => /* @__PURE__ */ jsxs8(
1073
+ CommandItem,
1074
+ {
1075
+ value: `${service.label} ${group.heading ?? ""} ${link.label}`,
1076
+ disabled,
1077
+ onSelect: disabled ? void 0 : () => onSelect(link.path),
1078
+ children: [
1079
+ /* @__PURE__ */ jsx11(Icon, { className: "mr-2 h-4 w-4 text-muted-foreground" }),
1080
+ /* @__PURE__ */ jsx11("span", { children: link.label }),
1081
+ disabled && reason ? /* @__PURE__ */ jsx11("span", { className: "ml-auto text-xs text-muted-foreground", children: reason }) : group.heading && /* @__PURE__ */ jsx11("span", { className: "ml-auto text-xs text-muted-foreground", children: group.heading })
1082
+ ]
1083
+ },
1084
+ link.path
1085
+ ))
1086
+ ) });
1087
+ }
1015
1088
 
1016
1089
  // src/ModuleEffects.tsx
1017
- import React3 from "react";
1090
+ import React5 from "react";
1018
1091
  import { warnLog as warnLog4 } from "@petrarca/sonnet-core";
1019
1092
  import { Fragment as Fragment3, jsx as jsx12 } from "react/jsx-runtime";
1020
- var ModuleEffectBoundary = class extends React3.Component {
1093
+ var ModuleEffectBoundary = class extends React5.Component {
1021
1094
  state = { caught: false };
1022
1095
  static getDerivedStateFromError() {
1023
1096
  return { caught: true };
@@ -1041,8 +1114,64 @@ function ModuleEffects({ modules }) {
1041
1114
  return /* @__PURE__ */ jsx12(Fragment3, { children: modules.filter((m) => typeof m.useEffects === "function").map((m) => /* @__PURE__ */ jsx12(ModuleEffectBoundary, { moduleId: m.id, children: /* @__PURE__ */ jsx12(ModuleEffectHost, { module: m }) }, m.id)) });
1042
1115
  }
1043
1116
 
1117
+ // src/ModuleFeatures.tsx
1118
+ import { useEffect as useEffect4 } from "react";
1119
+ import { Fragment as Fragment4, jsx as jsx13 } from "react/jsx-runtime";
1120
+ function ModuleFeatureHost({
1121
+ module,
1122
+ record
1123
+ }) {
1124
+ const navigation2 = useModuleNavigation(module);
1125
+ useEffect4(() => {
1126
+ const links = /* @__PURE__ */ new Map();
1127
+ for (const group of navigation2) {
1128
+ for (const link of group.links) {
1129
+ if (link.id) links.set(link.id, link.path);
1130
+ }
1131
+ }
1132
+ record.set(module.id, links);
1133
+ }, [module.id, navigation2, record]);
1134
+ return null;
1135
+ }
1136
+ function ModuleFeatures({
1137
+ modules,
1138
+ record
1139
+ }) {
1140
+ return /* @__PURE__ */ jsx13(Fragment4, { children: modules.map((m) => /* @__PURE__ */ jsx13(ModuleFeatureHost, { module: m, record }, m.id)) });
1141
+ }
1142
+
1143
+ // src/subNavCollapse.ts
1144
+ import { useCallback as useCallback2, useEffect as useEffect5, useState as useState3 } from "react";
1145
+ function useSubNavCollapse(isBelowTablet) {
1146
+ const [preference, setPreference] = useState3(false);
1147
+ const [requests, setRequests] = useState3(0);
1148
+ const [overridden, setOverridden] = useState3(false);
1149
+ useEffect5(() => {
1150
+ if (requests === 0) setOverridden(false);
1151
+ }, [requests]);
1152
+ const requested = requests > 0 && !overridden;
1153
+ const toggle = useCallback2(() => {
1154
+ if (requested) {
1155
+ setOverridden(true);
1156
+ setPreference(false);
1157
+ return;
1158
+ }
1159
+ setPreference((c) => !c);
1160
+ }, [requested]);
1161
+ const requestCollapse = useCallback2(() => {
1162
+ setRequests((n) => n + 1);
1163
+ return () => setRequests((n) => n - 1);
1164
+ }, []);
1165
+ return {
1166
+ collapsed: preference || isBelowTablet || requested,
1167
+ toggle,
1168
+ setCollapsed: setPreference,
1169
+ requestCollapse
1170
+ };
1171
+ }
1172
+
1044
1173
  // src/AppShell.tsx
1045
- import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
1174
+ import { Fragment as Fragment5, jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
1046
1175
  var PANEL_WIDTHS = {
1047
1176
  narrow: { css: "w-[480px]", px: 480 },
1048
1177
  default: { css: "w-[640px]", px: 640 },
@@ -1082,10 +1211,10 @@ function selectNavModule(service, navigate, setSubNavCollapsed, closeSidePane) {
1082
1211
  }
1083
1212
  function useShellApiInit(deps) {
1084
1213
  const latest = useRef2(deps);
1085
- useEffect3(() => {
1214
+ useEffect6(() => {
1086
1215
  latest.current = deps;
1087
1216
  });
1088
- useEffect3(() => {
1217
+ useEffect6(() => {
1089
1218
  initNavigation(
1090
1219
  (path) => latest.current.navigate(path),
1091
1220
  () => latest.current.openCommandMenu()
@@ -1115,7 +1244,7 @@ function useShellApiInit(deps) {
1115
1244
  }
1116
1245
  function useActiveService(modules) {
1117
1246
  const location = useLocation2();
1118
- const prefixToService = useMemo5(() => {
1247
+ const prefixToService = useMemo4(() => {
1119
1248
  const pairs = [];
1120
1249
  for (const m of modules) {
1121
1250
  if (m.hidden) continue;
@@ -1127,7 +1256,7 @@ function useActiveService(modules) {
1127
1256
  pairs.sort((a, b) => b[0].length - a[0].length);
1128
1257
  return pairs;
1129
1258
  }, [modules]);
1130
- const resolveServiceFromPath = useCallback2(
1259
+ const resolveServiceFromPath = useCallback3(
1131
1260
  (pathname) => {
1132
1261
  for (const [prefix, id] of prefixToService) {
1133
1262
  if (pathname.startsWith(prefix)) return id;
@@ -1136,10 +1265,10 @@ function useActiveService(modules) {
1136
1265
  },
1137
1266
  [prefixToService]
1138
1267
  );
1139
- const [selectedServiceId, setSelectedServiceId] = useState2(
1268
+ const [selectedServiceId, setSelectedServiceId] = useState4(
1140
1269
  () => resolveServiceFromPath(location.pathname)
1141
1270
  );
1142
- useEffect3(
1271
+ useEffect6(
1143
1272
  () => setSelectedServiceId(resolveServiceFromPath(location.pathname)),
1144
1273
  [location.pathname, resolveServiceFromPath]
1145
1274
  );
@@ -1147,26 +1276,26 @@ function useActiveService(modules) {
1147
1276
  return { activeService, selectedServiceId, setSelectedServiceId };
1148
1277
  }
1149
1278
  function useSidePaneState2() {
1150
- const [sidePaneState, setSidePaneState] = useState2(
1279
+ const [sidePaneState, setSidePaneState] = useState4(
1151
1280
  null
1152
1281
  );
1153
- const handleOpen = useCallback2(
1282
+ const handleOpen = useCallback3(
1154
1283
  (opts) => setSidePaneState(opts),
1155
1284
  []
1156
1285
  );
1157
- const handleClose = useCallback2(() => setSidePaneState(null), []);
1158
- const handleIsOpen = useCallback2(
1286
+ const handleClose = useCallback3(() => setSidePaneState(null), []);
1287
+ const handleIsOpen = useCallback3(
1159
1288
  () => sidePaneState !== null,
1160
1289
  [sidePaneState]
1161
1290
  );
1162
- const handleActiveModuleId = useCallback2(
1291
+ const handleActiveModuleId = useCallback3(
1163
1292
  () => sidePaneState?.moduleId ?? null,
1164
1293
  [sidePaneState]
1165
1294
  );
1166
- const handleSetFullWidth = useCallback2((fullWidth) => {
1295
+ const handleSetFullWidth = useCallback3((fullWidth) => {
1167
1296
  setSidePaneState((prev) => prev ? { ...prev, fullWidth } : prev);
1168
1297
  }, []);
1169
- const handleToggle = useCallback2((opts) => {
1298
+ const handleToggle = useCallback3((opts) => {
1170
1299
  setSidePaneState(
1171
1300
  (prev) => prev?.moduleId === opts.moduleId ? null : opts
1172
1301
  );
@@ -1202,13 +1331,13 @@ function resizeOptionsFromPanel(state, widthEntry) {
1202
1331
  };
1203
1332
  }
1204
1333
  function usePanelState() {
1205
- const [panelState, setPanelState] = useState2(null);
1334
+ const [panelState, setPanelState] = useState4(null);
1206
1335
  const panelOnCloseRef = useRef2(void 0);
1207
- const handleOpen = useCallback2(
1336
+ const handleOpen = useCallback3(
1208
1337
  (opts) => setPanelState(opts),
1209
1338
  []
1210
1339
  );
1211
- const handleClose = useCallback2(() => {
1340
+ const handleClose = useCallback3(() => {
1212
1341
  panelOnCloseRef.current = panelState?.onClose;
1213
1342
  setPanelState(null);
1214
1343
  }, [panelState]);
@@ -1226,48 +1355,57 @@ function usePanelState() {
1226
1355
  };
1227
1356
  }
1228
1357
  function useDialogState() {
1229
- const [dialogState, setDialogState] = useState2(null);
1230
- const handleConfirm = useCallback2(() => {
1358
+ const [dialogState, setDialogState] = useState4(null);
1359
+ const handleConfirm = useCallback3(() => {
1231
1360
  dialogState?.resolve(true);
1232
1361
  setDialogState(null);
1233
1362
  }, [dialogState]);
1234
- const handleCancel = useCallback2(() => {
1363
+ const handleCancel = useCallback3(() => {
1235
1364
  dialogState?.resolve(false);
1236
1365
  setDialogState(null);
1237
1366
  }, [dialogState]);
1238
1367
  return { dialogState, setDialogState, handleConfirm, handleCancel };
1239
1368
  }
1240
- function useFeatureLookup(modules) {
1241
- return useMemo5(() => {
1242
- const map = /* @__PURE__ */ new Map();
1369
+ function useFeatureLookup(modules, record) {
1370
+ return useMemo4(() => {
1371
+ const declared = /* @__PURE__ */ new Map();
1243
1372
  for (const m of modules) {
1244
1373
  for (const group of m.navigation) {
1245
1374
  for (const link of group.links) {
1246
- if (link.id) map.set(link.id, link.path);
1375
+ if (link.id) declared.set(link.id, link.path);
1247
1376
  }
1248
1377
  }
1249
1378
  }
1250
- return (featureId) => map.get(featureId) ?? null;
1251
- }, [modules]);
1379
+ return (featureId) => {
1380
+ for (const links of record.values()) {
1381
+ const path = links.get(featureId);
1382
+ if (path) return path;
1383
+ }
1384
+ return declared.get(featureId) ?? null;
1385
+ };
1386
+ }, [modules, record]);
1252
1387
  }
1253
1388
  function AppShell({ registry, sidebar }) {
1254
1389
  const navigate = useNavigate2();
1255
1390
  const config = useShellConfig();
1256
1391
  const { modules } = registry;
1257
1392
  const { activeService, setSelectedServiceId } = useActiveService(modules);
1258
- const [subNavCollapsed, setSubNavCollapsed] = useState2(false);
1259
- const [commandMenuOpen, setCommandMenuOpen] = useState2(false);
1260
- const openCommandMenu = useCallback2(() => setCommandMenuOpen(true), []);
1261
- const [fullscreenState, setFullscreenState] = useState2(null);
1262
- const handleFullscreenEnter = useCallback2(
1393
+ const isBelowTablet = useMediaQuery("(max-width: 1023px)");
1394
+ const subNav = useSubNavCollapse(isBelowTablet);
1395
+ const setSubNavCollapsed = subNav.setCollapsed;
1396
+ const [commandMenuOpen, setCommandMenuOpen] = useState4(false);
1397
+ const openCommandMenu = useCallback3(() => setCommandMenuOpen(true), []);
1398
+ const [fullscreenState, setFullscreenState] = useState4(null);
1399
+ const handleFullscreenEnter = useCallback3(
1263
1400
  (opts) => setFullscreenState(opts),
1264
1401
  []
1265
1402
  );
1266
- const handleFullscreenExit = useCallback2(() => setFullscreenState(null), []);
1403
+ const handleFullscreenExit = useCallback3(() => setFullscreenState(null), []);
1267
1404
  const sidePane2 = useSidePaneState2();
1268
1405
  const panel2 = usePanelState();
1269
1406
  const dialog2 = useDialogState();
1270
- const featureLookup = useFeatureLookup(modules);
1407
+ const featureRecord = useRef2(/* @__PURE__ */ new Map()).current;
1408
+ const featureLookup = useFeatureLookup(modules, featureRecord);
1271
1409
  useShellApiInit({
1272
1410
  navigate,
1273
1411
  openCommandMenu,
@@ -1283,7 +1421,7 @@ function AppShell({ registry, sidebar }) {
1283
1421
  setDialogState: dialog2.setDialogState,
1284
1422
  featureLookup
1285
1423
  });
1286
- const handleServiceSelect = useCallback2(
1424
+ const handleServiceSelect = useCallback3(
1287
1425
  (serviceId) => {
1288
1426
  const service = modules.find((s) => s.id === serviceId);
1289
1427
  if (!service) return;
@@ -1317,7 +1455,7 @@ function AppShell({ registry, sidebar }) {
1317
1455
  setSubNavCollapsed
1318
1456
  ]
1319
1457
  );
1320
- const sidePaneContextValue = useMemo5(
1458
+ const sidePaneContextValue = useMemo4(
1321
1459
  () => ({
1322
1460
  pane: sidePane2.sidePaneState,
1323
1461
  open: sidePane2.handleOpen,
@@ -1336,22 +1474,24 @@ function AppShell({ registry, sidebar }) {
1336
1474
  const navigationValue = {
1337
1475
  activeService,
1338
1476
  sidePaneModuleId: sidePane2.sidePaneState?.moduleId ?? null,
1339
- subNavCollapsed,
1340
- onToggleSubNav: () => setSubNavCollapsed((c) => !c),
1477
+ subNavCollapsed: subNav.collapsed,
1478
+ onToggleSubNav: subNav.toggle,
1479
+ requestSubNavCollapse: subNav.requestCollapse,
1341
1480
  onServiceSelect: handleServiceSelect
1342
1481
  };
1343
- return /* @__PURE__ */ jsx13(ShellModulesContext.Provider, { value: registry, children: /* @__PURE__ */ jsx13(SidePaneContext.Provider, { value: sidePaneContextValue, children: /* @__PURE__ */ jsx13(ShellNavigationContext.Provider, { value: navigationValue, children: /* @__PURE__ */ jsxs9(TooltipProvider, { children: [
1344
- /* @__PURE__ */ jsx13(ModuleEffects, { modules }),
1482
+ return /* @__PURE__ */ jsx14(ShellModulesContext.Provider, { value: registry, children: /* @__PURE__ */ jsx14(SidePaneContext.Provider, { value: sidePaneContextValue, children: /* @__PURE__ */ jsx14(ShellNavigationContext.Provider, { value: navigationValue, children: /* @__PURE__ */ jsxs9(TooltipProvider, { children: [
1483
+ /* @__PURE__ */ jsx14(ModuleEffects, { modules }),
1484
+ /* @__PURE__ */ jsx14(ModuleFeatures, { modules, record: featureRecord }),
1345
1485
  /* @__PURE__ */ jsxs9("div", { className: "flex h-screen w-screen flex-col overflow-hidden", children: [
1346
- /* @__PURE__ */ jsx13(TopBar, { children: config.topBar }),
1347
- /* @__PURE__ */ jsx13(
1486
+ /* @__PURE__ */ jsx14(TopBar, { children: config.topBar }),
1487
+ /* @__PURE__ */ jsx14(
1348
1488
  CommandMenu,
1349
1489
  {
1350
1490
  open: commandMenuOpen,
1351
1491
  onOpenChange: setCommandMenuOpen
1352
1492
  }
1353
1493
  ),
1354
- /* @__PURE__ */ jsx13(
1494
+ /* @__PURE__ */ jsx14(
1355
1495
  ShellBody,
1356
1496
  {
1357
1497
  activeService,
@@ -1359,8 +1499,8 @@ function AppShell({ registry, sidebar }) {
1359
1499
  sidebar
1360
1500
  }
1361
1501
  ),
1362
- config.footer && /* @__PURE__ */ jsx13(ShellFooter, { children: config.footer }),
1363
- dialog2.dialogState && /* @__PURE__ */ jsx13(
1502
+ config.footer && /* @__PURE__ */ jsx14(ShellFooter, { children: config.footer }),
1503
+ dialog2.dialogState && /* @__PURE__ */ jsx14(
1364
1504
  ConfirmDialog,
1365
1505
  {
1366
1506
  open: true,
@@ -1369,12 +1509,13 @@ function AppShell({ registry, sidebar }) {
1369
1509
  confirmLabel: dialog2.dialogState.opts.confirmLabel,
1370
1510
  cancelLabel: dialog2.dialogState.opts.cancelLabel,
1371
1511
  variant: dialog2.dialogState.opts.variant,
1512
+ confirmText: dialog2.dialogState.opts.confirmText,
1372
1513
  onConfirm: dialog2.handleConfirm,
1373
1514
  onCancel: dialog2.handleCancel
1374
1515
  }
1375
1516
  ),
1376
- fullscreenState && /* @__PURE__ */ jsx13("div", { className: "fixed inset-0 z-50 bg-background overflow-auto", children: fullscreenState.content }),
1377
- /* @__PURE__ */ jsx13(
1517
+ fullscreenState && /* @__PURE__ */ jsx14("div", { className: "fixed inset-0 z-50 bg-background overflow-auto", children: fullscreenState.content }),
1518
+ /* @__PURE__ */ jsx14(
1378
1519
  SlideOverPanel,
1379
1520
  {
1380
1521
  panelState: panel2.panelState,
@@ -1407,14 +1548,14 @@ function SlideOverPanel({
1407
1548
  },
1408
1549
  ...!panelState?.description && { "aria-describedby": void 0 }
1409
1550
  };
1410
- return /* @__PURE__ */ jsx13(
1551
+ return /* @__PURE__ */ jsx14(
1411
1552
  Sheet,
1412
1553
  {
1413
1554
  open: panelState !== null,
1414
1555
  onOpenChange: (open) => {
1415
1556
  if (!open) onClose();
1416
1557
  },
1417
- children: resize.enabled ? /* @__PURE__ */ jsx13(ResizableSheetContent, { ...sheetProps, resize, children: panelState && /* @__PURE__ */ jsx13(PanelContent, { state: panelState }) }) : /* @__PURE__ */ jsx13(SheetContent, { ...sheetProps, className: `${panelWidth} max-w-[90vw]`, children: panelState && /* @__PURE__ */ jsx13(PanelContent, { state: panelState }) })
1558
+ children: resize.enabled ? /* @__PURE__ */ jsx14(ResizableSheetContent, { ...sheetProps, resize, children: panelState && /* @__PURE__ */ jsx14(PanelContent, { state: panelState }) }) : /* @__PURE__ */ jsx14(SheetContent, { ...sheetProps, className: `${panelWidth} max-w-[90vw]`, children: panelState && /* @__PURE__ */ jsx14(PanelContent, { state: panelState }) })
1418
1559
  }
1419
1560
  );
1420
1561
  }
@@ -1441,7 +1582,7 @@ function ResizableSheetContent({
1441
1582
  className: "max-w-[90vw]",
1442
1583
  style: { width: dynWidth },
1443
1584
  children: [
1444
- /* @__PURE__ */ jsx13(
1585
+ /* @__PURE__ */ jsx14(
1445
1586
  DragHandle,
1446
1587
  {
1447
1588
  edge: "left",
@@ -1461,24 +1602,24 @@ function ShellBody({
1461
1602
  sidebar
1462
1603
  }) {
1463
1604
  return /* @__PURE__ */ jsxs9("div", { className: "flex flex-1 overflow-hidden", children: [
1464
- sidebar ?? /* @__PURE__ */ jsx13(ShellRail, {}),
1465
- /* @__PURE__ */ jsx13(SidePane, {}),
1466
- sidePaneFullWidth !== true && /* @__PURE__ */ jsx13("div", { className: "flex-1 min-w-0 h-full overflow-hidden", children: /* @__PURE__ */ jsx13(ContentArea, { layout: activeService?.layout }) })
1605
+ sidebar ?? /* @__PURE__ */ jsx14(ShellRail, {}),
1606
+ /* @__PURE__ */ jsx14(SidePane, {}),
1607
+ sidePaneFullWidth !== true && /* @__PURE__ */ jsx14("div", { className: "flex-1 min-w-0 h-full overflow-hidden", children: /* @__PURE__ */ jsx14(ContentArea, { layout: activeService?.layout }) })
1467
1608
  ] });
1468
1609
  }
1469
1610
  function ContentArea({ layout }) {
1470
1611
  if (layout === "full") {
1471
- return /* @__PURE__ */ jsx13("div", { className: "h-full w-full overflow-hidden", children: /* @__PURE__ */ jsx13(Outlet, {}) });
1612
+ return /* @__PURE__ */ jsx14("div", { className: "h-full w-full overflow-hidden", children: /* @__PURE__ */ jsx14(Outlet, {}) });
1472
1613
  }
1473
- return /* @__PURE__ */ jsx13(ScrollArea2, { className: "h-full", children: /* @__PURE__ */ jsx13("main", { className: "p-6", children: /* @__PURE__ */ jsx13(Outlet, {}) }) });
1614
+ return /* @__PURE__ */ jsx14(ScrollArea2, { className: "h-full", children: /* @__PURE__ */ jsx14("main", { className: "p-6", children: /* @__PURE__ */ jsx14(Outlet, {}) }) });
1474
1615
  }
1475
1616
  function PanelContent({ state }) {
1476
- return /* @__PURE__ */ jsxs9(Fragment4, { children: [
1617
+ return /* @__PURE__ */ jsxs9(Fragment5, { children: [
1477
1618
  state.title ? /* @__PURE__ */ jsxs9(SheetHeader, { children: [
1478
- /* @__PURE__ */ jsx13(SheetTitle, { children: state.title }),
1479
- state.description && /* @__PURE__ */ jsx13(SheetDescription, { children: state.description })
1480
- ] }) : /* @__PURE__ */ jsx13(SheetTitle, { className: "sr-only", children: "Panel" }),
1481
- /* @__PURE__ */ jsx13(SheetBody, { children: state.content })
1619
+ /* @__PURE__ */ jsx14(SheetTitle, { children: state.title }),
1620
+ state.description && /* @__PURE__ */ jsx14(SheetDescription, { children: state.description })
1621
+ ] }) : /* @__PURE__ */ jsx14(SheetTitle, { className: "sr-only", children: "Panel" }),
1622
+ /* @__PURE__ */ jsx14(SheetBody, { children: state.content })
1482
1623
  ] });
1483
1624
  }
1484
1625
 
@@ -1486,36 +1627,36 @@ function PanelContent({ state }) {
1486
1627
  import { Toaster } from "sonner";
1487
1628
 
1488
1629
  // src/ShellConfigProvider.tsx
1489
- import { jsx as jsx14 } from "react/jsx-runtime";
1630
+ import { jsx as jsx15 } from "react/jsx-runtime";
1490
1631
  function ShellConfigProvider({
1491
1632
  config,
1492
1633
  children
1493
1634
  }) {
1494
- return /* @__PURE__ */ jsx14(ShellConfigContext.Provider, { value: config, children });
1635
+ return /* @__PURE__ */ jsx15(ShellConfigContext.Provider, { value: config, children });
1495
1636
  }
1496
1637
 
1497
1638
  // src/RootLayout.tsx
1498
- import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
1639
+ import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
1499
1640
  function RootLayout({
1500
1641
  config,
1501
1642
  registry,
1502
1643
  sidebar
1503
1644
  }) {
1504
1645
  return /* @__PURE__ */ jsxs10(ShellConfigProvider, { config, children: [
1505
- /* @__PURE__ */ jsx15(Toaster, { position: "top-right", richColors: true, closeButton: true }),
1506
- /* @__PURE__ */ jsx15(AppShell, { registry, sidebar })
1646
+ /* @__PURE__ */ jsx16(Toaster, { position: "top-right", richColors: true, closeButton: true }),
1647
+ /* @__PURE__ */ jsx16(AppShell, { registry, sidebar })
1507
1648
  ] });
1508
1649
  }
1509
1650
 
1510
1651
  // src/PlaceholderPage.tsx
1511
1652
  import { useLocation as useLocation3 } from "react-router-dom";
1512
- import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
1653
+ import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
1513
1654
  function PlaceholderPage() {
1514
1655
  const { pathname } = useLocation3();
1515
1656
  return /* @__PURE__ */ jsxs11("div", { children: [
1516
- /* @__PURE__ */ jsx16("h1", { className: "mb-1", children: pathToTitle(pathname) }),
1517
- /* @__PURE__ */ jsx16("p", { className: "text-sm text-muted-foreground mb-6", children: pathname }),
1518
- /* @__PURE__ */ jsx16("div", { className: "rounded-lg border-2 border-dashed border-border p-12 flex items-center justify-center", children: /* @__PURE__ */ jsx16("span", { className: "text-sm text-muted-foreground", children: "Content area \u2014 not yet implemented" }) })
1657
+ /* @__PURE__ */ jsx17("h1", { className: "mb-1", children: pathToTitle(pathname) }),
1658
+ /* @__PURE__ */ jsx17("p", { className: "text-sm text-muted-foreground mb-6", children: pathname }),
1659
+ /* @__PURE__ */ jsx17("div", { className: "rounded-lg border-2 border-dashed border-border p-12 flex items-center justify-center", children: /* @__PURE__ */ jsx17("span", { className: "text-sm text-muted-foreground", children: "Content area \u2014 not yet implemented" }) })
1519
1660
  ] });
1520
1661
  }
1521
1662
  function pathToTitle(path) {
@@ -1524,46 +1665,148 @@ function pathToTitle(path) {
1524
1665
  return last.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1525
1666
  }
1526
1667
 
1527
- // src/Sidebar.tsx
1668
+ // src/Breadcrumbs.tsx
1669
+ import { useContext as useContext5 } from "react";
1670
+ import { Link as Link2 } from "react-router-dom";
1671
+ import { ChevronRight as ChevronRight2 } from "lucide-react";
1528
1672
  import { cn as cn6 } from "@petrarca/sonnet-core";
1673
+ import { jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
1674
+ var ELLIPSIS = { label: "\u2026" };
1675
+ function useTrail({
1676
+ items,
1677
+ omitModule,
1678
+ maxItems
1679
+ }) {
1680
+ const navigation2 = useContext5(ShellNavigationContext);
1681
+ const module = navigation2?.activeService;
1682
+ const root = !omitModule && module ? [{ label: module.label, to: module.basePath }] : [];
1683
+ const trail = [...root, ...items];
1684
+ if (trail.length <= maxItems || maxItems < 3) return trail;
1685
+ return [trail[0], ELLIPSIS, ...trail.slice(trail.length - 2)];
1686
+ }
1687
+ function titleOf(label) {
1688
+ return typeof label === "string" ? label : void 0;
1689
+ }
1690
+ function Crumb({
1691
+ item,
1692
+ current
1693
+ }) {
1694
+ const text = /* @__PURE__ */ jsx18("span", { className: "truncate max-w-[18rem]", children: item.label });
1695
+ if (current || !item.to) {
1696
+ return /* @__PURE__ */ jsx18(
1697
+ "span",
1698
+ {
1699
+ className: cn6(
1700
+ "inline-flex min-w-0",
1701
+ current ? "text-foreground font-medium" : "text-muted-foreground"
1702
+ ),
1703
+ "aria-current": current ? "page" : void 0,
1704
+ title: titleOf(item.label),
1705
+ children: text
1706
+ }
1707
+ );
1708
+ }
1709
+ return /* @__PURE__ */ jsx18(
1710
+ Link2,
1711
+ {
1712
+ to: item.to,
1713
+ className: "inline-flex min-w-0 text-muted-foreground hover:text-foreground transition-colors",
1714
+ title: titleOf(item.label),
1715
+ children: text
1716
+ }
1717
+ );
1718
+ }
1719
+ function Breadcrumbs({
1720
+ items,
1721
+ omitModule = false,
1722
+ maxItems = 5,
1723
+ className
1724
+ }) {
1725
+ const trail = useTrail({ items, omitModule, maxItems });
1726
+ return /* @__PURE__ */ jsx18("nav", { "aria-label": "Breadcrumb", className, children: /* @__PURE__ */ jsx18("ol", { className: "flex items-center gap-1.5 text-sm min-w-0", children: trail.map((item, index) => {
1727
+ const current = index === trail.length - 1;
1728
+ return /* @__PURE__ */ jsxs12(
1729
+ "li",
1730
+ {
1731
+ className: "inline-flex items-center gap-1.5 min-w-0",
1732
+ children: [
1733
+ /* @__PURE__ */ jsx18(Crumb, { item, current }),
1734
+ !current && /* @__PURE__ */ jsx18(
1735
+ ChevronRight2,
1736
+ {
1737
+ className: "h-3.5 w-3.5 shrink-0 text-muted-foreground",
1738
+ "aria-hidden": true
1739
+ }
1740
+ )
1741
+ ]
1742
+ },
1743
+ index
1744
+ );
1745
+ }) }) });
1746
+ }
1747
+
1748
+ // src/DrillDownHeader.tsx
1749
+ import { cn as cn7 } from "@petrarca/sonnet-core";
1750
+ import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
1751
+ function DrillDownHeader({
1752
+ breadcrumbs,
1753
+ title,
1754
+ adornment,
1755
+ actions,
1756
+ subtitle,
1757
+ className
1758
+ }) {
1759
+ return /* @__PURE__ */ jsxs13("header", { className: cn7("mb-4", className), children: [
1760
+ /* @__PURE__ */ jsx19(Breadcrumbs, { items: breadcrumbs, className: "mb-3" }),
1761
+ /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-3 mb-1 min-w-0", children: [
1762
+ /* @__PURE__ */ jsx19("h1", { className: "truncate", children: title }),
1763
+ adornment,
1764
+ actions && /* @__PURE__ */ jsx19("div", { className: "ml-auto flex items-center gap-2 shrink-0", children: actions })
1765
+ ] }),
1766
+ subtitle && /* @__PURE__ */ jsx19("p", { className: "text-xs text-muted-foreground", children: subtitle })
1767
+ ] });
1768
+ }
1769
+
1770
+ // src/Sidebar.tsx
1771
+ import { cn as cn8 } from "@petrarca/sonnet-core";
1529
1772
  import { ScrollArea as ScrollArea3 } from "@petrarca/sonnet-ui";
1530
- import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
1773
+ import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
1531
1774
  function Sidebar({
1532
1775
  children,
1533
1776
  header,
1534
1777
  width = 260,
1535
1778
  className
1536
1779
  }) {
1537
- return /* @__PURE__ */ jsxs12(
1780
+ return /* @__PURE__ */ jsxs14(
1538
1781
  "aside",
1539
1782
  {
1540
1783
  style: { width },
1541
- className: cn6(
1784
+ className: cn8(
1542
1785
  "shrink-0 border-r bg-muted/30 flex flex-col h-full",
1543
1786
  className
1544
1787
  ),
1545
1788
  children: [
1546
1789
  header,
1547
- /* @__PURE__ */ jsx17(ScrollArea3, { className: "flex-1", children: /* @__PURE__ */ jsx17("nav", { className: "px-1.5 py-2 space-y-3", children }) })
1790
+ /* @__PURE__ */ jsx20(ScrollArea3, { className: "flex-1", children: /* @__PURE__ */ jsx20("nav", { className: "px-1.5 py-2 space-y-3", children }) })
1548
1791
  ]
1549
1792
  }
1550
1793
  );
1551
1794
  }
1552
1795
 
1553
1796
  // src/ShellSidebar.tsx
1554
- import React5, { useMemo as useMemo6 } from "react";
1555
- import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
1797
+ import React8, { useMemo as useMemo5 } from "react";
1798
+ import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
1556
1799
  function ShellSidebar({
1557
1800
  bottomGroupLabel = "Tools",
1558
1801
  ...sidebarProps
1559
1802
  }) {
1560
1803
  const { mainModules, bottomModules } = useShellModules();
1561
1804
  const { resolveCapability, capabilityPolicy } = useShellConfig();
1562
- const policy = useMemo6(
1805
+ const policy = useMemo5(
1563
1806
  () => resolveCapabilityPolicy(capabilityPolicy),
1564
1807
  [capabilityPolicy]
1565
1808
  );
1566
- const effectOf = React5.useCallback(
1809
+ const effectOf = React8.useCallback(
1567
1810
  (moduleId) => {
1568
1811
  const module = [...mainModules, ...bottomModules].find(
1569
1812
  (m) => m.id === moduleId
@@ -1584,8 +1827,8 @@ function ShellSidebar({
1584
1827
  reason
1585
1828
  }));
1586
1829
  });
1587
- return /* @__PURE__ */ jsxs13(Sidebar, { ...sidebarProps, children: [
1588
- topNavItems.length > 0 && /* @__PURE__ */ jsx18(SidebarGroup, { separator: true, children: topNavItems.map(({ link, icon, disabled, reason }) => /* @__PURE__ */ jsx18(
1830
+ return /* @__PURE__ */ jsxs15(Sidebar, { ...sidebarProps, children: [
1831
+ topNavItems.length > 0 && /* @__PURE__ */ jsx21(SidebarGroup, { separator: true, children: topNavItems.map(({ link, icon, disabled, reason }) => /* @__PURE__ */ jsx21(
1589
1832
  SidebarItem,
1590
1833
  {
1591
1834
  icon,
@@ -1600,63 +1843,21 @@ function ShellSidebar({
1600
1843
  mainModules.map((mod) => {
1601
1844
  const { effect, reason } = effectOf(mod.id);
1602
1845
  if (effect === "hidden" || effect === "redirect") return null;
1603
- const disabled = effect === "disabled";
1604
- const title = disabled ? reason : void 0;
1605
- if (mod.navigation.length === 0) {
1606
- return /* @__PURE__ */ jsx18(
1607
- SidebarItem,
1608
- {
1609
- icon: mod.icon,
1610
- label: mod.label,
1611
- path: mod.basePath,
1612
- disabled,
1613
- title
1614
- },
1615
- mod.id
1616
- );
1617
- }
1618
- return /* @__PURE__ */ jsx18(SidebarGroup, { heading: mod.label, collapsible: true, children: mod.navigation.flatMap((group) => [
1619
- // Render NavGroup heading if it has one
1620
- ...group.heading ? [
1621
- /* @__PURE__ */ jsx18(
1622
- SidebarGroup,
1623
- {
1624
- heading: group.heading,
1625
- collapsible: group.collapsible,
1626
- children: group.links.map((link) => /* @__PURE__ */ jsx18(
1627
- SidebarItem,
1628
- {
1629
- icon: mod.icon,
1630
- label: link.label,
1631
- path: link.path,
1632
- badge: link.badge,
1633
- disabled,
1634
- title: disabled ? reason : void 0
1635
- },
1636
- link.path
1637
- ))
1638
- },
1639
- group.id
1640
- )
1641
- ] : group.links.map((link) => /* @__PURE__ */ jsx18(
1642
- SidebarItem,
1643
- {
1644
- icon: mod.icon,
1645
- label: link.label,
1646
- path: link.path,
1647
- badge: link.badge,
1648
- disabled,
1649
- title: disabled ? reason : void 0
1650
- },
1651
- link.path
1652
- ))
1653
- ]) }, mod.id);
1846
+ return /* @__PURE__ */ jsx21(
1847
+ ModuleSection,
1848
+ {
1849
+ mod,
1850
+ disabled: effect === "disabled",
1851
+ reason
1852
+ },
1853
+ mod.id
1854
+ );
1654
1855
  }),
1655
- bottomModules.length > 0 && /* @__PURE__ */ jsx18(SidebarGroup, { heading: bottomGroupLabel, collapsible: true, children: bottomModules.map((mod) => {
1856
+ bottomModules.length > 0 && /* @__PURE__ */ jsx21(SidebarGroup, { heading: bottomGroupLabel, collapsible: true, children: bottomModules.map((mod) => {
1656
1857
  const { effect, reason } = effectOf(mod.id);
1657
1858
  if (effect === "hidden" || effect === "redirect") return null;
1658
1859
  const disabled = effect === "disabled";
1659
- return /* @__PURE__ */ jsx18(
1860
+ return /* @__PURE__ */ jsx21(
1660
1861
  SidebarItem,
1661
1862
  {
1662
1863
  icon: mod.icon,
@@ -1670,16 +1871,61 @@ function ShellSidebar({
1670
1871
  }) })
1671
1872
  ] });
1672
1873
  }
1874
+ function ModuleSection({
1875
+ mod,
1876
+ disabled,
1877
+ reason
1878
+ }) {
1879
+ const navigation2 = useModuleNavigation(mod);
1880
+ const title = disabled ? reason : void 0;
1881
+ if (navigation2.length === 0) {
1882
+ return /* @__PURE__ */ jsx21(
1883
+ SidebarItem,
1884
+ {
1885
+ icon: mod.icon,
1886
+ label: mod.label,
1887
+ path: mod.basePath,
1888
+ disabled,
1889
+ title
1890
+ }
1891
+ );
1892
+ }
1893
+ return /* @__PURE__ */ jsx21(SidebarGroup, { heading: mod.label, collapsible: true, children: navigation2.flatMap((group) => {
1894
+ const items = group.links.map((link) => /* @__PURE__ */ jsx21(
1895
+ SidebarItem,
1896
+ {
1897
+ icon: mod.icon,
1898
+ label: link.label,
1899
+ path: link.path,
1900
+ badge: link.badge,
1901
+ disabled,
1902
+ title
1903
+ },
1904
+ link.path
1905
+ ));
1906
+ return group.heading ? [
1907
+ /* @__PURE__ */ jsx21(
1908
+ SidebarGroup,
1909
+ {
1910
+ heading: group.heading,
1911
+ collapsible: group.collapsible,
1912
+ children: items
1913
+ },
1914
+ group.id
1915
+ )
1916
+ ] : items;
1917
+ }) });
1918
+ }
1673
1919
 
1674
1920
  // src/ShellVersion.tsx
1675
- import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
1921
+ import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
1676
1922
  function ShellVersion({
1677
1923
  name,
1678
1924
  version
1679
1925
  }) {
1680
- return /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2", children: [
1681
- /* @__PURE__ */ jsx19("span", { children: name }),
1682
- version && /* @__PURE__ */ jsxs14("span", { className: "text-muted-foreground/50", children: [
1926
+ return /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2", children: [
1927
+ /* @__PURE__ */ jsx22("span", { children: name }),
1928
+ version && /* @__PURE__ */ jsxs16("span", { className: "text-tertiary-foreground", children: [
1683
1929
  "v",
1684
1930
  version
1685
1931
  ] })
@@ -1689,9 +1935,9 @@ function ShellVersion({
1689
1935
  // src/SearchTrigger.tsx
1690
1936
  import { Search } from "lucide-react";
1691
1937
  import { Button as Button3 } from "@petrarca/sonnet-ui";
1692
- import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
1938
+ import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
1693
1939
  function SearchTrigger() {
1694
- return /* @__PURE__ */ jsxs15(
1940
+ return /* @__PURE__ */ jsxs17(
1695
1941
  Button3,
1696
1942
  {
1697
1943
  variant: "outline",
@@ -1699,10 +1945,10 @@ function SearchTrigger() {
1699
1945
  className: "h-8 gap-2 text-sm text-muted-foreground w-56 justify-start",
1700
1946
  onClick: () => navigation.openCommandMenu(),
1701
1947
  children: [
1702
- /* @__PURE__ */ jsx20(Search, { className: "h-4 w-4" }),
1703
- /* @__PURE__ */ jsx20("span", { children: "Search..." }),
1704
- /* @__PURE__ */ jsxs15("kbd", { className: "ml-auto pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground", children: [
1705
- /* @__PURE__ */ jsx20("span", { className: "text-xs", children: "\u2318" }),
1948
+ /* @__PURE__ */ jsx23(Search, { className: "h-4 w-4" }),
1949
+ /* @__PURE__ */ jsx23("span", { children: "Search..." }),
1950
+ /* @__PURE__ */ jsxs17("kbd", { className: "ml-auto pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground", children: [
1951
+ /* @__PURE__ */ jsx23("span", { className: "text-xs", children: "\u2318" }),
1706
1952
  "K"
1707
1953
  ] })
1708
1954
  ]
@@ -1710,9 +1956,32 @@ function SearchTrigger() {
1710
1956
  );
1711
1957
  }
1712
1958
 
1959
+ // src/ThemeToggle.tsx
1960
+ import { Moon, Sun } from "lucide-react";
1961
+ import { Button as Button4 } from "@petrarca/sonnet-ui";
1962
+ import { useTheme } from "@petrarca/sonnet-core/hooks";
1963
+ import { jsx as jsx24 } from "react/jsx-runtime";
1964
+ function ThemeToggle() {
1965
+ const { theme, setTheme } = useTheme();
1966
+ const dark = theme === "dark";
1967
+ const label = dark ? "Switch to light mode" : "Switch to dark mode";
1968
+ return /* @__PURE__ */ jsx24(
1969
+ Button4,
1970
+ {
1971
+ variant: "ghost",
1972
+ size: "icon",
1973
+ className: "h-8 w-8",
1974
+ "aria-label": label,
1975
+ title: label,
1976
+ onClick: () => setTheme(dark ? "light" : "dark"),
1977
+ children: dark ? /* @__PURE__ */ jsx24(Sun, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx24(Moon, { className: "h-4 w-4" })
1978
+ }
1979
+ );
1980
+ }
1981
+
1713
1982
  // src/UserMenu.tsx
1714
1983
  import { LogOut, User } from "lucide-react";
1715
- import { Button as Button4 } from "@petrarca/sonnet-ui";
1984
+ import { Button as Button5 } from "@petrarca/sonnet-ui";
1716
1985
  import { Avatar, AvatarFallback } from "@petrarca/sonnet-ui";
1717
1986
  import {
1718
1987
  DropdownMenu,
@@ -1722,23 +1991,23 @@ import {
1722
1991
  DropdownMenuSeparator,
1723
1992
  DropdownMenuTrigger
1724
1993
  } from "@petrarca/sonnet-ui";
1725
- import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
1994
+ import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
1726
1995
  function UserMenu({ user, onSignOut }) {
1727
- return /* @__PURE__ */ jsxs16(DropdownMenu, { children: [
1728
- /* @__PURE__ */ jsx21(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx21(Button4, { variant: "ghost", size: "compact", className: "rounded-full", children: /* @__PURE__ */ jsx21(Avatar, { className: "h-7 w-7", children: /* @__PURE__ */ jsx21(AvatarFallback, { className: "text-xs", children: user.initials }) }) }) }),
1729
- /* @__PURE__ */ jsxs16(DropdownMenuContent, { align: "end", className: "w-48", children: [
1730
- /* @__PURE__ */ jsx21(DropdownMenuLabel, { className: "font-normal", children: /* @__PURE__ */ jsxs16("div", { className: "flex flex-col gap-1", children: [
1731
- /* @__PURE__ */ jsx21("p", { className: "text-sm font-medium", children: user.name }),
1732
- /* @__PURE__ */ jsx21("p", { className: "text-xs text-muted-foreground", children: user.email })
1996
+ return /* @__PURE__ */ jsxs18(DropdownMenu, { children: [
1997
+ /* @__PURE__ */ jsx25(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx25(Button5, { variant: "ghost", size: "compact", className: "rounded-full", children: /* @__PURE__ */ jsx25(Avatar, { className: "h-7 w-7", children: /* @__PURE__ */ jsx25(AvatarFallback, { className: "text-xs", children: user.initials }) }) }) }),
1998
+ /* @__PURE__ */ jsxs18(DropdownMenuContent, { align: "end", className: "w-48", children: [
1999
+ /* @__PURE__ */ jsx25(DropdownMenuLabel, { className: "font-normal", children: /* @__PURE__ */ jsxs18("div", { className: "flex flex-col gap-1", children: [
2000
+ /* @__PURE__ */ jsx25("p", { className: "text-sm font-medium", children: user.name }),
2001
+ /* @__PURE__ */ jsx25("p", { className: "text-xs text-muted-foreground", children: user.email })
1733
2002
  ] }) }),
1734
- /* @__PURE__ */ jsx21(DropdownMenuSeparator, {}),
1735
- /* @__PURE__ */ jsxs16(DropdownMenuItem, { children: [
1736
- /* @__PURE__ */ jsx21(User, { className: "mr-2 h-4 w-4" }),
2003
+ /* @__PURE__ */ jsx25(DropdownMenuSeparator, {}),
2004
+ /* @__PURE__ */ jsxs18(DropdownMenuItem, { children: [
2005
+ /* @__PURE__ */ jsx25(User, { className: "mr-2 h-4 w-4" }),
1737
2006
  "Profile"
1738
2007
  ] }),
1739
- /* @__PURE__ */ jsx21(DropdownMenuSeparator, {}),
1740
- /* @__PURE__ */ jsxs16(DropdownMenuItem, { onSelect: onSignOut, children: [
1741
- /* @__PURE__ */ jsx21(LogOut, { className: "mr-2 h-4 w-4" }),
2008
+ /* @__PURE__ */ jsx25(DropdownMenuSeparator, {}),
2009
+ /* @__PURE__ */ jsxs18(DropdownMenuItem, { onSelect: onSignOut, children: [
2010
+ /* @__PURE__ */ jsx25(LogOut, { className: "mr-2 h-4 w-4" }),
1742
2011
  "Sign out"
1743
2012
  ] })
1744
2013
  ] })
@@ -1750,7 +2019,6 @@ function createModuleRegistry(modules) {
1750
2019
  const visible = modules.filter((m) => !m.hidden);
1751
2020
  const mainModules = visible.filter((m) => !m.pinBottom);
1752
2021
  const bottomModules = visible.filter((m) => m.pinBottom);
1753
- const allRoutes = modules.flatMap((m) => m.routes);
1754
2022
  const contributionsByPoint = /* @__PURE__ */ new Map();
1755
2023
  for (const m of modules) {
1756
2024
  for (const c of m.contributions ?? []) {
@@ -1765,17 +2033,57 @@ function createModuleRegistry(modules) {
1765
2033
  function getExtensionPoint(name) {
1766
2034
  return contributionsByPoint.get(name) ?? [];
1767
2035
  }
2036
+ const allRoutes = nestContributedRoutes(
2037
+ modules.flatMap((m) => m.routes),
2038
+ contributionsByPoint
2039
+ );
1768
2040
  return { modules, mainModules, bottomModules, allRoutes, getExtensionPoint };
1769
2041
  }
2042
+ function nestContributedRoutes(routes, contributionsByPoint) {
2043
+ const hosted = /* @__PURE__ */ new Set();
2044
+ function nest(route) {
2045
+ if (route.index) return route;
2046
+ const pages = (contributionsByPoint.get(route.id ?? "") ?? []).filter(
2047
+ (c) => c.element !== void 0
2048
+ );
2049
+ const children = route.children?.map(nest);
2050
+ if (pages.length === 0) {
2051
+ return children ? { ...route, children } : route;
2052
+ }
2053
+ hosted.add(route.id);
2054
+ const contributed = pages.map((c) => ({
2055
+ path: c.path,
2056
+ element: c.element
2057
+ }));
2058
+ return { ...route, children: [...children ?? [], ...contributed] };
2059
+ }
2060
+ const nested = routes.map(nest);
2061
+ for (const [point, list] of contributionsByPoint) {
2062
+ if (!hosted.has(point) && list.some((c) => c.element !== void 0)) {
2063
+ throw new Error(
2064
+ `Pages are contributed to extension point "${point}", but no route has id "${point}" to host them.`
2065
+ );
2066
+ }
2067
+ }
2068
+ return nested;
2069
+ }
2070
+
2071
+ // src/contributionPath.ts
2072
+ function contributionPath(path, params) {
2073
+ return path.replace(
2074
+ /:([A-Za-z_][A-Za-z0-9_]*)/g,
2075
+ (token, name) => name in params ? encodeURIComponent(params[name]) : token
2076
+ );
2077
+ }
1770
2078
 
1771
2079
  // src/OverviewCard.tsx
1772
- import { cn as cn7 } from "@petrarca/sonnet-core";
1773
- import { jsx as jsx22, jsxs as jsxs17 } from "react/jsx-runtime";
2080
+ import { cn as cn9 } from "@petrarca/sonnet-core";
2081
+ import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
1774
2082
  function FeatureLink({
1775
2083
  feature,
1776
2084
  children
1777
2085
  }) {
1778
- return /* @__PURE__ */ jsx22(
2086
+ return /* @__PURE__ */ jsx26(
1779
2087
  "span",
1780
2088
  {
1781
2089
  role: "link",
@@ -1801,37 +2109,51 @@ function OverviewCard({
1801
2109
  title,
1802
2110
  description,
1803
2111
  feature,
2112
+ path,
1804
2113
  className
1805
2114
  }) {
1806
- return /* @__PURE__ */ jsxs17(
2115
+ const go = feature ? () => navigation.goToFeature(feature) : path ? () => navigation.goTo(path) : void 0;
2116
+ return /* @__PURE__ */ jsxs19(
1807
2117
  "button",
1808
2118
  {
1809
- onClick: feature ? () => navigation.goToFeature(feature) : void 0,
1810
- disabled: !feature,
1811
- className: cn7(
2119
+ onClick: go,
2120
+ disabled: !go,
2121
+ className: cn9(
1812
2122
  "rounded-lg border bg-card p-5 text-left flex flex-col gap-3",
1813
2123
  "disabled:cursor-default",
1814
- feature && "hover:bg-accent/50 transition-colors",
2124
+ go && "hover:bg-accent/50 transition-colors",
1815
2125
  className
1816
2126
  ),
1817
2127
  children: [
1818
- /* @__PURE__ */ jsx22("div", { className: "h-9 w-9 rounded-md bg-blue-50 flex items-center justify-center shrink-0", children: /* @__PURE__ */ jsx22(Icon, { className: "h-5 w-5 text-blue-600" }) }),
1819
- /* @__PURE__ */ jsxs17("div", { children: [
1820
- /* @__PURE__ */ jsx22("p", { className: "text-sm font-medium mb-1", children: title }),
1821
- /* @__PURE__ */ jsx22("p", { className: "text-xs text-muted-foreground leading-relaxed", children: description })
2128
+ /* @__PURE__ */ jsx26("div", { className: "h-9 w-9 rounded-md bg-blue-50 flex items-center justify-center shrink-0", children: /* @__PURE__ */ jsx26(Icon, { className: "h-5 w-5 text-blue-600" }) }),
2129
+ /* @__PURE__ */ jsxs19("div", { children: [
2130
+ /* @__PURE__ */ jsx26("p", { className: "text-sm font-medium mb-1", children: title }),
2131
+ /* @__PURE__ */ jsx26("p", { className: "text-xs text-muted-foreground leading-relaxed", children: description })
1822
2132
  ] })
1823
2133
  ]
1824
2134
  }
1825
2135
  );
1826
2136
  }
2137
+ function OverviewGroupHeading({
2138
+ heading,
2139
+ description
2140
+ }) {
2141
+ return /* @__PURE__ */ jsxs19("div", { className: "mb-3", children: [
2142
+ /* @__PURE__ */ jsx26("h2", { className: "text-sm font-semibold text-foreground", children: heading }),
2143
+ description && /* @__PURE__ */ jsx26("p", { className: "text-sm text-muted-foreground", children: description })
2144
+ ] });
2145
+ }
1827
2146
  export {
1828
2147
  AppShell,
2148
+ Breadcrumbs,
1829
2149
  CommandMenu,
1830
2150
  ConfirmDialog,
1831
2151
  DEFAULT_CAPABILITY_POLICY,
2152
+ DrillDownHeader,
1832
2153
  FeatureLink,
1833
2154
  IconRail,
1834
2155
  OverviewCard,
2156
+ OverviewGroupHeading,
1835
2157
  PlaceholderPage,
1836
2158
  RailIcon,
1837
2159
  RailSeparator,
@@ -1848,8 +2170,10 @@ export {
1848
2170
  SidebarGroup,
1849
2171
  SidebarItem,
1850
2172
  SubNavPanel,
2173
+ ThemeToggle,
1851
2174
  TopBar,
1852
2175
  UserMenu,
2176
+ contributionPath,
1853
2177
  createModuleRegistry,
1854
2178
  dialog,
1855
2179
  evaluateModule,
@@ -1866,8 +2190,11 @@ export {
1866
2190
  panel,
1867
2191
  resolveCapabilityPolicy,
1868
2192
  sidePane,
2193
+ useCollapsedSubNav,
2194
+ useContributedComponent,
1869
2195
  useExtensionPoint,
1870
2196
  useMainModules,
2197
+ useModuleNavigation,
1871
2198
  useShellConfig,
1872
2199
  useShellEvent,
1873
2200
  useShellModules,