domma-cms 0.25.15 → 0.25.16

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 (42) hide show
  1. package/admin/js/app.js +2 -2
  2. package/admin/js/templates/api-reference.html +68 -1233
  3. package/admin/js/templates/docs/api-actions.html +146 -0
  4. package/admin/js/templates/docs/api-authentication.html +202 -0
  5. package/admin/js/templates/docs/api-collections.html +347 -0
  6. package/admin/js/templates/docs/api-layouts.html +123 -0
  7. package/admin/js/templates/docs/api-media.html +158 -0
  8. package/admin/js/templates/docs/api-navigation.html +99 -0
  9. package/admin/js/templates/docs/api-pages.html +214 -0
  10. package/admin/js/templates/docs/api-plugins.html +136 -0
  11. package/admin/js/templates/docs/api-settings.html +148 -0
  12. package/admin/js/templates/docs/api-users.html +189 -0
  13. package/admin/js/templates/docs/api-views.html +142 -0
  14. package/admin/js/templates/docs/components-howto.html +175 -0
  15. package/admin/js/templates/docs/components-reference.html +259 -0
  16. package/admin/js/templates/docs/components-rules.html +167 -0
  17. package/admin/js/templates/docs/components-walkthrough.html +167 -0
  18. package/admin/js/templates/docs/tutorial-crud.html +344 -0
  19. package/admin/js/templates/docs/tutorial-forms.html +93 -0
  20. package/admin/js/templates/docs/tutorial-plugin.html +234 -0
  21. package/admin/js/templates/docs/usage-actions.html +122 -0
  22. package/admin/js/templates/docs/usage-cta-shortcode.html +124 -0
  23. package/admin/js/templates/docs/usage-dconfig.html +148 -0
  24. package/admin/js/templates/docs/usage-media.html +25 -0
  25. package/admin/js/templates/docs/usage-navigation.html +31 -0
  26. package/admin/js/templates/docs/usage-pages.html +68 -0
  27. package/admin/js/templates/docs/usage-plugins.html +36 -0
  28. package/admin/js/templates/docs/usage-shortcodes.html +810 -0
  29. package/admin/js/templates/docs/usage-site-settings.html +52 -0
  30. package/admin/js/templates/docs/usage-users-roles.html +84 -0
  31. package/admin/js/templates/docs/usage-views.html +105 -0
  32. package/admin/js/templates/documentation.html +60 -1522
  33. package/admin/js/templates/tutorials.html +46 -659
  34. package/admin/js/views/api-reference.js +1 -1
  35. package/admin/js/views/doc-pages.js +1 -0
  36. package/admin/js/views/documentation.js +1 -1
  37. package/admin/js/views/index.js +1 -1
  38. package/admin/js/views/tutorials.js +1 -1
  39. package/config/menus/admin-sidebar.json +76 -6
  40. package/package.json +1 -1
  41. package/server/server.js +3 -1
  42. package/server/services/sidebar-migration.js +145 -3
