@sonicjs-cms/core 3.0.0-beta.22 → 3.0.0-beta.23

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 (106) hide show
  1. package/dist/adapters.cjs +512 -0
  2. package/dist/adapters.cjs.map +1 -0
  3. package/dist/adapters.d.cts +342 -0
  4. package/dist/adapters.d.ts +342 -0
  5. package/dist/adapters.js +501 -0
  6. package/dist/adapters.js.map +1 -0
  7. package/dist/admin-documents-form.template-55V2ZGWT.js +6 -0
  8. package/dist/{admin-documents-form.template-DDSH6ROU.js.map → admin-documents-form.template-55V2ZGWT.js.map} +1 -1
  9. package/dist/admin-documents-form.template-YNAVMJCV.cjs +19 -0
  10. package/dist/{admin-layout-catalyst.template-YQ4EMF2J.js.map → admin-documents-form.template-YNAVMJCV.cjs.map} +1 -1
  11. package/dist/admin-layout-catalyst.template-3XZXDGF7.cjs +21 -0
  12. package/dist/{admin-layout-catalyst.template-KDHKVLXR.cjs.map → admin-layout-catalyst.template-3XZXDGF7.cjs.map} +1 -1
  13. package/dist/admin-layout-catalyst.template-6UQGLCKI.js +7 -0
  14. package/dist/{admin-documents-form.template-LSZKGA5J.cjs.map → admin-layout-catalyst.template-6UQGLCKI.js.map} +1 -1
  15. package/dist/{chunk-I2DM27HP.js → chunk-2MIYYAO4.js} +49 -4
  16. package/dist/chunk-2MIYYAO4.js.map +1 -0
  17. package/dist/{chunk-EUJAEB6W.cjs → chunk-4WMZGHIC.cjs} +53 -4
  18. package/dist/chunk-4WMZGHIC.cjs.map +1 -0
  19. package/dist/{chunk-2FCKKOA5.js → chunk-A46IRC5A.js} +3 -3
  20. package/dist/chunk-A46IRC5A.js.map +1 -0
  21. package/dist/{chunk-PYZYU7EK.cjs → chunk-AT4ZQGEZ.cjs} +17 -2
  22. package/dist/chunk-AT4ZQGEZ.cjs.map +1 -0
  23. package/dist/{chunk-CRGUD4KC.cjs → chunk-BBEKHLTW.cjs} +9 -9
  24. package/dist/{chunk-CRGUD4KC.cjs.map → chunk-BBEKHLTW.cjs.map} +1 -1
  25. package/dist/{chunk-RMRJGMDE.js → chunk-BHND5PEV.js} +3 -3
  26. package/dist/{chunk-RMRJGMDE.js.map → chunk-BHND5PEV.js.map} +1 -1
  27. package/dist/{chunk-OBA2RYZN.js → chunk-BJROXF4N.js} +3 -3
  28. package/dist/{chunk-OBA2RYZN.js.map → chunk-BJROXF4N.js.map} +1 -1
  29. package/dist/{chunk-QWSR4THH.cjs → chunk-C3ZQP5NW.cjs} +12 -5
  30. package/dist/chunk-C3ZQP5NW.cjs.map +1 -0
  31. package/dist/{chunk-JFLZHOQP.cjs → chunk-CTYLCWMO.cjs} +486 -409
  32. package/dist/chunk-CTYLCWMO.cjs.map +1 -0
  33. package/dist/{chunk-CHWT5KBK.js → chunk-DIBCKCUE.js} +6 -6
  34. package/dist/{chunk-CHWT5KBK.js.map → chunk-DIBCKCUE.js.map} +1 -1
  35. package/dist/{chunk-73O4GPJ6.js → chunk-DPLJYTHE.js} +12 -5
  36. package/dist/chunk-DPLJYTHE.js.map +1 -0
  37. package/dist/{chunk-PXNTCCPE.cjs → chunk-E66UVONV.cjs} +8 -8
  38. package/dist/{chunk-PXNTCCPE.cjs.map → chunk-E66UVONV.cjs.map} +1 -1
  39. package/dist/{chunk-FTXGZ3BF.cjs → chunk-IS54FPB7.cjs} +18 -18
  40. package/dist/{chunk-FTXGZ3BF.cjs.map → chunk-IS54FPB7.cjs.map} +1 -1
  41. package/dist/{chunk-N32OWET6.cjs → chunk-JMJM7DMM.cjs} +5 -5
  42. package/dist/{chunk-N32OWET6.cjs.map → chunk-JMJM7DMM.cjs.map} +1 -1
  43. package/dist/{chunk-DOMXI5PM.js → chunk-KS24NPPL.js} +17 -2
  44. package/dist/chunk-KS24NPPL.js.map +1 -0
  45. package/dist/{chunk-MGHHPNJN.cjs → chunk-RFBW5RRH.cjs} +11 -59
  46. package/dist/chunk-RFBW5RRH.cjs.map +1 -0
  47. package/dist/{chunk-6KNTZ67K.js → chunk-SK5CHFJP.js} +192 -116
  48. package/dist/chunk-SK5CHFJP.js.map +1 -0
  49. package/dist/{chunk-5V62WT6M.js → chunk-THFZSDIU.js} +4 -2
  50. package/dist/chunk-THFZSDIU.js.map +1 -0
  51. package/dist/{chunk-JQISFW6U.js → chunk-URIBTX27.js} +3 -3
  52. package/dist/{chunk-JQISFW6U.js.map → chunk-URIBTX27.js.map} +1 -1
  53. package/dist/{chunk-K623Q6WD.cjs → chunk-UYJQDDEE.cjs} +4 -2
  54. package/dist/chunk-UYJQDDEE.cjs.map +1 -0
  55. package/dist/{chunk-3CT3SUR4.cjs → chunk-VGHEKIJL.cjs} +3 -3
  56. package/dist/chunk-VGHEKIJL.cjs.map +1 -0
  57. package/dist/{chunk-GB2Q2DNK.js → chunk-XXRR54AB.js} +5 -49
  58. package/dist/chunk-XXRR54AB.js.map +1 -0
  59. package/dist/{define-plugin-DsKiGu1q.d.cts → define-plugin-DMcvEy_C.d.cts} +33 -0
  60. package/dist/{define-plugin-BYIey6sp.d.ts → define-plugin-D_6D9vVZ.d.ts} +33 -0
  61. package/dist/index.cjs +1758 -326
  62. package/dist/index.cjs.map +1 -1
  63. package/dist/index.d.cts +2 -2
  64. package/dist/index.d.ts +2 -2
  65. package/dist/index.js +1453 -21
  66. package/dist/index.js.map +1 -1
  67. package/dist/middleware.cjs +34 -34
  68. package/dist/middleware.js +5 -5
  69. package/dist/migrations-XQ6EX6HX.js +4 -0
  70. package/dist/{migrations-HLK2QF4T.js.map → migrations-XQ6EX6HX.js.map} +1 -1
  71. package/dist/migrations-ZD7Y5NUC.cjs +13 -0
  72. package/dist/{migrations-UIMA6ZL6.cjs.map → migrations-ZD7Y5NUC.cjs.map} +1 -1
  73. package/dist/plugins.cjs +40 -40
  74. package/dist/plugins.d.cts +1 -1
  75. package/dist/plugins.d.ts +1 -1
  76. package/dist/plugins.js +3 -3
  77. package/dist/routes.cjs +29 -29
  78. package/dist/routes.js +9 -9
  79. package/dist/services.cjs +18 -18
  80. package/dist/services.js +2 -2
  81. package/dist/templates.cjs +17 -17
  82. package/dist/templates.js +3 -3
  83. package/dist/utils.cjs +3 -3
  84. package/dist/utils.js +1 -1
  85. package/migrations/0001_core.sql +7 -1
  86. package/package.json +10 -3
  87. package/dist/admin-documents-form.template-DDSH6ROU.js +0 -6
  88. package/dist/admin-documents-form.template-LSZKGA5J.cjs +0 -19
  89. package/dist/admin-layout-catalyst.template-KDHKVLXR.cjs +0 -21
  90. package/dist/admin-layout-catalyst.template-YQ4EMF2J.js +0 -7
  91. package/dist/chunk-2FCKKOA5.js.map +0 -1
  92. package/dist/chunk-3CT3SUR4.cjs.map +0 -1
  93. package/dist/chunk-5V62WT6M.js.map +0 -1
  94. package/dist/chunk-6KNTZ67K.js.map +0 -1
  95. package/dist/chunk-73O4GPJ6.js.map +0 -1
  96. package/dist/chunk-DOMXI5PM.js.map +0 -1
  97. package/dist/chunk-EUJAEB6W.cjs.map +0 -1
  98. package/dist/chunk-GB2Q2DNK.js.map +0 -1
  99. package/dist/chunk-I2DM27HP.js.map +0 -1
  100. package/dist/chunk-JFLZHOQP.cjs.map +0 -1
  101. package/dist/chunk-K623Q6WD.cjs.map +0 -1
  102. package/dist/chunk-MGHHPNJN.cjs.map +0 -1
  103. package/dist/chunk-PYZYU7EK.cjs.map +0 -1
  104. package/dist/chunk-QWSR4THH.cjs.map +0 -1
  105. package/dist/migrations-HLK2QF4T.js +0 -4
  106. package/dist/migrations-UIMA6ZL6.cjs +0 -13
package/dist/index.js CHANGED
@@ -1,37 +1,37 @@
1
1
  import { createAuth } from './chunk-SOZUG52O.js';
2
- import { getEmailService, hasEmailService, setEmailService, invalidateVariablesCache, resolveVariablesInObject, resolveDocScope, denyIfNotAllowed, DocumentRepository, dispatchHookEvent, getCustomData, createDocumentSchema, renderTable, getVariablesCached, setVariablesCache, TenantService, isValidMemberRole, renderConfirmationDialog, getConfirmationDialogScript, VALID_MEMBER_ROLES, userProfilesPlugin, lexicalEditorPlugin, api_media_default, api_system_default, apiDocumentsRoutes, api_default, adminDocumentsRoutes, admin_api_default, adminCollectionsRoutes, adminSettingsRoutes, router2, admin_content_default, adminMediaRoutes, router, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default } from './chunk-6KNTZ67K.js';
3
- export { DocumentRepository, ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes, createUserProfilesPlugin, defineUserProfile, getUserProfileConfig, userProfilesPlugin } from './chunk-6KNTZ67K.js';
2
+ import { getEmailService, hasEmailService, setEmailService, reconcileMenuFromPlugins, invalidateVariablesCache, resolveVariablesInObject, resolveDocScope, denyIfNotAllowed, DocumentRepository, dispatchHookEvent, getCustomData, createDocumentSchema, renderTable, getVariablesCached, setVariablesCache, TenantService, isValidMemberRole, renderConfirmationDialog, getConfirmationDialogScript, VALID_MEMBER_ROLES, userProfilesPlugin, lexicalEditorPlugin, api_media_default, api_system_default, apiDocumentsRoutes, api_default, adminDocumentsRoutes, admin_api_default, adminCollectionsRoutes, adminSettingsRoutes, router2, admin_content_default, adminMediaRoutes, router, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default } from './chunk-SK5CHFJP.js';
3
+ export { DocumentRepository, ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes, createUserProfilesPlugin, defineUserProfile, getUserProfileConfig, userProfilesPlugin } from './chunk-SK5CHFJP.js';
4
4
  import './chunk-VVYVFCML.js';
