@open-mercato/ui 0.4.5-develop-6bdcebbece → 0.4.5-develop-986cfd8c37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/AGENTS.md +8 -0
  2. package/dist/backend/AppShell.js +395 -134
  3. package/dist/backend/AppShell.js.map +2 -2
  4. package/dist/backend/CrudForm.js +232 -21
  5. package/dist/backend/CrudForm.js.map +2 -2
  6. package/dist/backend/ProfileDropdown.js +214 -94
  7. package/dist/backend/ProfileDropdown.js.map +2 -2
  8. package/dist/backend/injection/InjectionSpot.js +74 -4
  9. package/dist/backend/injection/InjectionSpot.js.map +2 -2
  10. package/dist/backend/injection/SseEventIndicator.js +16 -0
  11. package/dist/backend/injection/SseEventIndicator.js.map +7 -0
  12. package/dist/backend/injection/WidgetSharedState.js +49 -0
  13. package/dist/backend/injection/WidgetSharedState.js.map +7 -0
  14. package/dist/backend/injection/eventBridge.js +105 -0
  15. package/dist/backend/injection/eventBridge.js.map +7 -0
  16. package/dist/backend/injection/mergeMenuItems.js +43 -0
  17. package/dist/backend/injection/mergeMenuItems.js.map +7 -0
  18. package/dist/backend/injection/resolveInjectedIcon.js +23 -0
  19. package/dist/backend/injection/resolveInjectedIcon.js.map +7 -0
  20. package/dist/backend/injection/spotIds.js +40 -1
  21. package/dist/backend/injection/spotIds.js.map +2 -2
  22. package/dist/backend/injection/useAppEvent.js +35 -0
  23. package/dist/backend/injection/useAppEvent.js.map +7 -0
  24. package/dist/backend/injection/useInjectedMenuItems.js +92 -0
  25. package/dist/backend/injection/useInjectedMenuItems.js.map +7 -0
  26. package/dist/backend/injection/useInjectionDataWidgets.js +36 -0
  27. package/dist/backend/injection/useInjectionDataWidgets.js.map +7 -0
  28. package/dist/backend/injection/useOperationProgress.js +64 -0
  29. package/dist/backend/injection/useOperationProgress.js.map +7 -0
  30. package/dist/backend/injection/useWidgetSharedState.js +26 -0
  31. package/dist/backend/injection/useWidgetSharedState.js.map +7 -0
  32. package/dist/backend/section-page/SectionNav.js +22 -2
  33. package/dist/backend/section-page/SectionNav.js.map +2 -2
  34. package/dist/backend/utils/api.js +9 -1
  35. package/dist/backend/utils/api.js.map +2 -2
  36. package/package.json +2 -2
  37. package/src/backend/AGENTS.md +50 -0
  38. package/src/backend/AppShell.tsx +317 -30
  39. package/src/backend/CrudForm.tsx +238 -21
  40. package/src/backend/ProfileDropdown.tsx +199 -78
  41. package/src/backend/injection/InjectionSpot.tsx +118 -16
  42. package/src/backend/injection/SseEventIndicator.tsx +24 -0
  43. package/src/backend/injection/WidgetSharedState.ts +58 -0
  44. package/src/backend/injection/eventBridge.ts +134 -0
  45. package/src/backend/injection/mergeMenuItems.ts +71 -0
  46. package/src/backend/injection/resolveInjectedIcon.tsx +30 -0
  47. package/src/backend/injection/spotIds.ts +38 -0
  48. package/src/backend/injection/useAppEvent.ts +76 -0
  49. package/src/backend/injection/useInjectedMenuItems.ts +125 -0
  50. package/src/backend/injection/useInjectionDataWidgets.ts +41 -0
  51. package/src/backend/injection/useOperationProgress.ts +105 -0
  52. package/src/backend/injection/useWidgetSharedState.ts +28 -0
  53. package/src/backend/section-page/SectionNav.tsx +22 -1
  54. package/src/backend/utils/api.ts +14 -5
@@ -17,18 +17,176 @@ import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
17
17
  import { slugifySidebarId } from "@open-mercato/shared/modules/navigation/sidebarPreferences";
18
18
  import { InjectionSpot } from "./injection/InjectionSpot.js";
19
19
  import { LEGACY_GLOBAL_MUTATION_INJECTION_SPOT_ID } from "./injection/mutationEvents.js";
20
+ import { mergeMenuItems } from "./injection/mergeMenuItems.js";
21
+ import { useInjectedMenuItems } from "./injection/useInjectedMenuItems.js";
22
+ import { resolveInjectedIcon } from "./injection/resolveInjectedIcon.js";
23
+ import { useEventBridge } from "./injection/eventBridge.js";
24
+ import { SseEventIndicator } from "./injection/SseEventIndicator.js";
20
25
  import {
21
26
  BACKEND_LAYOUT_FOOTER_INJECTION_SPOT_ID,
22
27
  BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID,
23
28
  BACKEND_RECORD_CURRENT_INJECTION_SPOT_ID,
24
29
  BACKEND_SIDEBAR_FOOTER_INJECTION_SPOT_ID,
25
- BACKEND_SIDEBAR_TOP_INJECTION_SPOT_ID
30
+ BACKEND_SIDEBAR_TOP_INJECTION_SPOT_ID,
31
+ BACKEND_SIDEBAR_NAV_FOOTER_INJECTION_SPOT_ID,
32
+ BACKEND_SIDEBAR_NAV_INJECTION_SPOT_ID,
33
+ BACKEND_TOPBAR_ACTIONS_INJECTION_SPOT_ID,
34
+ GLOBAL_HEADER_STATUS_INDICATORS_INJECTION_SPOT_ID,
35
+ GLOBAL_SIDEBAR_STATUS_BADGES_INJECTION_SPOT_ID
26
36
  } from "./injection/spotIds.js";