@@ -1 +1 @@
1
- export const apiReferenceView={templateUrl:"/admin/js/templates/api-reference.html",async onMount(e){E.tabs(e.find("#api-tabs").get(0)),Domma.icons.scan(),Domma.syntax.scan()}};
1
+ export const apiReferenceView={templateUrl:"/admin/js/templates/api-reference.html",async onMount(){Domma.icons.scan(),Domma.syntax.scan()}};
@@ -0,0 +1 @@
1
+ import{mountScaffolder as i}from"../lib/crud-tutorial.js";function t(o,n){return{templateUrl:`/admin/js/templates/docs/${o}.html`,async onMount(e){typeof n=="function"&&n(e),Domma.icons.scan(),Domma.syntax.scan()}}}export const docViews={tutorialCrud:t("tutorial-crud",o=>{const n=o.find("#tutorial-scaffolder-mount").get(0);n&&i(n,null,null)}),tutorialPlugin:t("tutorial-plugin"),tutorialForms:t("tutorial-forms"),componentsReference:t("components-reference"),componentsHowto:t("components-howto"),componentsWalkthrough:t("components-walkthrough"),componentsRules:t("components-rules")};const a=["pages","media","navigation","dconfig","shortcodes","site-settings","plugins","users-roles","views","actions","cta-shortcode"],c=["authentication","pages","settings","layouts","navigation","media","users","plugins","collections","views","actions"],s=o=>o.split("-").map(n=>n.charAt(0).toUpperCase()+n.slice(1)).join("");for(const o of a)docViews["usage"+s(o)]=t(`usage-${o}`);for(const o of c)docViews["apiRef"+s(o)]=t(`api-${o}`);
@@ -1 +1 @@
1
- export const documentationView={templateUrl:"/admin/js/templates/documentation.html",async onMount(t){E.tabs(t.find("#docs-tabs").get(0)),Domma.icons.scan(),Domma.syntax.scan()}};
1
+ export const documentationView={templateUrl:"/admin/js/templates/documentation.html",async onMount(){Domma.icons.scan(),Domma.syntax.scan()}};
@@ -1 +1 @@
1
- import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{navigationView as e}from"./navigation.js";import{notificationsView as m}from"./notifications.js";import{layoutsView as p}from"./layouts.js";import{mediaView as s}from"./media.js";import{loginView as n}from"./login.js";import{usersView as f}from"./users.js";import{userEditorView as w}from"./user-editor.js";import{pluginsView as c}from"./plugins.js";import{documentationView as V}from"./documentation.js";import{tutorialsView as a}from"./tutorials.js";import{apiReferenceView as d}from"./api-reference.js";import{collectionsView as l}from"./collections.js";import{collectionEditorView as E}from"./collection-editor.js";import{collectionEntriesView as u}from"./collection-entries.js";import{formsView as g}from"./forms.js";import{formEditorView as v}from"./form-editor.js";import{formSubmissionsView as b}from"./form-submissions.js";import{viewsListView as j}from"./views-list.js";import{viewEditorView as k}from"./view-editor.js";import{viewPreviewView as L}from"./view-preview.js";import{actionsListView as y}from"./actions-list.js";import{actionEditorView as h}from"./action-editor.js";import{proDocsView as D}from"./pro-docs.js";import{blocksView as P}from"./blocks.js";import{blockEditorView as R}from"./block-editor.js";import"./block-editor-enhance.js";import{componentsView as S}from"./components.js";import{componentEditorView as T}from"./component-editor.js";import{myProfileView as x}from"./my-profile.js";import{rolesView as M}from"./roles.js";import{roleEditorView as U}from"./role-editor.js";import{effectsView as q}from"./effects.js";import{menusView as z}from"./menus.js";import{menuEditorView as A}from"./menu-editor.js";import{menuLocationsView as B}from"./menu-locations.js";import{projectsView as C}from"./projects.js";import{projectEditorView as F}from"./project-editor.js";import{projectDetailView as G}from"./project-detail.js";import{projectSettingsView as H}from"./project-settings.js";import{apiTokensView as I}from"./api-tokens.js";import{apiEndpointsView as J}from"./api-endpoints.js";import{apiEndpointEditorView as K}from"./api-endpoint-editor.js";const N={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:C,projectEditor:F,projectDetail:G,projectSettings:H,apiTokens:I,apiEndpoints:J,apiEndpointEditor:K,dashboard:o,pages:i,pageEditor:r,settings:t,navigation:e,layouts:p,media:s,login:n,users:f,userEditor:w,plugins:c,documentation:V,tutorials:a,apiReference:d,collections:l,collectionEditor:E,collectionEntries:u,forms:g,formEditor:v,formSubmissions:b,viewsList:j,viewEditor:k,viewPreview:L,actionsList:y,actionEditor:h,proDocs:D,blocks:P,blockEditor:R,components:S,componentEditor:T,myProfile:x,roles:M,roleEditor:U,effects:q,notifications:m,menus:z,menuEditor:A,menuLocations:B,menusRedirect:N};
1
+ import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{navigationView as e}from"./navigation.js";import{notificationsView as m}from"./notifications.js";import{layoutsView as p}from"./layouts.js";import{mediaView as s}from"./media.js";import{loginView as n}from"./login.js";import{usersView as f}from"./users.js";import{userEditorView as w}from"./user-editor.js";import{pluginsView as c}from"./plugins.js";import{documentationView as V}from"./documentation.js";import{tutorialsView as a}from"./tutorials.js";import{docViews as d}from"./doc-pages.js";import{apiReferenceView as l}from"./api-reference.js";import{collectionsView as E}from"./collections.js";import{collectionEditorView as u}from"./collection-editor.js";import{collectionEntriesView as g}from"./collection-entries.js";import{formsView as v}from"./forms.js";import{formEditorView as b}from"./form-editor.js";import{formSubmissionsView as j}from"./form-submissions.js";import{viewsListView as k}from"./views-list.js";import{viewEditorView as L}from"./view-editor.js";import{viewPreviewView as y}from"./view-preview.js";import{actionsListView as h}from"./actions-list.js";import{actionEditorView as D}from"./action-editor.js";import{proDocsView as P}from"./pro-docs.js";import{blocksView as R}from"./blocks.js";import{blockEditorView as S}from"./block-editor.js";import"./block-editor-enhance.js";import{componentsView as T}from"./components.js";import{componentEditorView as x}from"./component-editor.js";import{myProfileView as M}from"./my-profile.js";import{rolesView as U}from"./roles.js";import{roleEditorView as q}from"./role-editor.js";import{effectsView as z}from"./effects.js";import{menusView as A}from"./menus.js";import{menuEditorView as B}from"./menu-editor.js";import{menuLocationsView as C}from"./menu-locations.js";import{projectsView as F}from"./projects.js";import{projectEditorView as G}from"./project-editor.js";import{projectDetailView as H}from"./project-detail.js";import{projectSettingsView as I}from"./project-settings.js";import{apiTokensView as J}from"./api-tokens.js";import{apiEndpointsView as K}from"./api-endpoints.js";import{apiEndpointEditorView as N}from"./api-endpoint-editor.js";const O={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:F,projectEditor:G,projectDetail:H,projectSettings:I,apiTokens:J,apiEndpoints:K,apiEndpointEditor:N,dashboard:o,pages:i,pageEditor:r,settings:t,navigation:e,layouts:p,media:s,login:n,users:f,userEditor:w,plugins:c,documentation:V,tutorials:a,apiReference:l,collections:E,collectionEditor:u,collectionEntries:g,forms:v,formEditor:b,formSubmissions:j,viewsList:k,viewEditor:L,viewPreview:y,actionsList:h,actionEditor:D,proDocs:P,blocks:R,blockEditor:S,components:T,componentEditor:x,myProfile:M,roles:U,roleEditor:q,effects:z,notifications:m,menus:A,menuEditor:B,menuLocations:C,menusRedirect:O,...d};
@@ -1 +1 @@
1
- import{mountScaffolder as a}from"../lib/crud-tutorial.js";export const tutorialsView={templateUrl:"/admin/js/templates/tutorials.html",async onMount(t){E.tabs(t.find("#tutorials-tabs").get(0));const o=t.find("#tutorial-scaffolder-mount").get(0);o&&a(o,null,null),Domma.icons.scan(),Domma.syntax.scan()}};
1
+ export const tutorialsView={templateUrl:"/admin/js/templates/tutorials.html",async onMount(){Domma.icons.scan(),Domma.syntax.scan()}};
@@ -165,18 +165,88 @@
165
165
  "items": [
166
166
  {
167
167
  "text": "Usage",
168
- "url": "#/documentation",
169
- "icon": "book"
168
+ "url": "",
169
+ "icon": "book",
170
+ "items": [
171
+ {"text": "Pages", "url": "#/docs/usage/pages", "icon": "file-text"},
172
+ {"text": "Media", "url": "#/docs/usage/media", "icon": "image"},
173
+ {"text": "Navigation", "url": "#/docs/usage/navigation", "icon": "menu"},
174
+ {"text": "DConfig", "url": "#/docs/usage/dconfig", "icon": "settings"},
175
+ {"text": "Shortcodes", "url": "#/docs/usage/shortcodes", "icon": "code"},
176
+ {"text": "Site Settings", "url": "#/docs/usage/site-settings", "icon": "settings"},
177
+ {"text": "Plugins", "url": "#/docs/usage/plugins", "icon": "package"},
178
+ {"text": "Users & Roles", "url": "#/docs/usage/users-roles", "icon": "users"},
179
+ {"text": "Views", "url": "#/docs/usage/views", "icon": "eye"},
180
+ {"text": "Actions", "url": "#/docs/usage/actions", "icon": "zap"},
181
+ {"text": "CTA Shortcode", "url": "#/docs/usage/cta-shortcode", "icon": "zap"}
182
+ ]
170
183
  },
171
184
  {
172
185
  "text": "Tutorials",
173
- "url": "#/tutorials",
174
- "icon": "document"
186
+ "url": "",
187
+ "icon": "document",
188
+ "items": [
189
+ {
190
+ "text": "Building a CRUD App",
191
+ "url": "#/tutorials/crud",
192
+ "icon": "database"
193
+ },
194
+ {
195
+ "text": "Writing a Plugin",
196
+ "url": "#/tutorials/plugin",
197
+ "icon": "package"
198
+ },
199
+ {
200
+ "text": "Form Follow-Up",
201
+ "url": "#/tutorials/forms",
202
+ "icon": "layout"
203
+ }
204
+ ]
205
+ },
206
+ {
207
+ "text": "Components",
208
+ "url": "",
209
+ "icon": "component",
210
+ "items": [
211
+ {
212
+ "text": "Reference",
213
+ "url": "#/docs/components",
214
+ "icon": "book"
215
+ },
216
+ {
217
+ "text": "How-To",
218
+ "url": "#/docs/components-howto",
219
+ "icon": "zap"
220
+ },
221
+ {
222
+ "text": "Walkthrough",
223
+ "url": "#/docs/components-walkthrough",
224
+ "icon": "document"
225
+ },
226
+ {
227
+ "text": "Rules",
228
+ "url": "#/docs/components-rules",
229
+ "icon": "shield"
230
+ }
231
+ ]
175
232
  },
176
233
  {
177
234
  "text": "API Reference",
178
- "url": "#/api-reference",
179
- "icon": "code"
235
+ "url": "",
236
+ "icon": "code",
237
+ "items": [
238
+ {"text": "Authentication", "url": "#/docs/api/authentication", "icon": "shield"},
239
+ {"text": "Pages", "url": "#/docs/api/pages", "icon": "file-text"},
240
+ {"text": "Settings", "url": "#/docs/api/settings", "icon": "settings"},
241
+ {"text": "Layouts", "url": "#/docs/api/layouts", "icon": "layout"},
242
+ {"text": "Navigation", "url": "#/docs/api/navigation", "icon": "menu"},
243
+ {"text": "Media", "url": "#/docs/api/media", "icon": "image"},
244
+ {"text": "Users", "url": "#/docs/api/users", "icon": "users"},
245
+ {"text": "Plugins", "url": "#/docs/api/plugins", "icon": "package"},
246
+ {"text": "Collections", "url": "#/docs/api/collections", "icon": "database"},
247
+ {"text": "Views API", "url": "#/docs/api/views", "icon": "eye"},
248
+ {"text": "Actions API", "url": "#/docs/api/actions", "icon": "zap"}
249
+ ]
180
250
  }
181
251
  ]
