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

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 (108) 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-FTXGZ3BF.cjs → chunk-37V3RET3.cjs} +21 -37
  16. package/dist/chunk-37V3RET3.cjs.map +1 -0
  17. package/dist/{chunk-3CT3SUR4.cjs → chunk-A2SN3RVL.cjs} +3 -3
  18. package/dist/chunk-A2SN3RVL.cjs.map +1 -0
  19. package/dist/{chunk-CRGUD4KC.cjs → chunk-BBEKHLTW.cjs} +9 -9
  20. package/dist/{chunk-CRGUD4KC.cjs.map → chunk-BBEKHLTW.cjs.map} +1 -1
  21. package/dist/{chunk-RMRJGMDE.js → chunk-BHND5PEV.js} +3 -3
  22. package/dist/{chunk-RMRJGMDE.js.map → chunk-BHND5PEV.js.map} +1 -1
  23. package/dist/{chunk-OBA2RYZN.js → chunk-BJROXF4N.js} +3 -3
  24. package/dist/{chunk-OBA2RYZN.js.map → chunk-BJROXF4N.js.map} +1 -1
  25. package/dist/{chunk-EUJAEB6W.cjs → chunk-BP52BFIV.cjs} +53 -4
  26. package/dist/chunk-BP52BFIV.cjs.map +1 -0
  27. package/dist/{chunk-PXNTCCPE.cjs → chunk-E66UVONV.cjs} +8 -8
  28. package/dist/{chunk-PXNTCCPE.cjs.map → chunk-E66UVONV.cjs.map} +1 -1
  29. package/dist/{chunk-CHWT5KBK.js → chunk-FHPEM77Y.js} +9 -25
  30. package/dist/chunk-FHPEM77Y.js.map +1 -0
  31. package/dist/{chunk-JFLZHOQP.cjs → chunk-I767LVN7.cjs} +531 -419
  32. package/dist/chunk-I767LVN7.cjs.map +1 -0
  33. package/dist/{chunk-N32OWET6.cjs → chunk-JMJM7DMM.cjs} +5 -5
  34. package/dist/{chunk-N32OWET6.cjs.map → chunk-JMJM7DMM.cjs.map} +1 -1
  35. package/dist/{chunk-QWSR4THH.cjs → chunk-KKNEYIZJ.cjs} +12 -5
  36. package/dist/chunk-KKNEYIZJ.cjs.map +1 -0
  37. package/dist/{chunk-73O4GPJ6.js → chunk-LNI3FYSI.js} +12 -5
  38. package/dist/chunk-LNI3FYSI.js.map +1 -0
  39. package/dist/{chunk-PYZYU7EK.cjs → chunk-QCJHCSOK.cjs} +36 -2
  40. package/dist/chunk-QCJHCSOK.cjs.map +1 -0
  41. package/dist/{chunk-MGHHPNJN.cjs → chunk-QJUJNTIU.cjs} +11 -59
  42. package/dist/chunk-QJUJNTIU.cjs.map +1 -0
  43. package/dist/{chunk-DOMXI5PM.js → chunk-RJ2O5IIE.js} +36 -2
  44. package/dist/chunk-RJ2O5IIE.js.map +1 -0
  45. package/dist/{chunk-5V62WT6M.js → chunk-THFZSDIU.js} +4 -2
  46. package/dist/chunk-THFZSDIU.js.map +1 -0
  47. package/dist/{chunk-2FCKKOA5.js → chunk-UIHIXTIS.js} +3 -3
  48. package/dist/chunk-UIHIXTIS.js.map +1 -0
  49. package/dist/{chunk-JQISFW6U.js → chunk-URIBTX27.js} +3 -3
  50. package/dist/{chunk-JQISFW6U.js.map → chunk-URIBTX27.js.map} +1 -1
  51. package/dist/{chunk-K623Q6WD.cjs → chunk-UYJQDDEE.cjs} +4 -2
  52. package/dist/chunk-UYJQDDEE.cjs.map +1 -0
  53. package/dist/{chunk-6KNTZ67K.js → chunk-VC5XVPLM.js} +234 -126
  54. package/dist/chunk-VC5XVPLM.js.map +1 -0
  55. package/dist/{chunk-I2DM27HP.js → chunk-VGEZNFWH.js} +49 -4
  56. package/dist/chunk-VGEZNFWH.js.map +1 -0
  57. package/dist/{chunk-GB2Q2DNK.js → chunk-VYQVDL2M.js} +5 -49
  58. package/dist/chunk-VYQVDL2M.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 +2587 -326
  62. package/dist/index.cjs.map +1 -1
  63. package/dist/index.d.cts +30 -3
  64. package/dist/index.d.ts +30 -3
  65. package/dist/index.js +2281 -22
  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-FGXZPKDC.cjs +13 -0
  70. package/dist/{migrations-UIMA6ZL6.cjs.map → migrations-FGXZPKDC.cjs.map} +1 -1
  71. package/dist/migrations-Z6D27NAW.js +4 -0
  72. package/dist/{migrations-HLK2QF4T.js.map → migrations-Z6D27NAW.js.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-CHWT5KBK.js.map +0 -1
  97. package/dist/chunk-DOMXI5PM.js.map +0 -1
  98. package/dist/chunk-EUJAEB6W.cjs.map +0 -1
  99. package/dist/chunk-FTXGZ3BF.cjs.map +0 -1
  100. package/dist/chunk-GB2Q2DNK.js.map +0 -1
  101. package/dist/chunk-I2DM27HP.js.map +0 -1
  102. package/dist/chunk-JFLZHOQP.cjs.map +0 -1
  103. package/dist/chunk-K623Q6WD.cjs.map +0 -1
  104. package/dist/chunk-MGHHPNJN.cjs.map +0 -1
  105. package/dist/chunk-PYZYU7EK.cjs.map +0 -1
  106. package/dist/chunk-QWSR4THH.cjs.map +0 -1
  107. package/dist/migrations-HLK2QF4T.js +0 -4
  108. 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, getDocumentRequestContext, updateDocumentSchema, effectiveTenantForType } from './chunk-VC5XVPLM.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-VC5XVPLM.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-FHPEM77Y.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-FHPEM77Y.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-RJ2O5IIE.js';