37
+ function convertInjectedMenuItemToSidebarItem(item, title) {
38
+ if (!item.href) return null;
39
+ return {
40
+ id: item.id,
41
+ href: item.href,
42
+ title,
43
+ defaultTitle: title,
44
+ icon: resolveInjectedIcon(item.icon) ?? void 0,
45
+ enabled: true,
46
+ hidden: false,
47
+ pageContext: "main"
48
+ };
49
+ }
50
+ function resolveInjectedMenuLabel(item, t) {
51
+ if (item.labelKey && item.label) return t(item.labelKey, item.label);
52
+ if (item.labelKey) return t(item.labelKey, item.id);
53
+ if (item.label && item.label.includes(".")) return t(item.label, item.id);
54
+ return item.label ?? item.id;
55
+ }
56
+ function mergeSidebarItemsWithInjected(items, injectedItems, t) {
57
+ if (injectedItems.length === 0) return items;
58
+ const builtInById = /* @__PURE__ */ new Map();
59
+ for (const item of items) {
60
+ builtInById.set(item.id ?? item.href, item);
61
+ }
62
+ const merged = mergeMenuItems(
63
+ items.map((item) => ({
64
+ id: item.id ?? item.href
65
+ })),
66
+ injectedItems
67
+ );
68
+ const result = [];
69
+ for (const entry of merged) {
70
+ if (entry.source === "built-in") {
71
+ const original = builtInById.get(entry.id);
72
+ if (original) result.push(original);
73
+ continue;
74
+ }
75
+ const translatedLabel = resolveInjectedMenuLabel(
76
+ { id: entry.id, label: entry.label, labelKey: entry.labelKey },
77
+ t
78
+ );
79
+ const converted = convertInjectedMenuItemToSidebarItem(
80
+ {
81
+ id: entry.id,
82
+ label: translatedLabel,
83
+ icon: entry.icon,
84
+ href: entry.href
85
+ },
86
+ translatedLabel
87
+ );
88
+ if (converted) result.push(converted);
89
+ }
90
+ return result;
91
+ }
92
+ function mergeSidebarGroupsWithInjected(groups, injectedItems, t) {
93
+ if (injectedItems.length === 0) return groups;
94
+ const injectedByGroup = /* @__PURE__ */ new Map();
95
+ const ungrouped = [];
96
+ for (const item of injectedItems) {
97
+ if (item.groupId && item.groupId.trim().length > 0) {
98
+ const groupItems = injectedByGroup.get(item.groupId) ?? [];
99
+ groupItems.push(item);
100
+ injectedByGroup.set(item.groupId, groupItems);
101
+ continue;
102
+ }
103
+ ungrouped.push(item);
104
+ }
105
+ const nextGroups = groups.map((group, index) => {
106
+ const groupId = group.id || resolveGroupKey(group);
107
+ const groupInjected = [
108
+ ...injectedByGroup.get(groupId) ?? [],
109
+ ...index === 0 ? ungrouped : []
110
+ ];
111
+ return {
112
+ ...group,
113
+ items: mergeSidebarItemsWithInjected(group.items, groupInjected, t)
114
+ };
115
+ });
116
+ const existingIds = new Set(nextGroups.map((group) => group.id || resolveGroupKey(group)));
117
+ for (const [groupId, items] of injectedByGroup.entries()) {
118
+ if (existingIds.has(groupId)) continue;
119
+ const first = items[0];
120
+ const label = first.groupLabelKey ? t(first.groupLabelKey, first.groupLabel ?? groupId) : first.groupLabel ?? groupId;
121
+ const groupItems = mergeSidebarItemsWithInjected([], items, t);
122
+ if (groupItems.length === 0) continue;
123
+ nextGroups.push({
124
+ id: groupId,
125
+ name: label,
126
+ defaultName: label,
127
+ items: groupItems
128
+ });
129
+ }
130
+ return nextGroups;
131
+ }
132
+ function mergeSectionGroupsWithInjected(sections, injectedItems, t) {
133
+ if (injectedItems.length === 0) return sections;
134
+ const byGroup = /* @__PURE__ */ new Map();
135
+ for (const item of injectedItems) {
136
+ const groupId = item.groupId && item.groupId.trim().length > 0 ? item.groupId : "injected";
137
+ const bucket = byGroup.get(groupId) ?? [];
138
+ bucket.push(item);
139
+ byGroup.set(groupId, bucket);
140
+ }
141
+ const nextSections = sections.map((section) => {
142
+ const sectionItems = byGroup.get(section.id) ?? [];
143
+ if (sectionItems.length === 0) return section;
144
+ const mergedItems = mergeMenuItems(
145
+ section.items.map((item) => ({ id: item.id, item })),
146
+ sectionItems
147
+ ).flatMap((item) => {
148
+ if (item.source === "built-in") {
149
+ const original = section.items.find((entry) => entry.id === item.id);
150
+ return original ? [original] : [];
151
+ }
152
+ if (!item.href) return [];
153
+ const label = resolveInjectedMenuLabel(item, t);
154
+ return [{
155
+ id: item.id,
156
+ label,
157
+ href: item.href
158
+ }];
159
+ });
160
+ return {
161
+ ...section,
162
+ items: mergedItems
163
+ };
164
+ });
165
+ for (const [sectionId, sectionItems] of byGroup.entries()) {
166
+ const exists = nextSections.some((section) => section.id === sectionId);
167
+ if (exists) continue;
168
+ const first = sectionItems[0];
169
+ const label = first.groupLabelKey ? t(first.groupLabelKey, first.groupLabel ?? sectionId) : first.groupLabel ?? sectionId;
170
+ const items = sectionItems.flatMap((item) => {
171
+ if (!item.href) return [];
172
+ const itemLabel = resolveInjectedMenuLabel(item, t);
173
+ return [{ id: item.id, label: itemLabel, href: item.href }];
174
+ });
175
+ if (items.length === 0) continue;
176
+ nextSections.push({ id: sectionId, label, items });
177
+ }
178
+ return nextSections;
179
+ }
27
180
  function resolveGroupKey(group) {
28
181
  if (group.id && group.id.length) return group.id;
29
182
  if (group.defaultName && group.defaultName.length) return slugifySidebarId(group.defaultName);
30
183
  return slugifySidebarId(group.name);
31
184
  }