5
- import './chunk-RMRJGMDE.js';
5
+ import './chunk-BHND5PEV.js';
6
6
  import { SettingsService, setAppInstance } from './chunk-2XAQTPAE.js';
7
7
  export { Logger, getLogger, initLogger } from './chunk-2XAQTPAE.js';
8
8
  import { getAllCacheStats, CACHE_CONFIGS, getCacheService, clearAllCaches, parseCacheKey } from './chunk-K25XHMM3.js';
9
9
  import { schema_exports } from './chunk-AI663NBO.js';
10
10
  export { apiTokens, content, contentVersions, insertContentSchema, insertLogConfigSchema, insertMediaSchema, insertPluginActivityLogSchema, insertPluginAssetSchema, insertPluginHookSchema, insertPluginRouteSchema, insertPluginSchema, insertSystemLogSchema, insertUserSchema, insertWorkflowHistorySchema, logConfig, media, pluginActivityLog, pluginAssets, pluginHooks, pluginRoutes, plugins, selectContentSchema, selectLogConfigSchema, selectMediaSchema, selectPluginActivityLogSchema, selectPluginAssetSchema, selectPluginHookSchema, selectPluginRouteSchema, selectPluginSchema, selectSystemLogSchema, selectUserSchema, selectWorkflowHistorySchema, systemLogs, users, workflowHistory } from './chunk-AI663NBO.js';
11
- import { requireAuth, DocumentTypeRegistry, requireRbac, requireRole, getJwtExpirySecondsFromDb, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection } from './chunk-CHWT5KBK.js';
12
- export { AuthManager, PermissionManager, bootstrapDocumentTypes, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeadersMiddleware as securityHeaders, securityLoggingMiddleware } from './chunk-CHWT5KBK.js';
11
+ import { requireAuth, DocumentTypeRegistry, requireRbac, requireRole, getJwtExpirySecondsFromDb, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection } from './chunk-DIBCKCUE.js';
12
+ export { AuthManager, PermissionManager, bootstrapDocumentTypes, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeadersMiddleware as securityHeaders, securityLoggingMiddleware } from './chunk-DIBCKCUE.js';
13
13
  import { RbacService, DocumentsService } from './chunk-3TMTYJAJ.js';
14
14
  export { DocumentsService, RbacService } from './chunk-3TMTYJAJ.js';
15
15
  import { nanoid } from './chunk-UWUP2VMT.js';
16
- import { PluginService, PLUGIN_REGISTRY, invalidateTenantCache, getCollectionRegistry, TENANT_COOKIE, MULTI_TENANT_PLUGIN_ID, collectionRecordToRow, tenantMiddleware } from './chunk-DOMXI5PM.js';
17
- export { PluginBootstrapService, PluginService as PluginServiceClass, getAvailableCollectionNames, loadCollectionConfig, loadCollectionConfigs, registerCollections, validateCollectionConfig } from './chunk-DOMXI5PM.js';
18
- export { MigrationService } from './chunk-2FCKKOA5.js';
19
- export { renderFilterBar } from './chunk-JQISFW6U.js';
20
- import { renderAdminLayout } from './chunk-OBA2RYZN.js';
21
- export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-OBA2RYZN.js';
22
- import { init_admin_layout_catalyst_template, renderAdminLayoutCatalyst } from './chunk-5V62WT6M.js';
23
- export { renderAdminLayoutCatalyst } from './chunk-5V62WT6M.js';
24
- import { TurnstileService, HookSystemImpl, createPluginWirer, registerPluginRoutes, setPluginMenu, resolvePluginMenuItems } from './chunk-GB2Q2DNK.js';
25
- export { HookSystemImpl, HookUtils, PluginDependencyCycleError, PluginManager as PluginManagerClass, PluginRegistryImpl, PluginValidator as PluginValidatorClass, RegisterPluginsError, ScopedHookSystem as ScopedHookSystemClass, collectCronSchedules, collectCrons, createScheduledHandler, dispatchCronTick, getPluginMenu, registerPlugins, resetPluginMenu, resolvePluginMenuItems, setPluginMenu, topoSort } from './chunk-GB2Q2DNK.js';
26
- import { definePlugin, setPluginDefinitions } from './chunk-I2DM27HP.js';
27
- export { applySchemaDefaults, definePlugin, isDefinedPlugin, parseConfigSchema, parseFormDataToSettings, renderSchemaFields } from './chunk-I2DM27HP.js';
16
+ import { PluginService, PLUGIN_REGISTRY, invalidateTenantCache, getCollectionRegistry, TENANT_COOKIE, MULTI_TENANT_PLUGIN_ID, collectionRecordToRow, tenantMiddleware } from './chunk-KS24NPPL.js';
17
+ export { PluginBootstrapService, PluginService as PluginServiceClass, getAvailableCollectionNames, loadCollectionConfig, loadCollectionConfigs, registerCollections, validateCollectionConfig } from './chunk-KS24NPPL.js';
18
+ export { MigrationService } from './chunk-A46IRC5A.js';
19
+ export { renderFilterBar } from './chunk-URIBTX27.js';
20
+ import { renderAdminLayout } from './chunk-BJROXF4N.js';
21
+ export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-BJROXF4N.js';
22
+ import { init_admin_layout_catalyst_template, renderAdminLayoutCatalyst } from './chunk-THFZSDIU.js';
23
+ export { renderAdminLayoutCatalyst } from './chunk-THFZSDIU.js';
24
+ import { TurnstileService, HookSystemImpl, createPluginWirer, registerPluginRoutes } from './chunk-XXRR54AB.js';
25
+ export { HookSystemImpl, HookUtils, PluginDependencyCycleError, PluginManager as PluginManagerClass, PluginRegistryImpl, PluginValidator as PluginValidatorClass, RegisterPluginsError, ScopedHookSystem as ScopedHookSystemClass, collectCronSchedules, collectCrons, createScheduledHandler, dispatchCronTick, registerPlugins, topoSort } from './chunk-XXRR54AB.js';
26
+ import { definePlugin, setPluginMenu, setPluginDefinitions, resolvePluginMenuItems } from './chunk-2MIYYAO4.js';
27
+ export { applySchemaDefaults, definePlugin, getPluginMenu, isDefinedPlugin, parseConfigSchema, parseFormDataToSettings, renderSchemaFields, resetPluginMenu, resolvePluginMenuItems, setPluginMenu } from './chunk-2MIYYAO4.js';
28
28
  import { setHookSystem } from './chunk-RNZFGN4R.js';
29
29
  export { getHookSystem, getTypedHooks, hasHookSystem, resetHookSystem, setHookSystem } from './chunk-RNZFGN4R.js';
30
30
  export { QueryFilterBuilder, TemplateRenderer, buildQuery, renderTemplate, templateRenderer } from './chunk-QZGABF2M.js';
31
31
  export { metricsTracker } from './chunk-FICTAGD4.js';
32
32
  import './chunk-X7ZAEI5S.js';
33
- import { getCoreVersion, package_default } from './chunk-73O4GPJ6.js';
34
- export { SONICJS_VERSION, getCoreVersion } from './chunk-73O4GPJ6.js';
33
+ import { getCoreVersion, package_default } from './chunk-DPLJYTHE.js';
34
+ export { SONICJS_VERSION, getCoreVersion } from './chunk-DPLJYTHE.js';
35
35
  import { escapeHtml, sanitizeInput } from './chunk-TQABQWOP.js';
36
36
  export { escapeHtml, sanitizeInput, sanitizeObject } from './chunk-TQABQWOP.js';
37
37
  export { HOOKS } from './chunk-QXOZI5Q2.js';