17
+ export { PluginBootstrapService, PluginService as PluginServiceClass, getAvailableCollectionNames, loadCollectionConfig, loadCollectionConfigs, registerCollections, validateCollectionConfig } from './chunk-RJ2O5IIE.js';
18
+ export { MigrationService } from './chunk-UIHIXTIS.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-VYQVDL2M.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-VYQVDL2M.js';
26
+ import { definePlugin, setPluginMenu, setPluginDefinitions, resolvePluginMenuItems } from './chunk-VGEZNFWH.js';
27
+ export { applySchemaDefaults, definePlugin, getPluginMenu, isDefinedPlugin, parseConfigSchema, parseFormDataToSettings, renderSchemaFields, resetPluginMenu, resolvePluginMenuItems, setPluginMenu } from './chunk-VGEZNFWH.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-LNI3FYSI.js';
34
+ export { SONICJS_VERSION, getCoreVersion } from './chunk-LNI3FYSI.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" },
@@ -20619,10 +22051,837 @@ var site_settings_collection_default = {
20619
22051
  function createDb(d1) {
20620
22052
  return drizzle(d1, { schema: schema_exports });
20621
22053
  }
22054
+ var mcpTypeConfigSchema = z.object({
22055
+ read: z.boolean().default(true),
22056
+ write: z.boolean().default(true)
22057
+ });
22058
+ var mcpConfigSchema = z.object({
22059
+ /** Collections exposed over MCP. Omitted → every active collection. */
22060
+ expose: z.array(z.string()).optional(),
22061
+ /** Per-collection read/write overrides, keyed by collection name. */
22062
+ types: z.record(z.string(), mcpTypeConfigSchema).default({}),
22063
+ /** Field names stripped from every tool response + write schema. */
22064
+ redactFields: z.array(z.string()).default([]),
22065
+ /** Max documents returned by list_* tools and document resources. */
22066
+ listLimit: z.number().int().positive().max(200).default(50)
22067
+ });
22068
+ function resolveMcpConfig(raw, collections) {
22069
+ const cfg = mcpConfigSchema.parse(raw ?? {});
22070
+ const exposeSet = cfg.expose ? new Set(cfg.expose) : null;
22071
+ const types = collections.filter((c) => exposeSet ? exposeSet.has(c.name) : true).map((c) => {
22072
+ const override = cfg.types[c.name];
22073
+ return {
22074
+ typeId: c.name,
22075
+ displayName: c.displayName ?? c.name,
22076
+ read: override?.read ?? true,
22077
+ write: override?.write ?? true
22078
+ };
22079
+ });
22080
+ return { types, redactFields: cfg.redactFields, listLimit: cfg.listLimit };
22081
+ }
22082
+
22083
+ // src/plugins/core-plugins/mcp-plugin/schema/field-to-jsonschema.ts
22084
+ var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
22085
+ "id",
22086
+ "root_id",
22087
+ "rootId",
22088
+ "created_at",
22089
+ "createdAt",
22090
+ "updated_at",
22091
+ "updatedAt",
22092
+ "version_number",
22093
+ "versionNumber"
22094
+ ]);
22095
+ function fieldToJsonSchema(field) {
22096
+ const base = {};
22097
+ if (field.title) base.title = field.title;
22098
+ if (field.description) base.description = field.description;
22099
+ switch (field.type) {
22100
+ case "number":
22101
+ return {
22102
+ ...base,
22103
+ type: "number",
22104
+ ...field.min != null ? { minimum: field.min } : {},
22105
+ ...field.max != null ? { maximum: field.max } : {}
22106
+ };
22107
+ case "boolean":
22108
+ case "checkbox":
22109
+ return { ...base, type: "boolean" };
22110
+ case "date":
22111
+ case "datetime":
22112
+ return { ...base, type: "string", format: "date-time" };
22113
+ case "select":
22114
+ case "radio":
22115
+ return { ...base, type: "string", ...field.enum ? { enum: field.enum } : {} };
22116
+ case "multiselect":
22117
+ return {
22118
+ ...base,
22119
+ type: "array",
22120
+ items: { type: "string", ...field.enum ? { enum: field.enum } : {} }
22121
+ };
22122
+ case "array":
22123
+ return {
22124
+ ...base,
22125
+ type: "array",
22126
+ items: field.items ? fieldToJsonSchema(field.items) : {}
22127
+ };
22128
+ case "object":
22129
+ return {
22130
+ ...base,
22131
+ type: "object",
22132
+ ...field.properties ? { properties: mapProperties(field.properties) } : {}
22133
+ };
22134
+ case "json":
22135
+ return { ...base };
22136
+ // string-like: string, slug, email, url, richtext, markdown, textarea, editors,
22137
+ // reference, media, color, file, user
22138
+ default: {
22139
+ const node = { ...base, type: "string" };
22140
+ if (field.minLength != null) node.minLength = field.minLength;
22141
+ if (field.maxLength != null) node.maxLength = field.maxLength;
22142
+ if (field.pattern) node.pattern = field.pattern;
22143
+ if (field.format) node.format = field.format;
22144
+ return node;
22145
+ }
22146
+ }
22147
+ }
22148
+ function mapProperties(props) {
22149
+ const out = {};
22150
+ for (const [name, field] of Object.entries(props)) {
22151
+ out[name] = fieldToJsonSchema(field);
22152
+ }
22153
+ return out;
22154
+ }
22155
+ function collectionToJsonSchema(schema, opts = {}) {
22156
+ const properties = {};
22157
+ const required = [];
22158
+ const redact2 = new Set(opts.redactFields ?? []);
22159
+ const schemaRequired = new Set(schema.required ?? []);
22160
+ for (const [name, field] of Object.entries(schema.properties ?? {})) {
22161
+ if (redact2.has(name)) continue;
22162
+ if (opts.forWrite && SYSTEM_FIELDS.has(name)) continue;
22163
+ properties[name] = fieldToJsonSchema(field);
22164
+ if (field.required || schemaRequired.has(name)) required.push(name);
22165
+ }
22166
+ return { type: "object", properties, ...required.length ? { required } : {} };
22167
+ }
22168
+
22169
+ // src/plugins/core-plugins/mcp-plugin/tools/registry.ts
22170
+ var PHASE_FLAGS = { includeWrite: true, includeSearch: false };
22171
+ var EMPTY_OBJECT_SCHEMA = { type: "object", properties: {} };
22172
+ function buildToolRegistry(cfg, collections, opts = {}) {
22173
+ const tools = [];
22174
+ tools.push({
22175
+ name: "list_collections",
22176
+ description: "List every content collection exposed over MCP, with its read/write access.",
22177
+ inputSchema: EMPTY_OBJECT_SCHEMA,
22178
+ op: "list_collections"
22179
+ });
22180
+ if (opts.includeSearch) {
22181
+ tools.push({
22182
+ name: "search_content",
22183
+ description: "Search content across exposed collections by title or slug.",
22184
+ inputSchema: {
22185
+ type: "object",
22186
+ properties: { query: { type: "string", description: "Text matched against document title and slug." } },
22187
+ required: ["query"]
22188
+ },
22189
+ op: "search_content"
22190
+ });
22191
+ }
22192
+ for (const t of cfg.types) {
22193
+ const col = collections.get(t.typeId);
22194
+ const writeSchema = col ? collectionToJsonSchema(col.schema, { forWrite: true, redactFields: cfg.redactFields }) : EMPTY_OBJECT_SCHEMA;
22195
+ if (t.read) {
22196
+ tools.push({
22197
+ name: `list_${t.typeId}`,
22198
+ description: `List ${t.displayName} documents.`,
22199
+ inputSchema: {
22200
+ type: "object",
22201
+ properties: {
22202
+ status: {
22203
+ type: "string",
22204
+ enum: ["published", "draft"],
22205
+ description: "Lifecycle to list. Defaults to published."
22206
+ },
22207
+ limit: { type: "number", description: `Max documents to return (capped at ${cfg.listLimit}).` }
22208
+ }
22209
+ },
22210
+ op: "list",
22211
+ typeId: t.typeId
22212
+ });
22213
+ tools.push({
22214
+ name: `get_${t.typeId}`,
22215
+ description: `Get one ${t.displayName} document by id or slug.`,
22216
+ inputSchema: {
22217
+ type: "object",
22218
+ properties: {
22219
+ id: { type: "string", description: "Document id or root id." },
22220
+ slug: { type: "string", description: "Document slug (alternative to id)." }
22221
+ }
22222
+ },
22223
+ op: "get",
22224
+ typeId: t.typeId
22225
+ });
22226
+ }
22227
+ if (opts.includeWrite && t.write) {
22228
+ tools.push({
22229
+ name: `create_${t.typeId}`,
22230
+ description: `Create a ${t.displayName} document.`,
22231
+ inputSchema: {
22232
+ type: "object",
22233
+ properties: {
22234
+ title: { type: "string" },
22235
+ slug: { type: "string" },
22236
+ data: writeSchema,
22237
+ publish: { type: "boolean", description: "Publish immediately. Defaults to false (draft)." }
22238
+ }
22239
+ },
22240
+ op: "create",
22241
+ typeId: t.typeId
22242
+ });
22243
+ tools.push({
22244
+ name: `update_${t.typeId}`,
22245
+ description: `Update a ${t.displayName} document (saves a new draft).`,
22246
+ inputSchema: {
22247
+ type: "object",
22248
+ properties: {
22249
+ id: { type: "string", description: "Root id of the document to update." },
22250
+ title: { type: "string" },
22251
+ slug: { type: "string" },
22252
+ data: writeSchema
22253
+ },
22254
+ required: ["id"]
22255
+ },
22256
+ op: "update",
22257
+ typeId: t.typeId
22258
+ });
22259
+ tools.push({
22260
+ name: `publish_${t.typeId}`,
22261
+ description: `Publish a ${t.displayName} document.`,
22262
+ inputSchema: {
22263
+ type: "object",
22264
+ properties: { id: { type: "string", description: "Root id of the document to publish." } },
22265
+ required: ["id"]
22266
+ },
22267
+ op: "publish",
22268
+ typeId: t.typeId
22269
+ });
22270
+ tools.push({
22271
+ name: `delete_${t.typeId}`,
22272
+ description: `Delete a ${t.displayName} document.`,
22273
+ inputSchema: {
22274
+ type: "object",
22275
+ properties: { id: { type: "string", description: "Root id of the document to delete." } },
22276
+ required: ["id"]
22277
+ },
22278
+ op: "delete",
22279
+ typeId: t.typeId
22280
+ });
22281
+ }
22282
+ }
22283
+ return tools;
22284
+ }
22285
+
22286
+ // src/plugins/core-plugins/mcp-plugin/jsonrpc.ts
22287
+ var JSON_RPC = {
22288
+ PARSE_ERROR: -32700,
22289
+ INVALID_REQUEST: -32600,
22290
+ METHOD_NOT_FOUND: -32601,
22291
+ INVALID_PARAMS: -32602,
22292
+ INTERNAL_ERROR: -32603,
22293
+ /** Custom: no valid API key on the request. */
22294
+ UNAUTHORIZED: -32001
22295
+ };
22296
+ var McpToolError = class extends Error {
22297
+ constructor(message) {
22298
+ super(message);
22299
+ this.name = "McpToolError";
22300
+ }
22301
+ };
22302
+ function rpcResult(id, result) {
22303
+ return { jsonrpc: "2.0", id: id ?? null, result };
22304
+ }
22305
+ function rpcError(id, code, message, data) {
22306
+ return { jsonrpc: "2.0", id: id ?? null, error: { code, message, ...{} } };
22307
+ }
22308
+ function parseJsonRpc(body) {
22309
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
22310
+ return { error: { code: JSON_RPC.INVALID_REQUEST, message: "Request must be a JSON object" } };
22311
+ }
22312
+ const b = body;
22313
+ if (b.jsonrpc !== "2.0") {
22314
+ return { error: { code: JSON_RPC.INVALID_REQUEST, message: 'jsonrpc must be "2.0"' } };
22315
+ }
22316
+ if (typeof b.method !== "string" || b.method.length === 0) {
22317
+ return { error: { code: JSON_RPC.INVALID_REQUEST, message: "method must be a non-empty string" } };
22318
+ }
22319
+ const id = b.id === void 0 ? null : b.id;
22320
+ const params = b.params && typeof b.params === "object" && !Array.isArray(b.params) ? b.params : {};
22321
+ return { jsonrpc: "2.0", id, method: b.method, params };
22322
+ }
22323
+
22324
+ // src/plugins/core-plugins/mcp-plugin/tools/documents.ts
22325
+ function redact(data, fields) {
22326
+ if (!fields.length) return data;
22327
+ const out = { ...data };
22328
+ for (const f of fields) delete out[f];
22329
+ return out;
22330
+ }
22331
+ function shapeDocument(doc, redactFields) {
22332
+ return {
22333
+ id: doc.id,
22334
+ rootId: doc.rootId,
22335
+ typeId: doc.typeId,
22336
+ title: doc.title,
22337
+ slug: doc.slug,
22338
+ status: doc.status,
22339
+ isPublished: doc.isPublished,
22340
+ publishedAt: doc.publishedAt,
22341
+ updatedAt: doc.updatedAt,
22342
+ data: redact(doc.data, redactFields)
22343
+ };
22344
+ }
22345
+ async function resolveType(ctx, typeId) {
22346
+ const docType = await new DocumentTypeRegistry(ctx.db).findById(typeId);
22347
+ if (!docType || !docType.isActive) throw new McpToolError(`Unknown document type: ${typeId}`);
22348
+ const effTenant = effectiveTenantForType(ctx.tenantId, docType.settings);
22349
+ const repo = new DocumentRepository(ctx.db, effTenant);
22350
+ return { docType, repo };
22351
+ }
22352
+ async function execList(ctx, typeId, args) {
22353
+ const { docType, repo } = await resolveType(ctx, typeId);
22354
+ const status = args.status === "draft" ? "draft" : "published";
22355
+ const limit = Math.min(Math.max(1, args.limit ?? ctx.listLimit), ctx.listLimit);
22356
+ const now = Math.floor(Date.now() / 1e3);
22357
+ const docs = await repo.list({
22358
+ typeId,
22359
+ status,
22360
+ timeWindow: status === "published",
22361
+ now,
22362
+ limit
22363
+ });
22364
+ const allowed = await Promise.all(
22365
+ docs.map((d) => repo.isAllowed(ctx.principalSet, d.rootId, "read", docType.settings))
22366
+ );
22367
+ return docs.filter((_, i) => allowed[i]).map((d) => shapeDocument(d, ctx.redactFields));
22368
+ }
22369
+ async function execGet(ctx, typeId, args) {
22370
+ const { docType, repo } = await resolveType(ctx, typeId);
22371
+ let doc = null;
22372
+ if (args.id) {
22373
+ doc = await repo.getById(args.id) ?? await repo.getPublished(args.id) ?? await repo.getCurrentDraft(args.id);
22374
+ } else if (args.slug) {
22375
+ doc = await repo.getBySlug(typeId, args.slug);
22376
+ } else {
22377
+ throw new McpToolError('get requires an "id" or "slug" argument');
22378
+ }
22379
+ if (!doc || doc.typeId !== typeId) throw new McpToolError("Document not found");
22380
+ const ok = await repo.isAllowed(ctx.principalSet, doc.rootId, "read", docType.settings);
22381
+ if (!ok) throw new McpToolError("Document not found");
22382
+ return shapeDocument(doc, ctx.redactFields);
22383
+ }
22384
+
22385
+ // src/plugins/core-plugins/mcp-plugin/tools/mutations.ts
22386
+ async function resolveWriteScope(ctx, typeId) {
22387
+ const docType = await new DocumentTypeRegistry(ctx.db).findById(typeId);
22388
+ if (!docType || !docType.isActive) throw new McpToolError(`Unknown document type: ${typeId}`);
22389
+ const effTenant = effectiveTenantForType(ctx.tenantId, docType.settings);
22390
+ const repo = new DocumentRepository(ctx.db, effTenant);
22391
+ const svc = new DocumentsService(ctx.db, {
22392
+ queryableFields: docType.queryableFields ?? [],
22393
+ typeSchemaVersion: docType.schemaVersion,
22394
+ maxVersionsPerRoot: docType.settings?.maxVersionsPerRoot,
22395
+ tenantId: effTenant,
22396
+ versioning: docType.settings?.versioning ?? false
22397
+ });
22398
+ return { docType, effTenant, repo, svc };
22399
+ }
22400
+ async function assertAllowed(scope, ctx, rootId, permission) {
22401
+ const ok = await scope.repo.isAllowed(ctx.principalSet, rootId, permission, scope.docType.settings ?? {});
22402
+ if (!ok) throw new McpToolError(`Permission denied: cannot ${permission} ${scope.docType.id}`);
22403
+ }
22404
+ function applyUserFieldDefaults(data, typeId, userId) {
22405
+ const col = getCollectionRegistry().getByName(typeId);
22406
+ if (!col?.schema?.properties) return data;
22407
+ const props = col.schema.properties;
22408
+ const patched = { ...data };
22409
+ for (const [name, field] of Object.entries(props)) {
22410
+ if (field.type === "user" && !patched[name]) patched[name] = userId;
22411
+ }
22412
+ return patched;
22413
+ }
22414
+ async function execCreate(ctx, typeId, args) {
22415
+ const scope = await resolveWriteScope(ctx, typeId);
22416
+ await assertAllowed(scope, ctx, "", "create");
22417
+ const data = redact(applyUserFieldDefaults(args.data ?? {}, typeId, ctx.userId), ctx.redactFields);
22418
+ const input = createDocumentSchema.parse({
22419
+ typeId,
22420
+ tenantId: scope.effTenant,
22421
+ title: args.title ?? null,
22422
+ slug: args.slug ?? null,
22423
+ data,
22424
+ ownerId: ctx.userId,
22425
+ publishOnCreate: Boolean(args.publish)
22426
+ });
22427
+ const doc = await scope.svc.create(input, ctx.userId);
22428
+ return shapeDocument(doc, ctx.redactFields);
22429
+ }
22430
+ async function execUpdate(ctx, typeId, args) {
22431
+ if (!args.id) throw new McpToolError('update requires an "id" (root id)');
22432
+ const scope = await resolveWriteScope(ctx, typeId);
22433
+ const current = await scope.repo.getCurrentDraft(args.id);
22434
+ if (!current || current.typeId !== typeId) throw new McpToolError("Document not found");
22435
+ await assertAllowed(scope, ctx, args.id, "update");
22436
+ const patch = updateDocumentSchema.parse({
22437
+ ...args.title !== void 0 ? { title: args.title } : {},
22438
+ ...args.slug !== void 0 ? { slug: args.slug } : {},
22439
+ ...args.data !== void 0 ? { data: redact(args.data, ctx.redactFields) } : {}
22440
+ });
22441
+ const doc = await scope.svc.saveDraft(args.id, patch, ctx.userId);
22442
+ return shapeDocument(doc, ctx.redactFields);
22443
+ }
22444
+ async function execPublish(ctx, typeId, args) {
22445
+ if (!args.id) throw new McpToolError('publish requires an "id" (root id)');
22446
+ const scope = await resolveWriteScope(ctx, typeId);
22447
+ const current = await scope.repo.getCurrentDraft(args.id);
22448
+ if (!current || current.typeId !== typeId) throw new McpToolError("Document not found");
22449
+ await assertAllowed(scope, ctx, args.id, "publish");
22450
+ const doc = await scope.svc.publish(current.id, ctx.userId);
22451
+ return shapeDocument(doc, ctx.redactFields);
22452
+ }
22453
+ async function execDelete(ctx, typeId, args) {
22454
+ if (!args.id) throw new McpToolError('delete requires an "id" (root id)');
22455
+ const scope = await resolveWriteScope(ctx, typeId);
22456
+ const current = await scope.repo.getCurrentDraft(args.id) ?? await scope.repo.getPublished(args.id);
22457
+ if (!current || current.typeId !== typeId) throw new McpToolError("Document not found");
22458
+ await assertAllowed(scope, ctx, args.id, "delete");
22459
+ if (scope.docType.settings?.pii) {
22460
+ await scope.svc.erase(args.id, scope.effTenant);
22461
+ return { rootId: args.id, deleted: true, erased: true };
22462
+ }
22463
+ const ids = /* @__PURE__ */ new Set();
22464
+ ids.add(current.id);
22465
+ if (current.isCurrentDraft) {
22466
+ const published = await scope.repo.getPublished(args.id);
22467
+ if (published) ids.add(published.id);
22468
+ } else {
22469
+ const draft = await scope.repo.getCurrentDraft(args.id);
22470
+ if (draft) ids.add(draft.id);
22471
+ }
22472
+ for (const id of ids) await scope.svc.softDelete(id);
22473
+ return { rootId: args.id, deleted: true, erased: false };
22474
+ }
22475
+
22476
+ // src/plugins/core-plugins/mcp-plugin/tools/static.ts
22477
+ function execListCollections(types) {
22478
+ return types.map((t) => ({ typeId: t.typeId, displayName: t.displayName, read: t.read, write: t.write }));
22479
+ }
22480
+
22481
+ // src/plugins/core-plugins/mcp-plugin/resources/schemas.ts
22482
+ function buildResourceList(cfg) {
22483
+ const out = [
22484
+ { uri: "sonicjs://collections", name: "All collections", mimeType: "application/json" }
22485
+ ];
22486
+ for (const t of cfg.types) {
22487
+ out.push({
22488
+ uri: `sonicjs://collections/${t.typeId}/schema`,
22489
+ name: `${t.displayName} schema`,
22490
+ mimeType: "application/json"
22491
+ });
22492
+ }
22493
+ return out;
22494
+ }
22495
+ var SCHEMA_URI = /^sonicjs:\/\/collections\/([^/]+)\/schema$/;
22496
+ function readResource(uri, cfg, collections) {
22497
+ if (uri === "sonicjs://collections") {
22498
+ const list = cfg.types.map((t) => ({
22499
+ typeId: t.typeId,
22500
+ displayName: t.displayName,
22501
+ read: t.read,
22502
+ write: t.write
22503
+ }));
22504
+ return { uri, mimeType: "application/json", text: JSON.stringify(list, null, 2) };
22505
+ }
22506
+ const m = uri.match(SCHEMA_URI);
22507
+ if (m) {
22508
+ const typeId = m[1];
22509
+ const exposed = cfg.types.find((t) => t.typeId === typeId);
22510
+ const col = collections.get(typeId);
22511
+ if (!exposed || !col) throw new McpToolError(`Unknown collection: ${typeId}`);
22512
+ const schema = collectionToJsonSchema(col.schema, { redactFields: cfg.redactFields });
22513
+ return { uri, mimeType: "application/json", text: JSON.stringify(schema, null, 2) };
22514
+ }
22515
+ throw new McpToolError(`Unknown resource URI: ${uri}`);
22516
+ }
22517
+
22518
+ // src/plugins/core-plugins/mcp-plugin/routes/mcp.ts
22519
+ var SERVER_INFO = { name: "sonicjs-mcp", version: "1.0.0" };
22520
+ var PROTOCOL_VERSION = "2024-11-05";
22521
+ function createMcpRoutes(options = {}) {
22522
+ const app2 = new Hono();
22523
+ app2.post("/", async (c) => {
22524
+ let body;
22525
+ try {
22526
+ body = await c.req.json();
22527
+ } catch {
22528
+ return c.json(rpcError(null, JSON_RPC.PARSE_ERROR, "Invalid JSON"));
22529
+ }
22530
+ const parsed = parseJsonRpc(body);
22531
+ if ("error" in parsed) {
22532
+ const id2 = body && typeof body === "object" ? body.id ?? null : null;
22533
+ return c.json(rpcError(id2, parsed.error.code, parsed.error.message));
22534
+ }
22535
+ const { id, method, params } = parsed;
22536
+ const user = c.get("user");
22537
+ if (!user) {
22538
+ return c.json(
22539
+ rpcError(id, JSON_RPC.UNAUTHORIZED, "Unauthorized: provide a valid API key via Authorization: Bearer sk_...")
22540
+ );
22541
+ }
22542
+ const { tenantId, principalSet, userId } = getDocumentRequestContext(c);
22543
+ const registry = getCollectionRegistry();
22544
+ const active = registry.listActive();
22545
+ const collections = new Map(active.map((col) => [col.name, col]));
22546
+ const cfg = resolveMcpConfig(options, active);
22547
+ const tools = buildToolRegistry(cfg, collections, PHASE_FLAGS);
22548
+ try {
22549
+ switch (method) {
22550
+ case "initialize":
22551
+ return c.json(
22552
+ rpcResult(id, {
22553
+ protocolVersion: PROTOCOL_VERSION,
22554
+ serverInfo: SERVER_INFO,
22555
+ capabilities: { tools: {}, resources: {} }
22556
+ })
22557
+ );
22558
+ case "tools/list":
22559
+ return c.json(
22560
+ rpcResult(id, {
22561
+ tools: tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }))
22562
+ })
22563
+ );
22564
+ case "tools/call": {
22565
+ const name = typeof params.name === "string" ? params.name : "";
22566
+ const args = params.arguments && typeof params.arguments === "object" ? params.arguments : {};
22567
+ const tool = tools.find((t) => t.name === name);
22568
+ if (!tool) return c.json(rpcError(id, JSON_RPC.METHOD_NOT_FOUND, `Unknown tool: ${name}`));
22569
+ const readCtx = {
22570
+ db: c.env.DB,
22571
+ tenantId,
22572
+ principalSet,
22573
+ listLimit: cfg.listLimit,
22574
+ redactFields: cfg.redactFields
22575
+ };
22576
+ const writeCtx = {
22577
+ db: c.env.DB,
22578
+ tenantId,
22579
+ principalSet,
22580
+ userId: userId ?? "",
22581
+ redactFields: cfg.redactFields
22582
+ };
22583
+ try {
22584
+ let result;
22585
+ switch (tool.op) {
22586
+ case "list_collections":
22587
+ result = execListCollections(cfg.types);
22588
+ break;
22589
+ case "list":
22590
+ result = await execList(readCtx, tool.typeId, args);
22591
+ break;
22592
+ case "get":
22593
+ result = await execGet(readCtx, tool.typeId, args);
22594
+ break;
22595
+ case "create":
22596
+ result = await execCreate(writeCtx, tool.typeId, args);
22597
+ break;
22598
+ case "update":
22599
+ result = await execUpdate(writeCtx, tool.typeId, args);
22600
+ break;
22601
+ case "publish":
22602
+ result = await execPublish(writeCtx, tool.typeId, args);
22603
+ break;
22604
+ case "delete":
22605
+ result = await execDelete(writeCtx, tool.typeId, args);
22606
+ break;
22607
+ default:
22608
+ return c.json(rpcError(id, JSON_RPC.METHOD_NOT_FOUND, `Tool not available: ${name}`));
22609
+ }
22610
+ return c.json(rpcResult(id, { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }));
22611
+ } catch (e) {
22612
+ if (e instanceof McpToolError) {
22613
+ return c.json(rpcResult(id, { content: [{ type: "text", text: e.message }], isError: true }));
22614
+ }
22615
+ throw e;
22616
+ }
22617
+ }
22618
+ case "resources/list":
22619
+ return c.json(rpcResult(id, { resources: buildResourceList(cfg) }));
22620
+ case "resources/read": {
22621
+ const uri = typeof params.uri === "string" ? params.uri : "";
22622
+ if (!uri) return c.json(rpcError(id, JSON_RPC.INVALID_PARAMS, 'resources/read requires a "uri" param'));
22623
+ try {
22624
+ const contents = readResource(uri, cfg, collections);
22625
+ return c.json(rpcResult(id, { contents: [contents] }));
22626
+ } catch (e) {
22627
+ if (e instanceof McpToolError) return c.json(rpcError(id, JSON_RPC.INVALID_PARAMS, e.message));
22628
+ throw e;
22629
+ }
22630
+ }
22631
+ default:
22632
+ return c.json(rpcError(id, JSON_RPC.METHOD_NOT_FOUND, `Method not found: ${method}`));
22633
+ }
22634
+ } catch (e) {
22635
+ console.error("[mcp] handler error:", e);
22636
+ return c.json(rpcError(id, JSON_RPC.INTERNAL_ERROR, "Internal error"));
22637
+ }
22638
+ });
22639
+ return app2;
22640
+ }
22641
+
22642
+ // src/plugins/core-plugins/mcp-plugin/admin/templates.ts
22643
+ init_admin_layout_catalyst_template();
22644
+ function accessBadge(ok) {
22645
+ return ok ? `<span class="inline-flex items-center rounded-full bg-emerald-50 dark:bg-emerald-900/20 px-2 py-0.5 text-xs font-medium text-emerald-700 dark:text-emerald-400 ring-1 ring-emerald-600/20 dark:ring-emerald-400/20">Yes</span>` : `<span class="inline-flex items-center rounded-full bg-zinc-100 dark:bg-zinc-800 px-2 py-0.5 text-xs font-medium text-zinc-500 dark:text-zinc-400 ring-1 ring-zinc-200 dark:ring-zinc-700">No</span>`;
22646
+ }
22647
+ function toolNamesForType(typeId, tools) {
22648
+ const names = tools.filter((t) => t.typeId === typeId).map((t) => `<code class="text-xs font-mono text-zinc-500 dark:text-zinc-400">${escapeHtml(t.name)}</code>`);
22649
+ return names.length ? names.join('<span class="text-zinc-300 dark:text-zinc-600 mx-1">,</span>') : "\u2014";
22650
+ }
22651
+ function buildClaudeJson(endpointUrl) {
22652
+ return JSON.stringify(
22653
+ {
22654
+ mcpServers: {
22655
+ sonicjs: {
22656
+ type: "http",
22657
+ url: endpointUrl,
22658
+ headers: { Authorization: "Bearer sk_YOUR_API_KEY" }
22659
+ }
22660
+ }
22661
+ },
22662
+ null,
22663
+ 2
22664
+ );
22665
+ }
22666
+ function buildCursorJson(endpointUrl) {
22667
+ return JSON.stringify(
22668
+ {
22669
+ mcpServers: {
22670
+ sonicjs: {
22671
+ url: endpointUrl,
22672
+ headers: { Authorization: "Bearer sk_YOUR_API_KEY" }
22673
+ }
22674
+ }
22675
+ },
22676
+ null,
22677
+ 2
22678
+ );
22679
+ }
22680
+ function renderMcpDashboardPage(data) {
22681
+ const { endpointUrl, types, tools, user, version, dynamicMenuItems } = data;
22682
+ const safeEndpoint = escapeHtml(endpointUrl);
22683
+ const typeCount = types.length;
22684
+ const toolCount = tools.length;
22685
+ const typeRows = types.length ? types.map(
22686
+ (t) => `
22687
+ <tr class="border-t border-zinc-950/5 dark:border-white/10">
22688
+ <td class="py-3 pr-4 text-sm font-mono text-zinc-950 dark:text-white">${escapeHtml(t.typeId)}</td>
22689
+ <td class="py-3 pr-4 text-sm text-zinc-500 dark:text-zinc-400">${escapeHtml(t.displayName)}</td>
22690
+ <td class="py-3 pr-4">${accessBadge(t.read)}</td>
22691
+ <td class="py-3 pr-4">${accessBadge(t.write)}</td>
22692
+ <td class="py-3 text-sm">${toolNamesForType(t.typeId, tools)}</td>
22693
+ </tr>`
22694
+ ).join("") : `<tr><td colspan="5" class="py-8 text-center text-sm text-zinc-500 dark:text-zinc-400">No collections exposed. Register collections via <code class="font-mono">registerCollections()</code> in your app entry point.</td></tr>`;
22695
+ const claudeJson = escapeHtml(buildClaudeJson(endpointUrl));
22696
+ const cursorJson = escapeHtml(buildCursorJson(endpointUrl));
22697
+ const content2 = `
22698
+ <div class="space-y-8">
22699
+
22700
+ <!-- Header -->
22701
+ <div class="sm:flex sm:items-center sm:justify-between">
22702
+ <div class="sm:flex-auto">
22703
+ <h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">MCP Server</h1>
22704
+ <p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
22705
+ Model Context Protocol endpoint for AI agents (Claude Code, Cursor, VS Code). Requests are authenticated with API keys.
22706
+ </p>
22707
+ </div>
22708
+ <div class="mt-4 sm:mt-0 sm:ml-16">
22709
+ <a href="/admin/plugins/api-keys"
22710
+ class="inline-flex items-center justify-center rounded-lg bg-cyan-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-cyan-500 transition-colors shadow-sm">
22711
+ Mint API Key
22712
+ </a>
22713
+ </div>
22714
+ </div>
22715
+
22716
+ <!-- Status -->
22717
+ <div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-6 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm">
22718
+ <h2 class="text-sm font-semibold text-zinc-950 dark:text-white mb-4">Endpoint</h2>
22719
+ <div class="flex items-center gap-3">
22720
+ <code id="endpoint-url" class="flex-1 rounded-lg bg-zinc-50 dark:bg-zinc-800 px-3 py-2 text-sm font-mono text-zinc-950 dark:text-white ring-1 ring-zinc-950/5 dark:ring-white/5 break-all">${safeEndpoint}</code>
22721
+ <button type="button" onclick="copyEndpoint()"
22722
+ class="shrink-0 rounded-lg bg-zinc-100 dark:bg-zinc-800 px-3 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700 ring-1 ring-zinc-950/5 dark:ring-white/10 transition-colors">
22723
+ Copy
22724
+ </button>
22725
+ </div>
22726
+ <div class="mt-4 flex flex-wrap gap-4 text-sm text-zinc-500 dark:text-zinc-400">
22727
+ <span>
22728
+ <span class="font-semibold text-zinc-950 dark:text-white">${typeCount}</span>
22729
+ collection${typeCount === 1 ? "" : "s"} exposed
22730
+ </span>
22731
+ <span class="text-zinc-300 dark:text-zinc-600">&middot;</span>
22732
+ <span>
22733
+ <span class="font-semibold text-zinc-950 dark:text-white">${toolCount}</span>
22734
+ tool${toolCount === 1 ? "" : "s"} available
22735
+ </span>
22736
+ <span class="text-zinc-300 dark:text-zinc-600">&middot;</span>
22737
+ <span>Auth via <a href="/admin/plugins/api-keys" class="text-cyan-600 dark:text-cyan-400 hover:underline">API Keys</a></span>
22738
+ </div>
22739
+ </div>
22740
+
22741
+ <!-- Exposed collections table -->
22742
+ <div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-6 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm overflow-x-auto">
22743
+ <h2 class="text-sm font-semibold text-zinc-950 dark:text-white mb-4">Exposed collections</h2>
22744
+ <table class="w-full text-left">
22745
+ <thead>
22746
+ <tr class="text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
22747
+ <th class="pb-2 pr-4">ID</th>
22748
+ <th class="pb-2 pr-4">Display name</th>
22749
+ <th class="pb-2 pr-4">Read</th>
22750
+ <th class="pb-2 pr-4">Write</th>
22751
+ <th class="pb-2">Tools</th>
22752
+ </tr>
22753
+ </thead>
22754
+ <tbody>${typeRows}</tbody>
22755
+ </table>
22756
+ </div>
22757
+
22758
+ <!-- Integration guide -->
22759
+ <div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-6 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm space-y-6">
22760
+ <div>
22761
+ <h2 class="text-sm font-semibold text-zinc-950 dark:text-white mb-1">Integration guide</h2>
22762
+ <p class="text-sm text-zinc-500 dark:text-zinc-400">
22763
+ <a href="/admin/plugins/api-keys" class="text-cyan-600 dark:text-cyan-400 hover:underline">Mint an API key</a>,
22764
+ then paste one of the configs below into your MCP client.
22765
+ Replace <code class="font-mono text-xs">sk_YOUR_API_KEY</code> with the key.
22766
+ </p>
22767
+ </div>
22768
+
22769
+ <!-- Claude Code -->
22770
+ <div>
22771
+ <div class="flex items-center justify-between mb-2">
22772
+ <h3 class="text-sm font-medium text-zinc-700 dark:text-zinc-300">
22773
+ Claude Code &mdash; <code class="font-mono text-xs">~/.claude/mcp.json</code>
22774
+ </h3>
22775
+ <button type="button" onclick="copyConfig('claude-config')"
22776
+ class="text-xs text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors">
22777
+ Copy
22778
+ </button>
22779
+ </div>
22780
+ <pre id="claude-config" class="rounded-lg bg-zinc-950 dark:bg-zinc-800 px-4 py-3 text-sm font-mono text-zinc-100 overflow-x-auto ring-1 ring-zinc-700 dark:ring-zinc-600 whitespace-pre">${claudeJson}</pre>
22781
+ </div>
22782
+
22783
+ <!-- Cursor -->
22784
+ <div>
22785
+ <div class="flex items-center justify-between mb-2">
22786
+ <h3 class="text-sm font-medium text-zinc-700 dark:text-zinc-300">
22787
+ Cursor &mdash; <code class="font-mono text-xs">~/.cursor/mcp.json</code>
22788
+ </h3>
22789
+ <button type="button" onclick="copyConfig('cursor-config')"
22790
+ class="text-xs text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors">
22791
+ Copy
22792
+ </button>
22793
+ </div>
22794
+ <pre id="cursor-config" class="rounded-lg bg-zinc-950 dark:bg-zinc-800 px-4 py-3 text-sm font-mono text-zinc-100 overflow-x-auto ring-1 ring-zinc-700 dark:ring-zinc-600 whitespace-pre">${cursorJson}</pre>
22795
+ </div>
22796
+
22797
+ <p class="text-xs text-zinc-400 dark:text-zinc-500">
22798
+ MCP v1 = HTTP POST only (no SSE stream). Clients that require stdio transport can use
22799
+ <code class="font-mono">npx mcp-remote ${safeEndpoint}</code> as a local bridge.
22800
+ </p>
22801
+ </div>
22802
+
22803
+ </div>
22804
+
22805
+ <script>
22806
+ function copyEndpoint() {
22807
+ navigator.clipboard.writeText(document.getElementById('endpoint-url').textContent.trim()).then(() => {
22808
+ window.showNotification && window.showNotification('Endpoint URL copied', 'success')
22809
+ })
22810
+ }
22811
+ function copyConfig(id) {
22812
+ navigator.clipboard.writeText(document.getElementById(id).textContent).then(() => {
22813
+ window.showNotification && window.showNotification('Config copied', 'success')
22814
+ })
22815
+ }
22816
+ </script>`;
22817
+ const layoutData = {
22818
+ title: "MCP Server",
22819
+ pageTitle: "MCP Server",
22820
+ currentPath: "/admin/mcp",
22821
+ user,
22822
+ content: content2,
22823
+ version,
22824
+ dynamicMenuItems
22825
+ };
22826
+ return renderAdminLayoutCatalyst(layoutData);
22827
+ }
22828
+
22829
+ // src/plugins/core-plugins/mcp-plugin/admin/routes.ts
22830
+ function createMcpAdminRoutes(options = {}) {
22831
+ const routes2 = new Hono();
22832
+ routes2.use("*", requireAuth());
22833
+ routes2.use("*", async (c, next) => {
22834
+ const user = c.get("user");
22835
+ if (user?.role !== "admin") return c.json({ error: "Access denied" }, 403);
22836
+ return next();
22837
+ });
22838
+ routes2.get("/", async (c) => {
22839
+ const url = new URL(c.req.url);
22840
+ const endpointUrl = `${url.protocol}//${url.host}/mcp`;
22841
+ const registry = getCollectionRegistry();
22842
+ const active = registry.listActive();
22843
+ const collections = new Map(active.map((col) => [col.name, col]));
22844
+ const cfg = resolveMcpConfig(options, active);
22845
+ const tools = buildToolRegistry(cfg, collections, PHASE_FLAGS);
22846
+ const user = c.get("user");
22847
+ return c.html(
22848
+ renderMcpDashboardPage({
22849
+ endpointUrl,
22850
+ types: cfg.types,
22851
+ tools,
22852
+ user: { name: user.email, email: user.email, role: user.role },
22853
+ version: c.get("appVersion"),
22854
+ dynamicMenuItems: c.get("pluginMenuItems")
22855
+ })
22856
+ );
22857
+ });
22858
+ return routes2;
22859
+ }
22860
+
22861
+ // src/plugins/core-plugins/mcp-plugin/index.ts
22862
+ var MCP_ICON = `<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="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>`;
22863
+ function mcpPlugin(options = {}) {
22864
+ return definePlugin({
22865
+ id: "mcp",
22866
+ version: "1.0.0",
22867
+ name: "MCP Server",
22868
+ description: "Exposes SonicJS content as Model Context Protocol tools for AI agents (Claude Code, Cursor, \u2026).",
22869
+ sonicjsVersionRange: "^3.0.0",
22870
+ author: { name: "SonicJS Team" },
22871
+ register(app2) {
22872
+ app2.route("/mcp", createMcpRoutes(options));
22873
+ app2.route("/admin/mcp", createMcpAdminRoutes(options));
22874
+ },
22875
+ menu: [{ label: "MCP Server", path: "/admin/mcp", icon: MCP_ICON, order: 87 }]
22876
+ });
22877
+ }
22878
+ function createMcpPlugin(options) {
22879
+ return mcpPlugin(options);
22880
+ }
20622
22881
 