185
+ function resolveItemKey(item) {
186
+ const candidate = item.id?.trim();
187
+ if (candidate && candidate.length > 0) return candidate;
188
+ return item.href;
189
+ }
32
190
  const HeaderContext = React.createContext(null);
33
191
  function ApplyBreadcrumb({ breadcrumb, title, titleKey }) {
34
192
  const ctx = React.useContext(HeaderContext);
@@ -79,6 +237,11 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
79
237
  const searchParams = useSearchParams();
80
238
  const t = useT();
81
239
  const locale = useLocale();
240
+ const { items: mainSidebarInjectedMenuItems } = useInjectedMenuItems("menu:sidebar:main");
241
+ const { items: settingsSidebarInjectedMenuItems } = useInjectedMenuItems("menu:sidebar:settings");
242
+ const { items: profileSidebarInjectedMenuItems } = useInjectedMenuItems("menu:sidebar:profile");
243
+ const { items: topbarInjectedMenuItems } = useInjectedMenuItems("menu:topbar:actions");
244
+ useEventBridge();
82
245
  const resolvedProductName = productName ?? t("appShell.productName");
83
246
  const [mobileOpen, setMobileOpen] = React.useState(false);
84
247
  const [collapsed, setCollapsed] = React.useState(sidebarCollapsedDefault);
@@ -117,6 +280,10 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
117
280
  return profilePathPrefixes.some((prefix) => pathname.startsWith(prefix));
118
281
  }, [pathname, profilePathPrefixes]);
119
282
  const sidebarMode = isOnSettingsPath ? "settings" : isOnProfilePath ? "profile" : "main";
283
+ const mainNavGroupsWithInjected = React.useMemo(
284
+ () => mergeSidebarGroupsWithInjected(navGroups, mainSidebarInjectedMenuItems, t),
285
+ [mainSidebarInjectedMenuItems, navGroups, t]
286
+ );
120
287
  React.useEffect(() => {
121
288
  if (!mobileOpen || typeof document === "undefined") return;
122
289
  const prev = document.body.style.overflow;
@@ -181,7 +348,7 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
181
348
  responseItemLabels[trimmedKey] = value;
182
349
  }
183
350
  }
184
- const responseHiddenItems = Array.isArray(rawSettings?.hiddenItems) ? rawSettings.hiddenItems.map((href) => typeof href === "string" ? href.trim() : "").filter((href) => href.length > 0) : [];
351
+ const responseHiddenItems = Array.isArray(rawSettings?.hiddenItems) ? rawSettings.hiddenItems.map((itemId) => typeof itemId === "string" ? itemId.trim() : "").filter((itemId) => itemId.length > 0) : [];
185
352
  const canManageRoles = data?.canApplyToRoles === true;
186
353
  setCanApplyToRoles(canManageRoles);
187
354
  if (canManageRoles) {
@@ -201,9 +368,9 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
201
368
  const order = mergeGroupOrder(responseOrder, currentIds);
202
369
  const { itemDefaults } = collectSidebarDefaults(baseSnapshot);
203
370
  const hiddenItemIds = {};
204
- for (const href of responseHiddenItems) {
205
- if (!itemDefaults.has(href)) continue;
206
- hiddenItemIds[href] = true;
371
+ for (const itemId of responseHiddenItems) {
372
+ if (!itemDefaults.has(itemId)) continue;
373
+ hiddenItemIds[itemId] = true;
207
374
  }
208
375
  const draft = {
209
376
  order,
@@ -261,17 +428,17 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
261
428
  if (trimmed !== base) sanitizedGroupLabels[key] = trimmed;
262
429
  }
263
430
  const sanitizedItemLabels = {};
264
- for (const [href, value] of Object.entries(customDraft.itemLabels)) {
431
+ for (const [itemId, value] of Object.entries(customDraft.itemLabels)) {
265
432
  const trimmed = value.trim();
266
- const base = itemDefaults.get(href);
433
+ const base = itemDefaults.get(itemId);
267
434
  if (!trimmed || !base) continue;
268
- if (trimmed !== base) sanitizedItemLabels[href] = trimmed;
435
+ if (trimmed !== base) sanitizedItemLabels[itemId] = trimmed;
269
436
  }
270
437
  const sanitizedHiddenItems = [];
271
- for (const [href, hidden] of Object.entries(customDraft.hiddenItemIds)) {
438
+ for (const [itemId, hidden] of Object.entries(customDraft.hiddenItemIds)) {
272
439
  if (!hidden) continue;
273
- if (!itemDefaults.has(href)) continue;
274
- sanitizedHiddenItems.push(href);
440
+ if (!itemDefaults.has(itemId)) continue;
441
+ sanitizedHiddenItems.push(itemId);
275
442
  }
276
443
  const applyToRolesPayload = canApplyToRoles ? [...selectedRoleIds] : [];
277
444
  const clearRoleIdsPayload = canApplyToRoles ? availableRoleTargets.filter((role) => role.hasPreference && !selectedRoleIds.includes(role.id)).map((role) => role.id) : [];
@@ -342,19 +509,19 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
342
509
  return { ...draft, groupLabels: next };
343
510
  });
344
511
  }, [updateDraft]);
