@petrarca/sonnet-shell 0.8.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,10 +1,10 @@
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
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";
@@ -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";
@@ -472,7 +528,7 @@ function SidebarGroup({
472
528
  separator = false,
473
529
  children
474
530
  }) {
475
- const [open, setOpen] = useState(defaultOpen);
531
+ const [open, setOpen] = useState2(defaultOpen);
476
532
  const canCollapse = collapsible && !!heading;
477
533
  return /* @__PURE__ */ jsxs3("div", { className: cn2(separator && "border-b border-border pb-2"), children: [
478
534
  heading && /* @__PURE__ */ jsx5(
@@ -586,27 +642,15 @@ function SidebarItem({
586
642
 
587
643
  // src/SubNavPanel.tsx
588
644
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
589
- function useMergedNavigation(service) {
590
- const contributions = useExtensionPoint(`${service.id}.nav`);
591
- return useMemo2(() => {
592
- if (contributions.length === 0) return service.navigation;
593
- const contributed = {
594
- id: `${service.id}-contributed`,
595
- links: contributions.map((c) => ({
596
- id: c.extensionPoint + "." + c.label.toLowerCase().replace(/\s+/g, "-"),
597
- label: c.label,
598
- path: c.path
599
- }))
600
- };
601
- return [...service.navigation, contributed];
602
- }, [service, contributions]);
645
+ function SubNavPanel(props) {
646
+ return /* @__PURE__ */ jsx7(ModuleSubNav, { ...props }, props.service.id);
603
647
  }
604
- function SubNavPanel({
648
+ function ModuleSubNav({
605
649
  service,
606
650
  collapsed,
607
651
  onToggleCollapse
608
652
  }) {
609
- const mergedNavigation = useMergedNavigation(service);
653
+ const mergedNavigation = useModuleNavigation(service);
610
654
  if (collapsed) {
611
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: [
612
656
  /* @__PURE__ */ jsx7(TooltipTrigger2, { asChild: true, children: /* @__PURE__ */ jsx7(
@@ -660,7 +704,7 @@ function SubNavPanel({
660
704
  }
661
705
 
662
706
  // src/shellNavigation.ts
663
- import { createContext as createContext2, useContext as useContext2 } from "react";
707
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect2 } from "react";
664
708
  var ShellNavigationContext = createContext2(null);
665
709
  function useShellNavigation() {
666
710
  const ctx = useContext2(ShellNavigationContext);
@@ -669,6 +713,13 @@ function useShellNavigation() {
669
713
  }
670
714
  return ctx;
671
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
+ }
672
723
 
673
724
  // src/shellConfig.ts
674
725
  import { createContext as createContext3, useContext as useContext3 } from "react";
@@ -754,7 +805,7 @@ function ShellRail() {
754
805
  onServiceSelect
755
806
  } = useShellNavigation();
756
807
  const isActive = (id) => activeService?.id === id || sidePaneModuleId === id;
757
- const policy = useMemo3(
808
+ const policy = useMemo2(
758
809
  () => resolveCapabilityPolicy(capabilityPolicy),
759
810
  [capabilityPolicy]
760
811
  );
@@ -901,7 +952,7 @@ function SidePaneOpen({
901
952
  }
902
953
 
903
954
  // src/CommandMenu.tsx
904
- import { useEffect as useEffect2, useCallback, useMemo as useMemo4 } from "react";
955
+ import { useEffect as useEffect3, useCallback, useMemo as useMemo3 } from "react";
905
956
  import { useNavigate } from "react-router-dom";
906
957
  import {
907
958
  CommandDialog,
@@ -917,7 +968,7 @@ function CommandMenu({ open, onOpenChange }) {
917
968
  const navigate = useNavigate();
918
969
  const { mainModules, bottomModules } = useShellModules();
919
970
  const { resolveCapability, capabilityPolicy } = useShellConfig();
920
- useEffect2(() => {
971
+ useEffect3(() => {
921
972
  const handleKeyDown = (e) => {
922
973
  if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
923
974
  e.preventDefault();
@@ -927,7 +978,7 @@ function CommandMenu({ open, onOpenChange }) {
927
978
  document.addEventListener("keydown", handleKeyDown);
928
979
  return () => document.removeEventListener("keydown", handleKeyDown);
929
980
  }, [open, onOpenChange]);
930
- const policy = useMemo4(
981
+ const policy = useMemo3(
931
982
  () => resolveCapabilityPolicy(capabilityPolicy),
932
983
  [capabilityPolicy]
933
984
  );
@@ -940,27 +991,18 @@ function CommandMenu({ open, onOpenChange }) {
940
991
  [navigate, onOpenChange]
941
992
  );
942
993
  const renderNavGroup = (service) => {
943
- if (service.navigation.length === 0) return null;
944
994
  const { effect, reason } = moduleEffect(service);
945
995
  if (effect === "hidden" || effect === "redirect") return null;
946
- const disabled = effect === "disabled";
947
- const Icon = service.icon;
948
- return /* @__PURE__ */ jsx11(CommandGroup, { heading: service.label, children: service.navigation.flatMap(
949
- (group) => group.links.map((link) => /* @__PURE__ */ jsxs8(
950
- CommandItem,
951
- {
952
- value: `${service.label} ${group.heading ?? ""} ${link.label}`,
953
- disabled,
954
- onSelect: disabled ? void 0 : () => handleSelect(link.path),
955
- children: [
956
- /* @__PURE__ */ jsx11(Icon, { className: "mr-2 h-4 w-4 text-muted-foreground" }),
957
- /* @__PURE__ */ jsx11("span", { children: link.label }),
958
- 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 })
959
- ]
960
- },
961
- link.path
962
- ))
963
- ) }, `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
+ );
964
1006
  };
965
1007
  const renderCommandGroup = (service) => {
966
1008
  if (!service.commands?.length) return null;
@@ -1017,12 +1059,38 @@ function CommandMenu({ open, onOpenChange }) {
1017
1059
  ] })
1018
1060
  ] });
1019
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
+ }
1020
1088
 
1021
1089
  // src/ModuleEffects.tsx
1022
- import React3 from "react";
1090
+ import React5 from "react";
1023
1091
  import { warnLog as warnLog4 } from "@petrarca/sonnet-core";
1024
1092
  import { Fragment as Fragment3, jsx as jsx12 } from "react/jsx-runtime";
1025
- var ModuleEffectBoundary = class extends React3.Component {
1093
+ var ModuleEffectBoundary = class extends React5.Component {
1026
1094
  state = { caught: false };
1027
1095
  static getDerivedStateFromError() {
1028
1096
  return { caught: true };
@@ -1046,8 +1114,64 @@ function ModuleEffects({ modules }) {
1046
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)) });
1047
1115
  }
1048
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
+
1049
1173
  // src/AppShell.tsx
1050
- 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";
1051
1175
  var PANEL_WIDTHS = {
1052
1176
  narrow: { css: "w-[480px]", px: 480 },
1053
1177
  default: { css: "w-[640px]", px: 640 },
@@ -1087,10 +1211,10 @@ function selectNavModule(service, navigate, setSubNavCollapsed, closeSidePane) {
1087
1211
  }
1088
1212
  function useShellApiInit(deps) {
1089
1213
  const latest = useRef2(deps);
1090
- useEffect3(() => {
1214
+ useEffect6(() => {
1091
1215
  latest.current = deps;
1092
1216
  });
1093
- useEffect3(() => {
1217
+ useEffect6(() => {
1094
1218
  initNavigation(
1095
1219
  (path) => latest.current.navigate(path),
1096
1220
  () => latest.current.openCommandMenu()
@@ -1120,7 +1244,7 @@ function useShellApiInit(deps) {
1120
1244
  }
1121
1245
  function useActiveService(modules) {
1122
1246
  const location = useLocation2();
1123
- const prefixToService = useMemo5(() => {
1247
+ const prefixToService = useMemo4(() => {
1124
1248
  const pairs = [];
1125
1249
  for (const m of modules) {
1126
1250
  if (m.hidden) continue;
@@ -1132,7 +1256,7 @@ function useActiveService(modules) {
1132
1256
  pairs.sort((a, b) => b[0].length - a[0].length);
1133
1257
  return pairs;
1134
1258
  }, [modules]);
1135
- const resolveServiceFromPath = useCallback2(
1259
+ const resolveServiceFromPath = useCallback3(
1136
1260
  (pathname) => {
1137
1261
  for (const [prefix, id] of prefixToService) {
1138
1262
  if (pathname.startsWith(prefix)) return id;
@@ -1141,10 +1265,10 @@ function useActiveService(modules) {
1141
1265
  },
1142
1266
  [prefixToService]
1143
1267
  );
1144
- const [selectedServiceId, setSelectedServiceId] = useState2(
1268
+ const [selectedServiceId, setSelectedServiceId] = useState4(
1145
1269
  () => resolveServiceFromPath(location.pathname)
1146
1270
  );
1147
- useEffect3(
1271
+ useEffect6(
1148
1272
  () => setSelectedServiceId(resolveServiceFromPath(location.pathname)),
1149
1273
  [location.pathname, resolveServiceFromPath]
1150
1274
  );
@@ -1152,26 +1276,26 @@ function useActiveService(modules) {
1152
1276
  return { activeService, selectedServiceId, setSelectedServiceId };
1153
1277
  }
1154
1278
  function useSidePaneState2() {
1155
- const [sidePaneState, setSidePaneState] = useState2(
1279
+ const [sidePaneState, setSidePaneState] = useState4(
1156
1280
  null
1157
1281
  );
1158
- const handleOpen = useCallback2(
1282
+ const handleOpen = useCallback3(
1159
1283
  (opts) => setSidePaneState(opts),
1160
1284
  []
1161
1285
  );
1162
- const handleClose = useCallback2(() => setSidePaneState(null), []);
1163
- const handleIsOpen = useCallback2(
1286
+ const handleClose = useCallback3(() => setSidePaneState(null), []);
1287
+ const handleIsOpen = useCallback3(
1164
1288
  () => sidePaneState !== null,
1165
1289
  [sidePaneState]
1166
1290
  );
1167
- const handleActiveModuleId = useCallback2(
1291
+ const handleActiveModuleId = useCallback3(
1168
1292
  () => sidePaneState?.moduleId ?? null,
1169
1293
  [sidePaneState]
1170
1294
  );
1171
- const handleSetFullWidth = useCallback2((fullWidth) => {
1295
+ const handleSetFullWidth = useCallback3((fullWidth) => {
1172
1296
  setSidePaneState((prev) => prev ? { ...prev, fullWidth } : prev);
1173
1297
  }, []);
1174
- const handleToggle = useCallback2((opts) => {
1298
+ const handleToggle = useCallback3((opts) => {
1175
1299
  setSidePaneState(
1176
1300
  (prev) => prev?.moduleId === opts.moduleId ? null : opts
1177
1301
  );
@@ -1207,13 +1331,13 @@ function resizeOptionsFromPanel(state, widthEntry) {
1207
1331
  };
1208
1332
  }
1209
1333
  function usePanelState() {
1210
- const [panelState, setPanelState] = useState2(null);
1334
+ const [panelState, setPanelState] = useState4(null);
1211
1335
  const panelOnCloseRef = useRef2(void 0);
1212
- const handleOpen = useCallback2(
1336
+ const handleOpen = useCallback3(
1213
1337
  (opts) => setPanelState(opts),
1214
1338
  []
1215
1339
  );
1216
- const handleClose = useCallback2(() => {
1340
+ const handleClose = useCallback3(() => {
1217
1341
  panelOnCloseRef.current = panelState?.onClose;
1218
1342
  setPanelState(null);
1219
1343
  }, [panelState]);
@@ -1231,49 +1355,57 @@ function usePanelState() {
1231
1355
  };
1232
1356
  }
1233
1357
  function useDialogState() {
1234
- const [dialogState, setDialogState] = useState2(null);
1235
- const handleConfirm = useCallback2(() => {
1358
+ const [dialogState, setDialogState] = useState4(null);
1359
+ const handleConfirm = useCallback3(() => {
1236
1360
  dialogState?.resolve(true);
1237
1361
  setDialogState(null);
1238
1362
  }, [dialogState]);
1239
- const handleCancel = useCallback2(() => {
1363
+ const handleCancel = useCallback3(() => {
1240
1364
  dialogState?.resolve(false);
1241
1365
  setDialogState(null);
1242
1366
  }, [dialogState]);
1243
1367
  return { dialogState, setDialogState, handleConfirm, handleCancel };
1244
1368
  }
1245
- function useFeatureLookup(modules) {
1246
- return useMemo5(() => {
1247
- const map = /* @__PURE__ */ new Map();
1369
+ function useFeatureLookup(modules, record) {
1370
+ return useMemo4(() => {
1371
+ const declared = /* @__PURE__ */ new Map();
1248
1372
  for (const m of modules) {
1249
1373
  for (const group of m.navigation) {
1250
1374
  for (const link of group.links) {
1251
- if (link.id) map.set(link.id, link.path);
1375
+ if (link.id) declared.set(link.id, link.path);
1252
1376
  }
1253
1377
  }
1254
1378
  }
1255
- return (featureId) => map.get(featureId) ?? null;
1256
- }, [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]);
1257
1387
  }
1258
1388
  function AppShell({ registry, sidebar }) {
1259
1389
  const navigate = useNavigate2();
1260
1390
  const config = useShellConfig();
1261
1391
  const { modules } = registry;
1262
1392
  const { activeService, setSelectedServiceId } = useActiveService(modules);
1263
- const [subNavCollapsed, setSubNavCollapsed] = useState2(false);
1264
1393
  const isBelowTablet = useMediaQuery("(max-width: 1023px)");
1265
- const [commandMenuOpen, setCommandMenuOpen] = useState2(false);
1266
- const openCommandMenu = useCallback2(() => setCommandMenuOpen(true), []);
1267
- const [fullscreenState, setFullscreenState] = useState2(null);
1268
- const handleFullscreenEnter = useCallback2(
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(
1269
1400
  (opts) => setFullscreenState(opts),
1270
1401
  []
1271
1402
  );
1272
- const handleFullscreenExit = useCallback2(() => setFullscreenState(null), []);
1403
+ const handleFullscreenExit = useCallback3(() => setFullscreenState(null), []);
1273
1404
  const sidePane2 = useSidePaneState2();
1274
1405
  const panel2 = usePanelState();
1275
1406
  const dialog2 = useDialogState();
1276
- const featureLookup = useFeatureLookup(modules);
1407
+ const featureRecord = useRef2(/* @__PURE__ */ new Map()).current;
1408
+ const featureLookup = useFeatureLookup(modules, featureRecord);
1277
1409
  useShellApiInit({
1278
1410
  navigate,
1279
1411
  openCommandMenu,
@@ -1289,7 +1421,7 @@ function AppShell({ registry, sidebar }) {
1289
1421
  setDialogState: dialog2.setDialogState,
1290
1422
  featureLookup
1291
1423
  });
1292
- const handleServiceSelect = useCallback2(
1424
+ const handleServiceSelect = useCallback3(
1293
1425
  (serviceId) => {
1294
1426
  const service = modules.find((s) => s.id === serviceId);
1295
1427
  if (!service) return;
@@ -1323,7 +1455,7 @@ function AppShell({ registry, sidebar }) {
1323
1455
  setSubNavCollapsed
1324
1456
  ]
1325
1457
  );
1326
- const sidePaneContextValue = useMemo5(
1458
+ const sidePaneContextValue = useMemo4(
1327
1459
  () => ({
1328
1460
  pane: sidePane2.sidePaneState,
1329
1461
  open: sidePane2.handleOpen,
@@ -1342,22 +1474,24 @@ function AppShell({ registry, sidebar }) {
1342
1474
  const navigationValue = {
1343
1475
  activeService,
1344
1476
  sidePaneModuleId: sidePane2.sidePaneState?.moduleId ?? null,
1345
- subNavCollapsed: subNavCollapsed || isBelowTablet,
1346
- onToggleSubNav: () => setSubNavCollapsed((c) => !c),
1477
+ subNavCollapsed: subNav.collapsed,
1478
+ onToggleSubNav: subNav.toggle,
1479
+ requestSubNavCollapse: subNav.requestCollapse,
1347
1480
  onServiceSelect: handleServiceSelect
1348
1481
  };
1349
- 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: [
1350
- /* @__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 }),
1351
1485
  /* @__PURE__ */ jsxs9("div", { className: "flex h-screen w-screen flex-col overflow-hidden", children: [
1352
- /* @__PURE__ */ jsx13(TopBar, { children: config.topBar }),
1353
- /* @__PURE__ */ jsx13(
1486
+ /* @__PURE__ */ jsx14(TopBar, { children: config.topBar }),
1487
+ /* @__PURE__ */ jsx14(
1354
1488
  CommandMenu,
1355
1489
  {
1356
1490
  open: commandMenuOpen,
1357
1491
  onOpenChange: setCommandMenuOpen
1358
1492
  }
1359
1493
  ),
1360
- /* @__PURE__ */ jsx13(
1494
+ /* @__PURE__ */ jsx14(
1361
1495
  ShellBody,
1362
1496
  {
1363
1497
  activeService,
@@ -1365,8 +1499,8 @@ function AppShell({ registry, sidebar }) {
1365
1499
  sidebar
1366
1500
  }
1367
1501
  ),
1368
- config.footer && /* @__PURE__ */ jsx13(ShellFooter, { children: config.footer }),
1369
- dialog2.dialogState && /* @__PURE__ */ jsx13(
1502
+ config.footer && /* @__PURE__ */ jsx14(ShellFooter, { children: config.footer }),
1503
+ dialog2.dialogState && /* @__PURE__ */ jsx14(
1370
1504
  ConfirmDialog,
1371
1505
  {
1372
1506
  open: true,
@@ -1375,12 +1509,13 @@ function AppShell({ registry, sidebar }) {
1375
1509
  confirmLabel: dialog2.dialogState.opts.confirmLabel,
1376
1510
  cancelLabel: dialog2.dialogState.opts.cancelLabel,
1377
1511
  variant: dialog2.dialogState.opts.variant,
1512
+ confirmText: dialog2.dialogState.opts.confirmText,
1378
1513
  onConfirm: dialog2.handleConfirm,
1379
1514
  onCancel: dialog2.handleCancel
1380
1515
  }
1381
1516
  ),
1382
- fullscreenState && /* @__PURE__ */ jsx13("div", { className: "fixed inset-0 z-50 bg-background overflow-auto", children: fullscreenState.content }),
1383
- /* @__PURE__ */ jsx13(
1517
+ fullscreenState && /* @__PURE__ */ jsx14("div", { className: "fixed inset-0 z-50 bg-background overflow-auto", children: fullscreenState.content }),
1518
+ /* @__PURE__ */ jsx14(
1384
1519
  SlideOverPanel,
1385
1520
  {
1386
1521
  panelState: panel2.panelState,
@@ -1413,14 +1548,14 @@ function SlideOverPanel({
1413
1548
  },
1414
1549
  ...!panelState?.description && { "aria-describedby": void 0 }
1415
1550
  };
1416
- return /* @__PURE__ */ jsx13(
1551
+ return /* @__PURE__ */ jsx14(
1417
1552
  Sheet,
1418
1553
  {
1419
1554
  open: panelState !== null,
1420
1555
  onOpenChange: (open) => {
1421
1556
  if (!open) onClose();
1422
1557
  },
1423
- 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 }) })
1424
1559
  }
1425
1560
  );
1426
1561
  }
@@ -1447,7 +1582,7 @@ function ResizableSheetContent({
1447
1582
  className: "max-w-[90vw]",
1448
1583
  style: { width: dynWidth },
1449
1584
  children: [
1450
- /* @__PURE__ */ jsx13(
1585
+ /* @__PURE__ */ jsx14(
1451
1586
  DragHandle,
1452
1587
  {
1453
1588
  edge: "left",
@@ -1467,24 +1602,24 @@ function ShellBody({
1467
1602
  sidebar
1468
1603
  }) {
1469
1604
  return /* @__PURE__ */ jsxs9("div", { className: "flex flex-1 overflow-hidden", children: [
1470
- sidebar ?? /* @__PURE__ */ jsx13(ShellRail, {}),
1471
- /* @__PURE__ */ jsx13(SidePane, {}),
1472
- 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 }) })
1473
1608
  ] });
1474
1609
  }
1475
1610
  function ContentArea({ layout }) {
1476
1611
  if (layout === "full") {
1477
- 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, {}) });
1478
1613
  }
1479
- 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, {}) }) });
1480
1615
  }
1481
1616
  function PanelContent({ state }) {
1482
- return /* @__PURE__ */ jsxs9(Fragment4, { children: [
1617
+ return /* @__PURE__ */ jsxs9(Fragment5, { children: [
1483
1618
  state.title ? /* @__PURE__ */ jsxs9(SheetHeader, { children: [
1484
- /* @__PURE__ */ jsx13(SheetTitle, { children: state.title }),
1485
- state.description && /* @__PURE__ */ jsx13(SheetDescription, { children: state.description })
1486
- ] }) : /* @__PURE__ */ jsx13(SheetTitle, { className: "sr-only", children: "Panel" }),
1487
- /* @__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 })
1488
1623
  ] });
1489
1624
  }
1490
1625
 
@@ -1492,36 +1627,36 @@ function PanelContent({ state }) {
1492
1627
  import { Toaster } from "sonner";
1493
1628
 
1494
1629
  // src/ShellConfigProvider.tsx
1495
- import { jsx as jsx14 } from "react/jsx-runtime";
1630
+ import { jsx as jsx15 } from "react/jsx-runtime";
1496
1631
  function ShellConfigProvider({
1497
1632
  config,
1498
1633
  children
1499
1634
  }) {
1500
- return /* @__PURE__ */ jsx14(ShellConfigContext.Provider, { value: config, children });
1635
+ return /* @__PURE__ */ jsx15(ShellConfigContext.Provider, { value: config, children });
1501
1636
  }
1502
1637
 
1503
1638
  // src/RootLayout.tsx
1504
- import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
1639
+ import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
1505
1640
  function RootLayout({
1506
1641
  config,
1507
1642
  registry,
1508
1643
  sidebar
1509
1644
  }) {
1510
1645
  return /* @__PURE__ */ jsxs10(ShellConfigProvider, { config, children: [
1511
- /* @__PURE__ */ jsx15(Toaster, { position: "top-right", richColors: true, closeButton: true }),
1512
- /* @__PURE__ */ jsx15(AppShell, { registry, sidebar })
1646
+ /* @__PURE__ */ jsx16(Toaster, { position: "top-right", richColors: true, closeButton: true }),
1647
+ /* @__PURE__ */ jsx16(AppShell, { registry, sidebar })
1513
1648
  ] });
1514
1649
  }
1515
1650
 
1516
1651
  // src/PlaceholderPage.tsx
1517
1652
  import { useLocation as useLocation3 } from "react-router-dom";
1518
- import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
1653
+ import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
1519
1654
  function PlaceholderPage() {
1520
1655
  const { pathname } = useLocation3();
1521
1656
  return /* @__PURE__ */ jsxs11("div", { children: [
1522
- /* @__PURE__ */ jsx16("h1", { className: "mb-1", children: pathToTitle(pathname) }),
1523
- /* @__PURE__ */ jsx16("p", { className: "text-sm text-muted-foreground mb-6", children: pathname }),
1524
- /* @__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" }) })
1525
1660
  ] });
1526
1661
  }
1527
1662
  function pathToTitle(path) {
@@ -1530,46 +1665,148 @@ function pathToTitle(path) {
1530
1665
  return last.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1531
1666
  }
1532
1667
 
1533
- // 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";
1534
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";
1535
1772
  import { ScrollArea as ScrollArea3 } from "@petrarca/sonnet-ui";
1536
- import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
1773
+ import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
1537
1774
  function Sidebar({
1538
1775
  children,
1539
1776
  header,
1540
1777
  width = 260,
1541
1778
  className
1542
1779
  }) {
1543
- return /* @__PURE__ */ jsxs12(
1780
+ return /* @__PURE__ */ jsxs14(
1544
1781
  "aside",
1545
1782
  {
1546
1783
  style: { width },
1547
- className: cn6(
1784
+ className: cn8(
1548
1785
  "shrink-0 border-r bg-muted/30 flex flex-col h-full",
1549
1786
  className
1550
1787
  ),
1551
1788
  children: [
1552
1789
  header,
1553
- /* @__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 }) })
1554
1791
  ]
1555
1792
  }
1556
1793
  );
1557
1794
  }
1558
1795
 
1559
1796
  // src/ShellSidebar.tsx
1560
- import React5, { useMemo as useMemo6 } from "react";
1561
- 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";
1562
1799
  function ShellSidebar({
1563
1800
  bottomGroupLabel = "Tools",
1564
1801
  ...sidebarProps
1565
1802
  }) {
1566
1803
  const { mainModules, bottomModules } = useShellModules();
1567
1804
  const { resolveCapability, capabilityPolicy } = useShellConfig();
1568
- const policy = useMemo6(
1805
+ const policy = useMemo5(
1569
1806
  () => resolveCapabilityPolicy(capabilityPolicy),
1570
1807
  [capabilityPolicy]
1571
1808
  );
1572
- const effectOf = React5.useCallback(
1809
+ const effectOf = React8.useCallback(
1573
1810
  (moduleId) => {
1574
1811
  const module = [...mainModules, ...bottomModules].find(
1575
1812
  (m) => m.id === moduleId
@@ -1590,8 +1827,8 @@ function ShellSidebar({
1590
1827
  reason
1591
1828
  }));
1592
1829
  });
1593
- return /* @__PURE__ */ jsxs13(Sidebar, { ...sidebarProps, children: [
1594
- 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(
1595
1832
  SidebarItem,
1596
1833
  {
1597
1834
  icon,
@@ -1606,63 +1843,21 @@ function ShellSidebar({
1606
1843
  mainModules.map((mod) => {
1607
1844
  const { effect, reason } = effectOf(mod.id);
1608
1845
  if (effect === "hidden" || effect === "redirect") return null;
1609
- const disabled = effect === "disabled";
1610
- const title = disabled ? reason : void 0;
1611
- if (mod.navigation.length === 0) {
1612
- return /* @__PURE__ */ jsx18(
1613
- SidebarItem,
1614
- {
1615
- icon: mod.icon,
1616
- label: mod.label,
1617
- path: mod.basePath,
1618
- disabled,
1619
- title
1620
- },
1621
- mod.id
1622
- );
1623
- }
1624
- return /* @__PURE__ */ jsx18(SidebarGroup, { heading: mod.label, collapsible: true, children: mod.navigation.flatMap((group) => [
1625
- // Render NavGroup heading if it has one
1626
- ...group.heading ? [
1627
- /* @__PURE__ */ jsx18(
1628
- SidebarGroup,
1629
- {
1630
- heading: group.heading,
1631
- collapsible: group.collapsible,
1632
- children: group.links.map((link) => /* @__PURE__ */ jsx18(
1633
- SidebarItem,
1634
- {
1635
- icon: mod.icon,
1636
- label: link.label,
1637
- path: link.path,
1638
- badge: link.badge,
1639
- disabled,
1640
- title: disabled ? reason : void 0
1641
- },
1642
- link.path
1643
- ))
1644
- },
1645
- group.id
1646
- )
1647
- ] : group.links.map((link) => /* @__PURE__ */ jsx18(
1648
- SidebarItem,
1649
- {
1650
- icon: mod.icon,
1651
- label: link.label,
1652
- path: link.path,
1653
- badge: link.badge,
1654
- disabled,
1655
- title: disabled ? reason : void 0
1656
- },
1657
- link.path
1658
- ))
1659
- ]) }, mod.id);
1846
+ return /* @__PURE__ */ jsx21(
1847
+ ModuleSection,
1848
+ {
1849
+ mod,
1850
+ disabled: effect === "disabled",
1851
+ reason
1852
+ },
1853
+ mod.id
1854
+ );
1660
1855
  }),
1661
- 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) => {
1662
1857
  const { effect, reason } = effectOf(mod.id);
1663
1858
  if (effect === "hidden" || effect === "redirect") return null;
1664
1859
  const disabled = effect === "disabled";
1665
- return /* @__PURE__ */ jsx18(
1860
+ return /* @__PURE__ */ jsx21(
1666
1861
  SidebarItem,
1667
1862
  {
1668
1863
  icon: mod.icon,
@@ -1676,16 +1871,61 @@ function ShellSidebar({
1676
1871
  }) })
1677
1872
  ] });
1678
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
+ }
1679
1919
 
1680
1920
  // src/ShellVersion.tsx
1681
- import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
1921
+ import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
1682
1922
  function ShellVersion({
1683
1923
  name,
1684
1924
  version
1685
1925
  }) {
1686
- return /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2", children: [
1687
- /* @__PURE__ */ jsx19("span", { children: name }),
1688
- version && /* @__PURE__ */ jsxs14("span", { className: "text-tertiary-foreground", 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: [
1689
1929
  "v",
1690
1930
  version
1691
1931
  ] })
@@ -1695,9 +1935,9 @@ function ShellVersion({
1695
1935
  // src/SearchTrigger.tsx
1696
1936
  import { Search } from "lucide-react";
1697
1937
  import { Button as Button3 } from "@petrarca/sonnet-ui";
1698
- import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
1938
+ import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
1699
1939
  function SearchTrigger() {
1700
- return /* @__PURE__ */ jsxs15(
1940
+ return /* @__PURE__ */ jsxs17(
1701
1941
  Button3,
1702
1942
  {
1703
1943
  variant: "outline",
@@ -1705,10 +1945,10 @@ function SearchTrigger() {
1705
1945
  className: "h-8 gap-2 text-sm text-muted-foreground w-56 justify-start",
1706
1946
  onClick: () => navigation.openCommandMenu(),
1707
1947
  children: [
1708
- /* @__PURE__ */ jsx20(Search, { className: "h-4 w-4" }),
1709
- /* @__PURE__ */ jsx20("span", { children: "Search..." }),
1710
- /* @__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: [
1711
- /* @__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" }),
1712
1952
  "K"
1713
1953
  ] })
1714
1954
  ]
@@ -1716,9 +1956,32 @@ function SearchTrigger() {
1716
1956
  );
1717
1957
  }
1718
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
+
1719
1982
  // src/UserMenu.tsx
1720
1983
  import { LogOut, User } from "lucide-react";
1721
- import { Button as Button4 } from "@petrarca/sonnet-ui";
1984
+ import { Button as Button5 } from "@petrarca/sonnet-ui";
1722
1985
  import { Avatar, AvatarFallback } from "@petrarca/sonnet-ui";
1723
1986
  import {
1724
1987
  DropdownMenu,
@@ -1728,23 +1991,23 @@ import {
1728
1991
  DropdownMenuSeparator,
1729
1992
  DropdownMenuTrigger
1730
1993
  } from "@petrarca/sonnet-ui";
1731
- import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
1994
+ import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
1732
1995
  function UserMenu({ user, onSignOut }) {
1733
- return /* @__PURE__ */ jsxs16(DropdownMenu, { children: [
1734
- /* @__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 }) }) }) }),
1735
- /* @__PURE__ */ jsxs16(DropdownMenuContent, { align: "end", className: "w-48", children: [
1736
- /* @__PURE__ */ jsx21(DropdownMenuLabel, { className: "font-normal", children: /* @__PURE__ */ jsxs16("div", { className: "flex flex-col gap-1", children: [
1737
- /* @__PURE__ */ jsx21("p", { className: "text-sm font-medium", children: user.name }),
1738
- /* @__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 })
1739
2002
  ] }) }),
1740
- /* @__PURE__ */ jsx21(DropdownMenuSeparator, {}),
1741
- /* @__PURE__ */ jsxs16(DropdownMenuItem, { children: [
1742
- /* @__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" }),
1743
2006
  "Profile"
1744
2007
  ] }),
1745
- /* @__PURE__ */ jsx21(DropdownMenuSeparator, {}),
1746
- /* @__PURE__ */ jsxs16(DropdownMenuItem, { onSelect: onSignOut, children: [
1747
- /* @__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" }),
1748
2011
  "Sign out"
1749
2012
  ] })
1750
2013
  ] })
@@ -1756,7 +2019,6 @@ function createModuleRegistry(modules) {
1756
2019
  const visible = modules.filter((m) => !m.hidden);
1757
2020
  const mainModules = visible.filter((m) => !m.pinBottom);
1758
2021
  const bottomModules = visible.filter((m) => m.pinBottom);
1759
- const allRoutes = modules.flatMap((m) => m.routes);
1760
2022
  const contributionsByPoint = /* @__PURE__ */ new Map();
1761
2023
  for (const m of modules) {
1762
2024
  for (const c of m.contributions ?? []) {
@@ -1771,17 +2033,57 @@ function createModuleRegistry(modules) {
1771
2033
  function getExtensionPoint(name) {
1772
2034
  return contributionsByPoint.get(name) ?? [];
1773
2035
  }
2036
+ const allRoutes = nestContributedRoutes(
2037
+ modules.flatMap((m) => m.routes),
2038
+ contributionsByPoint
2039
+ );
1774
2040
  return { modules, mainModules, bottomModules, allRoutes, getExtensionPoint };
1775
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
+ }
1776
2078
 
1777
2079
  // src/OverviewCard.tsx
1778
- import { cn as cn7 } from "@petrarca/sonnet-core";
1779
- 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";
1780
2082
  function FeatureLink({
1781
2083
  feature,
1782
2084
  children
1783
2085
  }) {
1784
- return /* @__PURE__ */ jsx22(
2086
+ return /* @__PURE__ */ jsx26(
1785
2087
  "span",
1786
2088
  {
1787
2089
  role: "link",
@@ -1807,37 +2109,51 @@ function OverviewCard({
1807
2109
  title,
1808
2110
  description,
1809
2111
  feature,
2112
+ path,
1810
2113
  className
1811
2114
  }) {
1812
- return /* @__PURE__ */ jsxs17(
2115
+ const go = feature ? () => navigation.goToFeature(feature) : path ? () => navigation.goTo(path) : void 0;
2116
+ return /* @__PURE__ */ jsxs19(
1813
2117
  "button",
1814
2118
  {
1815
- onClick: feature ? () => navigation.goToFeature(feature) : void 0,
1816
- disabled: !feature,
1817
- className: cn7(
2119
+ onClick: go,
2120
+ disabled: !go,
2121
+ className: cn9(
1818
2122
  "rounded-lg border bg-card p-5 text-left flex flex-col gap-3",
1819
2123
  "disabled:cursor-default",
1820
- feature && "hover:bg-accent/50 transition-colors",
2124
+ go && "hover:bg-accent/50 transition-colors",
1821
2125
  className
1822
2126
  ),
1823
2127
  children: [
1824
- /* @__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" }) }),
1825
- /* @__PURE__ */ jsxs17("div", { children: [
1826
- /* @__PURE__ */ jsx22("p", { className: "text-sm font-medium mb-1", children: title }),
1827
- /* @__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 })
1828
2132
  ] })
1829
2133
  ]
1830
2134
  }
1831
2135
  );
1832
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
+ }
1833
2146
  export {
1834
2147
  AppShell,
2148
+ Breadcrumbs,
1835
2149
  CommandMenu,
1836
2150
  ConfirmDialog,
1837
2151
  DEFAULT_CAPABILITY_POLICY,
2152
+ DrillDownHeader,
1838
2153
  FeatureLink,
1839
2154
  IconRail,
1840
2155
  OverviewCard,
2156
+ OverviewGroupHeading,
1841
2157
  PlaceholderPage,
1842
2158
  RailIcon,
1843
2159
  RailSeparator,
@@ -1854,8 +2170,10 @@ export {
1854
2170
  SidebarGroup,
1855
2171
  SidebarItem,
1856
2172
  SubNavPanel,
2173
+ ThemeToggle,
1857
2174
  TopBar,
1858
2175
  UserMenu,
2176
+ contributionPath,
1859
2177
  createModuleRegistry,
1860
2178
  dialog,
1861
2179
  evaluateModule,
@@ -1872,8 +2190,11 @@ export {
1872
2190
  panel,
1873
2191
  resolveCapabilityPolicy,
1874
2192
  sidePane,
2193
+ useCollapsedSubNav,
2194
+ useContributedComponent,
1875
2195
  useExtensionPoint,
1876
2196
  useMainModules,
2197
+ useModuleNavigation,
1877
2198
  useShellConfig,
1878
2199
  useShellEvent,
1879
2200
  useShellModules,