20623
22882
  // src/index.ts
20624
22883
  var VERSION = package_default.version;
20625
22884
 
20626
- export { BUILT_IN_PROVIDERS, CloudflareEmailProvider2 as CloudflareEmailProvider, OAuthService, VERSION, createDb, createHelloWorldPlugin, createOAuthProvidersPlugin, createRedirectPlugin, createSonicJSApp, emailReconciliationPlugin, generateTriggersComment, helloWorldPlugin, oauthProvidersPlugin, parseCronTriggers, redirectPlugin, setupCoreMiddleware, setupCoreRoutes, site_settings_collection_default as siteSettingsCollection, updateWranglerTriggers };
22885
+ export { BUILT_IN_PROVIDERS, CloudflareEmailProvider2 as CloudflareEmailProvider, OAuthService, VERSION, createDb, createHelloWorldPlugin, createMcpPlugin, createOAuthProvidersPlugin, createRedirectPlugin, createSonicJSApp, emailReconciliationPlugin, generateTriggersComment, helloWorldPlugin, mcpPlugin, oauthProvidersPlugin, parseCronTriggers, redirectPlugin, setupCoreMiddleware, setupCoreRoutes, site_settings_collection_default as siteSettingsCollection, updateWranglerTriggers };
20627
22886
  //# sourceMappingURL=index.js.map
20628
22887
  //# sourceMappingURL=index.js.map