345
- const setItemLabel = React.useCallback((href, value) => {
512
+ const setItemLabel = React.useCallback((itemId, value) => {
346
513
  updateDraft((draft) => {
347
514
  const next = { ...draft.itemLabels };
348
- if (value.trim().length === 0) delete next[href];
349
- else next[href] = value;
515
+ if (value.trim().length === 0) delete next[itemId];
516
+ else next[itemId] = value;
350
517
  return { ...draft, itemLabels: next };
351
518
  });
352
519
  }, [updateDraft]);
353
- const setItemHidden = React.useCallback((href, hidden) => {
520
+ const setItemHidden = React.useCallback((itemId, hidden) => {
354
521
  updateDraft((draft) => {
355
522
  const next = { ...draft.hiddenItemIds };
356
- if (hidden) next[href] = true;
357
- else delete next[href];
523
+ if (hidden) next[itemId] = true;
524
+ else delete next[itemId];
358
525
  return { ...draft, hiddenItemIds: next };
359
526
  });
360
527
  }, [updateDraft]);
@@ -565,6 +732,7 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
565
732
  className: `relative text-sm rounded inline-flex items-center ${base} ${isActive ? "bg-background border shadow-sm" : "hover:bg-accent hover:text-accent-foreground"}`,
566
733
  style: spacingStyle,
567
734
  title: compact ? label : void 0,
735
+ "data-menu-item-id": item.id,
568
736
  onClick: () => setMobileOpen(false),
569
737
  children: [
570
738
  isActive && /* @__PURE__ */ jsx("span", { className: "absolute left-0 top-1 bottom-1 w-0.5 rounded bg-foreground" }),
@@ -599,16 +767,26 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
599
767
  }
600
768
  function renderSidebar(compact, hideHeader) {
601
769
  if (sidebarMode === "settings" && settingsSections && settingsSections.length > 0) {
602
- return renderSectionSidebar(
770
+ const mergedSettingsSections = mergeSectionGroupsWithInjected(
603
771
  settingsSections,
772
+ settingsSidebarInjectedMenuItems,
773
+ t
774
+ );
775
+ return renderSectionSidebar(
776
+ mergedSettingsSections,
604
777
  settingsSectionTitle ?? t("backend.nav.settings", "Settings"),
605
778
  compact,
606
779
  hideHeader
607
780
  );
608
781
  }
609
782
  if (sidebarMode === "profile" && profileSections && profileSections.length > 0) {
610
- return renderSectionSidebar(
783
+ const mergedProfileSections = mergeSectionGroupsWithInjected(
611
784
  profileSections,
785
+ profileSidebarInjectedMenuItems,
786
+ t
787
+ );
788
+ return renderSectionSidebar(
789
+ mergedProfileSections,
612
790
  profileSectionTitle ?? t("backend.nav.profile", "Profile"),
613
791
  compact,
614
792
  hideHeader
@@ -616,13 +794,13 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
616
794
  }
617
795
  const isMobileVariant = !!hideHeader;
618
796
  const shouldRenderSidebarInjectionSpots = !isMobileVariant;
619
- const baseGroupsForDefaults = originalNavRef.current ?? navGroups;
797
+ const baseGroupsForDefaults = originalNavRef.current ?? mainNavGroupsWithInjected;
620
798
  const baseGroupMap = /* @__PURE__ */ new Map();
621
799
  for (const group of baseGroupsForDefaults) {
622
800
  baseGroupMap.set(resolveGroupKey(group), group);
623
801
  }
624
802
  const localeLabel = (locale || "").toUpperCase();
625
- const orderedGroupIds = customDraft ? mergeGroupOrder(customDraft.order, Array.from(baseGroupMap.keys())) : navGroups.map((group) => resolveGroupKey(group));
803
+ const orderedGroupIds = customDraft ? mergeGroupOrder(customDraft.order, Array.from(baseGroupMap.keys())) : mainNavGroupsWithInjected.map((group) => resolveGroupKey(group));
626
804
  const lastVisibleGroupIndex = (() => {
627
805
  for (let idx = navGroups.length - 1; idx >= 0; idx -= 1) {
628
806
  if (navGroups[idx].items.some((item) => item.hidden !== true)) return idx;
@@ -632,10 +810,11 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
632
810
  const renderEditableItems = (baseItems, currentItems, depth = 0) => {
633
811
  if (!customDraft) return null;
634
812
  return baseItems.map((baseItem) => {
813
+ const itemKey = resolveItemKey(baseItem);
635
814
  const current = currentItems.find((item) => item.href === baseItem.href) ?? baseItem;
636
815
  const placeholder = baseItem.defaultTitle ?? baseItem.title;
637
- const value = customDraft.itemLabels[baseItem.href] ?? "";
638
- const hidden = customDraft.hiddenItemIds[baseItem.href] === true;
816
+ const value = customDraft.itemLabels[itemKey] ?? "";
817
+ const hidden = customDraft.hiddenItemIds[itemKey] === true;
639
818
  return /* @__PURE__ */ jsxs(
640
819
  "div",
641
820
  {
@@ -650,7 +829,7 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
650
829
  type: "checkbox",
651
830
  className: "h-4 w-4 accent-foreground",
652
831
  checked: !hidden,
653
- onChange: (event) => setItemHidden(baseItem.href, !event.target.checked),
832
+ onChange: (event) => setItemHidden(itemKey, !event.target.checked),
654
833
  disabled: savingPreferences,
655
834
  "aria-label": t("appShell.sidebarCustomizationShowItem"),
656
835
  title: t("appShell.sidebarCustomizationShowItem")
@@ -660,7 +839,7 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
660
839
  "input",
661
840
  {
662
841
  value,
663
- onChange: (event) => setItemLabel(baseItem.href, event.target.value),
842
+ onChange: (event) => setItemLabel(itemKey, event.target.value),
664
843
  placeholder,
665
844
  disabled: savingPreferences,
666
845
  className: "h-8 flex-1 rounded border bg-background px-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-60"
@@ -670,7 +849,7 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
670
849
  baseItem.children && baseItem.children.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1", children: renderEditableItems(baseItem.children, current.children ?? [], depth + 1) }) : null
671
850
  ]
672
851
  },
673
- baseItem.href
852
+ itemKey
674
853
  );
675
854
  });
676
855
  };
@@ -810,7 +989,7 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
810
989
  if (isSettingsPath(item.href)) return false;
811
990
  return true;
812
991
  };
813
- const mainGroups = navGroups.map((g) => ({
992
+ const mainGroups = mainNavGroupsWithInjected.map((g) => ({
814
993
  ...g,
815
994
  items: g.items.filter((item) => isMainItem(item) && item.hidden !== true)
816
995
  })).filter((g) => g.items.length > 0);
@@ -821,117 +1000,146 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
821
1000
  return -1;
822
1001
  })();
823
1002
  return /* @__PURE__ */ jsxs(Fragment, { children: [
824
- /* @__PURE__ */ jsx("nav", { className: "flex flex-col gap-2", children: mainGroups.map((g, gi) => {
825
- const groupId = resolveGroupKey(g);
826
- const open = openGroups[groupId] !== false;
827
- const visibleItems = g.items.filter((item) => item.hidden !== true);
828
- if (visibleItems.length === 0) return null;
829
- return /* @__PURE__ */ jsxs("div", { children: [
830
- /* @__PURE__ */ jsxs(
831
- Button,
832
- {
833
- variant: "muted",
834
- onClick: () => toggleGroup(groupId),
835
- className: `w-full ${compact ? "px-0 justify-center" : "px-2 justify-between"} flex text-xs uppercase text-muted-foreground/90 py-2`,
836
- "aria-expanded": open,
837
- children: [
838
- !compact && /* @__PURE__ */ jsx("span", { children: g.name }),
839
- !compact && /* @__PURE__ */ jsx(Chevron, { open })
840
- ]
841
- }
842
- ),
843
- open && /* @__PURE__ */ jsx("div", { className: `flex flex-col ${compact ? "items-center" : ""} gap-1 ${!compact ? "pl-1" : ""}`, children: visibleItems.map((i) => {
844
- const childItems = (i.children ?? []).filter((child) => child.hidden !== true);
845
- const showChildren = !!pathname && childItems.length > 0 && pathname.startsWith(i.href);
846
- const hasActiveChild = !!(pathname && childItems.some((c) => pathname.startsWith(c.href)));
847
- const isParentActive = pathname === i.href || showChildren && !hasActiveChild;
848
- const base = compact ? "w-10 h-10 justify-center" : "px-2 py-1 gap-2";
849
- return /* @__PURE__ */ jsxs(React.Fragment, { children: [
850
- /* @__PURE__ */ jsxs(
851
- Link,
852
- {
853
- href: i.href,
854
- className: `relative text-sm rounded inline-flex items-center ${base} ${isParentActive ? "bg-background border shadow-sm" : "hover:bg-accent hover:text-accent-foreground"} ${i.enabled === false ? "pointer-events-none opacity-50" : ""}`,
855
- "aria-disabled": i.enabled === false,
856
- title: compact ? i.title : void 0,
857
- onClick: () => setMobileOpen(false),
858
- children: [
859
- isParentActive ? /* @__PURE__ */ jsx("span", { className: "absolute left-0 top-1 bottom-1 w-0.5 rounded bg-foreground" }) : null,
860
- /* @__PURE__ */ jsx("span", { className: `flex items-center justify-center shrink-0 ${compact ? "" : "text-muted-foreground"}`, children: i.icon ?? DefaultIcon }),
861
- !compact && /* @__PURE__ */ jsx("span", { children: i.title })
862
- ]
863
- }
864
- ),
865
- showChildren ? /* @__PURE__ */ jsx("div", { className: `flex flex-col ${compact ? "items-center" : ""} gap-1 ${!compact ? "pl-4" : ""}`, children: childItems.map((c) => {
866
- const childActive = pathname?.startsWith(c.href);
867
- const childBase = compact ? "w-10 h-8 justify-center" : "px-2 py-1 gap-2";
868
- return /* @__PURE__ */ jsxs(
1003
+ /* @__PURE__ */ jsxs("nav", { className: "flex flex-col gap-2", "data-testid": "sidebar", children: [
1004
+ shouldRenderSidebarInjectionSpots ? /* @__PURE__ */ jsx(
1005
+ InjectionSpot,
1006
+ {
1007
+ spotId: BACKEND_SIDEBAR_NAV_INJECTION_SPOT_ID,
1008
+ context: injectionContext
1009
+ }
1010
+ ) : null,
1011
+ mainGroups.map((g, gi) => {
1012
+ const groupId = resolveGroupKey(g);
1013
+ const open = openGroups[groupId] !== false;
1014
+ const visibleItems = g.items.filter((item) => item.hidden !== true);
1015
+ if (visibleItems.length === 0) return null;
1016
+ return /* @__PURE__ */ jsxs("div", { children: [
1017
+ /* @__PURE__ */ jsxs(
1018
+ Button,
1019
+ {
1020
+ variant: "muted",
1021
+ onClick: () => toggleGroup(groupId),
1022
+ className: `w-full ${compact ? "px-0 justify-center" : "px-2 justify-between"} flex text-xs uppercase text-muted-foreground/90 py-2`,
1023
+ "aria-expanded": open,
1024
+ children: [
1025
+ !compact && /* @__PURE__ */ jsx("span", { children: g.name }),
1026
+ !compact && /* @__PURE__ */ jsx(Chevron, { open })
1027
+ ]
1028
+ }
1029
+ ),
1030
+ open && /* @__PURE__ */ jsx("div", { className: `flex flex-col ${compact ? "items-center" : ""} gap-1 ${!compact ? "pl-1" : ""}`, children: visibleItems.map((i) => {
1031
+ const childItems = (i.children ?? []).filter((child) => child.hidden !== true);
1032
+ const showChildren = !!pathname && childItems.length > 0 && pathname.startsWith(i.href);
1033
+ const hasActiveChild = !!(pathname && childItems.some((c) => pathname.startsWith(c.href)));
1034
+ const isParentActive = pathname === i.href || showChildren && !hasActiveChild;
1035
+ const base = compact ? "w-10 h-10 justify-center" : "px-2 py-1 gap-2";
1036
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
1037
+ /* @__PURE__ */ jsxs(
869
1038
  Link,
870
1039
  {
871
- href: c.href,
872
- className: `relative text-sm rounded inline-flex items-center ${childBase} ${childActive ? "bg-background border shadow-sm" : "hover:bg-accent hover:text-accent-foreground"} ${c.enabled === false ? "pointer-events-none opacity-50" : ""}`,
873
- "aria-disabled": c.enabled === false,
874
- title: compact ? c.title : void 0,
1040
+ href: i.href,
1041
+ className: `relative text-sm rounded inline-flex items-center ${base} ${isParentActive ? "bg-background border shadow-sm" : "hover:bg-accent hover:text-accent-foreground"} ${i.enabled === false ? "pointer-events-none opacity-50" : ""}`,
1042
+ "aria-disabled": i.enabled === false,
1043
+ title: compact ? i.title : void 0,
1044
+ "data-menu-item-id": i.id ?? i.href,
875
1045
  onClick: () => setMobileOpen(false),
876
1046
  children: [
877
- childActive ? /* @__PURE__ */ jsx("span", { className: "absolute left-0 top-1 bottom-1 w-0.5 rounded bg-foreground" }) : null,
878
- /* @__PURE__ */ jsx("span", { className: `flex items-center justify-center shrink-0 ${compact ? "" : "text-muted-foreground"}`, children: c.icon ?? (c.href.includes("/backend/entities/user/") && c.href.endsWith("/records") ? DataTableIcon : DefaultIcon) }),
879
- !compact && /* @__PURE__ */ jsx("span", { children: c.title })
1047
+ isParentActive ? /* @__PURE__ */ jsx("span", { className: "absolute left-0 top-1 bottom-1 w-0.5 rounded bg-foreground" }) : null,
1048
+ /* @__PURE__ */ jsx("span", { className: `flex items-center justify-center shrink-0 ${compact ? "" : "text-muted-foreground"}`, children: i.icon ?? DefaultIcon }),
1049
+ !compact && /* @__PURE__ */ jsx("span", { children: i.title })
880
1050
  ]
881
- },
882
- c.href
883
- );
884
- }) }) : null
885
- ] }, i.href);
886
- }) }),
887
- gi !== mainLastVisibleGroupIndex && /* @__PURE__ */ jsx("div", { className: "my-2 border-t border-dotted" })
888
- ] }, groupId);
889
- }) }),
890
- /* @__PURE__ */ jsx("div", { className: "mt-4 pt-4 border-t", children: /* @__PURE__ */ jsxs(
891
- Link,
892
- {
893
- href: "/backend/settings",
894
- className: `relative text-sm rounded inline-flex items-center w-full ${compact ? "w-10 h-10 justify-center" : "px-2 py-1 gap-2"} ${pathname?.startsWith("/backend/settings") || pathname?.startsWith("/backend/config") || pathname?.startsWith("/backend/users") || pathname?.startsWith("/backend/roles") || pathname?.startsWith("/backend/api-keys") || pathname?.startsWith("/backend/entities") || pathname?.startsWith("/backend/query-indexes") || pathname?.startsWith("/backend/definitions") || pathname?.startsWith("/backend/instances") || pathname?.startsWith("/backend/tasks") || pathname?.startsWith("/backend/events") || pathname?.startsWith("/backend/rules") || pathname?.startsWith("/backend/sets") || pathname?.startsWith("/backend/logs") || pathname?.startsWith("/backend/directory") || pathname?.startsWith("/backend/feature-toggles") ? "bg-background border shadow-sm font-medium" : "hover:bg-accent hover:text-accent-foreground"}`,
895
- title: compact ? t("backend.nav.settings", "Settings") : void 0,
896
- onClick: () => setMobileOpen(false),
897
- children: [
898
- (pathname?.startsWith("/backend/settings") || pathname?.startsWith("/backend/config") || pathname?.startsWith("/backend/users") || pathname?.startsWith("/backend/roles") || pathname?.startsWith("/backend/api-keys") || pathname?.startsWith("/backend/entities") || pathname?.startsWith("/backend/query-indexes") || pathname?.startsWith("/backend/definitions") || pathname?.startsWith("/backend/instances") || pathname?.startsWith("/backend/tasks") || pathname?.startsWith("/backend/events") || pathname?.startsWith("/backend/rules") || pathname?.startsWith("/backend/sets") || pathname?.startsWith("/backend/logs") || pathname?.startsWith("/backend/directory") || pathname?.startsWith("/backend/feature-toggles")) && /* @__PURE__ */ jsx("span", { className: "absolute left-0 top-1 bottom-1 w-0.5 rounded bg-foreground" }),
899
- /* @__PURE__ */ jsx("span", { className: `flex items-center justify-center shrink-0 ${compact ? "" : "text-muted-foreground"}`, children: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
900
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
901
- /* @__PURE__ */ jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" })
902
- ] }) }),
903
- !compact && /* @__PURE__ */ jsx("span", { children: t("backend.nav.settings", "Settings") })
904
- ]
905
- }
906
- ) })
1051
+ }
1052
+ ),
1053
+ showChildren ? /* @__PURE__ */ jsx("div", { className: `flex flex-col ${compact ? "items-center" : ""} gap-1 ${!compact ? "pl-4" : ""}`, children: childItems.map((c) => {
1054
+ const childActive = pathname?.startsWith(c.href);
1055
+ const childBase = compact ? "w-10 h-8 justify-center" : "px-2 py-1 gap-2";
1056
+ return /* @__PURE__ */ jsxs(
1057
+ Link,
1058
+ {
1059
+ href: c.href,
1060
+ className: `relative text-sm rounded inline-flex items-center ${childBase} ${childActive ? "bg-background border shadow-sm" : "hover:bg-accent hover:text-accent-foreground"} ${c.enabled === false ? "pointer-events-none opacity-50" : ""}`,
1061
+ "aria-disabled": c.enabled === false,
1062
+ title: compact ? c.title : void 0,
1063
+ "data-menu-item-id": c.id ?? c.href,
1064
+ onClick: () => setMobileOpen(false),
1065
+ children: [
1066
+ childActive ? /* @__PURE__ */ jsx("span", { className: "absolute left-0 top-1 bottom-1 w-0.5 rounded bg-foreground" }) : null,
1067
+ /* @__PURE__ */ jsx("span", { className: `flex items-center justify-center shrink-0 ${compact ? "" : "text-muted-foreground"}`, children: c.icon ?? (c.href.includes("/backend/entities/user/") && c.href.endsWith("/records") ? DataTableIcon : DefaultIcon) }),
1068
+ !compact && /* @__PURE__ */ jsx("span", { children: c.title })
1069
+ ]
1070
+ },
1071
+ c.href
1072
+ );
1073
+ }) }) : null
1074
+ ] }, i.href);
1075
+ }) }),
1076
+ gi !== mainLastVisibleGroupIndex && /* @__PURE__ */ jsx("div", { className: "my-2 border-t border-dotted" })
1077
+ ] }, groupId);
1078
+ })
1079
+ ] }),
1080
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 pt-4 border-t", children: [
1081
+ shouldRenderSidebarInjectionSpots ? /* @__PURE__ */ jsx(
1082
+ InjectionSpot,
1083
+ {
1084
+ spotId: BACKEND_SIDEBAR_NAV_FOOTER_INJECTION_SPOT_ID,
1085
+ context: injectionContext
1086
+ }
1087
+ ) : null,
1088
+ /* @__PURE__ */ jsxs(
1089
+ Link,
1090
+ {
1091
+ href: "/backend/settings",
1092
+ className: `relative text-sm rounded inline-flex items-center w-full ${compact ? "w-10 h-10 justify-center" : "px-2 py-1 gap-2"} ${pathname?.startsWith("/backend/settings") || pathname?.startsWith("/backend/config") || pathname?.startsWith("/backend/users") || pathname?.startsWith("/backend/roles") || pathname?.startsWith("/backend/api-keys") || pathname?.startsWith("/backend/entities") || pathname?.startsWith("/backend/query-indexes") || pathname?.startsWith("/backend/definitions") || pathname?.startsWith("/backend/instances") || pathname?.startsWith("/backend/tasks") || pathname?.startsWith("/backend/events") || pathname?.startsWith("/backend/rules") || pathname?.startsWith("/backend/sets") || pathname?.startsWith("/backend/logs") || pathname?.startsWith("/backend/directory") || pathname?.startsWith("/backend/feature-toggles") ? "bg-background border shadow-sm font-medium" : "hover:bg-accent hover:text-accent-foreground"}`,
1093
+ title: compact ? t("backend.nav.settings", "Settings") : void 0,
1094
+ onClick: () => setMobileOpen(false),
1095
+ children: [
1096
+ (pathname?.startsWith("/backend/settings") || pathname?.startsWith("/backend/config") || pathname?.startsWith("/backend/users") || pathname?.startsWith("/backend/roles") || pathname?.startsWith("/backend/api-keys") || pathname?.startsWith("/backend/entities") || pathname?.startsWith("/backend/query-indexes") || pathname?.startsWith("/backend/definitions") || pathname?.startsWith("/backend/instances") || pathname?.startsWith("/backend/tasks") || pathname?.startsWith("/backend/events") || pathname?.startsWith("/backend/rules") || pathname?.startsWith("/backend/sets") || pathname?.startsWith("/backend/logs") || pathname?.startsWith("/backend/directory") || pathname?.startsWith("/backend/feature-toggles")) && /* @__PURE__ */ jsx("span", { className: "absolute left-0 top-1 bottom-1 w-0.5 rounded bg-foreground" }),
1097
+ /* @__PURE__ */ jsx("span", { className: `flex items-center justify-center shrink-0 ${compact ? "" : "text-muted-foreground"}`, children: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1098
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
1099
+ /* @__PURE__ */ jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" })
1100
+ ] }) }),
1101
+ !compact && /* @__PURE__ */ jsx("span", { children: t("backend.nav.settings", "Settings") })
1102
+ ]
1103
+ }
1104
+ )
1105
+ ] })
907
1106
  ] });
