@open-mercato/ui 0.4.2-canary-c02407ff85

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 (319) hide show
  1. package/build.mjs +62 -0
  2. package/dist/backend/AppShell.js +902 -0
  3. package/dist/backend/AppShell.js.map +7 -0
  4. package/dist/backend/ConfirmDialog.js +17 -0
  5. package/dist/backend/ConfirmDialog.js.map +7 -0
  6. package/dist/backend/ContextHelp.js +31 -0
  7. package/dist/backend/ContextHelp.js.map +7 -0
  8. package/dist/backend/CrudForm.js +2028 -0
  9. package/dist/backend/CrudForm.js.map +7 -0
  10. package/dist/backend/DataTable.js +1363 -0
  11. package/dist/backend/DataTable.js.map +7 -0
  12. package/dist/backend/EmptyState.js +52 -0
  13. package/dist/backend/EmptyState.js.map +7 -0
  14. package/dist/backend/FilterBar.js +140 -0
  15. package/dist/backend/FilterBar.js.map +7 -0
  16. package/dist/backend/FilterOverlay.js +279 -0
  17. package/dist/backend/FilterOverlay.js.map +7 -0
  18. package/dist/backend/FlashMessages.js +66 -0
  19. package/dist/backend/FlashMessages.js.map +7 -0
  20. package/dist/backend/JsonBuilder.js +322 -0
  21. package/dist/backend/JsonBuilder.js.map +7 -0
  22. package/dist/backend/JsonDisplay.js +203 -0
  23. package/dist/backend/JsonDisplay.js.map +7 -0
  24. package/dist/backend/Page.js +27 -0
  25. package/dist/backend/Page.js.map +7 -0
  26. package/dist/backend/PerspectiveSidebar.js +282 -0
  27. package/dist/backend/PerspectiveSidebar.js.map +7 -0
  28. package/dist/backend/RowActions.js +148 -0
  29. package/dist/backend/RowActions.js.map +7 -0
  30. package/dist/backend/TruncatedCell.js +92 -0
  31. package/dist/backend/TruncatedCell.js.map +7 -0
  32. package/dist/backend/UserMenu.js +107 -0
  33. package/dist/backend/UserMenu.js.map +7 -0
  34. package/dist/backend/ValueIcons.js +34 -0
  35. package/dist/backend/ValueIcons.js.map +7 -0
  36. package/dist/backend/custom-fields/FieldDefinitionsEditor.js +1264 -0
  37. package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +7 -0
  38. package/dist/backend/custom-fields/FieldDefinitionsManager.js +332 -0
  39. package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +7 -0
  40. package/dist/backend/dashboard/DashboardScreen.js +578 -0
  41. package/dist/backend/dashboard/DashboardScreen.js.map +7 -0
  42. package/dist/backend/dashboard/index.js +5 -0
  43. package/dist/backend/dashboard/index.js.map +7 -0
  44. package/dist/backend/dashboard/widgetRegistry.js +55 -0
  45. package/dist/backend/dashboard/widgetRegistry.js.map +7 -0
  46. package/dist/backend/detail/ActivitiesSection.js +962 -0
  47. package/dist/backend/detail/ActivitiesSection.js.map +7 -0
  48. package/dist/backend/detail/AddressEditor.js +413 -0
  49. package/dist/backend/detail/AddressEditor.js.map +7 -0
  50. package/dist/backend/detail/AddressTiles.js +437 -0
  51. package/dist/backend/detail/AddressTiles.js.map +7 -0
  52. package/dist/backend/detail/AddressesSection.js +264 -0
  53. package/dist/backend/detail/AddressesSection.js.map +7 -0
  54. package/dist/backend/detail/AttachmentDeleteDialog.js +41 -0
  55. package/dist/backend/detail/AttachmentDeleteDialog.js.map +7 -0
  56. package/dist/backend/detail/AttachmentMetadataDialog.js +517 -0
  57. package/dist/backend/detail/AttachmentMetadataDialog.js.map +7 -0
  58. package/dist/backend/detail/AttachmentsSection.js +367 -0
  59. package/dist/backend/detail/AttachmentsSection.js.map +7 -0
  60. package/dist/backend/detail/CustomDataSection.js +433 -0
  61. package/dist/backend/detail/CustomDataSection.js.map +7 -0
  62. package/dist/backend/detail/DetailFieldsSection.js +75 -0
  63. package/dist/backend/detail/DetailFieldsSection.js.map +7 -0
  64. package/dist/backend/detail/ErrorMessage.js +28 -0
  65. package/dist/backend/detail/ErrorMessage.js.map +7 -0
  66. package/dist/backend/detail/InlineEditors.js +681 -0
  67. package/dist/backend/detail/InlineEditors.js.map +7 -0
  68. package/dist/backend/detail/LoadingMessage.js +14 -0
  69. package/dist/backend/detail/LoadingMessage.js.map +7 -0
  70. package/dist/backend/detail/NotesSection.js +1032 -0
  71. package/dist/backend/detail/NotesSection.js.map +7 -0
  72. package/dist/backend/detail/TabEmptyState.js +25 -0
  73. package/dist/backend/detail/TabEmptyState.js.map +7 -0
  74. package/dist/backend/detail/TagsSection.js +254 -0
  75. package/dist/backend/detail/TagsSection.js.map +7 -0
  76. package/dist/backend/detail/addressFormat.js +77 -0
  77. package/dist/backend/detail/addressFormat.js.map +7 -0
  78. package/dist/backend/detail/index.js +34 -0
  79. package/dist/backend/detail/index.js.map +7 -0
  80. package/dist/backend/fields/registry.generated.js +8 -0
  81. package/dist/backend/fields/registry.generated.js.map +7 -0
  82. package/dist/backend/fields/registry.js +29 -0
  83. package/dist/backend/fields/registry.js.map +7 -0
  84. package/dist/backend/indexes/PartialIndexBanner.js +58 -0
  85. package/dist/backend/indexes/PartialIndexBanner.js.map +7 -0
  86. package/dist/backend/indexes/store.js +62 -0
  87. package/dist/backend/indexes/store.js.map +7 -0
  88. package/dist/backend/injection/InjectionSpot.js +179 -0
  89. package/dist/backend/injection/InjectionSpot.js.map +7 -0
  90. package/dist/backend/injection/PageInjectionBoundary.js +26 -0
  91. package/dist/backend/injection/PageInjectionBoundary.js.map +7 -0
  92. package/dist/backend/injection/helpers.js +26 -0
  93. package/dist/backend/injection/helpers.js.map +7 -0
  94. package/dist/backend/injection/widgetRegistry.js +55 -0
  95. package/dist/backend/injection/widgetRegistry.js.map +7 -0
  96. package/dist/backend/inputs/ComboboxInput.js +225 -0
  97. package/dist/backend/inputs/ComboboxInput.js.map +7 -0
  98. package/dist/backend/inputs/LookupSelect.js +191 -0
  99. package/dist/backend/inputs/LookupSelect.js.map +7 -0
  100. package/dist/backend/inputs/PhoneNumberField.js +100 -0
  101. package/dist/backend/inputs/PhoneNumberField.js.map +7 -0
  102. package/dist/backend/inputs/SwitchableMarkdownInput.js +92 -0
  103. package/dist/backend/inputs/SwitchableMarkdownInput.js.map +7 -0
  104. package/dist/backend/inputs/TagsInput.js +222 -0
  105. package/dist/backend/inputs/TagsInput.js.map +7 -0
  106. package/dist/backend/inputs/index.js +6 -0
  107. package/dist/backend/inputs/index.js.map +7 -0
  108. package/dist/backend/operations/LastOperationBanner.js +80 -0
  109. package/dist/backend/operations/LastOperationBanner.js.map +7 -0
  110. package/dist/backend/operations/store.js +183 -0
  111. package/dist/backend/operations/store.js.map +7 -0
  112. package/dist/backend/schedule/ScheduleAgenda.js +107 -0
  113. package/dist/backend/schedule/ScheduleAgenda.js.map +7 -0
  114. package/dist/backend/schedule/ScheduleGrid.js +107 -0
  115. package/dist/backend/schedule/ScheduleGrid.js.map +7 -0
  116. package/dist/backend/schedule/ScheduleToolbar.js +166 -0
  117. package/dist/backend/schedule/ScheduleToolbar.js.map +7 -0
  118. package/dist/backend/schedule/ScheduleView.js +165 -0
  119. package/dist/backend/schedule/ScheduleView.js.map +7 -0
  120. package/dist/backend/schedule/index.js +6 -0
  121. package/dist/backend/schedule/index.js.map +7 -0
  122. package/dist/backend/schedule/recurrence.js +83 -0
  123. package/dist/backend/schedule/recurrence.js.map +7 -0
  124. package/dist/backend/schedule/types.js +1 -0
  125. package/dist/backend/schedule/types.js.map +7 -0
  126. package/dist/backend/upgrades/UpgradeActionBanner.js +91 -0
  127. package/dist/backend/upgrades/UpgradeActionBanner.js.map +7 -0
  128. package/dist/backend/utils/api.js +127 -0
  129. package/dist/backend/utils/api.js.map +7 -0
  130. package/dist/backend/utils/apiCall.js +48 -0
  131. package/dist/backend/utils/apiCall.js.map +7 -0
  132. package/dist/backend/utils/crud.js +126 -0
  133. package/dist/backend/utils/crud.js.map +7 -0
  134. package/dist/backend/utils/customFieldColumns.js +56 -0
  135. package/dist/backend/utils/customFieldColumns.js.map +7 -0
  136. package/dist/backend/utils/customFieldDefs.js +143 -0
  137. package/dist/backend/utils/customFieldDefs.js.map +7 -0
  138. package/dist/backend/utils/customFieldFilters.js +126 -0
  139. package/dist/backend/utils/customFieldFilters.js.map +7 -0
  140. package/dist/backend/utils/customFieldForms.js +162 -0
  141. package/dist/backend/utils/customFieldForms.js.map +7 -0
  142. package/dist/backend/utils/customFieldValues.js +26 -0
  143. package/dist/backend/utils/customFieldValues.js.map +7 -0
  144. package/dist/backend/utils/flash.js +16 -0
  145. package/dist/backend/utils/flash.js.map +7 -0
  146. package/dist/backend/utils/nav.js +185 -0
  147. package/dist/backend/utils/nav.js.map +7 -0
  148. package/dist/backend/utils/serverErrors.js +230 -0
  149. package/dist/backend/utils/serverErrors.js.map +7 -0
  150. package/dist/frontend/AuthFooter.js +23 -0
  151. package/dist/frontend/AuthFooter.js.map +7 -0
  152. package/dist/frontend/LanguageSwitcher.js +57 -0
  153. package/dist/frontend/LanguageSwitcher.js.map +7 -0
  154. package/dist/frontend/Layout.js +14 -0
  155. package/dist/frontend/Layout.js.map +7 -0
  156. package/dist/index.js +32 -0
  157. package/dist/index.js.map +7 -0
  158. package/dist/primitives/DataLoader.js +67 -0
  159. package/dist/primitives/DataLoader.js.map +7 -0
  160. package/dist/primitives/ErrorNotice.js +20 -0
  161. package/dist/primitives/ErrorNotice.js.map +7 -0
  162. package/dist/primitives/alert.js +38 -0
  163. package/dist/primitives/alert.js.map +7 -0
  164. package/dist/primitives/badge.js +28 -0
  165. package/dist/primitives/badge.js.map +7 -0
  166. package/dist/primitives/button.js +44 -0
  167. package/dist/primitives/button.js.map +7 -0
  168. package/dist/primitives/card.js +91 -0
  169. package/dist/primitives/card.js.map +7 -0
  170. package/dist/primitives/checkbox.js +28 -0
  171. package/dist/primitives/checkbox.js.map +7 -0
  172. package/dist/primitives/dialog.js +90 -0
  173. package/dist/primitives/dialog.js.map +7 -0
  174. package/dist/primitives/input.js +22 -0
  175. package/dist/primitives/input.js.map +7 -0
  176. package/dist/primitives/label.js +21 -0
  177. package/dist/primitives/label.js.map +7 -0
  178. package/dist/primitives/separator.js +9 -0
  179. package/dist/primitives/separator.js.map +7 -0
  180. package/dist/primitives/spinner.js +24 -0
  181. package/dist/primitives/spinner.js.map +7 -0
  182. package/dist/primitives/switch.js +80 -0
  183. package/dist/primitives/switch.js.map +7 -0
  184. package/dist/primitives/table.js +29 -0
  185. package/dist/primitives/table.js.map +7 -0
  186. package/dist/primitives/tabs.js +87 -0
  187. package/dist/primitives/tabs.js.map +7 -0
  188. package/dist/primitives/textarea.js +21 -0
  189. package/dist/primitives/textarea.js.map +7 -0
  190. package/dist/primitives/tooltip.js +60 -0
  191. package/dist/primitives/tooltip.js.map +7 -0
  192. package/dist/theme/QueryProvider.js +44 -0
  193. package/dist/theme/QueryProvider.js.map +7 -0
  194. package/dist/theme/ThemeProvider.js +95 -0
  195. package/dist/theme/ThemeProvider.js.map +7 -0
  196. package/dist/theme/ThemeToggle.js +88 -0
  197. package/dist/theme/ThemeToggle.js.map +7 -0
  198. package/dist/theme/index.js +10 -0
  199. package/dist/theme/index.js.map +7 -0
  200. package/dist/types/react-big-calendar.d.js +1 -0
  201. package/dist/types/react-big-calendar.d.js.map +7 -0
  202. package/jest.config.cjs +23 -0
  203. package/jest.setup.ts +55 -0
  204. package/package.json +105 -0
  205. package/src/backend/AppShell.tsx +1096 -0
  206. package/src/backend/ConfirmDialog.tsx +19 -0
  207. package/src/backend/ContextHelp.tsx +38 -0
  208. package/src/backend/CrudForm.tsx +2503 -0
  209. package/src/backend/DataTable.tsx +1730 -0
  210. package/src/backend/EmptyState.tsx +65 -0
  211. package/src/backend/FilterBar.tsx +161 -0
  212. package/src/backend/FilterOverlay.tsx +328 -0
  213. package/src/backend/FlashMessages.tsx +82 -0
  214. package/src/backend/JsonBuilder.tsx +362 -0
  215. package/src/backend/JsonDisplay.tsx +254 -0
  216. package/src/backend/Page.tsx +30 -0
  217. package/src/backend/PerspectiveSidebar.tsx +337 -0
  218. package/src/backend/RowActions.tsx +151 -0
  219. package/src/backend/TruncatedCell.tsx +133 -0
  220. package/src/backend/UserMenu.tsx +118 -0
  221. package/src/backend/ValueIcons.tsx +48 -0
  222. package/src/backend/__tests__/AppShell.test.tsx +115 -0
  223. package/src/backend/__tests__/CrudForm.render.test.tsx +30 -0
  224. package/src/backend/__tests__/DataTable.render.test.tsx +48 -0
  225. package/src/backend/__tests__/custom-field-filters.test.ts +72 -0
  226. package/src/backend/__tests__/custom-field-forms.test.ts +54 -0
  227. package/src/backend/__tests__/serverErrors.test.ts +83 -0
  228. package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +1292 -0
  229. package/src/backend/custom-fields/FieldDefinitionsManager.tsx +381 -0
  230. package/src/backend/dashboard/DashboardScreen.tsx +684 -0
  231. package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +112 -0
  232. package/src/backend/dashboard/index.ts +1 -0
  233. package/src/backend/dashboard/widgetRegistry.ts +68 -0
  234. package/src/backend/detail/ActivitiesSection.tsx +1284 -0
  235. package/src/backend/detail/AddressEditor.tsx +472 -0
  236. package/src/backend/detail/AddressTiles.tsx +587 -0
  237. package/src/backend/detail/AddressesSection.tsx +346 -0
  238. package/src/backend/detail/AttachmentDeleteDialog.tsx +56 -0
  239. package/src/backend/detail/AttachmentMetadataDialog.tsx +672 -0
  240. package/src/backend/detail/AttachmentsSection.tsx +414 -0
  241. package/src/backend/detail/CustomDataSection.tsx +530 -0
  242. package/src/backend/detail/DetailFieldsSection.tsx +147 -0
  243. package/src/backend/detail/ErrorMessage.tsx +32 -0
  244. package/src/backend/detail/InlineEditors.tsx +877 -0
  245. package/src/backend/detail/LoadingMessage.tsx +14 -0
  246. package/src/backend/detail/NotesSection.tsx +1275 -0
  247. package/src/backend/detail/TabEmptyState.tsx +48 -0
  248. package/src/backend/detail/TagsSection.tsx +314 -0
  249. package/src/backend/detail/addressFormat.tsx +121 -0
  250. package/src/backend/detail/index.ts +44 -0
  251. package/src/backend/fields/registry.generated.ts +8 -0
  252. package/src/backend/fields/registry.ts +38 -0
  253. package/src/backend/indexes/PartialIndexBanner.tsx +68 -0
  254. package/src/backend/indexes/store.ts +88 -0
  255. package/src/backend/injection/InjectionSpot.tsx +236 -0
  256. package/src/backend/injection/PageInjectionBoundary.tsx +31 -0
  257. package/src/backend/injection/helpers.ts +35 -0
  258. package/src/backend/injection/widgetRegistry.ts +68 -0
  259. package/src/backend/inputs/ComboboxInput.tsx +269 -0
  260. package/src/backend/inputs/LookupSelect.tsx +247 -0
  261. package/src/backend/inputs/PhoneNumberField.tsx +129 -0
  262. package/src/backend/inputs/SwitchableMarkdownInput.tsx +128 -0
  263. package/src/backend/inputs/TagsInput.tsx +259 -0
  264. package/src/backend/inputs/index.ts +5 -0
  265. package/src/backend/operations/LastOperationBanner.tsx +85 -0
  266. package/src/backend/operations/__tests__/LastOperationBanner.test.tsx +99 -0
  267. package/src/backend/operations/store.ts +230 -0
  268. package/src/backend/schedule/ScheduleAgenda.tsx +136 -0
  269. package/src/backend/schedule/ScheduleGrid.tsx +136 -0
  270. package/src/backend/schedule/ScheduleToolbar.tsx +178 -0
  271. package/src/backend/schedule/ScheduleView.tsx +198 -0
  272. package/src/backend/schedule/index.ts +5 -0
  273. package/src/backend/schedule/recurrence.ts +99 -0
  274. package/src/backend/schedule/types.ts +26 -0
  275. package/src/backend/upgrades/UpgradeActionBanner.tsx +128 -0
  276. package/src/backend/utils/__tests__/apiCall.test.ts +109 -0
  277. package/src/backend/utils/__tests__/crud.test.ts +87 -0
  278. package/src/backend/utils/__tests__/customFieldDefs.test.ts +25 -0
  279. package/src/backend/utils/__tests__/customFieldValues.test.ts +35 -0
  280. package/src/backend/utils/api.ts +149 -0
  281. package/src/backend/utils/apiCall.ts +96 -0
  282. package/src/backend/utils/crud.ts +174 -0
  283. package/src/backend/utils/customFieldColumns.ts +71 -0
  284. package/src/backend/utils/customFieldDefs.ts +245 -0
  285. package/src/backend/utils/customFieldFilters.ts +145 -0
  286. package/src/backend/utils/customFieldForms.ts +196 -0
  287. package/src/backend/utils/customFieldValues.ts +41 -0
  288. package/src/backend/utils/flash.ts +17 -0
  289. package/src/backend/utils/nav.ts +238 -0
  290. package/src/backend/utils/serverErrors.ts +302 -0
  291. package/src/frontend/AuthFooter.tsx +29 -0
  292. package/src/frontend/LanguageSwitcher.tsx +66 -0
  293. package/src/frontend/Layout.tsx +13 -0
  294. package/src/index.ts +32 -0
  295. package/src/primitives/DataLoader.tsx +92 -0
  296. package/src/primitives/ErrorNotice.tsx +26 -0
  297. package/src/primitives/alert.tsx +52 -0
  298. package/src/primitives/badge.tsx +31 -0
  299. package/src/primitives/button.tsx +47 -0
  300. package/src/primitives/card.tsx +92 -0
  301. package/src/primitives/checkbox.tsx +28 -0
  302. package/src/primitives/dialog.tsx +110 -0
  303. package/src/primitives/input.tsx +20 -0
  304. package/src/primitives/label.tsx +18 -0
  305. package/src/primitives/separator.tsx +7 -0
  306. package/src/primitives/spinner.tsx +27 -0
  307. package/src/primitives/switch.tsx +86 -0
  308. package/src/primitives/table.tsx +27 -0
  309. package/src/primitives/tabs.tsx +128 -0
  310. package/src/primitives/textarea.tsx +20 -0
  311. package/src/primitives/tooltip.tsx +85 -0
  312. package/src/theme/QueryProvider.tsx +46 -0
  313. package/src/theme/ThemeProvider.tsx +120 -0
  314. package/src/theme/ThemeToggle.tsx +88 -0
  315. package/src/theme/index.ts +3 -0
  316. package/src/types/react-big-calendar.d.ts +16 -0
  317. package/tsconfig.build.json +11 -0
  318. package/tsconfig.json +9 -0
  319. package/watch.mjs +6 -0