@@ -8112,7 +8112,7 @@ adminRoutes5.get("/settings", async (c) => {
8112
8112
  const db = c.env.DB;
8113
8113
  const user = c.get("user");
8114
8114
  const settings = await getSettings4(db);
8115
- const { renderAdminLayoutCatalyst: renderAdminLayoutCatalyst2 } = await import('./admin-layout-catalyst.template-YQ4EMF2J.js');
8115
+ const { renderAdminLayoutCatalyst: renderAdminLayoutCatalyst2 } = await import('./admin-layout-catalyst.template-6UQGLCKI.js');
8116
8116
  const content2 = `
8117
8117
  <div>
8118
8118
  <div class="mb-6">
@@ -11777,6 +11777,1436 @@ function pluginMenuMiddleware() {
11777
11777
  }
11778
11778
  };
11779
11779
  }
11780
+
11781
+ // src/plugins/core-plugins/menu-plugin/services/menu-repository.ts
11782
+ async function listMenuItems(db, opts) {
11783
+ try {
11784
+ let sql = `
11785
+ SELECT id, slug, sort_order, data
11786
+ FROM documents
11787
+ WHERE type_id = 'menu_item'
11788
+ AND tenant_id = 'default'
11789
+ AND is_current_draft = 1
11790
+ AND deleted_at IS NULL
11791
+ `;
11792
+ const binds = [];
11793
+ if (opts?.source) ;
11794
+ sql += ` ORDER BY sort_order ASC`;
11795
+ const stmt = db.prepare(sql);
11796
+ const result = await (binds.length ? stmt.bind(...binds) : stmt).all();
11797
+ return (result.results ?? []).map((row) => {
11798
+ const data = JSON.parse(row.data);
11799
+ return {
11800
+ ...data,
11801
+ id: row.id,
11802
+ slug: row.slug,
11803
+ sortOrder: row.sort_order,
11804
+ tenantId: "default"
11805
+ };
11806
+ });
11807
+ } catch {
11808
+ return [];
11809
+ }
11810
+ }
11811
+ async function fetchPluginStatuses(db, pluginIds) {
11812
+ if (pluginIds.length === 0) return {};
11813
+ try {
11814
+ const rows = await db.prepare(
11815
+ `SELECT slug, json_extract(data, '$.status') AS status
11816
+ FROM documents
11817
+ WHERE type_id = 'plugin' AND tenant_id = 'default'
11818
+ AND is_current_draft = 1 AND deleted_at IS NULL`
11819
+ ).all();
11820
+ const statusMap = /* @__PURE__ */ new Map();
11821
+ for (const row of rows.results ?? []) {
11822
+ statusMap.set(row.slug, row.status ?? "inactive");
11823
+ }
11824
+ const result = {};
11825
+ for (const id of pluginIds) {
11826
+ const s = statusMap.get(id);
11827
+ result[id] = s === "active" ? "active" : "inactive";
11828
+ }
11829
+ return result;
11830
+ } catch {
11831
+ return {};
11832
+ }
11833
+ }
11834
+ function buildSidebarTree(items) {
11835
+ const visible = items.filter((i) => i.visible);
11836
+ visible.sort((a, b) => a.sortOrder - b.sortOrder);
11837
+ const byId = /* @__PURE__ */ new Map();
11838
+ const roots = [];
11839
+ for (const item of visible) {
11840
+ byId.set(item.id, {
11841
+ id: item.id,
11842
+ label: item.label,
11843
+ url: item.url,
11844
+ icon: item.icon,
11845
+ target: item.target,
11846
+ isExternal: item.isExternal,
11847
+ visible: item.visible,
11848
+ source: item.source,
11849
+ sortOrder: item.sortOrder,
11850
+ children: []
11851
+ });
11852
+ }
11853
+ for (const item of visible) {
11854
+ const node = byId.get(item.id);
11855
+ if (item.parent && byId.has(item.parent)) {
11856
+ byId.get(item.parent).children.push(node);
11857
+ } else {
11858
+ roots.push(node);
11859
+ }
11860
+ }
11861
+ for (const node of byId.values()) {
11862
+ node.children.sort((a, b) => a.sortOrder - b.sortOrder);
11863
+ }
11864
+ return roots;
11865
+ }
11866
+ async function upsertSystemItem(db, slug, data, sortOrder) {
11867
+ const now = Math.floor(Date.now() / 1e3);
11868
+ const existing = await db.prepare(
11869
+ `SELECT id FROM documents WHERE slug = ? AND type_id = 'menu_item' AND tenant_id = 'default' AND is_current_draft = 1 AND deleted_at IS NULL`
11870
+ ).bind(slug).first();
11871
+ if (existing) {
11872
+ return;
11873
+ }
11874
+ const id = nanoid();
11875
+ await db.prepare(
11876
+ `INSERT INTO documents (
11877
+ id, root_id, type_id, type_version, version_of_id, version_number,
11878
+ is_current_draft, is_published, status, parent_root_id,
11879
+ slug, path, title, zone, sort_order, visible,
11880
+ published_at, scheduled_at, expires_at, deleted_at,
11881
+ tenant_id, locale, translation_group_id,
11882
+ data, metadata, owner_id, created_by, updated_by, created_at, updated_at
11883
+ ) VALUES (
11884
+ ?, ?, ?, ?, ?, ?,
11885
+ ?, ?, ?, ?,
11886
+ ?, ?, ?, ?, ?, ?,
11887
+ ?, ?, ?, ?,
11888
+ ?, ?, ?,
11889
+ ?, ?, ?, ?, ?, ?, ?
11890
+ )`
11891
+ ).bind(
11892
+ id,
11893
+ // id
11894
+ id,
11895
+ // root_id
11896
+ "menu_item",
11897
+ // type_id
11898
+ 1,
11899
+ // type_version
11900
+ null,
11901
+ // version_of_id
11902
+ 1,
11903
+ // version_number
11904
+ 1,
11905
+ // is_current_draft
11906
+ 1,
11907
+ // is_published
11908
+ "published",
11909
+ // status
11910
+ "",
11911
+ // parent_root_id
11912
+ slug,
11913
+ // slug
11914
+ null,
11915
+ // path
11916
+ data.label,
11917
+ // title
11918
+ null,
11919
+ // zone
11920
+ sortOrder,
11921
+ // sort_order
11922
+ data.visible ? 1 : 0,
11923
+ // visible
11924
+ now,
11925
+ // published_at
11926
+ null,
11927
+ // scheduled_at
11928
+ null,
11929
+ // expires_at
11930
+ null,
11931
+ // deleted_at
11932
+ "default",
11933
+ // tenant_id
11934
+ "default",
11935
+ // locale
11936
+ "",
11937
+ // translation_group_id
11938
+ JSON.stringify(data),
11939
+ // data
11940
+ "{}",
11941
+ // metadata
11942
+ null,
11943
+ // owner_id
11944
+ "system",
11945
+ // created_by
11946
+ "system",
11947
+ // updated_by
11948
+ now,
11949
+ // created_at
11950
+ now
11951
+ // updated_at
11952
+ ).run();
11953
+ }
11954
+ async function reorderItems(db, items) {
11955
+ if (items.length === 0) return;
11956
+ const now = Math.floor(Date.now() / 1e3);
11957
+ const stmts = items.map(
11958
+ (item) => db.prepare(
11959
+ `UPDATE documents SET
11960
+ sort_order = ?,
11961
+ data = json_set(data, '$.parent', ?),
11962
+ updated_at = ?
11963
+ WHERE id = ? AND tenant_id = 'default'`
11964
+ ).bind(item.sortOrder, item.parent ?? null, now, item.id)
11965
+ );
11966
+ await db.batch(stmts);
11967
+ }
11968
+ async function toggleVisibility(db, id, visible) {
11969
+ const now = Math.floor(Date.now() / 1e3);
11970
+ const visInt = visible ? 1 : 0;
11971
+ await db.prepare(
11972
+ `UPDATE documents SET
11973
+ data = json_set(data, '$.visible', ?),
11974
+ visible = ?,
11975
+ updated_at = ?
11976
+ WHERE id = ? AND tenant_id = 'default' AND type_id = 'menu_item'`
11977
+ ).bind(visInt, visInt, now, id).run();
11978
+ }
11979
+ async function updateItem(db, id, changes, lockedFields) {
11980
+ const lockedKeys = new Set(lockedFields.filter((f) => f !== "parent"));
11981
+ for (const key of Object.keys(changes)) {
11982
+ if (lockedKeys.has(key)) {
11983
+ return false;
11984
+ }
11985
+ }
11986
+ const now = Math.floor(Date.now() / 1e3);
11987
+ const jsonSetParts = [];
11988
+ const binds = [];
11989
+ const dataFields = ["label", "icon", "target", "permissions", "url", "parent", "visible"];
11990
+ for (const field of dataFields) {
11991
+ if (field in changes) {
11992
+ jsonSetParts.push(`'$.${field}'`, "?");
11993
+ binds.push(
11994
+ field === "permissions" ? JSON.stringify(changes[field]) : changes[field] ?? null
11995
+ );
11996
+ }
11997
+ }
11998
+ const setParts = [];
11999
+ if (jsonSetParts.length > 0) {
12000
+ setParts.push(`data = json_set(data, ${jsonSetParts.join(", ")})`);
12001
+ }
12002
+ if ("sortOrder" in changes && changes.sortOrder !== void 0) {
12003
+ setParts.push(`sort_order = ?`);
12004
+ binds.push(changes.sortOrder);
12005
+ }
12006
+ if ("visible" in changes && changes.visible !== void 0) {
12007
+ setParts.push(`visible = ?`);
12008
+ binds.push(changes.visible ? 1 : 0);
12009
+ }
12010
+ if (setParts.length === 0) {
12011
+ return true;
12012
+ }
12013
+ setParts.push(`updated_at = ?`);
12014
+ binds.push(now);
12015
+ binds.push(id);
12016
+ const sql = `UPDATE documents SET ${setParts.join(", ")} WHERE id = ? AND tenant_id = 'default' AND type_id = 'menu_item'`;
12017
+ await db.prepare(sql).bind(...binds).run();
12018
+ return true;
12019
+ }
12020
+ async function deleteItem(db, id) {
12021
+ try {
12022
+ const row = await db.prepare(
12023
+ `SELECT data FROM documents WHERE id = ? AND tenant_id = 'default' AND is_current_draft = 1 AND deleted_at IS NULL`
12024
+ ).bind(id).first();
12025
+ if (!row) {
12026
+ return { ok: false, reason: "Item not found" };
12027
+ }
12028
+ const data = JSON.parse(row.data);
12029
+ if (data.source === "system" || data.source === "plugin") {
12030
+ return { ok: false, reason: "Cannot delete system or plugin items" };
12031
+ }
12032
+ const now = Math.floor(Date.now() / 1e3);
12033
+ await db.prepare(
12034
+ `UPDATE documents SET deleted_at = ? WHERE id = ? AND tenant_id = 'default'`
12035
+ ).bind(now, id).run();
12036
+ return { ok: true };
12037
+ } catch (err) {
12038
+ return { ok: false, reason: err instanceof Error ? err.message : "Unknown error" };
12039
+ }
12040
+ }
12041
+
12042
+ // src/services/menu-icons.ts
12043
+ var MENU_ICON_MAP = {
12044
+ "puzzle-piece": `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M14.5 2A1.5 1.5 0 0013 3.5V4H8.5A1.5 1.5 0 007 5.5V10h-.5a1.5 1.5 0 100 3H7v4.5A1.5 1.5 0 008.5 19H13v-.5a1.5 1.5 0 113 0V19h.5a1.5 1.5 0 001.5-1.5V13h-.5a1.5 1.5 0 110-3h.5V5.5A1.5 1.5 0 0017 4h-.5v-.5A1.5 1.5 0 0014.5 2z"/></svg>`,
12045
+ envelope: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"/><path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"/></svg>`,
12046
+ cog: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"/></svg>`,
12047
+ chart: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z"/></svg>`,
12048
+ sparkles: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M9 4.804A7.968 7.968 0 005.5 4c-1.255 0-2.443.29-3.5.804v10A7.969 7.969 0 015.5 14c1.669 0 3.218.51 4.5 1.385A7.962 7.962 0 0114.5 14c1.255 0 2.443.29 3.5.804v-10A7.968 7.968 0 0014.5 4c-1.255 0-2.443.29-3.5.804V12a1 1 0 11-2 0V4.804z"/></svg>`,
12049
+ bolt: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clip-rule="evenodd"/></svg>`,
12050
+ document: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm3 1h6v2H7V5zm0 4h6v2H7V9zm0 4h4v2H7v-2z" clip-rule="evenodd"/></svg>`,
12051
+ lock: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/></svg>`,
12052
+ photo: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"/></svg>`,
12053
+ "magnifying-glass": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"/></svg>`,
12054
+ "chart-bar": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z"/></svg>`,
12055
+ image: `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"/></svg>`,
12056
+ palette: `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.098 19.902a3.75 3.75 0 0 0 5.304 0l6.401-6.402M6.75 21A3.75 3.75 0 0 1 3 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 0 0 3.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008Z"/></svg>`,
12057
+ "hand-raised": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.05 4.575a1.575 1.575 0 1 0-3.15 0v3m3.15-3v-1.5a1.575 1.575 0 0 1 3.15 0v1.5m-3.15 0 .075 5.925m3.075-5.925v2.925m0-2.925a1.575 1.575 0 0 1 3.15 0V9.9m-3.15-2.4v5.325M16.5 9.9a1.575 1.575 0 0 1 3.15 0V15a6.15 6.15 0 0 1-6.15 6.15H12A6.15 6.15 0 0 1 5.85 15V9.525"/></svg>`,
12058
+ key: `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z"/></svg>`,
12059
+ "arrow-right": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"/></svg>`,
12060
+ "shield-check": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>`,
12061
+ "credit-card": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Z"/></svg>`,
12062
+ "document-text": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5.586a1 1 0 0 1 .707.293l5.414 5.414a1 1 0 0 1 .293.707V19a2 2 0 0 1-2 2z"/></svg>`,
12063
+ "pencil-square": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>`,
12064
+ server: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M2 5a2 2 0 012-2h12a2 2 0 012 2v2a2 2 0 01-2 2H4a2 2 0 01-2-2V5zm14 1a1 1 0 11-2 0 1 1 0 012 0zM2 13a2 2 0 012-2h12a2 2 0 012 2v2a2 2 0 01-2 2H4a2 2 0 01-2-2v-2zm14 1a1 1 0 11-2 0 1 1 0 012 0z" clip-rule="evenodd"/></svg>`,
12065
+ "building-office": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m3-6H15m-1.5 3H15m-1.5 3H15M9 21v-3.375c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125V21"/></svg>`,
12066
+ home: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/></svg>`,
12067
+ users: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z"/></svg>`,
12068
+ collection: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"/></svg>`,
12069
+ "bars-3": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/></svg>`,
12070
+ "cog-6-tooth": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/></svg>`,
12071
+ star: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>`,
12072
+ tag: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"/></svg>`,
12073
+ link: `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>`,
12074
+ "external-link": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>`,
12075
+ eye: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/><path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/></svg>`,
12076
+ "eye-slash": `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M3.707 2.293a1 1 0 00-1.414 1.414l14 14a1 1 0 001.414-1.414l-1.473-1.473A10.014 10.014 0 0019.542 10C18.268 5.943 14.478 3 10 3a9.958 9.958 0 00-4.512 1.074l-1.78-1.781zm4.261 4.26l1.514 1.515a2.003 2.003 0 012.45 2.45l1.514 1.514a4 4 0 00-5.478-5.478z" clip-rule="evenodd"/><path d="M12.454 16.697L9.75 13.992a4 4 0 01-3.742-3.741L2.335 6.578A9.98 9.98 0 00.458 10c1.274 4.057 5.065 7 9.542 7 .847 0 1.669-.105 2.454-.303z"/></svg>`
12077
+ };
12078
+ var FALLBACK = `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M14.5 2A1.5 1.5 0 0013 3.5V4H8.5A1.5 1.5 0 007 5.5V10h-.5a1.5 1.5 0 100 3H7v4.5A1.5 1.5 0 008.5 19H13v-.5a1.5 1.5 0 113 0V19h.5a1.5 1.5 0 001.5-1.5V13h-.5a1.5 1.5 0 110-3h.5V5.5A1.5 1.5 0 0017 4h-.5v-.5A1.5 1.5 0 0014.5 2z"/></svg>`;
12079
+ function resolveIcon2(nameOrSvg) {
12080
+ if (!nameOrSvg) return FALLBACK;
12081
+ if (nameOrSvg.startsWith("<svg")) return nameOrSvg;
12082
+ return MENU_ICON_MAP[nameOrSvg] ?? FALLBACK;
12083
+ }
12084
+
12085
+ // src/middleware/menu.ts
12086
+ var EXTERNAL_ICON = `<svg class="h-3 w-3 ml-1 shrink-0 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>`;
12087
+ var CHEVRON = `<svg data-menu-chevron class="h-4 w-4 rotate-180 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>`;
12088
+ function renderTopLevelItem(item, currentPath) {
12089
+ const isActive = currentPath === item.url || item.url !== "/admin" && currentPath.startsWith(item.url);
12090
+ const icon = resolveIcon2(item.icon);
12091
+ const label = escapeHtml(item.label);
12092
+ const targetAttr = item.target === "_blank" ? ' target="_blank" rel="noopener noreferrer"' : "";
12093
+ const externalAffordance = item.isExternal ? EXTERNAL_ICON : "";
12094
+ if (item.children.length > 0) {
12095
+ const accordionId = `acc-${item.id.replace(/[^a-z0-9]/gi, "-")}`;
12096
+ const isChildActive = item.children.some(
12097
+ (c) => currentPath === c.url || currentPath.startsWith(c.url)
12098
+ );
12099
+ const accordionActive = isActive || isChildActive;
12100
+ return `
12101
+ <div data-menu-accordion class="relative">
12102
+ ${accordionActive ? '<span class="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-cyan-500 dark:bg-cyan-400"></span>' : ""}
12103
+ <div class="flex w-full items-center">
12104
+ <a href="${item.url}"${targetAttr}
12105
+ class="flex flex-1 items-center gap-3 rounded-lg px-2 py-2.5 text-left text-sm/5 font-medium ${accordionActive ? "text-zinc-950 dark:text-white" : "text-zinc-950 hover:bg-zinc-950/5 dark:text-white dark:hover:bg-white/5"}"
12106
+ ${accordionActive ? 'data-current="true"' : ""}
12107
+ >
12108
+ <span class="shrink-0 ${accordionActive ? "fill-zinc-950 dark:fill-white" : "fill-zinc-500 dark:fill-zinc-400"}">${icon}</span>
12109
+ <span class="truncate">${label}</span>${externalAffordance}
12110
+ </a>
12111
+ <button
12112
+ onclick="toggleMenuAccordion('${accordionId}')"
12113
+ class="flex items-center justify-center rounded-lg p-2 text-zinc-500 hover:bg-zinc-950/5 dark:text-zinc-400 dark:hover:bg-white/5 flex-shrink-0"
12114
+ aria-label="Toggle ${label} submenu"
12115
+ >
12116
+ ${CHEVRON}
12117
+ </button>
12118
+ </div>
12119
+ <div id="${accordionId}" class="pl-6 mt-0.5 flex flex-col gap-0.5">
12120
+ ${item.children.map((child) => renderChildItem(child, currentPath)).join("")}
12121
+ </div>
12122
+ </div>`;
12123
+ }
12124
+ return `
12125
+ <span class="relative">
12126
+ ${isActive ? '<span class="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-cyan-500 dark:bg-cyan-400"></span>' : ""}
12127
+ <a href="${item.url}"${targetAttr}
12128
+ class="flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-sm/5 font-medium ${isActive ? "text-zinc-950 dark:text-white" : "text-zinc-950 hover:bg-zinc-950/5 dark:text-white dark:hover:bg-white/5"}"
12129
+ ${isActive ? 'data-current="true"' : ""}
12130
+ >
12131
+ <span class="shrink-0 ${isActive ? "fill-zinc-950 dark:fill-white" : "fill-zinc-500 dark:fill-zinc-400"}">${icon}</span>
12132
+ <span class="truncate">${label}</span>${externalAffordance}
12133
+ </a>
12134
+ </span>`;
12135
+ }
12136
+ function renderChildItem(item, currentPath) {
12137
+ const isActive = currentPath === item.url || item.url !== "/admin" && currentPath.startsWith(item.url);
12138
+ const icon = resolveIcon2(item.icon);
12139
+ const label = escapeHtml(item.label);
12140
+ const targetAttr = item.target === "_blank" ? ' target="_blank" rel="noopener noreferrer"' : "";
12141
+ const externalAffordance = item.isExternal ? EXTERNAL_ICON : "";
12142
+ return `
12143
+ <span class="relative">
12144
+ ${isActive ? '<span class="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-cyan-500 dark:bg-cyan-400"></span>' : ""}
12145
+ <a href="${item.url}"${targetAttr}
12146
+ class="flex w-full items-center gap-3 rounded-lg px-2 py-2 text-left text-sm/5 font-medium ${isActive ? "text-zinc-950 dark:text-white" : "text-zinc-600 hover:bg-zinc-950/5 dark:text-zinc-400 dark:hover:bg-white/5"}"
12147
+ ${isActive ? 'data-current="true"' : ""}
12148
+ >
12149
+ <span class="shrink-0 ${isActive ? "fill-zinc-950 dark:fill-white" : "fill-zinc-500 dark:fill-zinc-400"}">${icon}</span>
12150
+ <span class="truncate">${label}</span>${externalAffordance}
12151
+ </a>
12152
+ </span>`;
12153
+ }
12154
+ var ACCORDION_SCRIPT = `
12155
+ <script>
12156
+ function toggleMenuAccordion(id) {
12157
+ var el = document.getElementById(id)
12158
+ if (!el) return
12159
+ var chevrons = el.parentElement ? el.parentElement.querySelectorAll('[data-menu-chevron]') : []
12160
+ if (el.style.display === 'none' || el.style.display === '') {
12161
+ el.style.display = 'flex'
12162
+ el.style.flexDirection = 'column'
12163
+ chevrons.forEach(function(c) { c.style.transform = 'rotate(180deg)' })
12164
+ } else {
12165
+ el.style.display = 'none'
12166
+ chevrons.forEach(function(c) { c.style.transform = 'rotate(0deg)' })
12167
+ }
12168
+ }
12169
+ // Open active accordions on load
12170
+ document.addEventListener('DOMContentLoaded', function() {
12171
+ document.querySelectorAll('[data-menu-accordion]').forEach(function(acc) {
12172
+ if (acc.querySelector('[data-current]')) {
12173
+ var sub = acc.querySelector('[id^="acc-"]')
12174
+ if (sub) { sub.style.display = 'flex'; sub.style.flexDirection = 'column' }
12175
+ }
12176
+ })
12177
+ })
12178
+ </script>`;
12179
+ async function isMenuPluginActive(db) {
12180
+ try {
12181
+ const row = await db.prepare(
12182
+ `SELECT json_extract(data, '$.status') AS status
12183
+ FROM documents
12184
+ WHERE slug = 'menu' AND type_id = 'plugin' AND tenant_id = 'default'
12185
+ AND is_current_draft = 1 AND deleted_at IS NULL
12186
+ LIMIT 1`
12187
+ ).first();
12188
+ if (!row) return false;
12189
+ return row.status === "active";
12190
+ } catch {
12191
+ return false;
12192
+ }
12193
+ }
12194
+ function menuMiddleware() {
12195
+ return async (c, next) => {
12196
+ const path = new URL(c.req.url).pathname;
12197
+ if (!path.startsWith("/admin")) {
12198
+ return next();
12199
+ }
12200
+ await next();
12201
+ if (!c.res.headers.get("content-type")?.includes("text/html")) return;
12202
+ if (!await isMenuPluginActive(c.env.DB)) return;
12203
+ let items = [];
12204
+ try {
12205
+ items = await listMenuItems(c.env.DB);
12206
+ } catch {
12207
+ return;
12208
+ }
12209
+ if (items.length === 0) return;
12210
+ const tree = buildSidebarTree(items);
12211
+ const renderedItems = tree.map((item) => renderTopLevelItem(item, path)).join("");
12212
+ const navItemsHtml = renderedItems + ACCORDION_SCRIPT;
12213
+ const status = c.res.status;
12214
+ const headers = new Headers(c.res.headers);
12215
+ const html4 = await c.res.text();
12216
+ const markerRegex = /<!-- ADMIN_SIDEBAR_NAV_ITEMS -->[\s\S]*?<!-- \/ADMIN_SIDEBAR_NAV_ITEMS -->/g;
12217
+ if (markerRegex.test(html4)) {
12218
+ const newHtml = html4.replace(
12219
+ /<!-- ADMIN_SIDEBAR_NAV_ITEMS -->[\s\S]*?<!-- \/ADMIN_SIDEBAR_NAV_ITEMS -->/g,
12220
+ navItemsHtml
12221
+ );
12222
+ c.res = new Response(newHtml, { status, headers });
12223
+ } else {
12224
+ c.res = new Response(html4, { status, headers });
12225
+ }
12226
+ };
12227
+ }
12228
+
12229
+ // src/plugins/core-plugins/menu-plugin/services/menu-defaults.ts
12230
+ var SYSTEM_MENU_ITEMS = [
12231
+ {
12232
+ id: "menu:system:content",
12233
+ label: "Content",
12234
+ url: "/admin/content",
12235
+ icon: "document",
12236
+ target: "_self",
12237
+ isExternal: false,
12238
+ visible: true,
12239
+ parent: null,
12240
+ source: "system",
12241
+ pluginId: null,
12242
+ permissions: [],
12243
+ lockedFields: ["url"],
12244
+ sortOrder: 10
12245
+ },
12246
+ {
12247
+ id: "menu:system:collections",
12248
+ label: "Collections",
12249
+ url: "/admin/collections",
12250
+ icon: "collection",
12251
+ target: "_self",
12252
+ isExternal: false,
12253
+ visible: true,
12254
+ parent: null,
12255
+ source: "system",
12256
+ pluginId: null,
12257
+ permissions: [],
12258
+ lockedFields: ["url"],
12259
+ sortOrder: 20
12260
+ },
12261
+ {
12262
+ id: "menu:system:users",
12263
+ label: "Users",
12264
+ url: "/admin/users",
12265
+ icon: "users",
12266
+ target: "_self",
12267
+ isExternal: false,
12268
+ visible: true,
12269
+ parent: null,
12270
+ source: "system",
12271
+ pluginId: null,
12272
+ permissions: [],
12273
+ lockedFields: ["url"],
12274
+ sortOrder: 30
12275
+ },
12276
+ {
12277
+ id: "menu:system:settings",
12278
+ label: "Settings",
12279
+ url: "/admin/settings",
12280
+ icon: "cog",
12281
+ target: "_self",
12282
+ isExternal: false,
12283
+ visible: true,
12284
+ parent: null,
12285
+ source: "system",
12286
+ pluginId: null,
12287
+ permissions: [],
12288
+ lockedFields: ["url"],
12289
+ sortOrder: 40
12290
+ },
12291
+ {
12292
+ id: "menu:system:plugins",
12293
+ label: "Plugins",
12294
+ url: "/admin/plugins",
12295
+ icon: "collection",
12296
+ target: "_self",
12297
+ isExternal: false,
12298
+ visible: true,
12299
+ parent: null,
12300
+ source: "system",
12301
+ pluginId: null,
12302
+ permissions: [],
12303
+ lockedFields: ["url"],
12304
+ sortOrder: 50
12305
+ },
12306
+ {
12307
+ id: "menu:system:menu",
12308
+ label: "Menu",
12309
+ url: "/admin/menu",
12310
+ icon: "bars-3",
12311
+ target: "_self",
12312
+ isExternal: false,
12313
+ visible: true,
12314
+ parent: null,
12315
+ source: "system",
12316
+ pluginId: null,
12317
+ permissions: [],
12318
+ lockedFields: ["url"],
12319
+ sortOrder: 60
12320
+ }
12321
+ ];
12322
+
12323
+ // src/plugins/core-plugins/menu-plugin/templates/admin-menu-list.template.ts
12324
+ init_admin_layout_catalyst_template();
12325
+ function pluginStatusBadge(status) {
12326
+ if (status === "inactive") {
12327
+ return `<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-400 ring-1 ring-inset ring-red-600/20 dark:ring-red-500/20 ml-1">disabled</span>`;
12328
+ }
12329
+ return `<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-green-50 dark:bg-green-500/10 text-green-700 dark:text-green-400 ring-1 ring-inset ring-green-600/20 dark:ring-green-500/20 ml-1">enabled</span>`;
12330
+ }
12331
+ function sourceBadge(source) {
12332
+ switch (source) {
12333
+ case "system":
12334
+ return `<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-zinc-100 dark:bg-zinc-700/50 text-zinc-600 dark:text-zinc-300 ring-1 ring-inset ring-zinc-300/50 dark:ring-zinc-600/50">system</span>`;
12335
+ case "plugin":
12336
+ return `<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-blue-50 dark:bg-blue-500/10 text-blue-700 dark:text-blue-300 ring-1 ring-inset ring-blue-700/10 dark:ring-blue-400/20">plugin</span>`;
12337
+ default:
12338
+ return `<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-green-50 dark:bg-green-500/10 text-green-700 dark:text-green-300 ring-1 ring-inset ring-green-700/10 dark:ring-green-400/20">user</span>`;
12339
+ }
12340
+ }
12341
+ function iconPreview(icon) {
12342
+ if (!icon) return '<span class="text-zinc-400 dark:text-zinc-500">\u2014</span>';
12343
+ if (icon.trim().startsWith("<")) {
12344
+ return `<span class="w-5 h-5 inline-flex items-center justify-center">${icon}</span>`;
12345
+ }
12346
+ return `<span class="text-xs font-mono text-zinc-500 dark:text-zinc-400 truncate max-w-[80px]">${escapeHtml(icon)}</span>`;
12347
+ }
12348
+ function renderMenuListPage(data) {
12349
+ const messageBanner = data.message ? `<div class="mb-6 rounded-lg bg-cyan-50 dark:bg-cyan-900/20 border border-cyan-200 dark:border-cyan-700 px-4 py-3 text-sm text-cyan-800 dark:text-cyan-200">
12350
+ ${escapeHtml(data.message)}
12351
+ </div>` : "";
12352
+ const pluginStatuses = data.pluginStatuses ?? {};
12353
+ const rows = data.items.map((item, index) => {
12354
+ const isFirst = index === 0;
12355
+ const isLast = index === data.items.length - 1;
12356
+ const moveUpForm = !isFirst ? `<form method="POST" action="/admin/menu/${escapeHtml(item.id)}/move-up" class="inline">
12357
+ <button type="submit" class="p-1 rounded hover:bg-zinc-100 dark:hover:bg-zinc-700 text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors" title="Move up">
12358
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12359
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/>
12360
+ </svg>
12361
+ </button>
12362
+ </form>` : `<span class="w-6 inline-block"></span>`;
12363
+ const moveDownForm = !isLast ? `<form method="POST" action="/admin/menu/${escapeHtml(item.id)}/move-down" class="inline">
12364
+ <button type="submit" class="p-1 rounded hover:bg-zinc-100 dark:hover:bg-zinc-700 text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors" title="Move down">
12365
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12366
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
12367
+ </svg>
12368
+ </button>
12369
+ </form>` : `<span class="w-6 inline-block"></span>`;
12370
+ const deleteButton = item.source === "user" ? `<form method="POST" action="/admin/menu/${escapeHtml(item.id)}/delete" onsubmit="return confirm('Delete this item?')" class="inline">
12371
+ <button type="submit" class="inline-flex items-center px-2.5 py-1.5 text-xs font-medium rounded-md text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-500/10 hover:bg-red-100 dark:hover:bg-red-500/20 ring-1 ring-inset ring-red-600/20 dark:ring-red-500/20 transition-colors">
12372
+ Delete
12373
+ </button>
12374
+ </form>` : "";
12375
+ return `
12376
+ <tr class="border-b border-zinc-100 dark:border-zinc-800 hover:bg-zinc-50/50 dark:hover:bg-zinc-800/30 transition-colors cursor-pointer" onclick="if(!event.target.closest('a,button,input,form')){window.location='/admin/menu/${escapeHtml(item.id)}'}">
12377
+ <td class="px-4 py-3 w-16">
12378
+ <div class="flex items-center gap-0.5">
12379
+ ${moveUpForm}
12380
+ ${moveDownForm}
12381
+ </div>
12382
+ </td>
12383
+ <td class="px-4 py-3 w-16">
12384
+ ${iconPreview(item.icon)}
12385
+ </td>
12386
+ <td class="px-4 py-3">
12387
+ <span class="text-sm font-medium text-zinc-900 dark:text-zinc-100">${escapeHtml(item.label)}</span>
12388
+ ${item.parent ? `<span class="ml-2 text-xs text-zinc-400 dark:text-zinc-500">child</span>` : ""}
12389
+ </td>
12390
+ <td class="px-4 py-3 max-w-[200px]">
12391
+ <span class="text-sm text-zinc-500 dark:text-zinc-400 truncate block">${escapeHtml(item.url)}</span>
12392
+ </td>
12393
+ <td class="px-4 py-3">
12394
+ ${sourceBadge(item.source)}
12395
+ ${item.source === "plugin" && item.pluginId ? pluginStatusBadge(pluginStatuses[item.pluginId] ?? "active") : ""}
12396
+ </td>
12397
+ <td class="px-4 py-3">
12398
+ <form method="POST" action="/admin/menu/${escapeHtml(item.id)}/visibility" class="inline">
12399
+ <label class="relative inline-flex items-center cursor-pointer">
12400
+ <input
12401
+ type="checkbox"
12402
+ name="visible"
12403
+ value="true"
12404
+ ${item.visible ? "checked" : ""}
12405
+ onchange="this.form.requestSubmit()"
12406
+ class="sr-only peer"
12407
+ >
12408
+ <div class="w-9 h-5 bg-zinc-200 dark:bg-zinc-700 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-cyan-500 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-zinc-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-cyan-600"></div>
12409
+ </label>
12410
+ </form>
12411
+ </td>
12412
+ <td class="px-4 py-3">
12413
+ <div class="flex items-center gap-2">
12414
+ <a href="/admin/menu/${escapeHtml(item.id)}" class="inline-flex items-center px-2.5 py-1.5 text-xs font-medium rounded-md text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-700 hover:bg-zinc-200 dark:hover:bg-zinc-600 ring-1 ring-inset ring-zinc-300/50 dark:ring-zinc-600/50 transition-colors">
12415
+ Edit
12416
+ </a>
12417
+ ${deleteButton}
12418
+ </div>
12419
+ </td>
12420
+ </tr>`;
12421
+ }).join("\n");
12422
+ const emptyState = data.items.length === 0 ? `<tr>
12423
+ <td colspan="7" class="px-4 py-12 text-center text-sm text-zinc-400 dark:text-zinc-500">
12424
+ No menu items yet. <a href="/admin/menu/new" class="text-cyan-600 dark:text-cyan-400 underline hover:no-underline">Add a link</a> to get started.
12425
+ </td>
12426
+ </tr>` : "";
12427
+ const pageContent = `
12428
+ <div>
12429
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
12430
+ <div>
12431
+ <h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Menu Manager</h1>
12432
+ <p class="mt-1 text-sm/6 text-zinc-500 dark:text-zinc-400">Manage navigation links and their order</p>
12433
+ </div>
12434
+ <div class="mt-4 sm:mt-0">
12435
+ <a href="/admin/menu/new" class="inline-flex items-center justify-center rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm">
12436
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
12437
+ <path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"/>
12438
+ </svg>
12439
+ Add Link
12440
+ </a>
12441
+ </div>
12442
+ </div>
12443
+
12444
+ ${messageBanner}
12445
+
12446
+ <div class="rounded-xl overflow-hidden bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
12447
+ <table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-800">
12448
+ <thead>
12449
+ <tr class="bg-zinc-50 dark:bg-zinc-800/50">
12450
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider w-16">Order</th>
12451
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider w-16">Icon</th>
12452
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Label</th>
12453
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">URL</th>
12454
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Source</th>
12455
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Visible</th>
12456
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Actions</th>
12457
+ </tr>
12458
+ </thead>
12459
+ <tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
12460
+ ${rows}
12461
+ ${emptyState}
12462
+ </tbody>
12463
+ </table>
12464
+ </div>
12465
+ </div>
12466
+ `;
12467
+ const layoutData = {
12468
+ title: "Menu Manager",
12469
+ pageTitle: "Menu Manager",
12470
+ currentPath: data.currentPath ?? "/admin/menu",
12471
+ user: data.user,
12472
+ version: data.version,
12473
+ dynamicMenuItems: data.dynamicMenuItems,
12474
+ content: pageContent
12475
+ };
12476
+ return renderAdminLayoutCatalyst(layoutData);
12477
+ }
12478
+ function renderMenuSettingsContent(items, pluginStatuses = {}, message) {
12479
+ const messageBanner = "";
12480
+ const rows = items.map((item, index) => {
12481
+ const isFirst = index === 0;
12482
+ const isLast = index === items.length - 1;
12483
+ const moveUpForm = !isFirst ? `<form method="POST" action="/admin/menu/${escapeHtml(item.id)}/move-up" class="inline">
12484
+ <button type="submit" class="p-1 rounded hover:bg-zinc-100 dark:hover:bg-zinc-700 text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors" title="Move up">
12485
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/></svg>
12486
+ </button>
12487
+ </form>` : `<span class="w-6 inline-block"></span>`;
12488
+ const moveDownForm = !isLast ? `<form method="POST" action="/admin/menu/${escapeHtml(item.id)}/move-down" class="inline">
12489
+ <button type="submit" class="p-1 rounded hover:bg-zinc-100 dark:hover:bg-zinc-700 text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors" title="Move down">
12490
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
12491
+ </button>
12492
+ </form>` : `<span class="w-6 inline-block"></span>`;
12493
+ const deleteButton = item.source === "user" ? `<form method="POST" action="/admin/menu/${escapeHtml(item.id)}/delete" onsubmit="return confirm('Delete this item?')" class="inline">
12494
+ <button type="submit" class="inline-flex items-center px-2.5 py-1.5 text-xs font-medium rounded-md text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-500/10 hover:bg-red-100 dark:hover:bg-red-500/20 ring-1 ring-inset ring-red-600/20 dark:ring-red-500/20 transition-colors">Delete</button>
12495
+ </form>` : "";
12496
+ return `
12497
+ <tr class="border-b border-zinc-100 dark:border-zinc-800 hover:bg-zinc-50/50 dark:hover:bg-zinc-800/30 transition-colors cursor-pointer" onclick="if(!event.target.closest('a,button,input,form')){window.location='/admin/menu/${escapeHtml(item.id)}'}">
12498
+ <td class="px-4 py-3 w-16">
12499
+ <div class="flex items-center gap-0.5">${moveUpForm}${moveDownForm}</div>
12500
+ </td>
12501
+ <td class="px-4 py-3 w-16">${iconPreview(item.icon)}</td>
12502
+ <td class="px-4 py-3">
12503
+ <span class="text-sm font-medium text-zinc-900 dark:text-zinc-100">${escapeHtml(item.label)}</span>
12504
+ ${item.parent ? `<span class="ml-2 text-xs text-zinc-400 dark:text-zinc-500">child</span>` : ""}
12505
+ </td>
12506
+ <td class="px-4 py-3 max-w-[200px]">
12507
+ <span class="text-sm text-zinc-500 dark:text-zinc-400 truncate block">${escapeHtml(item.url)}</span>
12508
+ </td>
12509
+ <td class="px-4 py-3">${sourceBadge(item.source)}${item.source === "plugin" && item.pluginId ? pluginStatusBadge(pluginStatuses[item.pluginId] ?? "active") : ""}</td>
12510
+ <td class="px-4 py-3">
12511
+ <form method="POST" action="/admin/menu/${escapeHtml(item.id)}/visibility" class="inline">
12512
+ <label class="relative inline-flex items-center cursor-pointer">
12513
+ <input type="checkbox" name="visible" value="true" ${item.visible ? "checked" : ""} onchange="this.form.requestSubmit()" class="sr-only peer">
12514
+ <div class="w-9 h-5 bg-zinc-200 dark:bg-zinc-700 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-cyan-500 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-zinc-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-cyan-600"></div>
12515
+ </label>
12516
+ </form>
12517
+ </td>
12518
+ <td class="px-4 py-3">
12519
+ <div class="flex items-center gap-2">
12520
+ <a href="/admin/menu/${escapeHtml(item.id)}" class="inline-flex items-center px-2.5 py-1.5 text-xs font-medium rounded-md text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-700 hover:bg-zinc-200 dark:hover:bg-zinc-600 ring-1 ring-inset ring-zinc-300/50 dark:ring-zinc-600/50 transition-colors">Edit</a>
12521
+ ${deleteButton}
12522
+ </div>
12523
+ </td>
12524
+ </tr>`;
12525
+ }).join("\n");
12526
+ const emptyState = items.length === 0 ? `<tr><td colspan="7" class="px-4 py-12 text-center text-sm text-zinc-400 dark:text-zinc-500">No menu items yet. <a href="/admin/menu/new" class="text-cyan-600 dark:text-cyan-400 underline hover:no-underline">Add a link</a> to get started.</td></tr>` : "";
12527
+ return `
12528
+ <script>
12529
+ (function() {
12530
+ function getCsrfToken() {
12531
+ var c = document.cookie.split('; ').find(function(r) { return r.startsWith('csrf_token='); });
12532
+ return c ? c.substring(c.indexOf('=') + 1) : '';
12533
+ }
12534
+ document.addEventListener('submit', function(e) {
12535
+ var form = e.target;
12536
+ if (!form || form.tagName !== 'FORM') return;
12537
+ if ((form.method || 'GET').toUpperCase() === 'GET') return;
12538
+ if (!form.querySelector('input[name="_csrf"]')) {
12539
+ var input = document.createElement('input');
12540
+ input.type = 'hidden';
12541
+ input.name = '_csrf';
12542
+ input.value = getCsrfToken();
12543
+ form.appendChild(input);
12544
+ }
12545
+ });
12546
+ })();
12547
+ </script>
12548
+ <div>
12549
+ <div class="flex items-center justify-between mb-4">
12550
+ <p class="text-sm text-zinc-500 dark:text-zinc-400">Manage navigation links and their order in the sidebar.</p>
12551
+ <a href="/admin/menu/new" class="inline-flex items-center justify-center rounded-lg bg-zinc-950 dark:bg-white px-3 py-2 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm">
12552
+ <svg class="-ml-0.5 mr-1.5 h-4 w-4" viewBox="0 0 20 20" fill="currentColor"><path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"/></svg>
12553
+ Add Link
12554
+ </a>
12555
+ </div>
12556
+ ${messageBanner}
12557
+ <div class="rounded-xl overflow-hidden bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
12558
+ <table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-800">
12559
+ <thead>
12560
+ <tr class="bg-zinc-50 dark:bg-zinc-800/50">
12561
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider w-16">Order</th>
12562
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider w-16">Icon</th>
12563
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Label</th>
12564
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">URL</th>
12565
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Source</th>
12566
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Visible</th>
12567
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Actions</th>
12568
+ </tr>
12569
+ </thead>
12570
+ <tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
12571
+ ${rows}
12572
+ ${emptyState}
12573
+ </tbody>
12574
+ </table>
12575
+ </div>
12576
+ </div>
12577
+ `;
12578
+ }
12579
+
12580
+ // src/plugins/core-plugins/menu-plugin/templates/admin-menu-form.template.ts
12581
+ init_admin_layout_catalyst_template();
12582
+
12583
+ // src/plugins/core-plugins/menu-plugin/templates/icon-picker.template.ts
12584
+ var ICONS = [
12585
+ {
12586
+ name: "puzzle-piece",
12587
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"/>'
12588
+ },
12589
+ {
12590
+ name: "document",
12591
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>'
12592
+ },
12593
+ {
12594
+ name: "collection",
12595
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>'
12596
+ },
12597
+ {
12598
+ name: "users",
12599
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>'
12600
+ },
12601
+ {
12602
+ name: "cog",
12603
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>'
12604
+ },
12605
+ {
12606
+ name: "envelope",
12607
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>'
12608
+ },
12609
+ {
12610
+ name: "chart",
12611
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>'
12612
+ },
12613
+ {
12614
+ name: "bolt",
12615
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>'
12616
+ },
12617
+ {
12618
+ name: "sparkles",
12619
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"/>'
12620
+ },
12621
+ {
12622
+ name: "photo",
12623
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>'
12624
+ },
12625
+ {
12626
+ name: "lock",
12627
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>'
12628
+ },
12629
+ {
12630
+ name: "magnifying-glass",
12631
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>'
12632
+ },
12633
+ {
12634
+ name: "chart-bar",
12635
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>'
12636
+ },
12637
+ {
12638
+ name: "image",
12639
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>'
12640
+ },
12641
+ {
12642
+ name: "key",
12643
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>'
12644
+ },
12645
+ {
12646
+ name: "shield-check",
12647
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>'
12648
+ },
12649
+ {
12650
+ name: "credit-card",
12651
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>'
12652
+ },
12653
+ {
12654
+ name: "document-text",
12655
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>'
12656
+ },
12657
+ {
12658
+ name: "pencil-square",
12659
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>'
12660
+ },
12661
+ {
12662
+ name: "server",
12663
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/>'
12664
+ },
12665
+ {
12666
+ name: "building-office",
12667
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>'
12668
+ },
12669
+ {
12670
+ name: "home",
12671
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>'
12672
+ },
12673
+ {
12674
+ name: "star",
12675
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>'
12676
+ },
12677
+ {
12678
+ name: "tag",
12679
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>'
12680
+ },
12681
+ {
12682
+ name: "link",
12683
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>'
12684
+ },
12685
+ {
12686
+ name: "external-link",
12687
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>'
12688
+ },
12689
+ {
12690
+ name: "eye",
12691
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>'
12692
+ },
12693
+ {
12694
+ name: "bars-3",
12695
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>'
12696
+ },
12697
+ {
12698
+ name: "arrow-right",
12699
+ svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/>'
12700
+ }
12701
+ ];
12702
+ function renderIconPicker(selectedIcon) {
12703
+ const safeSelected = escapeHtml(selectedIcon);
12704
+ const iconButtons = ICONS.map(({ name, svg }) => {
12705
+ const isSelected = name === selectedIcon;
12706
+ const selectedClasses = isSelected ? "ring-2 ring-cyan-500 bg-cyan-50 dark:bg-cyan-900/30" : "ring-1 ring-zinc-200 dark:ring-zinc-700 hover:ring-cyan-300 dark:hover:ring-cyan-600 hover:bg-zinc-50 dark:hover:bg-zinc-800";
12707
+ return `
12708
+ <button
12709
+ type="button"
12710
+ data-icon-name="${name}"
12711
+ onclick="selectIcon('${name}')"
12712
+ class="icon-btn flex flex-col items-center gap-1 p-2 rounded-lg transition-all duration-150 ${selectedClasses}"
12713
+ title="${name}"
12714
+ >
12715
+ <svg class="w-6 h-6 text-zinc-700 dark:text-zinc-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12716
+ ${svg}
12717
+ </svg>
12718
+ <span class="text-[10px] text-zinc-500 dark:text-zinc-400 truncate max-w-[56px] text-center leading-tight">${name}</span>
12719
+ </button>`;
12720
+ }).join("\n");
12721
+ return `
12722
+ <div class="space-y-4">
12723
+ <input type="hidden" name="icon" id="icon-input" value="${safeSelected}">
12724
+
12725
+ <div>
12726
+ <p class="text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Choose an icon</p>
12727
+ <div class="grid grid-cols-6 sm:grid-cols-8 md:grid-cols-10 gap-2 max-h-64 overflow-y-auto p-1 rounded-lg border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900">
12728
+ ${iconButtons}
12729
+ </div>
12730
+ </div>
12731
+
12732
+ <div>
12733
+ <label class="text-sm font-medium text-zinc-700 dark:text-zinc-300 block mb-1">Custom SVG (overrides selection above)</label>
12734
+ <textarea
12735
+ id="custom-svg-input"
12736
+ rows="3"
12737
+ placeholder='<svg xmlns="http://www.w3.org/2000/svg" ...>...</svg>'
12738
+ oninput="handleCustomSvg(this.value)"
12739
+ class="w-full rounded-lg border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 font-mono"
12740
+ ></textarea>
12741
+ <p class="mt-1 text-xs text-zinc-400 dark:text-zinc-500">Paste raw SVG markup to use a custom icon.</p>
12742
+ </div>
12743
+ </div>
12744
+
12745
+ <script>
12746
+ (function () {
12747
+ function selectIcon(name) {
12748
+ document.getElementById('icon-input').value = name;
12749
+ document.getElementById('custom-svg-input').value = '';
12750
+ document.querySelectorAll('.icon-btn').forEach(function (btn) {
12751
+ var isSelected = btn.getAttribute('data-icon-name') === name;
12752
+ btn.className = btn.className
12753
+ .replace(/ring-2 ring-cyan-500 bg-cyan-50 dark:bg-cyan-900\\/30/g, '')
12754
+ .replace(/ring-1 ring-zinc-200 dark:ring-zinc-700 hover:ring-cyan-300 dark:hover:ring-cyan-600 hover:bg-zinc-50 dark:hover:bg-zinc-800/g, '')
12755
+ .trim();
12756
+ if (isSelected) {
12757
+ btn.className += ' ring-2 ring-cyan-500 bg-cyan-50 dark:bg-cyan-900/30';
12758
+ } else {
12759
+ btn.className += ' ring-1 ring-zinc-200 dark:ring-zinc-700 hover:ring-cyan-300 dark:hover:ring-cyan-600 hover:bg-zinc-50 dark:hover:bg-zinc-800';
12760
+ }
12761
+ });
12762
+ }
12763
+
12764
+ function handleCustomSvg(value) {
12765
+ var trimmed = value.trim();
12766
+ if (trimmed) {
12767
+ document.getElementById('icon-input').value = trimmed;
12768
+ document.querySelectorAll('.icon-btn').forEach(function (btn) {
12769
+ btn.className = btn.className
12770
+ .replace(/ring-2 ring-cyan-500 bg-cyan-50 dark:bg-cyan-900\\/30/g, '')
12771
+ .replace(/ring-1 ring-zinc-200 dark:ring-zinc-700 hover:ring-cyan-300 dark:hover:ring-cyan-600 hover:bg-zinc-50 dark:hover:bg-zinc-800/g, '')
12772
+ .trim();
12773
+ btn.className += ' ring-1 ring-zinc-200 dark:ring-zinc-700 hover:ring-cyan-300 dark:hover:ring-cyan-600 hover:bg-zinc-50 dark:hover:bg-zinc-800';
12774
+ });
12775
+ }
12776
+ }
12777
+
12778
+ window.selectIcon = selectIcon;
12779
+ window.handleCustomSvg = handleCustomSvg;
12780
+ })();
12781
+ </script>
12782
+ `;
12783
+ }
12784
+
12785
+ // src/plugins/core-plugins/menu-plugin/templates/admin-menu-form.template.ts
12786
+ function renderMenuFormPage(data) {
12787
+ const { item, topLevelItems } = data;
12788
+ const isEdit = item !== null;
12789
+ const errorBanner = data.error ? `<div class="mb-6 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700 px-4 py-3 text-sm text-red-800 dark:text-red-300">
12790
+ ${escapeHtml(data.error)}
12791
+ </div>` : "";
12792
+ const parentOptions = topLevelItems.filter((t) => !isEdit || t.id !== item?.id).sort((a, b) => a.label.localeCompare(b.label)).map((t) => {
12793
+ const selected = item?.parent === t.id ? "selected" : "";
12794
+ return `<option value="${escapeHtml(t.id)}" ${selected}>${escapeHtml(t.label)}</option>`;
12795
+ }).join("\n");
12796
+ const targetSelfSelected = !item || item.target === "_self" ? "selected" : "";
12797
+ const targetBlankSelected = item?.target === "_blank" ? "selected" : "";
12798
+ const visibleChecked = item ? item.visible ? "checked" : "" : "checked";
12799
+ const formAttrs = isEdit ? `method="POST" action="/admin/menu/${escapeHtml(item.id)}/update"` : `method="POST" action="/admin/menu"`;
12800
+ const submitLabel = isEdit ? "Save Changes" : "Add Item";
12801
+ const pageTitle = isEdit ? `Edit: ${escapeHtml(item.label)}` : "Add Menu Item";
12802
+ const pageContent = `
12803
+ <div class="max-w-2xl">
12804
+ <div class="mb-6">
12805
+ <a href="/admin/menu" class="inline-flex items-center gap-1.5 text-sm text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors">
12806
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12807
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
12808
+ </svg>
12809
+ Back to Menu Manager
12810
+ </a>
12811
+ <h1 class="mt-3 text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">${pageTitle}</h1>
12812
+ </div>
12813
+
12814
+ ${errorBanner}
12815
+
12816
+ <div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 p-6">
12817
+ <form ${formAttrs} class="space-y-6">
12818
+
12819
+ <div>
12820
+ <label for="menu-label" class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">
12821
+ Label <span class="text-red-500">*</span>
12822
+ </label>
12823
+ <input
12824
+ id="menu-label"
12825
+ type="text"
12826
+ name="label"
12827
+ required
12828
+ value="${isEdit ? escapeHtml(item.label) : ""}"
12829
+ placeholder="e.g. Documentation"
12830
+ class="w-full rounded-lg border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 transition-shadow"
12831
+ >
12832
+ </div>
12833
+
12834
+ <div>
12835
+ <label for="menu-url" class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">URL</label>
12836
+ <input
12837
+ id="menu-url"
12838
+ type="text"
12839
+ name="url"
12840
+ value="${isEdit ? escapeHtml(item.url) : ""}"
12841
+ placeholder="/path/to/page or https://example.com"
12842
+ class="w-full rounded-lg border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 transition-shadow"
12843
+ >
12844
+ </div>
12845
+
12846
+ <div class="grid grid-cols-2 gap-4">
12847
+ <div>
12848
+ <label for="menu-target" class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">Open in</label>
12849
+ <select
12850
+ id="menu-target"
12851
+ name="target"
12852
+ class="w-full rounded-lg border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-cyan-500 transition-shadow"
12853
+ >
12854
+ <option value="_self" ${targetSelfSelected}>Same tab</option>
12855
+ <option value="_blank" ${targetBlankSelected}>New tab</option>
12856
+ </select>
12857
+ </div>
12858
+
12859
+ <div>
12860
+ <label for="menu-parent" class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">Parent</label>
12861
+ <select
12862
+ id="menu-parent"
12863
+ name="parent"
12864
+ class="w-full rounded-lg border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-cyan-500 transition-shadow"
12865
+ >
12866
+ <option value="">Top level (none)</option>
12867
+ ${parentOptions}
12868
+ </select>
12869
+ </div>
12870
+ </div>
12871
+
12872
+ <div class="flex items-center gap-3">
12873
+ <label class="relative inline-flex items-center cursor-pointer">
12874
+ <input
12875
+ id="menu-visible"
12876
+ type="checkbox"
12877
+ name="visible"
12878
+ value="1"
12879
+ ${visibleChecked}
12880
+ class="sr-only peer"
12881
+ >
12882
+ <div class="w-9 h-5 bg-zinc-200 dark:bg-zinc-700 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-cyan-500 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-zinc-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-cyan-600"></div>
12883
+ </label>
12884
+ <label for="menu-visible" class="text-sm font-medium text-zinc-700 dark:text-zinc-300 cursor-pointer">Visible in navigation</label>
12885
+ </div>
12886
+
12887
+ <div>
12888
+ <p class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Icon</p>
12889
+ ${renderIconPicker(item?.icon ?? "link")}
12890
+ </div>
12891
+
12892
+ <div class="flex items-center justify-end gap-3 pt-2 border-t border-zinc-100 dark:border-zinc-800">
12893
+ <a href="/admin/menu" class="inline-flex items-center px-3.5 py-2 text-sm font-medium rounded-lg text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-700 hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors">
12894
+ Cancel
12895
+ </a>
12896
+ <button
12897
+ type="submit"
12898
+ class="inline-flex items-center px-3.5 py-2 text-sm font-semibold rounded-lg bg-zinc-950 dark:bg-white text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm"
12899
+ >
12900
+ ${submitLabel}
12901
+ </button>
12902
+ </div>
12903
+
12904
+ </form>
12905
+ </div>
12906
+ </div>
12907
+ `;
12908
+ const layoutData = {
12909
+ title: isEdit ? "Edit Menu Item" : "Add Menu Item",
12910
+ pageTitle: isEdit ? "Edit Menu Item" : "Add Menu Item",
12911
+ currentPath: data.currentPath ?? "/admin/menu",
12912
+ user: data.user,
12913
+ version: data.version,
12914
+ dynamicMenuItems: data.dynamicMenuItems,
12915
+ content: pageContent
12916
+ };
12917
+ return renderAdminLayoutCatalyst(layoutData);
12918
+ }
12919
+
12920
+ // src/plugins/core-plugins/menu-plugin/routes/admin-menu.ts
12921
+ function sanitizeUrl(url) {
12922
+ const trimmed = url.trim();
12923
+ if (/^javascript:/i.test(trimmed) || /^data:/i.test(trimmed)) return "";
12924
+ return trimmed;
12925
+ }
12926
+ var adminMenuRoutes = new Hono();
12927
+ adminMenuRoutes.use("*", requireAuth());
12928
+ adminMenuRoutes.use("*", requireRole(["admin"]));
12929
+ adminMenuRoutes.get("/", async (c) => {
12930
+ const db = c.env.DB;
12931
+ const user = c.get("user");
12932
+ const items = await listMenuItems(db);
12933
+ buildSidebarTree(items);
12934
+ const pluginIds = [...new Set(items.filter((i) => i.pluginId).map((i) => i.pluginId))];
12935
+ const pluginStatuses = await fetchPluginStatuses(db, pluginIds);
12936
+ return c.html(renderMenuListPage({
12937
+ items,
12938
+ pluginStatuses,
12939
+ user,
12940
+ currentPath: "/admin/menu",
12941
+ version: c.get("appVersion"),
12942
+ dynamicMenuItems: c.get("pluginMenuItems"),
12943
+ message: c.req.query("message")
12944
+ }));
12945
+ });
12946
+ adminMenuRoutes.get("/new", async (c) => {
12947
+ const db = c.env.DB;
12948
+ const topLevelItems = await listMenuItems(db);
12949
+ return c.html(renderMenuFormPage({
12950
+ item: null,
12951
+ topLevelItems: topLevelItems.filter((i) => !i.parent),
12952
+ user: c.get("user"),
12953
+ currentPath: "/admin/menu",
12954
+ version: c.get("appVersion"),
12955
+ dynamicMenuItems: c.get("pluginMenuItems")
12956
+ }));
12957
+ });
12958
+ adminMenuRoutes.get("/:id", async (c) => {
12959
+ const db = c.env.DB;
12960
+ const id = c.req.param("id");
12961
+ const items = await listMenuItems(db);
12962
+ const item = items.find((i) => i.id === id);
12963
+ if (!item) return c.notFound();
12964
+ const topLevelItems = items.filter((i) => !i.parent && i.id !== id);
12965
+ return c.html(renderMenuFormPage({
12966
+ item,
12967
+ topLevelItems,
12968
+ user: c.get("user"),
12969
+ currentPath: "/admin/menu",
12970
+ version: c.get("appVersion"),
12971
+ dynamicMenuItems: c.get("pluginMenuItems")
12972
+ }));
12973
+ });
12974
+ adminMenuRoutes.post("/", async (c) => {
12975
+ const db = c.env.DB;
12976
+ const form = await c.req.formData();
12977
+ const label = String(form.get("label") ?? "").trim();
12978
+ const url = String(form.get("url") ?? "").trim();
12979
+ const icon = String(form.get("icon") ?? "link").trim();
12980
+ const target = form.get("target") === "_blank" ? "_blank" : "_self";
12981
+ const parent = String(form.get("parent") ?? "").trim() || null;
12982
+ const visible = form.has("visible");
12983
+ if (!label) {
12984
+ return c.html(renderMenuFormPage({
12985
+ item: null,
12986
+ topLevelItems: [],
12987
+ user: c.get("user"),
12988
+ currentPath: "/admin/menu",
12989
+ version: c.get("appVersion"),
12990
+ dynamicMenuItems: c.get("pluginMenuItems"),
12991
+ error: "Label is required"
12992
+ }), 400);
12993
+ }
12994
+ const now = Math.floor(Date.now() / 1e3);
12995
+ const id = nanoid();
12996
+ const slug = `menu:user:${id}`;
12997
+ const safeUrl = sanitizeUrl(url);
12998
+ const isExternal = /^https?:\/\//.test(safeUrl);
12999
+ const data = JSON.stringify({
13000
+ label,
13001
+ url: safeUrl,
13002
+ icon,
13003
+ target,
13004
+ isExternal,
13005
+ visible,
13006
+ parent,
13007
+ source: "user",
13008
+ pluginId: null,
13009
+ permissions: [],
13010
+ lockedFields: []
13011
+ });
13012
+ const items = await listMenuItems(db);
13013
+ const maxSort = items.reduce((m, i) => Math.max(m, i.sortOrder), 0);
13014
+ const sortOrder = maxSort + 10;
13015
+ await db.prepare(
13016
+ `INSERT INTO documents (id, root_id, type_id, type_version, version_of_id, version_number,
13017
+ is_current_draft, is_published, status, parent_root_id, slug, path, title, zone,
13018
+ sort_order, visible, published_at, scheduled_at, expires_at, deleted_at,
13019
+ tenant_id, locale, translation_group_id, data, metadata,
13020
+ owner_id, created_by, updated_by, created_at, updated_at)
13021
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
13022
+ ).bind(
13023
+ id,
13024
+ id,
13025
+ "menu_item",
13026
+ 1,
13027
+ null,
13028
+ 1,
13029
+ 1,
13030
+ 1,
13031
+ "published",
13032
+ "",
13033
+ slug,
13034
+ null,
13035
+ label,
13036
+ null,
13037
+ sortOrder,
13038
+ visible ? 1 : 0,
13039
+ now,
13040
+ null,
13041
+ null,
13042
+ null,
13043
+ "default",
13044
+ "default",
13045
+ "",
13046
+ data,
13047
+ "{}",
13048
+ null,
13049
+ c.get("user")?.userId ?? "system",
13050
+ c.get("user")?.userId ?? "system",
13051
+ now,
13052
+ now
13053
+ ).run();
13054
+ return c.redirect("/admin/menu?message=Item+created");
13055
+ });
13056
+ adminMenuRoutes.put("/reorder", async (c) => {
13057
+ const db = c.env.DB;
13058
+ const body = await c.req.json();
13059
+ if (!Array.isArray(body)) return c.json({ error: "Invalid body" }, 400);
13060
+ await reorderItems(db, body);
13061
+ return c.json({ ok: true });
13062
+ });
13063
+ adminMenuRoutes.post("/:id/visibility", async (c) => {
13064
+ const db = c.env.DB;
13065
+ const id = c.req.param("id");
13066
+ const form = await c.req.formData();
13067
+ const visible = form.get("visible") === "true";
13068
+ await toggleVisibility(db, id, visible);
13069
+ return c.redirect("/admin/menu?message=Visibility+updated");
13070
+ });
13071
+ async function handleMenuItemUpdate(c) {
13072
+ const db = c.env.DB;
13073
+ const id = c.req.param("id");
13074
+ const form = await c.req.formData();
13075
+ const items = await listMenuItems(db);
13076
+ const item = items.find((i) => i.id === id);
13077
+ if (!item) return c.json({ error: "Not found" }, 404);
13078
+ const locked = new Set(item.lockedFields);
13079
+ const changes = {};
13080
+ if (form.has("label") && !locked.has("label")) changes.label = String(form.get("label")).trim();
13081
+ if (form.has("icon") && !locked.has("icon")) changes.icon = String(form.get("icon")).trim();
13082
+ if (form.has("target") && !locked.has("target")) changes.target = form.get("target") === "_blank" ? "_blank" : "_self";
13083
+ if (form.has("url") && !locked.has("url")) changes.url = sanitizeUrl(String(form.get("url")));
13084
+ if (form.has("parent")) changes.parent = String(form.get("parent")).trim() || null;
13085
+ changes.visible = form.has("visible");
13086
+ const ok = await updateItem(db, id, changes, item.lockedFields);
13087
+ if (!ok) return c.json({ error: "Cannot modify locked fields" }, 403);
13088
+ return c.redirect("/admin/menu?message=Item+updated");
13089
+ }
13090
+ adminMenuRoutes.put("/:id", handleMenuItemUpdate);
13091
+ adminMenuRoutes.post("/:id/update", handleMenuItemUpdate);
13092
+ adminMenuRoutes.delete("/:id", async (c) => {
13093
+ const db = c.env.DB;
13094
+ const id = c.req.param("id");
13095
+ const result = await deleteItem(db, id);
13096
+ if (!result.ok) return c.json({ error: result.reason }, 403);
13097
+ return c.redirect("/admin/menu?message=Item+deleted");
13098
+ });
13099
+ adminMenuRoutes.post("/:id/delete", async (c) => {
13100
+ const db = c.env.DB;
13101
+ const id = c.req.param("id");
13102
+ const result = await deleteItem(db, id);
13103
+ if (!result.ok) return c.json({ error: result.reason }, 403);
13104
+ return c.redirect("/admin/menu?message=Item+deleted");
13105
+ });
13106
+ adminMenuRoutes.post("/:id/move-up", async (c) => {
13107
+ const db = c.env.DB;
13108
+ const id = c.req.param("id");
13109
+ const allItems = await listMenuItems(db);
13110
+ const cur = allItems.find((i) => i.id === id);
13111
+ if (!cur) return c.redirect("/admin/menu");
13112
+ const siblings = allItems.filter((i) => i.parent === cur.parent);
13113
+ const idx = siblings.findIndex((i) => i.id === id);
13114
+ const prev = siblings[idx - 1];
13115
+ if (idx > 0 && prev) {
13116
+ await reorderItems(db, [
13117
+ { id: cur.id, sortOrder: prev.sortOrder, parent: cur.parent },
13118
+ { id: prev.id, sortOrder: cur.sortOrder, parent: prev.parent }
13119
+ ]);
13120
+ return c.redirect("/admin/menu?message=Reordered");
13121
+ }
13122
+ return c.redirect("/admin/menu");
13123
+ });
13124
+ adminMenuRoutes.post("/:id/move-down", async (c) => {
13125
+ const db = c.env.DB;
13126
+ const id = c.req.param("id");
13127
+ const allItems = await listMenuItems(db);
13128
+ const cur = allItems.find((i) => i.id === id);
13129
+ if (!cur) return c.redirect("/admin/menu");
13130
+ const siblings = allItems.filter((i) => i.parent === cur.parent);
13131
+ const idx = siblings.findIndex((i) => i.id === id);
13132
+ const next = siblings[idx + 1];
13133
+ if (idx !== -1 && idx < siblings.length - 1 && next) {
13134
+ await reorderItems(db, [
13135
+ { id: cur.id, sortOrder: next.sortOrder, parent: cur.parent },
13136
+ { id: next.id, sortOrder: cur.sortOrder, parent: next.parent }
13137
+ ]);
13138
+ return c.redirect("/admin/menu?message=Reordered");
13139
+ }
13140
+ return c.redirect("/admin/menu");
13141
+ });
13142
+ var menuPlugin = definePlugin({
13143
+ id: "menu",
13144
+ version: "1.0.0",
13145
+ name: "Menu Manager",
13146
+ description: "Admin sidebar navigation manager.",
13147
+ sonicjsVersionRange: "^3.0.0",
13148
+ author: { name: "SonicJS Team", email: "team@sonicjs.com" },
13149
+ register(app2) {
13150
+ app2.route("/admin/menu", adminMenuRoutes);
13151
+ },
13152
+ async onBoot(ctx) {
13153
+ const env = ctx.env ?? {};
13154
+ const db = env.DB;
13155
+ if (!db) return;
13156
+ try {
13157
+ const typeRegistry = new DocumentTypeRegistry(db);
13158
+ await typeRegistry.register({
13159
+ id: "menu_item",
13160
+ name: "menu_item",
13161
+ displayName: "Menu Item",
13162
+ description: "Admin sidebar navigation item",
13163
+ schema: z.object({}),
13164
+ pluginId: "menu",
13165
+ source: "plugin",
13166
+ queryableFields: [
13167
+ { name: "parent", path: "$.parent", kind: "scalar", type: "text", column: "q_menu_parent" },
13168
+ { name: "visible", path: "$.visible", kind: "scalar", type: "boolean", column: "q_menu_visible" },
13169
+ { name: "source", path: "$.source", kind: "scalar", type: "text", column: "q_menu_source" },
13170
+ { name: "pluginId", path: "$.pluginId", kind: "scalar", type: "text", column: "q_menu_plugin_id" }
13171
+ ],
13172
+ settings: {
13173
+ baseGrants: { admin: ["read", "create", "update", "delete", "manage"] },
13174
+ internal: true
13175
+ }
13176
+ });
13177
+ for (const item of SYSTEM_MENU_ITEMS) {
13178
+ await upsertSystemItem(db, item.id, {
13179
+ label: item.label,
13180
+ url: item.url,
13181
+ icon: item.icon,
13182
+ target: item.target,
13183
+ isExternal: item.isExternal,
13184
+ visible: item.visible,
13185
+ parent: item.parent,
13186
+ source: item.source,
13187
+ pluginId: item.pluginId,
13188
+ permissions: [...item.permissions],
13189
+ lockedFields: [...item.lockedFields]
13190
+ }, item.sortOrder);
13191
+ }
13192
+ await reconcileMenuFromPlugins(db);
13193
+ } catch {
13194
+ }
13195
+ },
13196
+ settingsTabContent: {
13197
+ async loadData(db) {
13198
+ const items = await listMenuItems(db);
13199
+ const pluginIds = [...new Set(items.filter((i) => i.pluginId).map((i) => i.pluginId))];
13200
+ const pluginStatuses = await fetchPluginStatuses(db, pluginIds);
13201
+ return { items, pluginStatuses };
13202
+ },
13203
+ render({ data }) {
13204
+ const items = data?.items ?? [];
13205
+ const pluginStatuses = data?.pluginStatuses ?? {};
13206
+ return renderMenuSettingsContent(items, pluginStatuses);
13207
+ }
13208
+ }
13209
+ });
11780
13210
  init_admin_layout_catalyst_template();
11781
13211
  var adminRoutes6 = new Hono();
11782
13212
  adminRoutes6.use("*", requireAuth());
@@ -16762,7 +18192,8 @@ function createSonicJSApp(config = {}) {
16762
18192
  helloWorldPlugin,
16763
18193
  multiTenantPlugin,
16764
18194
  lexicalEditorPlugin,
16765
- versioningPlugin
18195
+ versioningPlugin,
18196
+ menuPlugin
16766
18197
  ];
16767
18198
  const corePluginsAfterCatchAll = [emailPluginV3, magicLinkPlugin, emailReconciliationPlugin];
16768
18199
  const wirePlugins = createPluginWirer(
@@ -16900,6 +18331,7 @@ function createSonicJSApp(config = {}) {
16900
18331
  app2.use("/admin/*", requireAuth());
16901
18332
  app2.use("/admin/*", requireRbac("portal", "access"));
16902
18333
  app2.use("/admin/*", pluginMenuMiddleware());
18334
+ app2.use("/admin/*", menuMiddleware());
16903
18335
  const NAV_LANDING = [
16904
18336
  { path: "/admin/content", perm: "content:read" },
16905
18337
  { path: "/admin/media", perm: "media:read" },