908
1107
  })() }),
909
- !customizing && /* @__PURE__ */ jsx(Fragment, { children: compact || isMobileVariant ? /* @__PURE__ */ jsx(
910
- IconButton,
911
- {
912
- variant: "outline",
913
- size: "lg",
914
- className: "mt-auto",
915
- onClick: startCustomization,
916
- disabled: loadingPreferences,
917
- "aria-label": t("appShell.customizeSidebar"),
918
- children: CustomizeIcon
919
- }
920
- ) : /* @__PURE__ */ jsxs(
921
- Button,
922
- {
923
- variant: "outline",
924
- size: "default",
925
- className: "mt-auto",
926
- onClick: startCustomization,
927
- disabled: loadingPreferences,
928
- "aria-label": t("appShell.customizeSidebar"),
929
- children: [
930
- CustomizeIcon,
931
- loadingPreferences ? t("appShell.sidebarCustomizationLoading") : t("appShell.customizeSidebar")
932
- ]
933
- }
934
- ) }),
1108
+ !customizing && /* @__PURE__ */ jsxs(Fragment, { children: [
1109
+ shouldRenderSidebarInjectionSpots ? /* @__PURE__ */ jsx(
1110
+ InjectionSpot,
1111
+ {
1112
+ spotId: GLOBAL_SIDEBAR_STATUS_BADGES_INJECTION_SPOT_ID,
1113
+ context: injectionContext
1114
+ }
1115
+ ) : null,
1116
+ compact || isMobileVariant ? /* @__PURE__ */ jsx(
1117
+ IconButton,
1118
+ {
1119
+ variant: "outline",
1120
+ size: "lg",
1121
+ className: "mt-auto",
1122
+ onClick: startCustomization,
1123
+ disabled: loadingPreferences,
1124
+ "aria-label": t("appShell.customizeSidebar"),
1125
+ children: CustomizeIcon
1126
+ }
1127
+ ) : /* @__PURE__ */ jsxs(
1128
+ Button,
1129
+ {
1130
+ variant: "outline",
1131
+ size: "default",
1132
+ className: "mt-auto",
1133
+ onClick: startCustomization,
1134
+ disabled: loadingPreferences,
1135
+ "aria-label": t("appShell.customizeSidebar"),
1136
+ children: [
1137
+ CustomizeIcon,
1138
+ loadingPreferences ? t("appShell.sidebarCustomizationLoading") : t("appShell.customizeSidebar")
1139
+ ]
1140
+ }
1141
+ )
1142
+ ] }),
935
1143
  shouldRenderSidebarInjectionSpots ? /* @__PURE__ */ jsx(
936
1144
  InjectionSpot,
937
1145
  {
@@ -946,6 +1154,37 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
946
1154
  setBreadcrumb: setHeaderBreadcrumb,
947
1155
  setTitle: setHeaderTitle
948
1156
  }), []);