@@ -0,0 +1,902 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import Link from "next/link";
5
+ import Image from "next/image";
6
+ import { Separator } from "../primitives/separator.js";
7
+ import { FlashMessages } from "./FlashMessages.js";
8
+ import { usePathname } from "next/navigation";
9
+ import { apiCall } from "./utils/apiCall.js";
10
+ import { LanguageSwitcher } from "../frontend/LanguageSwitcher.js";
11
+ import { ThemeToggle } from "../theme/ThemeToggle.js";
12
+ import { LastOperationBanner } from "./operations/LastOperationBanner.js";
13
+ import { UpgradeActionBanner } from "./upgrades/UpgradeActionBanner.js";
14
+ import { PartialIndexBanner } from "./indexes/PartialIndexBanner.js";
15
+ import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
16
+ import { slugifySidebarId } from "@open-mercato/shared/modules/navigation/sidebarPreferences";
17
+ function resolveGroupKey(group) {
18
+ if (group.id && group.id.length) return group.id;
19
+ if (group.defaultName && group.defaultName.length) return slugifySidebarId(group.defaultName);
20
+ return slugifySidebarId(group.name);
21
+ }
22
+ const HeaderContext = React.createContext(null);
23
+ function ApplyBreadcrumb({ breadcrumb, title, titleKey }) {
24
+ const ctx = React.useContext(HeaderContext);
25
+ const t = useT();
26
+ const resolvedBreadcrumb = React.useMemo(() => {
27
+ if (!breadcrumb) return void 0;
28
+ return breadcrumb.map(({ label, labelKey, href }) => {
29
+ const translated = labelKey ? t(labelKey) : void 0;
30
+ const finalLabel = translated && translated !== labelKey ? translated : label;
31
+ return {
32
+ href,
33
+ label: finalLabel
34
+ };
35
+ });
36
+ }, [breadcrumb, t]);
37
+ const resolvedTitle = React.useMemo(() => {
38
+ if (!titleKey) return title;
39
+ const translated = t(titleKey);
40
+ if (translated && translated !== titleKey) return translated;
41
+ return title;
42
+ }, [titleKey, title, t]);
43
+ React.useEffect(() => {
44
+ ctx?.setBreadcrumb(resolvedBreadcrumb);
45
+ if (resolvedTitle !== void 0) ctx?.setTitle(resolvedTitle);
46
+ }, [ctx, resolvedBreadcrumb, resolvedTitle]);
47
+ return null;
48
+ }
49
+ const DefaultIcon = /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
50
+ /* @__PURE__ */ jsx("path", { d: "M8 6h13M8 12h13M8 18h13" }),
51
+ /* @__PURE__ */ jsx("path", { d: "M3 6h.01M3 12h.01M3 18h.01" })
52
+ ] });
53
+ const DataTableIcon = /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
54
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2", ry: "2" }),
55
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "8", x2: "21", y2: "8" }),
56
+ /* @__PURE__ */ jsx("line", { x1: "9", y1: "8", x2: "9", y2: "20" }),
57
+ /* @__PURE__ */ jsx("line", { x1: "15", y1: "8", x2: "15", y2: "20" })
58
+ ] });
59
+ const CustomizeIcon = /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
60
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
61
+ /* @__PURE__ */ jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.05.05a2 2 0 1 1-2.83 2.83l-.05-.05A1.65 1.65 0 0 0 15 19.4a1.65 1.65 0 0 0-1 .6 1.65 1.65 0 0 0-.33 1.82l-.05.05a2 2 0 1 1-2.83-2.83l.05-.05A1.65 1.65 0 0 0 9 15a1.65 1.65 0 0 0-1-.6 1.65 1.65 0 0 0-1.82.33l-.05.05a2 2 0 1 1-2.83-2.83l.05-.05A1.65 1.65 0 0 0 4.6 9 1.65 1.65 0 0 0 4 8a1.65 1.65 0 0 0-.6-1.82l-.05-.05a2 2 0 1 1 2.83-2.83l.05.05A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-.6 1.65 1.65 0 0 0 .33-1.82l.05-.05a2 2 0 1 1 2.83 2.83l-.05.05A1.65 1.65 0 0 0 15 9a1.65 1.65 0 0 0 1 .6 1.65 1.65 0 0 0 1.82-.33l.05-.05a2 2 0 1 1 2.83 2.83l-.05.05A1.65 1.65 0 0 0 19.4 15z" })
62
+ ] });
63
+ function Chevron({ open }) {
64
+ return /* @__PURE__ */ jsx("svg", { className: `transition-transform ${open ? "rotate-180" : ""}`, width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { d: "M6 9l6 6 6-6" }) });
65
+ }
66
+ function AppShell({ productName, email, groups, rightHeaderSlot, children, sidebarCollapsedDefault = false, currentTitle, breadcrumb, adminNavApi, version }) {
67
+ const pathname = usePathname();
68
+ const t = useT();
69
+ const locale = useLocale();
70
+ const resolvedProductName = productName ?? t("appShell.productName");
71
+ const [mobileOpen, setMobileOpen] = React.useState(false);
72
+ const [collapsed, setCollapsed] = React.useState(sidebarCollapsedDefault);
73
+ const [navGroups, setNavGroups] = React.useState(AppShell.cloneGroups(groups));
74
+ const [openGroups, setOpenGroups] = React.useState(
75
+ () => Object.fromEntries(groups.map((g) => [resolveGroupKey(g), true]))
76
+ );
77
+ const [customizing, setCustomizing] = React.useState(false);
78
+ const [customDraft, setCustomDraft] = React.useState(null);
79
+ const [loadingPreferences, setLoadingPreferences] = React.useState(false);
80
+ const [savingPreferences, setSavingPreferences] = React.useState(false);
81
+ const [customizationError, setCustomizationError] = React.useState(null);
82
+ const [availableRoleTargets, setAvailableRoleTargets] = React.useState([]);
83
+ const [selectedRoleIds, setSelectedRoleIds] = React.useState([]);
84
+ const [canApplyToRoles, setCanApplyToRoles] = React.useState(false);
85
+ const originalNavRef = React.useRef(null);
86
+ const [headerTitle, setHeaderTitle] = React.useState(currentTitle);
87
+ const [headerBreadcrumb, setHeaderBreadcrumb] = React.useState(breadcrumb);
88
+ const effectiveCollapsed = customizing ? false : collapsed;
89
+ const expandedSidebarWidth = customizing ? "320px" : "240px";
90
+ React.useEffect(() => {
91
+ try {
92
+ const savedOpen = typeof window !== "undefined" ? localStorage.getItem("om:sidebarOpenGroups") : null;
93
+ if (!savedOpen) return;
94
+ const parsed = JSON.parse(savedOpen);
95
+ setOpenGroups((prev) => {
96
+ const next = { ...prev };
97
+ for (const group of groups) {
98
+ const key = resolveGroupKey(group);
99
+ if (key in parsed) next[key] = !!parsed[key];
100
+ else if (group.name in parsed) next[key] = !!parsed[group.name];
101
+ }
102
+ return next;
103
+ });
104
+ } catch {
105
+ }
106
+ }, [groups]);
107
+ const toggleGroup = (groupId) => setOpenGroups((prev) => ({ ...prev, [groupId]: !prev[groupId] }));
108
+ const updateDraft = React.useCallback((updater) => {
109
+ setCustomDraft((prev) => {
110
+ if (!prev) return prev;
111
+ const next = updater(prev);
112
+ if (originalNavRef.current) {
113
+ setNavGroups(applyCustomizationDraft(originalNavRef.current, next));
114
+ }
115
+ return next;
116
+ });
117
+ }, []);
118
+ const startCustomization = React.useCallback(async () => {
119
+ if (customizing || loadingPreferences) return;
120
+ setCustomizationError(null);
121
+ setLoadingPreferences(true);
122
+ try {
123
+ const baseSnapshot = AppShell.cloneGroups(navGroups);
124
+ const call = await apiCall("/api/auth/sidebar/preferences");
125
+ const data = call.ok ? call.result ?? null : null;
126
+ const rawSettings = data?.settings;
127
+ const responseOrder = Array.isArray(rawSettings?.groupOrder) ? rawSettings.groupOrder.map((id) => typeof id === "string" ? id.trim() : "").filter((id) => id.length > 0) : [];
128
+ const responseGroupLabels = {};
129
+ if (rawSettings?.groupLabels && typeof rawSettings.groupLabels === "object") {
130
+ for (const [key, value] of Object.entries(rawSettings.groupLabels)) {
131
+ if (typeof value !== "string") continue;
132
+ const trimmedKey = key.trim();
133
+ if (!trimmedKey) continue;
134
+ responseGroupLabels[trimmedKey] = value;
135
+ }
136
+ }
137
+ const responseItemLabels = {};
138
+ if (rawSettings?.itemLabels && typeof rawSettings.itemLabels === "object") {
139
+ for (const [key, value] of Object.entries(rawSettings.itemLabels)) {
140
+ if (typeof value !== "string") continue;
141
+ const trimmedKey = key.trim();
142
+ if (!trimmedKey) continue;
143
+ responseItemLabels[trimmedKey] = value;
144
+ }
145
+ }
146
+ const responseHiddenItems = Array.isArray(rawSettings?.hiddenItems) ? rawSettings.hiddenItems.map((href) => typeof href === "string" ? href.trim() : "").filter((href) => href.length > 0) : [];
147
+ const canManageRoles = data?.canApplyToRoles === true;
148
+ setCanApplyToRoles(canManageRoles);
149
+ if (canManageRoles) {
150
+ const roles = Array.isArray(data?.roles) ? data.roles.filter((role) => typeof role?.id === "string" && typeof role?.name === "string") : [];
151
+ const mappedRoles = roles.map((role) => ({
152
+ id: role.id,
153
+ name: role.name,
154
+ hasPreference: role.hasPreference === true
155
+ }));
156
+ setAvailableRoleTargets(mappedRoles);
157
+ setSelectedRoleIds(mappedRoles.filter((role) => role.hasPreference).map((role) => role.id));
158
+ } else {
159
+ setAvailableRoleTargets([]);
160
+ setSelectedRoleIds([]);
161
+ }
162
+ const currentIds = baseSnapshot.map((group) => resolveGroupKey(group));
163
+ const order = mergeGroupOrder(responseOrder, currentIds);
164
+ const { itemDefaults } = collectSidebarDefaults(baseSnapshot);
165
+ const hiddenItemIds = {};
166
+ for (const href of responseHiddenItems) {
167
+ if (!itemDefaults.has(href)) continue;
168
+ hiddenItemIds[href] = true;
169
+ }
170
+ const draft = {
171
+ order,
172
+ groupLabels: { ...responseGroupLabels },
173
+ itemLabels: { ...responseItemLabels },
174
+ hiddenItemIds
175
+ };
176
+ originalNavRef.current = baseSnapshot;
177
+ setCustomDraft(draft);
178
+ setNavGroups(applyCustomizationDraft(baseSnapshot, draft));
179
+ setCustomizing(true);
180
+ } catch (error) {
181
+ console.error("Failed to load sidebar preferences", error);
182
+ setCustomizationError(t("appShell.sidebarCustomizationLoadError"));
183
+ } finally {
184
+ setLoadingPreferences(false);
185
+ }
186
+ }, [customizing, loadingPreferences, navGroups, t]);
187
+ const cancelCustomization = React.useCallback(() => {
188
+ setCustomizing(false);
189
+ setCustomDraft(null);
190
+ setCustomizationError(null);
191
+ setAvailableRoleTargets([]);
192
+ setSelectedRoleIds([]);
193
+ setCanApplyToRoles(false);
194
+ if (originalNavRef.current) {
195
+ setNavGroups(AppShell.cloneGroups(originalNavRef.current));
196
+ }
197
+ originalNavRef.current = null;
198
+ }, []);
199
+ const resetCustomization = React.useCallback(() => {
200
+ if (!originalNavRef.current) return;
201
+ const base = AppShell.cloneGroups(originalNavRef.current);
202
+ const order = base.map((group) => resolveGroupKey(group));
203
+ const draft = { order, groupLabels: {}, itemLabels: {}, hiddenItemIds: {} };
204
+ originalNavRef.current = base;
205
+ setCustomDraft(draft);
206
+ setNavGroups(applyCustomizationDraft(base, draft));
207
+ if (canApplyToRoles) {
208
+ setSelectedRoleIds(availableRoleTargets.filter((role) => role.hasPreference).map((role) => role.id));
209
+ }
210
+ }, [availableRoleTargets, canApplyToRoles]);
211
+ const saveCustomization = React.useCallback(async () => {
212
+ if (!customDraft) return;
213
+ setSavingPreferences(true);
214
+ setCustomizationError(null);
215
+ try {
216
+ const baseGroups = originalNavRef.current ?? AppShell.cloneGroups(navGroups);
217
+ const { groupDefaults, itemDefaults } = collectSidebarDefaults(baseGroups);
218
+ const sanitizedGroupLabels = {};
219
+ for (const [key, value] of Object.entries(customDraft.groupLabels)) {
220
+ const trimmed = value.trim();
221
+ const base = groupDefaults.get(key);
222
+ if (!trimmed || !base) continue;
223
+ if (trimmed !== base) sanitizedGroupLabels[key] = trimmed;
224
+ }
225
+ const sanitizedItemLabels = {};
226
+ for (const [href, value] of Object.entries(customDraft.itemLabels)) {
227
+ const trimmed = value.trim();
228
+ const base = itemDefaults.get(href);
229
+ if (!trimmed || !base) continue;
230
+ if (trimmed !== base) sanitizedItemLabels[href] = trimmed;
231
+ }
232
+ const sanitizedHiddenItems = [];
233
+ for (const [href, hidden] of Object.entries(customDraft.hiddenItemIds)) {
234
+ if (!hidden) continue;
235
+ if (!itemDefaults.has(href)) continue;
236
+ sanitizedHiddenItems.push(href);
237
+ }
238
+ const applyToRolesPayload = canApplyToRoles ? [...selectedRoleIds] : [];
239
+ const clearRoleIdsPayload = canApplyToRoles ? availableRoleTargets.filter((role) => role.hasPreference && !selectedRoleIds.includes(role.id)).map((role) => role.id) : [];
240
+ const payload = {
241
+ groupOrder: customDraft.order,
242
+ groupLabels: sanitizedGroupLabels,
243
+ itemLabels: sanitizedItemLabels,
244
+ hiddenItems: sanitizedHiddenItems
245
+ };
246
+ if (canApplyToRoles) {
247
+ payload.applyToRoles = applyToRolesPayload;
248
+ payload.clearRoleIds = clearRoleIdsPayload;
249
+ }
250
+ const call = await apiCall("/api/auth/sidebar/preferences", {
251
+ method: "PUT",
252
+ headers: { "content-type": "application/json" },
253
+ body: JSON.stringify(payload)
254
+ });
255
+ if (!call.ok) {
256
+ setCustomizationError(t("appShell.sidebarCustomizationSaveError"));
257
+ return;
258
+ }
259
+ const data = call.result ?? null;
260
+ if (data?.canApplyToRoles !== void 0) {
261
+ setCanApplyToRoles(data.canApplyToRoles === true);
262
+ }
263
+ if (Array.isArray(data?.roles)) {
264
+ const mappedRoles = data.roles.filter((role) => typeof role?.id === "string" && typeof role?.name === "string").map((role) => ({
265
+ id: role.id,
266
+ name: role.name,
267
+ hasPreference: role.hasPreference === true
268
+ }));
269
+ setAvailableRoleTargets(mappedRoles);
270
+ setSelectedRoleIds(mappedRoles.filter((role) => role.hasPreference).map((role) => role.id));
271
+ }
272
+ originalNavRef.current = applyCustomizationDraft(baseGroups, customDraft);
273
+ setNavGroups(AppShell.cloneGroups(originalNavRef.current));
274
+ setCustomizing(false);
275
+ setCustomDraft(null);
276
+ try {
277
+ window.dispatchEvent(new Event("om:refresh-sidebar"));
278
+ } catch {
279
+ }
280
+ } catch (error) {
281
+ console.error("Failed to save sidebar preferences", error);
282
+ setCustomizationError(t("appShell.sidebarCustomizationSaveError"));
283
+ } finally {
284
+ setSavingPreferences(false);
285
+ }
286
+ }, [customDraft, navGroups, t]);
287
+ const moveGroup = React.useCallback((groupId, offset) => {
288
+ updateDraft((draft) => {
289
+ const order = [...draft.order];
290
+ const index = order.indexOf(groupId);
291
+ if (index === -1) return draft;
292
+ const nextIndex = Math.max(0, Math.min(order.length - 1, index + offset));
293
+ if (nextIndex === index) return draft;
294
+ order.splice(index, 1);
295
+ order.splice(nextIndex, 0, groupId);
296
+ return { ...draft, order };
297
+ });
298
+ }, [updateDraft]);
299
+ const setGroupLabel = React.useCallback((groupId, value) => {
300
+ updateDraft((draft) => {
301
+ const next = { ...draft.groupLabels };
302
+ if (value.trim().length === 0) delete next[groupId];
303
+ else next[groupId] = value;
304
+ return { ...draft, groupLabels: next };
305
+ });
306
+ }, [updateDraft]);
307
+ const setItemLabel = React.useCallback((href, value) => {
308
+ updateDraft((draft) => {
309
+ const next = { ...draft.itemLabels };
310
+ if (value.trim().length === 0) delete next[href];
311
+ else next[href] = value;
312
+ return { ...draft, itemLabels: next };
313
+ });
314
+ }, [updateDraft]);
315
+ const setItemHidden = React.useCallback((href, hidden) => {
316
+ updateDraft((draft) => {
317
+ const next = { ...draft.hiddenItemIds };
318
+ if (hidden) next[href] = true;
319
+ else delete next[href];
320
+ return { ...draft, hiddenItemIds: next };
321
+ });
322
+ }, [updateDraft]);
323
+ const toggleRoleSelection = React.useCallback((roleId) => {
324
+ setSelectedRoleIds((prev) => prev.includes(roleId) ? prev.filter((id) => id !== roleId) : [...prev, roleId]);
325
+ }, []);
326
+ const asideWidth = effectiveCollapsed ? "72px" : expandedSidebarWidth;
327
+ const asideClassesBase = `border-r bg-background/60 py-4 min-h-svh overflow-y-auto`;
328
+ React.useEffect(() => {
329
+ try {
330
+ localStorage.setItem("om:sidebarCollapsed", collapsed ? "1" : "0");
331
+ } catch {
332
+ }
333
+ try {
334
+ document.cookie = `om_sidebar_collapsed=${collapsed ? "1" : "0"}; path=/; max-age=31536000; samesite=lax`;
335
+ } catch {
336
+ }
337
+ }, [collapsed]);
338
+ React.useEffect(() => {
339
+ try {
340
+ localStorage.setItem("om:sidebarOpenGroups", JSON.stringify(openGroups));
341
+ } catch {
342
+ }
343
+ }, [openGroups]);
344
+ React.useEffect(() => {
345
+ const activeGroup = navGroups.find((g) => g.items.some((i) => pathname?.startsWith(i.href)));
346
+ if (!activeGroup) return;
347
+ const key = resolveGroupKey(activeGroup);
348
+ setOpenGroups((prev) => prev[key] === false ? { ...prev, [key]: true } : prev);
349
+ }, [pathname, navGroups]);
350
+ React.useEffect(() => {
351
+ setHeaderTitle(currentTitle);
352
+ setHeaderBreadcrumb(breadcrumb);
353
+ }, [currentTitle, breadcrumb]);
354
+ React.useEffect(() => {
355
+ if (customizing && customDraft && originalNavRef.current) {
356
+ originalNavRef.current = AppShell.cloneGroups(groups);
357
+ setNavGroups(applyCustomizationDraft(originalNavRef.current, customDraft));
358
+ return;
359
+ }
360
+ setNavGroups(AppShell.cloneGroups(groups));
361
+ }, [groups, customizing, customDraft]);
362
+ React.useEffect(() => {
363
+ let cancelled = false;
364
+ function indexIcons(groupsToIndex) {
365
+ const map = /* @__PURE__ */ new Map();
366
+ for (const g of groupsToIndex) {
367
+ for (const i of g.items) {
368
+ map.set(i.href, i.icon);
369
+ if (i.children) for (const c of i.children) map.set(c.href, c.icon);
370
+ }
371
+ }
372
+ return map;
373
+ }
374
+ function mergePreservingIcons(oldG, newG) {
375
+ const iconMap = indexIcons(oldG);
376
+ const merged = newG.map((g) => ({
377
+ id: g.id,
378
+ name: g.name,
379
+ defaultName: g.defaultName,
380
+ items: g.items.map((i) => ({
381
+ href: i.href,
382
+ title: i.title,
383
+ defaultTitle: i.defaultTitle,
384
+ enabled: i.enabled,
385
+ hidden: i.hidden,
386
+ icon: i.icon ?? iconMap.get(i.href),
387
+ children: i.children?.map((c) => ({
388
+ href: c.href,
389
+ title: c.title,
390
+ defaultTitle: c.defaultTitle,
391
+ enabled: c.enabled,
392
+ hidden: c.hidden,
393
+ icon: c.icon ?? iconMap.get(c.href)
394
+ }))
395
+ }))
396
+ }));
397
+ return merged;
398
+ }
399
+ async function refreshFullNav() {
400
+ if (!adminNavApi) return;
401
+ try {
402
+ const call = await apiCall(adminNavApi, { credentials: "include" });
403
+ if (!call.ok) return;
404
+ const data = call.result;
405
+ if (cancelled) return;
406
+ const nextGroups = Array.isArray(data?.groups) ? data.groups : [];
407
+ if (nextGroups.length) setNavGroups((prev) => AppShell.cloneGroups(mergePreservingIcons(prev, nextGroups)));
408
+ } catch {
409
+ }
410
+ }
411
+ const onFocus = () => refreshFullNav();
412
+ window.addEventListener("focus", onFocus);
413
+ return () => {
414
+ cancelled = true;
415
+ window.removeEventListener("focus", onFocus);
416
+ };
417
+ }, [adminNavApi]);
418
+ React.useEffect(() => {
419
+ if (!adminNavApi) return;
420
+ const api = adminNavApi;
421
+ let cancelled = false;
422
+ function indexIcons(groupsToIndex) {
423
+ const map = /* @__PURE__ */ new Map();
424
+ for (const g of groupsToIndex) {
425
+ for (const i of g.items) {
426
+ map.set(i.href, i.icon);
427
+ if (i.children) for (const c of i.children) map.set(c.href, c.icon);
428
+ }
429
+ }
430
+ return map;
431
+ }
432
+ function mergePreservingIcons(oldG, newG) {
433
+ const iconMap = indexIcons(oldG);
434
+ const merged = newG.map((g) => ({
435
+ name: g.name,
436
+ items: g.items.map((i) => ({
437
+ href: i.href,
438
+ title: i.title,
439
+ enabled: i.enabled,
440
+ hidden: i.hidden,
441
+ icon: i.icon ?? iconMap.get(i.href),
442
+ children: i.children?.map((c) => ({
443
+ href: c.href,
444
+ title: c.title,
445
+ enabled: c.enabled,
446
+ hidden: c.hidden,
447
+ icon: c.icon ?? iconMap.get(c.href)
448
+ }))
449
+ }))
450
+ }));
451
+ return merged;
452
+ }
453
+ async function refreshFullNav() {
454
+ try {
455
+ const call = await apiCall(api, { credentials: "include" });
456
+ if (!call.ok) return;
457
+ const data = call.result;
458
+ if (cancelled) return;
459
+ const nextGroups = Array.isArray(data?.groups) ? data.groups : [];
460
+ if (nextGroups.length) setNavGroups((prev) => AppShell.cloneGroups(mergePreservingIcons(prev, nextGroups)));
461
+ } catch {
462
+ }
463
+ }
464
+ const onRefresh = () => {
465
+ refreshFullNav();
466
+ };
467
+ window.addEventListener("om:refresh-sidebar", onRefresh);
468
+ return () => {
469
+ cancelled = true;
470
+ window.removeEventListener("om:refresh-sidebar", onRefresh);
471
+ };
472
+ }, [adminNavApi]);
473
+ function renderSidebar(compact, hideHeader) {
474
+ const isMobileVariant = !!hideHeader;
475
+ const baseGroupsForDefaults = originalNavRef.current ?? navGroups;
476
+ const baseGroupMap = /* @__PURE__ */ new Map();
477
+ for (const group of baseGroupsForDefaults) {
478
+ baseGroupMap.set(resolveGroupKey(group), group);
479
+ }
480
+ const localeLabel = (locale || "").toUpperCase();
481
+ const orderedGroupIds = customDraft ? mergeGroupOrder(customDraft.order, Array.from(baseGroupMap.keys())) : navGroups.map((group) => resolveGroupKey(group));
482
+ const lastVisibleGroupIndex = (() => {
483
+ for (let idx = navGroups.length - 1; idx >= 0; idx -= 1) {
484
+ if (navGroups[idx].items.some((item) => item.hidden !== true)) return idx;
485
+ }
486
+ return -1;
487
+ })();
488
+ const renderEditableItems = (baseItems, currentItems, depth = 0) => {
489
+ if (!customDraft) return null;
490
+ return baseItems.map((baseItem) => {
491
+ const current = currentItems.find((item) => item.href === baseItem.href) ?? baseItem;
492
+ const placeholder = baseItem.defaultTitle ?? baseItem.title;
493
+ const value = customDraft.itemLabels[baseItem.href] ?? "";
494
+ const hidden = customDraft.hiddenItemIds[baseItem.href] === true;
495
+ return /* @__PURE__ */ jsxs(
496
+ "div",
497
+ {
498
+ className: `flex flex-col gap-1 ${hidden ? "opacity-60" : ""}`,
499
+ style: depth ? { marginLeft: depth * 16 } : void 0,
500
+ children: [
501
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: placeholder }),
502
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
503
+ /* @__PURE__ */ jsx(
504
+ "input",
505
+ {
506
+ type: "checkbox",
507
+ className: "h-4 w-4 accent-foreground",
508
+ checked: !hidden,
509
+ onChange: (event) => setItemHidden(baseItem.href, !event.target.checked),
510
+ disabled: savingPreferences,
511
+ "aria-label": t("appShell.sidebarCustomizationShowItem"),
512
+ title: t("appShell.sidebarCustomizationShowItem")
513
+ }
514
+ ),
515
+ /* @__PURE__ */ jsx(
516
+ "input",
517
+ {
518
+ value,
519
+ onChange: (event) => setItemLabel(baseItem.href, event.target.value),
520
+ placeholder,
521
+ disabled: savingPreferences,
522
+ 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"
523
+ }
524
+ )
525
+ ] }),
526
+ baseItem.children && baseItem.children.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1", children: renderEditableItems(baseItem.children, current.children ?? [], depth + 1) }) : null
527
+ ]
528
+ },
529
+ baseItem.href
530
+ );
531
+ });
532
+ };
533
+ const customizationEditor = customizing ? customDraft ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 rounded border border-dashed bg-muted/20 p-3", children: [
534
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
535
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-semibold", children: t("appShell.sidebarCustomizationHeading") }),
536
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
537
+ /* @__PURE__ */ jsx(
538
+ "button",
539
+ {
540
+ type: "button",
541
+ className: "h-8 rounded border px-3 text-sm",
542
+ onClick: resetCustomization,
543
+ disabled: savingPreferences,
544
+ children: t("appShell.sidebarCustomizationReset")
545
+ }
546
+ ),
547
+ /* @__PURE__ */ jsx(
548
+ "button",
549
+ {
550
+ type: "button",
551
+ className: "h-8 rounded border px-3 text-sm",
552
+ onClick: cancelCustomization,
553
+ disabled: savingPreferences,
554
+ children: t("appShell.sidebarCustomizationCancel")
555
+ }
556
+ ),
557
+ /* @__PURE__ */ jsx(
558
+ "button",
559
+ {
560
+ type: "button",
561
+ className: "h-8 rounded bg-foreground px-3 text-sm font-medium text-background disabled:opacity-60",
562
+ onClick: saveCustomization,
563
+ disabled: savingPreferences,
564
+ children: savingPreferences ? t("appShell.sidebarCustomizationSaving") : t("appShell.sidebarCustomizationSave")
565
+ }
566
+ )
567
+ ] })
568
+ ] }),
569
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("appShell.sidebarCustomizationHint", { locale: localeLabel }) }),
570
+ canApplyToRoles ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 rounded border bg-background/70 p-3 shadow-sm", children: [
571
+ /* @__PURE__ */ jsxs("div", { children: [
572
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-semibold", children: t("appShell.sidebarApplyToRolesTitle") }),
573
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("appShell.sidebarApplyToRolesDescription") })
574
+ ] }),
575
+ availableRoleTargets.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: availableRoleTargets.map((role) => {
576
+ const checked = selectedRoleIds.includes(role.id);
577
+ const willClear = role.hasPreference && !checked;
578
+ return /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 rounded border bg-background px-2 py-1 text-sm shadow-sm", children: [
579
+ /* @__PURE__ */ jsx(
580
+ "input",
581
+ {
582
+ type: "checkbox",
583
+ className: "h-4 w-4 accent-foreground",
584
+ checked,
585
+ onChange: () => toggleRoleSelection(role.id),
586
+ disabled: savingPreferences
587
+ }
588
+ ),
589
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: role.name }),
590
+ role.hasPreference ? /* @__PURE__ */ jsx("span", { className: `text-xs ${willClear ? "text-destructive" : "text-muted-foreground"}`, children: willClear ? t("appShell.sidebarRoleWillClear") : t("appShell.sidebarRoleHasPreset") }) : null
591
+ ] }, role.id);
592
+ }) }) : /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("appShell.sidebarApplyToRolesEmpty") })
593
+ ] }) : null,
594
+ customizationError ? /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: customizationError }) : null,
595
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: orderedGroupIds.map((groupId, index) => {
596
+ const baseGroup = baseGroupMap.get(groupId);
597
+ if (!baseGroup) return null;
598
+ const currentGroup = navGroups.find((group) => resolveGroupKey(group) === groupId) ?? baseGroup;
599
+ const placeholder = baseGroup.defaultName ?? baseGroup.name;
600
+ const value = customDraft.groupLabels[groupId] ?? "";
601
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 rounded border bg-background p-3 shadow-sm", children: [
602
+ /* @__PURE__ */ jsxs("div", { className: `flex ${compact ? "flex-col gap-2" : "items-center gap-2"}`, children: [
603
+ /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
604
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: t("appShell.sidebarCustomizationGroupLabel") }),
605
+ /* @__PURE__ */ jsx(
606
+ "input",
607
+ {
608
+ value,
609
+ onChange: (event) => setGroupLabel(groupId, event.target.value),
610
+ placeholder,
611
+ disabled: savingPreferences,
612
+ className: "mt-1 h-8 w-full rounded border bg-background px-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-60"
613
+ }
614
+ )
615
+ ] }),
616
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 self-start", children: [
617
+ /* @__PURE__ */ jsx(
618
+ "button",
619
+ {
620
+ type: "button",
621
+ className: "h-8 w-8 rounded border text-muted-foreground hover:text-foreground disabled:opacity-40",
622
+ onClick: () => moveGroup(groupId, -1),
623
+ disabled: index === 0 || savingPreferences,
624
+ "aria-label": t("appShell.sidebarCustomizationMoveUp"),
625
+ children: "\u25B2"
626
+ }
627
+ ),
628
+ /* @__PURE__ */ jsx(
629
+ "button",
630
+ {
631
+ type: "button",
632
+ className: "h-8 w-8 rounded border text-muted-foreground hover:text-foreground disabled:opacity-40",
633
+ onClick: () => moveGroup(groupId, 1),
634
+ disabled: index === orderedGroupIds.length - 1 || savingPreferences,
635
+ "aria-label": t("appShell.sidebarCustomizationMoveDown"),
636
+ children: "\u25BC"
637
+ }
638
+ )
639
+ ] })
640
+ ] }),
641
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: renderEditableItems(baseGroup.items, currentGroup.items) })
642
+ ] }, groupId);
643
+ }) })
644
+ ] }) : /* @__PURE__ */ jsx("div", { className: "rounded border border-dashed bg-muted/20 p-3 text-sm text-muted-foreground", children: t("appShell.sidebarCustomizationLoading") }) : null;
645
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-full gap-3", children: [
646
+ !hideHeader && /* @__PURE__ */ jsx("div", { className: `flex items-center ${compact ? "justify-center" : "justify-between"} mb-2`, children: /* @__PURE__ */ jsxs(Link, { href: "/backend", className: "flex items-center gap-2", "aria-label": t("appShell.goToDashboard"), children: [
647
+ /* @__PURE__ */ jsx(Image, { src: "/open-mercato.svg", alt: resolvedProductName, width: 32, height: 32, className: "rounded m-4" }),
648
+ !compact && /* @__PURE__ */ jsx("div", { className: "text-m font-semibold", children: resolvedProductName })
649
+ ] }) }),
650
+ /* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col gap-3 overflow-y-auto pr-1", children: customizing ? customizationEditor : /* @__PURE__ */ jsx("nav", { className: "flex flex-col gap-2", children: navGroups.map((g, gi) => {
651
+ const groupId = resolveGroupKey(g);
652
+ const open = openGroups[groupId] !== false;
653
+ const visibleItems = g.items.filter((item) => item.hidden !== true);
654
+ if (visibleItems.length === 0) return null;
655
+ return /* @__PURE__ */ jsxs("div", { children: [
656
+ /* @__PURE__ */ jsxs(
657
+ "button",
658
+ {
659
+ type: "button",
660
+ onClick: () => toggleGroup(groupId),
661
+ className: `w-full ${compact ? "px-0 justify-center" : "px-2 justify-between"} flex items-center text-xs uppercase text-muted-foreground/90 py-2`,
662
+ "aria-expanded": open,
663
+ children: [
664
+ !compact && /* @__PURE__ */ jsx("span", { children: g.name }),
665
+ !compact && /* @__PURE__ */ jsx(Chevron, { open })
666
+ ]
667
+ }
668
+ ),
669
+ open && /* @__PURE__ */ jsx("div", { className: `flex flex-col ${compact ? "items-center" : ""} gap-1 ${!compact ? "pl-1" : ""}`, children: visibleItems.map((i) => {
670
+ const childItems = (i.children ?? []).filter((child) => child.hidden !== true);
671
+ const showChildren = !!pathname && childItems.length > 0 && pathname.startsWith(i.href);
672
+ const hasActiveChild = !!(pathname && childItems.some((c) => pathname.startsWith(c.href)));
673
+ const isParentActive = pathname === i.href || showChildren && !hasActiveChild;
674
+ const base = compact ? "w-10 h-10 justify-center" : "px-2 py-1 gap-2";
675
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
676
+ /* @__PURE__ */ jsxs(
677
+ Link,
678
+ {
679
+ href: i.href,
680
+ 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" : ""}`,
681
+ "aria-disabled": i.enabled === false,
682
+ title: compact ? i.title : void 0,
683
+ onClick: () => setMobileOpen(false),
684
+ children: [
685
+ isParentActive ? /* @__PURE__ */ jsx("span", { className: "absolute left-0 top-1 bottom-1 w-0.5 rounded bg-foreground" }) : null,
686
+ /* @__PURE__ */ jsx("span", { className: `flex items-center justify-center shrink-0 ${compact ? "" : "text-muted-foreground"}`, children: i.icon ?? DefaultIcon }),
687
+ !compact && /* @__PURE__ */ jsx("span", { children: i.title })
688
+ ]
689
+ }
690
+ ),
691
+ showChildren ? /* @__PURE__ */ jsx("div", { className: `flex flex-col ${compact ? "items-center" : ""} gap-1 ${!compact ? "pl-4" : ""}`, children: childItems.map((c) => {
692
+ const childActive = pathname?.startsWith(c.href);
693
+ const childBase = compact ? "w-10 h-8 justify-center" : "px-2 py-1 gap-2";
694
+ return /* @__PURE__ */ jsxs(
695
+ Link,
696
+ {
697
+ href: c.href,
698
+ 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" : ""}`,
699
+ "aria-disabled": c.enabled === false,
700
+ title: compact ? c.title : void 0,
701
+ onClick: () => setMobileOpen(false),
702
+ children: [
703
+ childActive ? /* @__PURE__ */ jsx("span", { className: "absolute left-0 top-1 bottom-1 w-0.5 rounded bg-foreground" }) : null,
704
+ /* @__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) }),
705
+ !compact && /* @__PURE__ */ jsx("span", { children: c.title })
706
+ ]
707
+ },
708
+ c.href
709
+ );
710
+ }) }) : null
711
+ ] }, i.href);
712
+ }) }),
713
+ gi !== lastVisibleGroupIndex && /* @__PURE__ */ jsx("div", { className: "my-2 border-t border-dotted" })
714
+ ] }, groupId);
715
+ }) }) }),
716
+ !customizing && /* @__PURE__ */ jsxs(
717
+ "button",
718
+ {
719
+ type: "button",
720
+ onClick: startCustomization,
721
+ className: `mt-auto inline-flex items-center justify-center gap-2 rounded border hover:bg-accent hover:text-accent-foreground disabled:opacity-60 ${compact || isMobileVariant ? "h-10 w-10 p-0" : "h-9 px-3 text-sm font-medium"}`,
722
+ disabled: loadingPreferences,
723
+ "aria-label": t("appShell.customizeSidebar"),
724
+ children: [
725
+ /* @__PURE__ */ jsx("span", { className: "flex items-center justify-center", children: CustomizeIcon }),
726
+ !(compact || isMobileVariant) && /* @__PURE__ */ jsx("span", { children: loadingPreferences ? t("appShell.sidebarCustomizationLoading") : t("appShell.customizeSidebar") })
727
+ ]
728
+ }
729
+ )
730
+ ] });
731
+ }
732
+ const gridColsClass = customizing ? "lg:grid-cols-[320px_1fr]" : effectiveCollapsed ? "lg:grid-cols-[72px_1fr]" : "lg:grid-cols-[240px_1fr]";
733
+ const headerCtxValue = React.useMemo(() => ({
734
+ setBreadcrumb: setHeaderBreadcrumb,
735
+ setTitle: setHeaderTitle
736
+ }), []);
737
+ return /* @__PURE__ */ jsx(HeaderContext.Provider, { value: headerCtxValue, children: /* @__PURE__ */ jsxs("div", { className: `min-h-svh lg:grid ${gridColsClass}`, children: [
738
+ /* @__PURE__ */ jsx("aside", { className: `${asideClassesBase} ${effectiveCollapsed ? "px-2" : "px-3"} hidden lg:block`, style: { width: asideWidth }, children: renderSidebar(effectiveCollapsed) }),
739
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-svh flex-col min-w-0", children: [
740
+ /* @__PURE__ */ jsxs("header", { className: "border-b bg-background/60 px-3 lg:px-4 py-3 flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between", children: [
741
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [
742
+ /* @__PURE__ */ jsx("button", { type: "button", className: "lg:hidden rounded border px-2 py-1", "aria-label": t("appShell.openMenu"), onClick: () => setMobileOpen(true), children: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { d: "M3 6h18M3 12h18M3 18h18" }) }) }),
743
+ /* @__PURE__ */ jsx(
744
+ "button",
745
+ {
746
+ type: "button",
747
+ className: "hidden lg:inline-flex rounded border px-2 py-1 disabled:opacity-60",
748
+ "aria-label": t("appShell.toggleSidebar"),
749
+ onClick: () => setCollapsed((c) => !c),
750
+ disabled: customizing,
751
+ children: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
752
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }),
753
+ /* @__PURE__ */ jsx("path", { d: "M9 4v16" })
754
+ ] })
755
+ }
756
+ ),
757
+ (() => {
758
+ const dashboardLabel = t("dashboard.title");
759
+ const root = [{ label: dashboardLabel, href: "/backend" }];
760
+ let rest = [];
761
+ if (headerBreadcrumb && headerBreadcrumb.length) {
762
+ const first = headerBreadcrumb[0];
763
+ const dup = first && (first.href === "/backend" || first.label === dashboardLabel || first.label?.toLowerCase() === "dashboard");
764
+ rest = dup ? headerBreadcrumb.slice(1) : headerBreadcrumb;
765
+ } else if (headerTitle) {
766
+ rest = [{ label: headerTitle }];
767
+ }
768
+ const items = [...root, ...rest];
769
+ return /* @__PURE__ */ jsx("nav", { className: "flex items-center gap-2 text-sm", children: items.map((b, i) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
770
+ i > 0 && /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "/" }),
771
+ b.href ? /* @__PURE__ */ jsx(Link, { href: b.href, className: "text-muted-foreground hover:text-foreground", children: b.label }) : /* @__PURE__ */ jsx("span", { className: "font-medium truncate max-w-[60vw]", children: b.label })
772
+ ] }, i)) });
773
+ })()
774
+ ] }),
775
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm w-full lg:w-auto lg:justify-end", children: [
776
+ /* @__PURE__ */ jsx(ThemeToggle, {}),
777
+ /* @__PURE__ */ jsx(Separator, { className: "w-px h-5 mx-1" }),
778
+ rightHeaderSlot ? rightHeaderSlot : /* @__PURE__ */ jsx("span", { className: "opacity-80", children: email || t("appShell.userFallback") })
779
+ ] })
780
+ ] }),
781
+ /* @__PURE__ */ jsxs("main", { className: "flex-1 p-4 lg:p-6", children: [
782
+ /* @__PURE__ */ jsx(FlashMessages, {}),
783
+ /* @__PURE__ */ jsx(PartialIndexBanner, {}),
784
+ /* @__PURE__ */ jsx(UpgradeActionBanner, {}),
785
+ /* @__PURE__ */ jsx(LastOperationBanner, {}),
786
+ children
787
+ ] }),
788
+ /* @__PURE__ */ jsxs("footer", { className: "border-t bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/50 px-4 py-3 flex flex-wrap items-center justify-end gap-4", children: [
789
+ version ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("appShell.version", { version }) }) : null,
790
+ /* @__PURE__ */ jsxs("nav", { className: "flex items-center gap-3 text-xs text-muted-foreground", children: [
791
+ /* @__PURE__ */ jsx(Link, { href: "/terms", className: "transition hover:text-foreground", children: t("common.terms") }),
792
+ /* @__PURE__ */ jsx(Link, { href: "/privacy", className: "transition hover:text-foreground", children: t("common.privacy") })
793
+ ] }),
794
+ /* @__PURE__ */ jsx(LanguageSwitcher, {})
795
+ ] })
796
+ ] }),
797
+ mobileOpen && /* @__PURE__ */ jsxs("div", { className: "lg:hidden fixed inset-0 z-50", children: [
798
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/40", onClick: () => setMobileOpen(false) }),
799
+ /* @__PURE__ */ jsxs("aside", { className: "absolute left-0 top-0 h-full w-[260px] bg-background border-r p-3", children: [
800
+ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between", children: [
801
+ /* @__PURE__ */ jsxs(Link, { href: "/backend", className: "flex items-center gap-2 text-sm font-semibold", onClick: () => setMobileOpen(false), "aria-label": t("appShell.goToDashboard"), children: [
802
+ /* @__PURE__ */ jsx(Image, { src: "/open-mercato.svg", alt: resolvedProductName, width: 28, height: 28, className: "rounded" }),
803
+ resolvedProductName
804
+ ] }),
805
+ /* @__PURE__ */ jsx("button", { className: "rounded border px-2 py-1", onClick: () => setMobileOpen(false), "aria-label": t("appShell.closeMenu"), children: "\u2715" })
806
+ ] }),
807
+ renderSidebar(false, true)
808
+ ] })
809
+ ] })
810
+ ] }) });
811
+ }
812
+ AppShell.cloneGroups = function cloneGroups(groups) {
813
+ const cloneItem = (item) => ({
814
+ href: item.href,
815
+ title: item.title,
816
+ defaultTitle: item.defaultTitle,
817
+ icon: item.icon,
818
+ enabled: item.enabled,
819
+ hidden: item.hidden,
820
+ children: item.children ? item.children.map((child) => cloneItem(child)) : void 0
821
+ });
822
+ return groups.map((group) => ({
823
+ id: group.id,
824
+ name: group.name,
825
+ defaultName: group.defaultName,
826
+ items: group.items.map((item) => cloneItem(item))
827
+ }));
828
+ };
829
+ function applyCustomizationDraft(baseGroups, draft) {
830
+ const clones = AppShell.cloneGroups(baseGroups);
831
+ const byId = /* @__PURE__ */ new Map();
832
+ for (const group of clones) {
833
+ byId.set(resolveGroupKey(group), group);
834
+ }
835
+ const orderedIds = mergeGroupOrder(draft.order, Array.from(byId.keys()));
836
+ const seen = /* @__PURE__ */ new Set();
837
+ const result = [];
838
+ for (const id of orderedIds) {
839
+ if (seen.has(id)) continue;
840
+ const group = byId.get(id);
841
+ if (!group) continue;
842
+ seen.add(id);
843
+ const baseName = group.defaultName ?? group.name;
844
+ const override = draft.groupLabels[id]?.trim();
845
+ result.push({
846
+ ...group,
847
+ name: override && override.length > 0 ? override : baseName,
848
+ items: group.items.map((item) => applyItemDraft(item, draft))
849
+ });
850
+ }
851
+ return result;
852
+ }
853
+ function applyItemDraft(item, draft) {
854
+ const baseTitle = item.defaultTitle ?? item.title;
855
+ const override = draft.itemLabels[item.href]?.trim();
856
+ const children = item.children ? item.children.map((child) => applyItemDraft(child, draft)) : void 0;
857
+ const hidden = draft.hiddenItemIds[item.href] === true;
858
+ return {
859
+ ...item,
860
+ title: override && override.length > 0 ? override : baseTitle,
861
+ hidden,
862
+ children
863
+ };
864
+ }
865
+ function mergeGroupOrder(preferred, current) {
866
+ const seen = /* @__PURE__ */ new Set();
867
+ const merged = [];
868
+ for (const id of preferred) {
869
+ const trimmed = id.trim();
870
+ if (!trimmed || seen.has(trimmed) || !current.includes(trimmed)) continue;
871
+ seen.add(trimmed);
872
+ merged.push(trimmed);
873
+ }
874
+ for (const id of current) {
875
+ if (seen.has(id)) continue;
876
+ seen.add(id);
877
+ merged.push(id);
878
+ }
879
+ return merged;
880
+ }
881
+ function collectSidebarDefaults(groups) {
882
+ const groupDefaults = /* @__PURE__ */ new Map();
883
+ const itemDefaults = /* @__PURE__ */ new Map();
884
+ const visitItems = (items) => {
885
+ for (const item of items) {
886
+ const baseTitle = item.defaultTitle ?? item.title;
887
+ itemDefaults.set(item.href, baseTitle);
888
+ if (item.children && item.children.length > 0) visitItems(item.children);
889
+ }
890
+ };
891
+ for (const group of groups) {
892
+ const key = resolveGroupKey(group);
893
+ groupDefaults.set(key, group.defaultName ?? group.name);
894
+ visitItems(group.items);
895
+ }
896
+ return { groupDefaults, itemDefaults };
897
+ }
898
+ export {
899
+ AppShell,
900
+ ApplyBreadcrumb
901
+ };
902
+ //# sourceMappingURL=AppShell.js.map