182
252
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.25.15",
3
+ "version": "0.25.16",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
package/server/server.js CHANGED
@@ -202,7 +202,7 @@ try {
202
202
  }
203
203
 
204
204
  try {
205
- const {runMigration: runSidebarMigration, ensureSidebarItem} = await import('./services/sidebar-migration.js');
205
+ const {runMigration: runSidebarMigration, ensureSidebarItem, ensureDocumentationSubmenus} = await import('./services/sidebar-migration.js');
206
206
  await runSidebarMigration();
207
207
  // Surface admin pages added after first boot on existing installs.
208
208
  await ensureSidebarItem({
@@ -213,6 +213,8 @@ try {
213
213
  groupText: 'Data',
214
214
  item: {text: 'API Builder', url: '#/api-endpoints', icon: 'code', permission: 'api-endpoints'}
215
215
  });
216
+ // Restructure Documentation: Tutorials → submenu, add Components reference set.
217
+ await ensureDocumentationSubmenus();
216
218
  } catch (err) {
217
219
  app.log.warn(`[admin-sidebar] Migration skipped: ${err.message}`);
218
220
  }
@@ -63,13 +63,80 @@ const SEED_ITEMS = [
63
63
  {
64
64
  text: 'Documentation', icon: 'book',
65
65
  items: [
66
- {text: 'Usage', url: '#/documentation', icon: 'book'},
67
- {text: 'Tutorials', url: '#/tutorials', icon: 'document'},
68
- {text: 'API Reference', url: '#/api-reference', icon: 'code'}
66
+ DOC_USAGE_SUBMENU(),
67
+ DOC_TUTORIALS_SUBMENU(),
68
+ DOC_COMPONENTS_SUBMENU(),
69
+ DOC_API_SUBMENU()
69
70
  ]
70
71
  }
71
72
  ];
72
73
 
74
+ // ─── Documentation sub-trees ──────────────────────────────────────────────
75
+ // Defined as factory functions so the seed and the migration each get their
76
+ // own fresh object (never a shared reference). Used by both SEED_ITEMS (fresh
77
+ // installs) and ensureDocumentationSubmenus() (existing installs).
78
+
79
+ function DOC_USAGE_SUBMENU() {
80
+ return {
81
+ text: 'Usage', icon: 'book',
82
+ items: [
83
+ {text: 'Pages', url: '#/docs/usage/pages', icon: 'file-text'},
84
+ {text: 'Media', url: '#/docs/usage/media', icon: 'image'},
85
+ {text: 'Navigation', url: '#/docs/usage/navigation', icon: 'menu'},
86
+ {text: 'DConfig', url: '#/docs/usage/dconfig', icon: 'settings'},
87
+ {text: 'Shortcodes', url: '#/docs/usage/shortcodes', icon: 'code'},
88
+ {text: 'Site Settings', url: '#/docs/usage/site-settings', icon: 'settings'},
89
+ {text: 'Plugins', url: '#/docs/usage/plugins', icon: 'package'},
90
+ {text: 'Users & Roles', url: '#/docs/usage/users-roles', icon: 'users'},
91
+ {text: 'Views', url: '#/docs/usage/views', icon: 'eye'},
92
+ {text: 'Actions', url: '#/docs/usage/actions', icon: 'zap'},
93
+ {text: 'CTA Shortcode', url: '#/docs/usage/cta-shortcode', icon: 'zap'}
94
+ ]
95
+ };
96
+ }
97
+
98
+ function DOC_TUTORIALS_SUBMENU() {
99
+ return {
100
+ text: 'Tutorials', icon: 'document',
101
+ items: [
102
+ {text: 'Building a CRUD App', url: '#/tutorials/crud', icon: 'database'},
103
+ {text: 'Writing a Plugin', url: '#/tutorials/plugin', icon: 'package'},
104
+ {text: 'Form Follow-Up', url: '#/tutorials/forms', icon: 'layout'}
105
+ ]
106
+ };
107
+ }
108
+
109
+ function DOC_COMPONENTS_SUBMENU() {
110
+ return {
111
+ text: 'Components', icon: 'component',
112
+ items: [
113
+ {text: 'Reference', url: '#/docs/components', icon: 'book'},
114
+ {text: 'How-To', url: '#/docs/components-howto', icon: 'zap'},
115
+ {text: 'Walkthrough', url: '#/docs/components-walkthrough', icon: 'document'},
116
+ {text: 'Rules', url: '#/docs/components-rules', icon: 'shield'}
117
+ ]
118
+ };
119
+ }
120
+
121
+ function DOC_API_SUBMENU() {
122
+ return {
123
+ text: 'API Reference', icon: 'code',
124
+ items: [
125
+ {text: 'Authentication', url: '#/docs/api/authentication', icon: 'shield'},
126
+ {text: 'Pages', url: '#/docs/api/pages', icon: 'file-text'},
127
+ {text: 'Settings', url: '#/docs/api/settings', icon: 'settings'},
128
+ {text: 'Layouts', url: '#/docs/api/layouts', icon: 'layout'},
129
+ {text: 'Navigation', url: '#/docs/api/navigation', icon: 'menu'},
130
+ {text: 'Media', url: '#/docs/api/media', icon: 'image'},
131
+ {text: 'Users', url: '#/docs/api/users', icon: 'users'},
132
+ {text: 'Plugins', url: '#/docs/api/plugins', icon: 'package'},
133
+ {text: 'Collections', url: '#/docs/api/collections', icon: 'database'},
134
+ {text: 'Views API', url: '#/docs/api/views', icon: 'eye'},
135
+ {text: 'Actions API', url: '#/docs/api/actions', icon: 'zap'}
136
+ ]
137
+ };
138
+ }
139
+
73
140
  async function exists(p) {
74
141
  try { await fs.access(p); return true; } catch { return false; }
75
142
  }
@@ -160,3 +227,78 @@ export async function ensureSidebarItem({groupText, item}, opts = {}) {
160
227
  console.log(`[admin-sidebar] Added "${item.text}" to the ${groupText} group`);
161
228
  return {ensured: true};
162
229
  }
230
+
231
+ /**
232
+ * Upgrade EXISTING installs so the Documentation group's one-page docs become
233
+ * per-topic submenus. Idempotent and non-clobbering:
234
+ *
235
+ * - If the persisted menu has no "Documentation" group, do nothing (the admin
236
+ * may have removed it deliberately — don't resurrect it).
237
+ * - Convert each flat leaf — "Usage" (`#/documentation`), "Tutorials"
238
+ * (`#/tutorials`) and "API Reference" (`#/api-reference`) — into its
239
+ * submenu, replacing the node in place so its position is preserved. A leaf
240
+ * that the admin already turned into a parent, or whose new pages already
241
+ * appear anywhere in the group, is left untouched.
242
+ * - Add the Components submenu only if `#/docs/components` isn't present yet —
243
+ * inserted before "API Reference" when that node exists.
244
+ *
245
+ * Returns `{updated: boolean, reason?: string}`.
246
+ *
247
+ * @param {{configDir?: string}} [opts]
248
+ */
249
+ export async function ensureDocumentationSubmenus(opts = {}) {
250
+ const configDir = opts.configDir || DEFAULT_CONFIG_DIR;
251
+ const menuPath = path.join(configDir, 'menus', 'admin-sidebar.json');
252
+
253
+ if (!await exists(menuPath)) {
254
+ return {updated: false, reason: 'admin-sidebar.json not present'};
255
+ }
256
+
257
+ const menu = await readJson(menuPath);
258
+ const items = Array.isArray(menu.items) ? menu.items : [];
259
+ const docGroup = items.find(n => typeof n?.text === 'string' && n.text.toLowerCase() === 'documentation');
260
+ if (!docGroup || !Array.isArray(docGroup.items)) {
261
+ return {updated: false, reason: 'no Documentation group'};
262
+ }
263
+
264
+ const hasUrl = (nodes, url) => Array.isArray(nodes) &&
265
+ nodes.some(n => n?.url === url || hasUrl(n?.items, url));
266
+ const findByText = (text) => docGroup.items.findIndex(n =>
267
+ typeof n?.text === 'string' && n.text.toLowerCase() === text.toLowerCase());
268
+
269
+ let changed = false;
270
+
271
+ // Leaf → submenu conversions. `guardUrl` is a page from the submenu: if it's
272
+ // already present anywhere we've migrated before and skip.
273
+ const CONVERSIONS = [
274
+ {text: 'Usage', guardUrl: '#/docs/usage/pages', build: DOC_USAGE_SUBMENU},
275
+ {text: 'Tutorials', guardUrl: '#/tutorials/crud', build: DOC_TUTORIALS_SUBMENU},
276
+ {text: 'API Reference', guardUrl: '#/docs/api/authentication', build: DOC_API_SUBMENU}
277
+ ];
278
+ for (const {text, guardUrl, build} of CONVERSIONS) {
279
+ if (hasUrl(docGroup.items, guardUrl)) continue;
280
+ const idx = findByText(text);
281
+ if (idx === -1) continue;
282
+ const node = docGroup.items[idx];
283
+ const alreadyParent = Array.isArray(node.items) && node.items.length > 0;
284
+ if (alreadyParent) continue;
285
+ docGroup.items[idx] = build();
286
+ changed = true;
287
+ }
288
+
289
+ // Components submenu — add if absent, before API Reference.
290
+ if (!hasUrl(docGroup.items, '#/docs/components')) {
291
+ const apiIdx = findByText('API Reference');
292
+ const node = DOC_COMPONENTS_SUBMENU();
293
+ if (apiIdx !== -1) docGroup.items.splice(apiIdx, 0, node);
294
+ else docGroup.items.push(node);
295
+ changed = true;
296
+ }
297
+
298
+ if (!changed) return {updated: false, reason: 'already up to date'};
299
+
300
+ menu.meta = {...(menu.meta || {}), updatedAt: new Date().toISOString()};
301
+ await writeJson(menuPath, menu);
302
+ console.log('[admin-sidebar] Upgraded Documentation group with per-topic submenus');
303
+ return {updated: true};
304
+ }