1157
+ const renderedTopbarInjectedActions = React.useMemo(
1158
+ () => topbarInjectedMenuItems.map((item) => {
1159
+ const label = resolveInjectedMenuLabel(item, t);
1160
+ if (item.href) {
1161
+ return /* @__PURE__ */ jsx(
1162
+ Link,
1163
+ {
1164
+ href: item.href,
1165
+ className: "inline-flex items-center rounded border px-2 py-1 text-xs hover:bg-accent hover:text-accent-foreground",
1166
+ "data-menu-item-id": item.id,
1167
+ children: label
1168
+ },
1169
+ item.id
1170
+ );
1171
+ }
1172
+ return /* @__PURE__ */ jsx(
1173
+ Button,
1174
+ {
1175
+ type: "button",
1176
+ variant: "outline",
1177
+ size: "sm",
1178
+ className: "h-7 text-xs",
1179
+ "data-menu-item-id": item.id,
1180
+ onClick: () => item.onClick?.(),
1181
+ children: label
1182
+ },
1183
+ item.id
1184
+ );
1185
+ }),
1186
+ [t, topbarInjectedMenuItems]
1187
+ );
949
1188
  return /* @__PURE__ */ jsx(HeaderContext.Provider, { value: headerCtxValue, children: /* @__PURE__ */ jsxs("div", { className: `min-h-svh lg:grid ${gridColsClass}`, children: [
950
1189
  /* @__PURE__ */ jsx("aside", { className: `${asideClassesBase} ${effectiveCollapsed ? "px-2" : "px-3"} hidden lg:block`, style: { width: asideWidth }, children: renderSidebar(effectiveCollapsed) }),
951
1190
  /* @__PURE__ */ jsxs("div", { className: "flex min-h-svh flex-col min-w-0", children: [
@@ -990,12 +1229,30 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
990
1229
  }) });
991
1230
  })()
992
1231
  ] }),
