domma-cms 0.25.14 → 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 (45) 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/public/js/site.js +1 -1
  42. package/server/server.js +3 -1
  43. package/server/services/menus.js +18 -0
  44. package/server/services/renderer.js +2 -0
  45. 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.14",
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/public/js/site.js CHANGED
@@ -1 +1 @@
1
- import{makeBadgeEl as _,applyColour as A,applyPill as I}from"/public/js/menu-decor.mjs";$(()=>{const C=window.__CMS_NAV__||{},g=window.__CMS_SITE__||{};if(g.autoTheme?.enabled){let o=function(e){const f=(e||"07:00").split(":");return+f[0]*60+(+f[1]||0)},a=function(){const e=new Date,f=e.getHours()*60+e.getMinutes();return f>=o(t.dayStart)&&f<o(t.nightStart)?t.dayTheme:t.nightTheme};const t=g.autoTheme;Domma.theme.set(a()),setInterval(()=>Domma.theme.set(a()),6e4)}const b=$("#site-navbar");if(b.length&&C.brand){let a=function(c,r=0){return Array.isArray(c)?c.filter(i=>!(i&&i.type==="separator"&&r===0)).map(i=>i&&i.type==="separator"?{divider:!0}:Array.isArray(i?.items)?{...i,items:a(i.items,r+1)}:i):c},e=function(c){return Array.isArray(c)?c.filter(r=>!(r&&r.type==="separator")):[]};const t={...C.brand},o=t.size&&t.size!=="md"?` navbar-brand-${t.size}`:"";if(t.logo||t.icon||t.tagline){let c="";t.logo?c+=`<img src="${t.logo}" class="navbar-brand-logo" alt="${t.text||""}">`:t.icon&&(c+=`<span data-icon="${t.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),t.text&&(c+=`<span class="navbar-brand-text${o}">${t.text}</span>`),t.tagline&&(c+=`<small class="navbar-brand-tagline">${t.tagline}</small>`),t.html=c}else o&&t.text&&(t.html=`<span class="navbar-brand-text${o}">${t.text}</span>`);Domma.elements.navbar("#site-navbar",{brand:t,items:a(C.items||[]),variant:C.variant||"dark",position:C.position||"sticky",collapsible:!0}),Domma.icons.scan("#site-navbar");const f=b.get(0),d=f.querySelector(".navbar-toggle"),l=f.querySelector(".navbar-collapse");if(d&&l){let c=null;const r=()=>{l.classList.contains("show")&&d.click()},i=()=>{c&&(c.remove(),c=null),document.body.classList.remove("dm-drawer-open")},n=()=>{c||(c=document.createElement("div"),c.className="dm-navbar-backdrop",c.addEventListener("click",r),document.body.appendChild(c),document.body.classList.add("dm-drawer-open"))};d.addEventListener("click",()=>{requestAnimationFrame(()=>{l.classList.contains("show")?n():i()})}),l.addEventListener("click",u=>{u.target.closest("a")&&requestAnimationFrame(i)}),document.addEventListener("keydown",u=>{u.key==="Escape"&&l.classList.contains("show")&&r()})}const y=e(C.items||[]);f.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(c=>{const r=y[parseInt(c.dataset.index,10)];if(!r)return;const i=c.dataset.subindex!=null,n=i&&Array.isArray(r.items)?r.items[parseInt(c.dataset.subindex,10)]:r;if(!n)return;n.colour&&A(c,n.colour),n.pill&&!i&&I(c,n.pill);const u=_(n.badge);u&&c.appendChild(u)})}const v=$("#site-footer");if(v.length){let f=function(s){return String(s??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},d=function(s){return f(s).replace(/'/g,"&#39;")},l=function(s){if(s&&s.type==="separator")return'<li class="footer-sep"><hr></li>';const m=d(s.url||"#"),h=f(s.text||""),k=Array.isArray(s.items)&&s.items.length?`<ul>${s.items.map(l).join("")}</ul>`:"";return`<li><a href="${m}">${h}</a>${k}</li>`},y=function(s,m){return(s||[]).forEach(h=>{h&&h.type==="separator"||(m.push(h),Array.isArray(h.items)&&h.items.length&&y(h.items,m))}),m};const t=g.social||{},o={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let a='<div class="footer-inner container">';const e=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(e.copyright&&(a+=`<p>${f(e.copyright)}</p>`),e.primary.length&&(a+=`<nav class="footer-nav footer-nav--primary"><ul>${e.primary.map(l).join("")}</ul></nav>`),e.legal.length&&(a+=`<nav class="footer-nav footer-nav--legal"><ul>${e.legal.map(l).join("")}</ul></nav>`),g.footer){const s=Object.keys(o).filter(m=>t[m]);s.length&&(a+='<div class="footer-social">',s.forEach(m=>{const{label:h,svg:k}=o[m];a+=`<a href="${t[m]}" target="_blank" rel="noopener noreferrer" aria-label="${h}" class="footer-social-link">${k}</a>`}),a+="</div>")}a+="</div>",v.html(a);const c=[...y(e.primary,[]),...y(e.legal,[])];v.get(0).querySelectorAll(".footer-nav a").forEach((s,m)=>{const h=c[m];if(!h)return;h.colour&&A(s,h.colour);const k=_(h.badge);k&&s.appendChild(k)});const i=S.get("reduced_motion"),n=i!==null?!!i:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),u=v.get(0).querySelector(".footer-inner");if(u){const s=document.createElement("input");s.type="checkbox",s.className="form-switch-input",s.id="dm-motion-switch",s.checked=n,s.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const m=document.createElement("span");m.className="form-switch-label",m.textContent="Reduce motion";const h=document.createElement("label");h.className="form-switch footer-motion-switch",h.title="Reduce motion",h.appendChild(s),h.appendChild(m),u.appendChild(h)}}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan();const p=$(".page-body");if(p.length){p.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),p.find(".tabs").each(function(){Domma.elements.tabs(this)}),p.find(".carousel").each(function(){const t={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const o=parseInt(this.dataset.animationDuration,10);Number.isFinite(o)&&(t.animationDuration=o)}this.dataset.animationEasing&&(t.animationEasing=this.dataset.animationEasing),Domma.elements.carousel(this,t)}),p.find(".dm-countdown").each(function(){const t={autoStart:!0};this.dataset.to&&(t.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(t.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(t.format=this.dataset.format),Domma.elements.timer(this,t)}),p.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),p.find(".dm-progression").each(function(){Domma.elements.progression(this,{layout:this.dataset.layout||"vertical",theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(t=>{const o=t.dataset.revealMode||"stagger",a=t.dataset.revealAnimation||"slide-up",e=parseInt(t.dataset.revealDuration,10)||400,f=parseInt(t.dataset.revealStagger,10)||60,d=parseInt(t.dataset.revealDelay,10)||0,l=t.dataset.revealDirection==="rtl",y=Array.from(t.children),c={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};y.forEach((r,i)=>{r.style.opacity="0",r.style.transform=c[a]||"",r.style.transition=`opacity ${e}ms ease, transform ${e}ms ease`;const n=l?y.length-1-i:i;r.style.transitionDelay=o==="stagger"?`${d+n*f}ms`:`${d}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const r=new IntersectionObserver(i=>{i.forEach(n=>{n.isIntersecting&&(n.target.offsetWidth,n.target.style.opacity="1",n.target.style.transform="none",r.unobserve(n.target))})},{threshold:.1});y.forEach(i=>r.observe(i))}))}),p.find(".card[data-collapsible]").each(function(){const t=this.querySelector(".card-header");t&&t.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),p.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const t=this.dataset.soTarget,o=document.getElementById(t);if(!o)return;const a=E.slideover({title:o.dataset.soTitle||"",size:o.dataset.soSize||"md",position:o.dataset.soPosition||"right"});o.style.display="",a.setContent(o),a.open()})})}if(typeof $.setup=="function"){const t=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(o=>{try{const a=atob(o.dataset.config),e=JSON.parse(a);Object.assign(t,e)}catch{}}),Object.keys(t).length>0){const o={};for(const[a,e]of Object.entries(t)){const f=e?.events?.click,{confirm:d,toast:l,alert:y,prompt:c,...r}=f||{};d||l||y||c?($(a).on("click",async function(n){if(n.preventDefault(),d&&!await E.confirm(d))return;let u=null;if(!(c&&(u=await E.prompt(c,{inputPlaceholder:r.promptPlaceholder||"",inputValue:r.promptDefault||""}),u===null))){if(r.target){const s=$(r.target);r.toggleClass&&s.toggleClass(r.toggleClass),r.addClass&&s.addClass(r.addClass),r.removeClass&&s.removeClass(r.removeClass),u!==null&&(r.setText&&s.text(u),r.setVal&&s.val(u),r.setAttr&&s.attr(r.setAttr,u))}r.href&&(window.location.href=r.href),l&&E.toast(l,{type:r.toastType||"success"}),y&&E.alert(y)}}),Object.keys(r).length&&(o[a]={...e,events:{...e.events,click:r}})):o[a]=e}$.setup(o)}}p.length&&x(p.get(0))});function x(C){C.querySelectorAll(".dm-cta-trigger").forEach(g=>{g.__ctaWired||(g.__ctaWired=!0,g.addEventListener("click",async()=>{const b=g.dataset.action,v=g.dataset.entry,w=g.dataset.confirm;let p=S.get("auth_token");if(!p){E.toast("Please log in to perform this action.",{type:"warning"});return}if(w&&!await E.confirm(w))return;const t=Array.from(g.childNodes).map(a=>a.cloneNode(!0));g.disabled=!0,g.textContent="Running\u2026";const o=a=>fetch(`/api/actions/${b}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({entryId:v})});try{let a=await o(p);if(a.status===401){const f=S.get("auth_refresh_token");if(f){const d=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:f})});if(d.ok){const{token:l}=await d.json();S.set("auth_token",l),p=l,a=await o(p)}}}const e=await a.json().catch(()=>({}));if(!a.ok)throw new Error(e.error||e.message||`Error ${a.status}`);E.toast(e.message||"Action completed.",{type:"success"})}catch(a){E.toast(a.message||"Action failed.",{type:"error"})}finally{g.disabled=!1,g.textContent="",t.forEach(a=>g.appendChild(a)),Domma.icons.scan(g)}}))})}(function(){const g=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!g.length)return;const b=S.get("auth_token");function v(p,t){const o=document.createElement("div");o.className="dm-collection-display dm-collection-empty";const a=document.createElement("p");if(t){a.appendChild(document.createTextNode("Please "));const e=document.createElement("a");e.href="/login",e.textContent="sign in",a.appendChild(e),a.appendChild(document.createTextNode(" to view your entries."))}else a.textContent=p;return o.appendChild(a),o}function w(p,t){const o=document.createElement("template");o.innerHTML=t||"",p.replaceWith(o.content)}g.forEach(async p=>{const t=p.dataset.collectionAttrs;if(!t)return;if(!b){p.replaceChildren(v("",!0));return}const o=a=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({attrs:t})});try{let a=b,e=await o(a);if(e.status===401){const d=S.get("auth_refresh_token");if(d){const l=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:d})});if(l.ok){const{token:y}=await l.json();S.set("auth_token",y),a=y,e=await o(a)}}}if(!e.ok){const{error:d}=await e.json().catch(()=>({}));p.replaceChildren(v(d||"Unable to load entries.",!1));return}const{html:f}=await e.json();w(p,f)}catch{p.replaceChildren(v("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),x(document.body)},100)})(),(function(){const g=document.querySelectorAll("[data-collection-table]");!g.length||typeof T>"u"||!T.create||g.forEach(b=>{let v;try{v=JSON.parse(atob(b.dataset.payload))}catch{return}const{columns:w,rows:p,search:t,sortable:o,exportable:a,pageSize:e,empty:f,ctaConfig:d}=v;if(!w?.length)return;if(d){const c=w.findIndex(r=>r.key==="_cta");c!==-1&&(w[c]={key:"_cta",title:"",render:(r,i)=>{const n=document.createElement("button");if(n.className=`btn btn-${d.style||"primary"} dm-cta-trigger`,n.dataset.action=d.action||"",n.dataset.entry=i._entryId||"",d.confirm&&(n.dataset.confirm=d.confirm),d.icon){const u=document.createElement("span");u.dataset.icon=d.icon,n.appendChild(u),n.appendChild(document.createTextNode(" "))}return n.appendChild(document.createTextNode(d.label||"Run")),n}})}const l="col-table-"+Math.random().toString(36).slice(2,7),y=document.createElement("div");y.id=l,b.replaceChildren(y),T.create("#"+l,{data:p,columns:w,search:t,sortable:o,exportable:a,pageSize:e,emptyMessage:f}),d&&x(y)})})(),(function(){const g=document.querySelectorAll("[data-form-inline]");if(!g.length||typeof F>"u")return;function b(t,o){const a={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const f=e.type==="checkbox"?"boolean":e.type==="date"?"string":e.type,d={...e.formConfig||{}};d.span==="full"&&o&&(d.span=o);const l={type:f,label:e.label,required:e.required,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...d}};e.type==="chooser"&&(e.variant&&(l.variant=e.variant),e.multiple&&(l.multiple=!0),e.density&&(l.density=e.density),e.columns&&(l.columns=e.columns),e.accent&&(l.accent=e.accent),e.accentStyle&&(l.accentStyle=e.accentStyle),e.glow&&(l.glow=!0),e.glowColour&&(l.glowColour=e.glowColour),e.shadow&&(l.shadow=e.shadow),e.shadowColour&&(l.shadowColour=e.shadowColour)),a[e.name]=l}),a}function v(t){const o={};return(t||[]).forEach(a=>{if(!(!a.name||a.type==="page-break"||a.type==="spacer")&&(a.type==="select"||a.type==="multiselect")&&a.required){const e=(a.options||[])[0];e&&(o[a.name]=typeof e=="object"?e.value:e)}}),o}function w(t,o){(o||[]).forEach(a=>{if(a.type!=="date"||!a.name)return;const e=t.querySelector(`[name="${a.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function p(t,o,a){let e=t.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",t.appendChild(e)),e.textContent=o,e.style.cssText=a?"color:var(--danger,#f87171);margin-top:.75rem;":"color:var(--success,#4ade80);margin-top:.75rem;"}g.forEach(t=>{let o;try{o=JSON.parse(atob(t.dataset.formInline))}catch{return}const a=o.fields||[],e=o.settings||{},f=e.columns||1,d=e.layout||"stacked",l=a.some(i=>i.type==="page-break"),y=async i=>{try{const n=t.querySelector('[name="website"]'),u=t.querySelector('[name="_t"]'),s=Object.assign({},i);n!==null&&(s._hp=n.value),u!==null&&(s._t=u.value);const m=await H.post(`/api/forms/submit/${o.slug}`,s);if(m&&m.error){p(t,m.error,!0);const h=new Error(m.error);throw h.formSubmitFailed=!0,h}if(m?.redirect){window.location.href=m.redirect;return}for(;t.firstChild;)t.removeChild(t.firstChild);p(t,m?.message||e.successMessage||"Thank you for your submission.",!1)}catch(n){throw n.formSubmitFailed||p(t,n.message||"Submission failed. Please try again.",!0),n}};function c(i){const n=i.querySelector("form");if(!n)return;const u=document.createElement("div");u.className="fb-form-honeypot",u.setAttribute("aria-hidden","true");const s=document.createElement("input");s.name="website",s.type="text",s.tabIndex=-1,s.autocomplete="url",s.placeholder="https://",u.appendChild(s);const m=document.createElement("input");m.name="_t",m.type="hidden",m.value=Date.now(),u.appendChild(m),n.appendChild(u)}function r(){if(!window.FormLogicEngine||!a.some(n=>n.logic))return;const i=new window.FormLogicEngine.FormLogicRuntime(o,t);if(i.init(),t._formLogicRuntime=i,t.parentNode&&typeof MutationObserver<"u"){const n=new MutationObserver(function(u){for(const s of u)for(const m of s.removedNodes)if(m===t||m.nodeType===1&&m.contains&&m.contains(t)){i.destroy(),n.disconnect();return}});n.observe(t.parentNode,{childList:!0,subtree:!1})}}if(l&&F.wizard){const i=[];let n=[],u=o.title||"Step 1",s="";a.forEach(h=>{h.type==="page-break"?(i.push({title:u,description:s,fields:b(n,f)}),n=[],u=h.label||`Step ${i.length+1}`,s=h.description||""):h.type!=="spacer"&&n.push(h)}),i.push({title:u,description:s,fields:b(n,f)});const m=F.wizard(t,{schema:{steps:i},onSubmit:y});Promise.resolve(m).then(function(){w(t,a),e.honeypot!==!1&&c(t),r()})}else if(F.render){const i=F.render(t,b(a,f),v(a),{submitText:e.submitText||"Submit",layout:d,columns:f,onSubmit:y});Promise.resolve(i).then(function(){if(d==="grid"&&e.submitSpan==="full"){const n=t.querySelector(".form-buttons");n&&n.classList.add("col-span-full")}w(t,a),e.honeypot!==!1&&c(t),r()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
1
+ import{makeBadgeEl as _,applyColour as A,applyPill as I}from"/public/js/menu-decor.mjs";$(()=>{const C=window.__CMS_NAV__||{},g=window.__CMS_SITE__||{};if(g.autoTheme?.enabled){let o=function(e){const f=(e||"07:00").split(":");return+f[0]*60+(+f[1]||0)},a=function(){const e=new Date,f=e.getHours()*60+e.getMinutes();return f>=o(t.dayStart)&&f<o(t.nightStart)?t.dayTheme:t.nightTheme};const t=g.autoTheme;Domma.theme.set(a()),setInterval(()=>Domma.theme.set(a()),6e4)}const b=$("#site-navbar");if(b.length&&C.brand){let a=function(i,n=0){return Array.isArray(i)?i.filter(c=>!(c&&c.type==="separator"&&n===0)).map(c=>c&&c.type==="separator"?{divider:!0}:Array.isArray(c?.items)?{...c,items:a(c.items,n+1)}:c):i},e=function(i){return Array.isArray(i)?i.filter(n=>!(n&&n.type==="separator")):[]};const t={...C.brand},o=t.size&&t.size!=="md"?` navbar-brand-${t.size}`:"";if(t.logo||t.icon||t.tagline){let i="";t.logo?i+=`<img src="${t.logo}" class="navbar-brand-logo" alt="${t.text||""}">`:t.icon&&(i+=`<span data-icon="${t.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),t.text&&(i+=`<span class="navbar-brand-text${o}">${t.text}</span>`),t.tagline&&(i+=`<small class="navbar-brand-tagline">${t.tagline}</small>`),t.html=i}else o&&t.text&&(t.html=`<span class="navbar-brand-text${o}">${t.text}</span>`);Domma.elements.navbar("#site-navbar",{brand:t,items:a(C.items||[]),variant:C.variant||"dark",position:C.position||"sticky",collapsible:!0}),Domma.icons.scan("#site-navbar");const f=b.get(0);if(C.appearOnHover){const i=window.matchMedia("(min-width: 993px)");f.querySelectorAll(".navbar-dropdown").forEach(n=>{n.addEventListener("mouseenter",()=>{i.matches&&n.classList.add("open")}),n.addEventListener("mouseleave",()=>{i.matches&&n.classList.remove("open")})})}const d=f.querySelector(".navbar-toggle"),l=f.querySelector(".navbar-collapse");if(d&&l){let i=null;const n=()=>{l.classList.contains("show")&&d.click()},c=()=>{i&&(i.remove(),i=null),document.body.classList.remove("dm-drawer-open")},s=()=>{i||(i=document.createElement("div"),i.className="dm-navbar-backdrop",i.addEventListener("click",n),document.body.appendChild(i),document.body.classList.add("dm-drawer-open"))};d.addEventListener("click",()=>{requestAnimationFrame(()=>{l.classList.contains("show")?s():c()})}),l.addEventListener("click",u=>{u.target.closest("a")&&requestAnimationFrame(c)}),document.addEventListener("keydown",u=>{u.key==="Escape"&&l.classList.contains("show")&&n()})}const y=e(C.items||[]);f.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(i=>{const n=y[parseInt(i.dataset.index,10)];if(!n)return;const c=i.dataset.subindex!=null,s=c&&Array.isArray(n.items)?n.items[parseInt(i.dataset.subindex,10)]:n;if(!s)return;s.colour&&A(i,s.colour),s.pill&&!c&&I(i,s.pill);const u=_(s.badge);u&&i.appendChild(u)})}const v=$("#site-footer");if(v.length){let f=function(r){return String(r??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},d=function(r){return f(r).replace(/'/g,"&#39;")},l=function(r){if(r&&r.type==="separator")return'<li class="footer-sep"><hr></li>';const m=d(r.url||"#"),h=f(r.text||""),k=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(l).join("")}</ul>`:"";return`<li><a href="${m}">${h}</a>${k}</li>`},y=function(r,m){return(r||[]).forEach(h=>{h&&h.type==="separator"||(m.push(h),Array.isArray(h.items)&&h.items.length&&y(h.items,m))}),m};const t=g.social||{},o={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let a='<div class="footer-inner container">';const e=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(e.copyright&&(a+=`<p>${f(e.copyright)}</p>`),e.primary.length&&(a+=`<nav class="footer-nav footer-nav--primary"><ul>${e.primary.map(l).join("")}</ul></nav>`),e.legal.length&&(a+=`<nav class="footer-nav footer-nav--legal"><ul>${e.legal.map(l).join("")}</ul></nav>`),g.footer){const r=Object.keys(o).filter(m=>t[m]);r.length&&(a+='<div class="footer-social">',r.forEach(m=>{const{label:h,svg:k}=o[m];a+=`<a href="${t[m]}" target="_blank" rel="noopener noreferrer" aria-label="${h}" class="footer-social-link">${k}</a>`}),a+="</div>")}a+="</div>",v.html(a);const i=[...y(e.primary,[]),...y(e.legal,[])];v.get(0).querySelectorAll(".footer-nav a").forEach((r,m)=>{const h=i[m];if(!h)return;h.colour&&A(r,h.colour);const k=_(h.badge);k&&r.appendChild(k)});const c=S.get("reduced_motion"),s=c!==null?!!c:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),u=v.get(0).querySelector(".footer-inner");if(u){const r=document.createElement("input");r.type="checkbox",r.className="form-switch-input",r.id="dm-motion-switch",r.checked=s,r.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const m=document.createElement("span");m.className="form-switch-label",m.textContent="Reduce motion";const h=document.createElement("label");h.className="form-switch footer-motion-switch",h.title="Reduce motion",h.appendChild(r),h.appendChild(m),u.appendChild(h)}}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan();const p=$(".page-body");if(p.length){p.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),p.find(".tabs").each(function(){Domma.elements.tabs(this)}),p.find(".carousel").each(function(){const t={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const o=parseInt(this.dataset.animationDuration,10);Number.isFinite(o)&&(t.animationDuration=o)}this.dataset.animationEasing&&(t.animationEasing=this.dataset.animationEasing),Domma.elements.carousel(this,t)}),p.find(".dm-countdown").each(function(){const t={autoStart:!0};this.dataset.to&&(t.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(t.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(t.format=this.dataset.format),Domma.elements.timer(this,t)}),p.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),p.find(".dm-progression").each(function(){Domma.elements.progression(this,{layout:this.dataset.layout||"vertical",theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(t=>{const o=t.dataset.revealMode||"stagger",a=t.dataset.revealAnimation||"slide-up",e=parseInt(t.dataset.revealDuration,10)||400,f=parseInt(t.dataset.revealStagger,10)||60,d=parseInt(t.dataset.revealDelay,10)||0,l=t.dataset.revealDirection==="rtl",y=Array.from(t.children),i={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};y.forEach((n,c)=>{n.style.opacity="0",n.style.transform=i[a]||"",n.style.transition=`opacity ${e}ms ease, transform ${e}ms ease`;const s=l?y.length-1-c:c;n.style.transitionDelay=o==="stagger"?`${d+s*f}ms`:`${d}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const n=new IntersectionObserver(c=>{c.forEach(s=>{s.isIntersecting&&(s.target.offsetWidth,s.target.style.opacity="1",s.target.style.transform="none",n.unobserve(s.target))})},{threshold:.1});y.forEach(c=>n.observe(c))}))}),p.find(".card[data-collapsible]").each(function(){const t=this.querySelector(".card-header");t&&t.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),p.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const t=this.dataset.soTarget,o=document.getElementById(t);if(!o)return;const a=E.slideover({title:o.dataset.soTitle||"",size:o.dataset.soSize||"md",position:o.dataset.soPosition||"right"});o.style.display="",a.setContent(o),a.open()})})}if(typeof $.setup=="function"){const t=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(o=>{try{const a=atob(o.dataset.config),e=JSON.parse(a);Object.assign(t,e)}catch{}}),Object.keys(t).length>0){const o={};for(const[a,e]of Object.entries(t)){const f=e?.events?.click,{confirm:d,toast:l,alert:y,prompt:i,...n}=f||{};d||l||y||i?($(a).on("click",async function(s){if(s.preventDefault(),d&&!await E.confirm(d))return;let u=null;if(!(i&&(u=await E.prompt(i,{inputPlaceholder:n.promptPlaceholder||"",inputValue:n.promptDefault||""}),u===null))){if(n.target){const r=$(n.target);n.toggleClass&&r.toggleClass(n.toggleClass),n.addClass&&r.addClass(n.addClass),n.removeClass&&r.removeClass(n.removeClass),u!==null&&(n.setText&&r.text(u),n.setVal&&r.val(u),n.setAttr&&r.attr(n.setAttr,u))}n.href&&(window.location.href=n.href),l&&E.toast(l,{type:n.toastType||"success"}),y&&E.alert(y)}}),Object.keys(n).length&&(o[a]={...e,events:{...e.events,click:n}})):o[a]=e}$.setup(o)}}p.length&&x(p.get(0))});function x(C){C.querySelectorAll(".dm-cta-trigger").forEach(g=>{g.__ctaWired||(g.__ctaWired=!0,g.addEventListener("click",async()=>{const b=g.dataset.action,v=g.dataset.entry,w=g.dataset.confirm;let p=S.get("auth_token");if(!p){E.toast("Please log in to perform this action.",{type:"warning"});return}if(w&&!await E.confirm(w))return;const t=Array.from(g.childNodes).map(a=>a.cloneNode(!0));g.disabled=!0,g.textContent="Running\u2026";const o=a=>fetch(`/api/actions/${b}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({entryId:v})});try{let a=await o(p);if(a.status===401){const f=S.get("auth_refresh_token");if(f){const d=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:f})});if(d.ok){const{token:l}=await d.json();S.set("auth_token",l),p=l,a=await o(p)}}}const e=await a.json().catch(()=>({}));if(!a.ok)throw new Error(e.error||e.message||`Error ${a.status}`);E.toast(e.message||"Action completed.",{type:"success"})}catch(a){E.toast(a.message||"Action failed.",{type:"error"})}finally{g.disabled=!1,g.textContent="",t.forEach(a=>g.appendChild(a)),Domma.icons.scan(g)}}))})}(function(){const g=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!g.length)return;const b=S.get("auth_token");function v(p,t){const o=document.createElement("div");o.className="dm-collection-display dm-collection-empty";const a=document.createElement("p");if(t){a.appendChild(document.createTextNode("Please "));const e=document.createElement("a");e.href="/login",e.textContent="sign in",a.appendChild(e),a.appendChild(document.createTextNode(" to view your entries."))}else a.textContent=p;return o.appendChild(a),o}function w(p,t){const o=document.createElement("template");o.innerHTML=t||"",p.replaceWith(o.content)}g.forEach(async p=>{const t=p.dataset.collectionAttrs;if(!t)return;if(!b){p.replaceChildren(v("",!0));return}const o=a=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({attrs:t})});try{let a=b,e=await o(a);if(e.status===401){const d=S.get("auth_refresh_token");if(d){const l=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:d})});if(l.ok){const{token:y}=await l.json();S.set("auth_token",y),a=y,e=await o(a)}}}if(!e.ok){const{error:d}=await e.json().catch(()=>({}));p.replaceChildren(v(d||"Unable to load entries.",!1));return}const{html:f}=await e.json();w(p,f)}catch{p.replaceChildren(v("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),x(document.body)},100)})(),(function(){const g=document.querySelectorAll("[data-collection-table]");!g.length||typeof T>"u"||!T.create||g.forEach(b=>{let v;try{v=JSON.parse(atob(b.dataset.payload))}catch{return}const{columns:w,rows:p,search:t,sortable:o,exportable:a,pageSize:e,empty:f,ctaConfig:d}=v;if(!w?.length)return;if(d){const i=w.findIndex(n=>n.key==="_cta");i!==-1&&(w[i]={key:"_cta",title:"",render:(n,c)=>{const s=document.createElement("button");if(s.className=`btn btn-${d.style||"primary"} dm-cta-trigger`,s.dataset.action=d.action||"",s.dataset.entry=c._entryId||"",d.confirm&&(s.dataset.confirm=d.confirm),d.icon){const u=document.createElement("span");u.dataset.icon=d.icon,s.appendChild(u),s.appendChild(document.createTextNode(" "))}return s.appendChild(document.createTextNode(d.label||"Run")),s}})}const l="col-table-"+Math.random().toString(36).slice(2,7),y=document.createElement("div");y.id=l,b.replaceChildren(y),T.create("#"+l,{data:p,columns:w,search:t,sortable:o,exportable:a,pageSize:e,emptyMessage:f}),d&&x(y)})})(),(function(){const g=document.querySelectorAll("[data-form-inline]");if(!g.length||typeof F>"u")return;function b(t,o){const a={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const f=e.type==="checkbox"?"boolean":e.type==="date"?"string":e.type,d={...e.formConfig||{}};d.span==="full"&&o&&(d.span=o);const l={type:f,label:e.label,required:e.required,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...d}};e.type==="chooser"&&(e.variant&&(l.variant=e.variant),e.multiple&&(l.multiple=!0),e.density&&(l.density=e.density),e.columns&&(l.columns=e.columns),e.accent&&(l.accent=e.accent),e.accentStyle&&(l.accentStyle=e.accentStyle),e.glow&&(l.glow=!0),e.glowColour&&(l.glowColour=e.glowColour),e.shadow&&(l.shadow=e.shadow),e.shadowColour&&(l.shadowColour=e.shadowColour)),a[e.name]=l}),a}function v(t){const o={};return(t||[]).forEach(a=>{if(!(!a.name||a.type==="page-break"||a.type==="spacer")&&(a.type==="select"||a.type==="multiselect")&&a.required){const e=(a.options||[])[0];e&&(o[a.name]=typeof e=="object"?e.value:e)}}),o}function w(t,o){(o||[]).forEach(a=>{if(a.type!=="date"||!a.name)return;const e=t.querySelector(`[name="${a.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function p(t,o,a){let e=t.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",t.appendChild(e)),e.textContent=o,e.style.cssText=a?"color:var(--danger,#f87171);margin-top:.75rem;":"color:var(--success,#4ade80);margin-top:.75rem;"}g.forEach(t=>{let o;try{o=JSON.parse(atob(t.dataset.formInline))}catch{return}const a=o.fields||[],e=o.settings||{},f=e.columns||1,d=e.layout||"stacked",l=a.some(c=>c.type==="page-break"),y=async c=>{try{const s=t.querySelector('[name="website"]'),u=t.querySelector('[name="_t"]'),r=Object.assign({},c);s!==null&&(r._hp=s.value),u!==null&&(r._t=u.value);const m=await H.post(`/api/forms/submit/${o.slug}`,r);if(m&&m.error){p(t,m.error,!0);const h=new Error(m.error);throw h.formSubmitFailed=!0,h}if(m?.redirect){window.location.href=m.redirect;return}for(;t.firstChild;)t.removeChild(t.firstChild);p(t,m?.message||e.successMessage||"Thank you for your submission.",!1)}catch(s){throw s.formSubmitFailed||p(t,s.message||"Submission failed. Please try again.",!0),s}};function i(c){const s=c.querySelector("form");if(!s)return;const u=document.createElement("div");u.className="fb-form-honeypot",u.setAttribute("aria-hidden","true");const r=document.createElement("input");r.name="website",r.type="text",r.tabIndex=-1,r.autocomplete="url",r.placeholder="https://",u.appendChild(r);const m=document.createElement("input");m.name="_t",m.type="hidden",m.value=Date.now(),u.appendChild(m),s.appendChild(u)}function n(){if(!window.FormLogicEngine||!a.some(s=>s.logic))return;const c=new window.FormLogicEngine.FormLogicRuntime(o,t);if(c.init(),t._formLogicRuntime=c,t.parentNode&&typeof MutationObserver<"u"){const s=new MutationObserver(function(u){for(const r of u)for(const m of r.removedNodes)if(m===t||m.nodeType===1&&m.contains&&m.contains(t)){c.destroy(),s.disconnect();return}});s.observe(t.parentNode,{childList:!0,subtree:!1})}}if(l&&F.wizard){const c=[];let s=[],u=o.title||"Step 1",r="";a.forEach(h=>{h.type==="page-break"?(c.push({title:u,description:r,fields:b(s,f)}),s=[],u=h.label||`Step ${c.length+1}`,r=h.description||""):h.type!=="spacer"&&s.push(h)}),c.push({title:u,description:r,fields:b(s,f)});const m=F.wizard(t,{schema:{steps:c},onSubmit:y});Promise.resolve(m).then(function(){w(t,a),e.honeypot!==!1&&i(t),n()})}else if(F.render){const c=F.render(t,b(a,f),v(a),{submitText:e.submitText||"Submit",layout:d,columns:f,onSubmit:y});Promise.resolve(c).then(function(){if(d==="grid"&&e.submitSpan==="full"){const s=t.querySelector(".form-buttons");s&&s.classList.add("col-span-full")}w(t,a),e.honeypot!==!1&&i(t),n()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
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
  }
@@ -13,6 +13,19 @@ import path from 'path';
13
13
  import {fileURLToPath} from 'url';
14
14
  import {checkVisibility} from '../middleware/auth.js';
15
15
  import {listEntries} from './collections.js';
16
+ import * as cache from './cache/index.js';
17
+
18
+ /**
19
+ * Bust the response-cache tags that menu changes affect. Public pages are
20
+ * cached with the `nav` and `site` tags (see routes/public.js), so any menu
21
+ * mutation — content, behaviour/style overrides, or slot mapping — must clear
22
+ * them or the navbar/footer/[menu] output stays stale until TTL or restart.
23
+ * No-op when the cache is disabled (e.g. dev / boot-time seeding).
24
+ */
25
+ async function invalidateNavCache() {
26
+ try { await cache.invalidateTags(['nav', 'site']); }
27
+ catch { /* cache not ready (boot) — safe to ignore */ }
28
+ }
16
29
 
17
30
  const __filename = fileURLToPath(import.meta.url);
18
31
  const __dirname = path.dirname(__filename);
@@ -124,6 +137,8 @@ async function readMenuFile(slug) {
124
137
  async function writeMenuFile(menu) {
125
138
  await ensureMenusDir();
126
139
  await fs.writeFile(menuPath(menu.slug), JSON.stringify(menu, null, 2) + '\n', 'utf8');
140
+ // Menu content / behaviour / style changed — drop the cached public pages.
141
+ await invalidateNavCache();
127
142
  }
128
143
 
129
144
  /**
@@ -263,6 +278,7 @@ export async function deleteMenu(slug) {
263
278
  if (!validateSlug(slug)) return false;
264
279
  try {
265
280
  await fs.unlink(menuPath(slug));
281
+ await invalidateNavCache();
266
282
  return true;
267
283
  } catch (err) {
268
284
  if (err.code === 'ENOENT') return false;
@@ -477,6 +493,8 @@ export async function setLocations(map) {
477
493
  }
478
494
  }
479
495
  await fs.writeFile(LOCATIONS_FILE, JSON.stringify(map, null, 2) + '\n', 'utf8');
496
+ // Slot → menu mapping changed (e.g. a different menu now drives the navbar).
497
+ await invalidateNavCache();
480
498
  }
481
499
 
482
500
  // ---------------------------------------------------------------------------
@@ -97,6 +97,7 @@ export async function renderPage(page, opts = {}) {
97
97
  items: navItemsDecorated,
98
98
  variant: navMenu.variant || 'default',
99
99
  position: navMenu.position || 'sticky',
100
+ ...(navMenu.appearOnHover && {appearOnHover: true}),
100
101
  ...(navMenu.style && {style: navMenu.style})
101
102
  }
102
103
  : (await readLegacyNavBackup()) || {brand: site.brand || {}, items: []};
@@ -481,6 +482,7 @@ export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
481
482
  items: navItemsDecorated,
482
483
  variant: navMenu.variant || 'default',
483
484
  position: navMenu.position || 'sticky',
485
+ ...(navMenu.appearOnHover && {appearOnHover: true}),
484
486
  ...(navMenu.style && {style: navMenu.style})
485
487
  }
486
488
  : (await readLegacyNavBackup()) || {brand: site.brand || {}, items: []};
@@ -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
+ }