993
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1 md:gap-2 text-sm shrink-0", children: rightHeaderSlot ? rightHeaderSlot : /* @__PURE__ */ jsx("span", { className: "opacity-80", children: email || t("appShell.userFallback") }) })
1232
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 md:gap-2 text-sm shrink-0", children: [
1233
+ /* @__PURE__ */ jsx(
1234
+ InjectionSpot,
1235
+ {
1236
+ spotId: GLOBAL_HEADER_STATUS_INDICATORS_INJECTION_SPOT_ID,
1237
+ context: injectionContext
1238
+ }
1239
+ ),
1240
+ /* @__PURE__ */ jsx(
1241
+ InjectionSpot,
1242
+ {
1243
+ spotId: BACKEND_TOPBAR_ACTIONS_INJECTION_SPOT_ID,
1244
+ context: injectionContext
1245
+ }
1246
+ ),
1247
+ renderedTopbarInjectedActions,
1248
+ rightHeaderSlot ? rightHeaderSlot : /* @__PURE__ */ jsx("span", { className: "opacity-80", children: email || t("appShell.userFallback") })
1249
+ ] })
994
1250
  ] }),
995
1251
  /* @__PURE__ */ jsx(ProgressTopBar, { t, className: "sticky top-0 z-10" }),
996
1252
  /* @__PURE__ */ jsxs("main", { className: "flex-1 p-4 lg:p-6", children: [
997
1253
  /* @__PURE__ */ jsx(InjectionSpot, { spotId: BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID, context: injectionContext }),
998
1254
  /* @__PURE__ */ jsx(FlashMessages, {}),
1255
+ /* @__PURE__ */ jsx(SseEventIndicator, {}),
999
1256
  /* @__PURE__ */ jsx(PartialIndexBanner, {}),
1000
1257
  /* @__PURE__ */ jsx(UpgradeActionBanner, {}),
1001
1258
  /* @__PURE__ */ jsx(LastOperationBanner, {}),
@@ -1037,6 +1294,7 @@ function AppShell({ productName, email, groups, rightHeaderSlot, children, sideb
1037
1294
  }
1038
1295
  AppShell.cloneGroups = function cloneGroups(groups) {
1039
1296
  const cloneItem = (item) => ({
1297
+ id: item.id,
1040
1298
  href: item.href,
1041
1299
  title: item.title,
1042
1300
  defaultTitle: item.defaultTitle,
@@ -1078,10 +1336,11 @@ function applyCustomizationDraft(baseGroups, draft) {
1078
1336
  return result;
1079
1337
  }
1080
1338
  function applyItemDraft(item, draft) {
1339
+ const itemKey = resolveItemKey(item);
1081
1340
  const baseTitle = item.defaultTitle ?? item.title;
1082
- const override = draft.itemLabels[item.href]?.trim();
1341
+ const override = draft.itemLabels[itemKey]?.trim();
1083
1342
  const children = item.children ? item.children.map((child) => applyItemDraft(child, draft)) : void 0;
1084
- const hidden = draft.hiddenItemIds[item.href] === true;
1343
+ const hidden = draft.hiddenItemIds[itemKey] === true;
1085
1344
  return {
1086
1345
  ...item,
1087
1346
  title: override && override.length > 0 ? override : baseTitle,
@@ -1110,7 +1369,9 @@ function collectSidebarDefaults(groups) {
1110
1369
  const itemDefaults = /* @__PURE__ */ new Map();
1111
1370
  const visitItems = (items) => {
1112
1371
  for (const item of items) {
1372
+ const key = resolveItemKey(item);
1113
1373
  const baseTitle = item.defaultTitle ?? item.title;
1374
+ itemDefaults.set(key, baseTitle);
1114
1375
  itemDefaults.set(item.href, baseTitle);
1115
1376
  if (item.children && item.children.length > 0) visitItems(item.children);
1116
1377
  }