domma-cms 1.6.59 → 1.7.0

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 (143) hide show
  1. package/CHANGELOG.md +4344 -0
  2. package/README.md +1 -1
  3. package/admin/dist/domma/domma-tools.css +3 -3
  4. package/admin/dist/domma/domma-tools.min.js +3 -3
  5. package/admin/js/api.js +1 -1
  6. package/admin/js/app.js +2 -2
  7. package/admin/js/lib/draft-owner.js +1 -0
  8. package/admin/js/lib/effect-defs.js +1 -1
  9. package/admin/js/lib/effects-builder.js +2 -2
  10. package/admin/js/lib/page-editor-reference.js +5 -5
  11. package/admin/js/lib/shortcode-tags.js +1 -1
  12. package/admin/js/templates/docs/usage-shortcodes.html +39 -0
  13. package/admin/js/views/user-editor.js +1 -1
  14. package/docs/DommaFeatures.md +3005 -0
  15. package/docs/MultiTenant.md +176 -0
  16. package/docs/api-reference.md +1667 -0
  17. package/docs/cache.md +96 -0
  18. package/docs/configuration.md +1811 -0
  19. package/docs/editions-and-licences.md +190 -0
  20. package/docs/getting-started.md +201 -0
  21. package/docs/guides/form-triggers.md +260 -0
  22. package/docs/guides/mongodb-storage-adapter.md +282 -0
  23. package/docs/guides/multi-page-forms.md +121 -0
  24. package/docs/images/scribe-composer.png +0 -0
  25. package/docs/markdown-shortcodes.md +1738 -0
  26. package/docs/pentest-report-2026-04-03.md +181 -0
  27. package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
  28. package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
  29. package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
  30. package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
  31. package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
  32. package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
  33. package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
  34. package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
  35. package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
  36. package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
  37. package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
  38. package/docs/plans/2026-03-05-user-types-design.md +139 -0
  39. package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
  40. package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
  41. package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
  42. package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
  43. package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
  44. package/docs/plans/2026-06-10-core-project-design.md +240 -0
  45. package/docs/plugin-development.md +827 -0
  46. package/docs/recruitment-recipe.md +501 -0
  47. package/docs/scaffolding.md +312 -0
  48. package/docs/screenshots/calculator-floating.png +0 -0
  49. package/docs/screenshots/calculator-standard.png +0 -0
  50. package/docs/screenshots/email-settings.png +0 -0
  51. package/docs/screenshots/messages-count.png +0 -0
  52. package/docs/structure/user-types.md +107 -0
  53. package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
  54. package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
  55. package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
  56. package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
  57. package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
  58. package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
  59. package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
  60. package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
  61. package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
  62. package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
  63. package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
  64. package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
  65. package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
  66. package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
  67. package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
  68. package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
  69. package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
  70. package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
  71. package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
  72. package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
  73. package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
  74. package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
  75. package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
  76. package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
  77. package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
  78. package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
  79. package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
  80. package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
  81. package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
  82. package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
  83. package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
  84. package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
  85. package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
  86. package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
  87. package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
  88. package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
  89. package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
  90. package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
  91. package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
  92. package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
  93. package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
  94. package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
  95. package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
  96. package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
  97. package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
  98. package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
  99. package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
  100. package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
  101. package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
  102. package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
  103. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
  104. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
  105. package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
  106. package/docs/theming.md +320 -0
  107. package/package.json +2 -2
  108. package/plugins/_lib/admin/mail/keyboard.js +49 -0
  109. package/plugins/_lib/admin/mail/reader-view.js +4 -2
  110. package/public/js/celebrations/core/canvas.js +1 -1
  111. package/public/js/celebrations/index.js +1 -1
  112. package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
  113. package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
  114. package/public/js/celebrations/message-attrs.mjs +1 -0
  115. package/public/js/celebrations/themes/anniversary.js +1 -0
  116. package/public/js/celebrations/themes/autumn.js +1 -0
  117. package/public/js/celebrations/themes/birthday.js +1 -0
  118. package/public/js/celebrations/themes/butterfly-flight.js +1 -0
  119. package/public/js/celebrations/themes/calendar.js +1 -0
  120. package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
  121. package/public/js/celebrations/themes/chinese-new-year.js +1 -0
  122. package/public/js/celebrations/themes/christmas.js +1 -1
  123. package/public/js/celebrations/themes/easter.js +1 -0
  124. package/public/js/celebrations/themes/garden-drawing.js +1 -0
  125. package/public/js/celebrations/themes/garden.js +1 -0
  126. package/public/js/celebrations/themes/graduation.js +1 -0
  127. package/public/js/celebrations/themes/greeting.js +1 -0
  128. package/public/js/celebrations/themes/make-it-rain.js +1 -0
  129. package/public/js/celebrations/themes/money-rain.js +1 -0
  130. package/public/js/celebrations/themes/new-year.js +1 -0
  131. package/public/js/celebrations/themes/occasion-drawing.js +1 -0
  132. package/public/js/celebrations/themes/occasions.js +1 -0
  133. package/public/js/celebrations/themes/rain-control.js +1 -0
  134. package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
  135. package/public/js/celebrations/themes/sparkler-font.js +1 -0
  136. package/public/js/celebrations/themes/sparkler-writing.js +1 -0
  137. package/public/js/celebrations/themes/summer-garden.js +1 -0
  138. package/public/js/celebrations/themes/winter.js +1 -0
  139. package/public/js/celebrations/trait-data.mjs +1 -1
  140. package/public/js/effects.js +1 -1
  141. package/scripts/build.js +4 -0
  142. package/server/services/markdown.js +5 -2
  143. package/server/templates/page.html +1 -1
@@ -0,0 +1,466 @@
1
+ # Menus Design
2
+
3
+ **Date:** 2026-05-24
4
+ **Status:** Approved
5
+ **Scope:** Replace `config/navigation.json` (single navbar) and `config/site.json.footer.links` (single footer) with a multi-menu system. Arbitrary named menus, each a tree of items with per-menu style/behaviour overrides, mapped to named locations (slots) for theme injection and renderable inline via a `[menu]` shortcode.
6
+
7
+ ## Problem
8
+
9
+ The CMS today has exactly two implicit menus:
10
+
11
+ - A single navbar described by `config/navigation.json` (`brand`, `items`, `variant`, `position`, `style`). Items support one level of nesting (parent + one dropdown level).
12
+ - A single footer described by `config/site.json` → `footer.links` (`text`, `url`, `hidden`).
13
+
14
+ These are conceptually the same thing - a list of links rendered in a slot - stored in two different config files with two different shapes. Sites that want a third menu (sidebar, members area, legal footer split from primary footer) have no path. Sites that want to render a curated list of links inside page content can only do it by hand-writing Markdown.
15
+
16
+ The recipe scaffolder seeds collections, forms, actions, roles, and users - but cannot seed navigation. As scaffolder recipes become the unit of "install a feature", a plugin or recipe that installs e.g. a members area can create the pages but cannot wire them into the nav.
17
+
18
+ ## Goals
19
+
20
+ - Multiple named menus, each a tree of items with unbounded depth.
21
+ - Per-menu style/behaviour overrides (variant, position, style) that travel with the menu.
22
+ - Named locations (slots) mapped to menus; theme/plugin code renders the menu mapped to its slot.
23
+ - Plugin-contributed locations via a `registerMenuLocation` hook.
24
+ - `[menu]` shortcode renders any menu (by slug or by location) inline anywhere in page content.
25
+ - Per-item visibility gating matching the page-visibility shape.
26
+ - Recipe scaffolder can seed menus and (optionally) map them to locations.
27
+ - Auto-migration from `config/navigation.json` + `site.json.footer.links` on first boot, with a one-release `.bak` read-fallback for safety.
28
+
29
+ ## Non-Goals (YAGNI)
30
+
31
+ - Mega-menu layouts (multi-column dropdowns with sections, images, embedded blocks). Tree depth handles this conceptually; presentation is a follow-up.
32
+ - Per-menu A/B variants or scheduled menus.
33
+ - Drag-and-drop reordering of locations themselves (registry is registered-only; the order is presentational, not behavioural).
34
+ - Multi-site menu sharing.
35
+ - A bespoke renderer for the third level of dropdowns in the CMS layer - that work lands upstream in `domma-js` (see Prerequisites).
36
+
37
+ ## Prerequisites - `domma-js` work
38
+
39
+ Tracked separately; Menus depends on a `domma-js` release containing both:
40
+
41
+ - **Navbar flyout.** Nested `.navbar-dropdown` panels at depth ≥ 2. Hover-on-desktop / click-on-touch. ARIA `aria-haspopup="menu"` and `aria-expanded` on toggles. Keyboard arrow navigation, Home/End, Escape-to-close. Mobile accordion mode below the `md` breakpoint (configurable).
42
+ - **Navbar `position: "floating"`.** New value alongside `sticky | fixed | static`. Renders with viewport margin (configurable via CSS custom property), border-radius, soft shadow. Transitions smoothly to `static` on mobile.
43
+
44
+ The CMS-side renderer assembles the correct nested markup; `domma-js` handles behaviour and styling.
45
+
46
+ ## Data Model
47
+
48
+ ### Per-menu file: `config/menus/<slug>.json`
49
+
50
+ ```json
51
+ {
52
+ "slug": "main",
53
+ "name": "Main navigation",
54
+ "description": "Top-level site nav",
55
+ "variant": "transparent",
56
+ "position": "floating",
57
+ "style": {
58
+ "fontFamily": "Inter",
59
+ "fontSize": "14px",
60
+ "fontWeight": "500",
61
+ "letterSpacing": "0.01em",
62
+ "iconSize": "15px"
63
+ },
64
+ "items": [
65
+ {
66
+ "text": "Home",
67
+ "url": "/",
68
+ "icon": "home",
69
+ "hidden": false,
70
+ "bundled": false,
71
+ "visibility": "public",
72
+ "items": []
73
+ },
74
+ {
75
+ "text": "Resources",
76
+ "url": "/resources",
77
+ "icon": "book-open",
78
+ "items": [
79
+ {
80
+ "text": "Guides",
81
+ "url": "/resources/guides",
82
+ "items": [
83
+ { "text": "Beginner", "url": "/resources/guides/beginner" },
84
+ { "text": "Advanced", "url": "/resources/guides/advanced" }
85
+ ]
86
+ }
87
+ ]
88
+ }
89
+ ],
90
+ "meta": {
91
+ "createdAt": "2026-05-24T10:00:00Z",
92
+ "updatedAt": "2026-05-24T10:00:00Z",
93
+ "bundled": false,
94
+ "presetOwner": null
95
+ }
96
+ }
97
+ ```
98
+
99
+ **Menu fields**
100
+
101
+ | Field | Required | Type | Description |
102
+ |---------------|----------|-------------------------|--------------------------------------------------------------------------------------------|
103
+ | `slug` | yes | string | URL-safe identifier; matches filename |
104
+ | `name` | yes | string | Human-readable label shown in admin |
105
+ | `description` | no | string | Free text |
106
+ | `variant` | no | string | Domma navbar variant - only meaningful in the navbar location |
107
+ | `position` | no | string | `sticky | fixed | static | floating` - only meaningful in the navbar location |
108
+ | `style` | no | object | Font/spacing overrides; applied as CSS custom properties on the menu root |
109
+ | `items` | yes | array | Items tree (depth-unbounded) |
110
+ | `meta` | no | object | Audit / lifecycle metadata |
111
+
112
+ **Item fields**
113
+
114
+ | Field | Required | Type | Description |
115
+ |--------------|----------|----------------------------|-----------------------------------------------------------------------------------|
116
+ | `text` | yes | string | Label |
117
+ | `url` | yes | string | href |
118
+ | `icon` | no | string | Domma icon name |
119
+ | `hidden` | no | boolean | Hard off-switch - never rendered |
120
+ | `bundled` | no | boolean | Preset/plugin-seeded marker (mirrors today's nav editor) |
121
+ | `visibility` | no | string \| string[] | `public | private | <role> | [<role>, ...]` (page-style). Defaults to `public`. |
122
+ | `items` | no | array | Nested children |
123
+
124
+ `visibility` is checked at render time by `checkVisibility(userRole, visibility)` from `server/middleware/auth.js` (already in use for pages). Items the requesting user can't see are removed from the rendered output server-side.
125
+
126
+ ### Slot mapping: `config/menu-locations.json`
127
+
128
+ ```json
129
+ {
130
+ "navbar": "main",
131
+ "footer-primary": "footer",
132
+ "footer-legal": "legal"
133
+ }
134
+ ```
135
+
136
+ Keys are slot names (must exist in the registry). Values are menu slugs (must resolve to a file in `config/menus/`). Unmapped slots render empty. Mappings to non-existent menus log a warning at request time and render empty.
137
+
138
+ ### Locations registry
139
+
140
+ Built-in slots registered by core in `server/services/menus.js`:
141
+
142
+ | Slot | Where it renders | Default `maxDepth` |
143
+ |------------------|------------------------------------------------------|--------------------|
144
+ | `navbar` | `server/templates/page.html` navbar | unbounded *(post-`domma-js` upgrade)* |
145
+ | `footer-primary` | Primary footer column rendered by `public/js/site.js`| 1 |
146
+ | `footer-legal` | Legal/secondary footer column | 1 |
147
+
148
+ Plugins register additional slots through a new hook on `server/services/hooks.js`:
149
+
150
+ ```js
151
+ hooks.registerMenuLocation('sidebar', {
152
+ label: 'Sidebar',
153
+ description: 'Renders in the left sidebar of pages with sidebar layout',
154
+ maxDepth: 1
155
+ });
156
+ ```
157
+
158
+ `maxDepth` is advisory - the admin UI surfaces a warning when a mapped menu's depth exceeds it, but rendering still attempts to flatten beyond the cap.
159
+
160
+ ## Server-side Services
161
+
162
+ ### `server/services/menus.js` (new)
163
+
164
+ | Export | Description |
165
+ |------------------------------|--------------------------------------------------------------------------------|
166
+ | `listMenus()` | Returns array of `{slug, name, description, meta}` (no items) |
167
+ | `getMenu(slug)` | Returns the full menu file as parsed JSON, or `null` |
168
+ | `createMenu(menu)` | Validates + writes a new menu file; refuses on slug collision |
169
+ | `updateMenu(slug, menu)` | Validates + writes; preserves `meta.createdAt`, updates `meta.updatedAt` |
170
+ | `deleteMenu(slug)` | Refuses if the slug appears in `menu-locations.json`; otherwise unlinks file |
171
+ | `duplicateMenu(slug)` | Copies as `<slug>-copy` (next-available suffix) |
172
+ | `getLocations()` | Reads `menu-locations.json` (default `{}`) |
173
+ | `setLocations(map)` | Validates keys against registry + values against existing menus; writes file |
174
+ | `registerLocation(slot, def)`| Core/plugin slot registration |
175
+ | `getLocationRegistry()` | Returns `[{slot, label, description, maxDepth, source}]` |
176
+ | `resolveLocation(slot, user)`| Returns the menu mapped to `slot` with items filtered by `user`'s visibility |
177
+
178
+ Core registers `navbar`, `footer-primary`, `footer-legal` at module load. Plugin hooks fire during `setupPlugin()`.
179
+
180
+ ### Validation
181
+
182
+ Implemented in `server/services/menus.js` and shared by the API + scaffolder paths:
183
+
184
+ - `slug` matches `/^[a-z0-9][a-z0-9-]*$/` (mirrors existing collection slug rules).
185
+ - `items[].url` is a non-empty string starting with `/`, `#`, `http://`, `https://`, or `mailto:`.
186
+ - `items[].visibility` parsed by the same helper as page frontmatter; unknown roles fail validation.
187
+ - Cycle detection on save: no node may appear twice in the tree (slug + position fingerprint).
188
+ - `position` ∈ `{sticky, fixed, static, floating}` when set.
189
+
190
+ ## API Surface
191
+
192
+ `server/routes/api/menus.js` (new) and `server/routes/api/menu-locations.js` (new), registered from `server/server.js`.
193
+
194
+ | Method | Path | Permission | Description |
195
+ |----------|-------------------------------------|------------------|----------------------------------------------------------------------|
196
+ | `GET` | `/api/menus` | `menus.read` | List menus (slug, name, description, meta) |
197
+ | `GET` | `/api/menus/:slug` | `menus.read` | Full menu including items |
198
+ | `POST` | `/api/menus` | `menus.create` | Create a new menu |
199
+ | `PUT` | `/api/menus/:slug` | `menus.update` | Replace a menu |
200
+ | `DELETE` | `/api/menus/:slug` | `menus.delete` | Delete (409 if mapped to a location) |
201
+ | `POST` | `/api/menus/:slug/duplicate` | `menus.create` | Copy as `<slug>-copy` |
202
+ | `GET` | `/api/menu-locations` | `menus.read` | Current slot → menu map |
203
+ | `PUT` | `/api/menu-locations` | `menus.update` | Replace slot map (validates against registry + existing menus) |
204
+ | `GET` | `/api/menu-locations/registry` | `menus.read` | All registered slots (core + plugin-contributed) with metadata |
205
+
206
+ ### Permissions
207
+
208
+ New family `menus.{read, create, update, delete}` added to `server/services/permissionRegistry.js`. Defaults:
209
+
210
+ | Role | read | create | update | delete |
211
+ |-------------|:----:|:------:|:------:|:------:|
212
+ | subscriber | ❌ | ❌ | ❌ | ❌ |
213
+ | editor | ✅ | ❌ | ❌ | ❌ |
214
+ | manager | ✅ | ✅ | ✅ | ❌ |
215
+ | admin | ✅ | ✅ | ✅ | ✅ |
216
+
217
+ ## Public Renderer Integration
218
+
219
+ ### Navbar slot
220
+
221
+ `server/services/renderer.js` currently injects `window.__CMS_NAV__` from `getConfig('navigation')`. Updated flow:
222
+
223
+ 1. Read `menu-locations.json` → resolve slot `navbar` to a menu slug.
224
+ 2. `resolveLocation('navbar', user)` returns the menu with items filtered by visibility.
225
+ 3. Read `site.json.brand` (new top-level field, parallel to existing `adminBrand` - populated by migration; admins edit it from the Settings view in a follow-up task).
226
+ 4. Compose `{brand, items, variant, position, style}`:
227
+ - `items` from the resolved menu.
228
+ - `variant`, `position`, `style` from the menu's own overrides, falling back to defaults.
229
+ 5. Inject as `window.__CMS_NAV__` - key preserved so `public/js/site.js` reads it unchanged.
230
+
231
+ ### Footer slots
232
+
233
+ `public/js/site.js` footer init reads a new injected global `window.__CMS_FOOTER__`:
234
+
235
+ ```js
236
+ window.__CMS_FOOTER__ = {
237
+ primary: [/* items from menu mapped to footer-primary */],
238
+ legal: [/* items from menu mapped to footer-legal */],
239
+ copyright: "© 2026 My Company"
240
+ };
241
+ ```
242
+
243
+ `copyright` stays on `site.json`. Footer items come from the mapped menus.
244
+
245
+ ### `[menu]` shortcode
246
+
247
+ Processed by `server/services/markdown.js` after grids and cards (same processing tier as `[block]` / `[view]` / `[collection]`).
248
+
249
+ ```
250
+ [menu slug="legal" /]
251
+ [menu location="footer-primary" /]
252
+ [menu slug="legal" variant="inline" class="prose-nav" /]
253
+ [menu slug="legal" depth="2" /]
254
+ ```
255
+
256
+ **Attributes**
257
+
258
+ | Attr | Description |
259
+ |------------|------------------------------------------------------------------------------------------|
260
+ | `slug` | Render this menu by slug. One of `slug` or `location` required. |
261
+ | `location` | Render whichever menu is currently mapped to this slot. |
262
+ | `variant` | Optional render variant override |
263
+ | `class` | Extra CSS class on the wrapper |
264
+ | `depth` | Cap render depth (integer ≥ 1); items deeper than this are dropped from the output |
265
+
266
+ **Output**
267
+
268
+ ```html
269
+ <nav class="dm-menu dm-menu--legal {class}" data-menu="legal">
270
+ <ul>
271
+ <li><a href="/privacy">Privacy</a></li>
272
+ <li>
273
+ <a href="/legal">Legal</a>
274
+ <ul>
275
+ <li><a href="/legal/terms">Terms</a></li>
276
+ </ul>
277
+ </li>
278
+ </ul>
279
+ </nav>
280
+ ```
281
+
282
+ Visibility filtering runs server-side per request, so each user sees only the items their role permits.
283
+
284
+ ## Admin UI
285
+
286
+ ### Sidebar entry
287
+
288
+ "Navigation" sidebar item is renamed to "Menus" and points at `#/menus`. A hash redirect from `#/navigation` to `#/menus` preserves any bookmarked links during the transition.
289
+
290
+ ### `#/menus` - list view
291
+
292
+ `admin/js/views/menus.js` + `admin/js/templates/menus.html`.
293
+
294
+ `T.create()` table:
295
+
296
+ | Column | Content |
297
+ |----------|-------------------------------------------------------------------------------|
298
+ | Slug | Menu slug |
299
+ | Name | Human-readable name |
300
+ | Items | Top-level item count, with a `(depth N)` suffix when the tree depth > 1 |
301
+ | Mapped | Chips for each location the menu is mapped to (`navbar`, `footer-primary`) |
302
+ | Bundled | Badge if `meta.bundled === true` |
303
+ | Actions | Edit · Duplicate · Delete (Delete disabled with tooltip if mapped) |
304
+
305
+ Top-right toolbar: "New menu" button, "Manage locations" button (opens `#/menu-locations`).
306
+
307
+ ### `#/menus/:slug` - per-menu editor
308
+
309
+ `admin/js/views/menu-editor.js` + `admin/js/templates/menu-editor.html`.
310
+
311
+ Three tabs (using `E.tabs()` with the verified class names - `.tabs`, `.tab-list`, `button.tab-item`, `.tab-panel`):
312
+
313
+ **Items tab.** Tree editor with:
314
+
315
+ - Recursive item rows, indented per depth level
316
+ - Drag-to-reparent (HTML5 drag-and-drop on row handles)
317
+ - Indent / outdent buttons per row (keyboard fallback to drag-and-drop)
318
+ - Per-row fields: text, url, icon, visibility chooser (`F.create` blueprint reused from page editor)
319
+ - Per-row toggles: bundled, hidden, delete
320
+ - "Add item" button inserts a new item at root
321
+ - Move-up / move-down arrows for ordering within a parent
322
+
323
+ **Behaviour tab.** `F.create` blueprint:
324
+
325
+ - `variant` - select (`transparent`, `dark`, `light`, `default`)
326
+ - `position` - select (`sticky`, `fixed`, `static`, `floating`)
327
+ - `style` group - text inputs for `fontFamily`, `fontSize`, `fontWeight`, `letterSpacing`, `iconSize`
328
+
329
+ **Meta tab.** `F.create` blueprint:
330
+
331
+ - `name`, `description`
332
+ - `slug` - rename (with `await E.confirm()` warning if the menu is currently mapped to any location or referenced by any `[menu slug="..."]` shortcode found in pages)
333
+
334
+ Save sends `PUT /api/menus/:slug`. On rename, the editor issues a three-request sequence: `POST /api/menus` (write the menu under the new slug) → `PUT /api/menu-locations` (rewrite any slot mapped to the old slug to point at the new one) → `DELETE /api/menus/<old>` (now unmapped, deletion succeeds). If the third call fails for any reason, the new menu and updated mappings are already live, so the worst-case state is a leftover orphan menu file under the old slug - surfaced for cleanup in the menus list rather than a broken site.
335
+
336
+ ### `#/menu-locations` - locations editor
337
+
338
+ `admin/js/views/menu-locations.js`.
339
+
340
+ Table of registered slots from `/api/menu-locations/registry`:
341
+
342
+ | Column | Content |
343
+ |-------------|----------------------------------------------------------------------|
344
+ | Slot | `slot` (machine name) |
345
+ | Label | `def.label` (human-readable) |
346
+ | Source | `core` or plugin name |
347
+ | Description | `def.description` |
348
+ | Mapped menu | `<select>` of menu slugs (+ "- none -" for unmapped) |
349
+ | Warnings | If selected menu's depth > `maxDepth`, surfaces an inline warning |
350
+
351
+ Save sends `PUT /api/menu-locations` with the complete map.
352
+
353
+ ## Migration (Auto on Boot)
354
+
355
+ `server/services/menus-migration.js` (new). Called from `server/server.js` startup, after config load, before route registration.
356
+
357
+ ```js
358
+ async function migrateLegacyNavIfPresent() {
359
+ // 1. Ensure config/menus/ exists
360
+ // 2. If config/menu-locations.json exists → already migrated, return
361
+ // 3. If config/navigation.json present:
362
+ // - Write config/menus/main.json with slug='main', name='Main navigation',
363
+ // and the items + variant + position + style from navigation.json
364
+ // - Copy navigation.json.brand into site.json under a new top-level
365
+ // `brand` field (parallel to the existing `adminBrand`). The public
366
+ // renderer reads site.json.brand when composing the navbar.
367
+ // - Record navbar → main in locations map
368
+ // - Rename original to config/navigation.json.bak
369
+ // 4. If site.json.footer.links.length:
370
+ // - Convert to config/menus/footer.json
371
+ // - Record footer-primary → footer in locations map
372
+ // - Set site.json.footer.links = [] (preserve copyright)
373
+ // 5. Write config/menu-locations.json
374
+ // 6. Log:
375
+ // [menus] Migrated navigation.json → menus/main.json + locations.navbar
376
+ // [menus] Migrated footer.links → menus/footer.json + locations.footer-primary
377
+ }
378
+ ```
379
+
380
+ Migration is one-shot - re-running has no effect (the existence of `config/menu-locations.json` is the guard).
381
+
382
+ ### Read-fallback (one release)
383
+
384
+ If the navbar location resolves to no menu (admin deleted it, mapping is empty, or the file was lost) **and** `config/navigation.json.bak` exists, the renderer falls back to reading the `.bak` so a site that restored its backup keeps rendering. Logged at WARN level:
385
+
386
+ ```
387
+ [menus] navbar location unmapped - falling back to navigation.json.bak (will be removed in v2)
388
+ ```
389
+
390
+ The fallback is documented as deprecated and removed in the next major version.
391
+
392
+ ## Scaffolder Integration
393
+
394
+ Recipe format (`server/services/recipes/*.json`) gains an optional `menus` array, parallel to `collection`, `form`, `actions`:
395
+
396
+ ```json
397
+ "menus": [
398
+ {
399
+ "slug": "{{menuSlug}}",
400
+ "name": "{{menuName}}",
401
+ "items": [
402
+ { "text": "Members", "url": "/members" },
403
+ { "text": "Profile", "url": "/members/profile" }
404
+ ],
405
+ "variant": "default",
406
+ "position": "sticky",
407
+ "locations": ["sidebar"]
408
+ }
409
+ ]
410
+ ```
411
+
412
+ `server/services/scaffolder.js` gains a new step in the apply pipeline, **between roles and collection**:
413
+
414
+ 1. Pre-flight: refuse if any menu slug already exists.
415
+ 2. Apply: validate + write each menu file. If `locations[]` present, validate each slot against the registry; refuse to overwrite an existing mapping unless the recipe entry sets `force: true`.
416
+
417
+ Failures degrade gracefully - a single conflicting menu slug aborts pre-flight (consistent with existing recipe behaviour); a missing slot reference logs a warning and continues.
418
+
419
+ The Projects feature (next brainstorm) builds on this: a Project IS a recipe's output, so Projects will own menus alongside collections, forms, and actions under the project's namespace.
420
+
421
+ ## Caching
422
+
423
+ New cache tags registered with `server/services/cache/index.js`:
424
+
425
+ | Tag | Invalidated when |
426
+ |-----------------------|---------------------------------------------------------------------------------|
427
+ | `menu:<slug>` | The menu is created / updated / deleted |
428
+ | `menu-locations` | The slot map is saved |
429
+ | `nav` *(preserved)* | Either of the above, when the affected menu/slot is the one feeding the navbar |
430
+
431
+ Per-request cache key composition is unchanged. The renderer's existing per-role cache key (`role${userRole ?? 'anon'}`) gates visibility-filtered menus correctly - anonymous users and members see different navs but each tier is cached.
432
+
433
+ ## Plugin Hook
434
+
435
+ `server/services/hooks.js` gains `registerMenuLocation(slot, def)`. Definition shape:
436
+
437
+ | Field | Required | Description |
438
+ |---------------|----------|------------------------------------------------------|
439
+ | `label` | yes | Human-readable slot name shown in admin |
440
+ | `description` | no | What the slot is for and where it renders |
441
+ | `maxDepth` | no | Advisory depth cap; admin warns on overflow |
442
+
443
+ Slot names must match `/^[a-z0-9][a-z0-9-]*$/` (same shape as menu slugs). Re-registering an existing slot logs a warning and the second registration wins (last-write semantics - the same pattern used by other plugin hooks).
444
+
445
+ ## Documentation Updates
446
+
447
+ - `docs/configuration.md` - replace the `config/navigation.json` section with `config/menus/` and `config/menu-locations.json`; document the menu item shape and the location registry.
448
+ - `docs/markdown-shortcodes.md` - add `[menu]` entry alongside `[block]` / `[view]` / `[collection]`.
449
+ - `docs/plugin-development.md` - document `hooks.registerMenuLocation()` alongside existing hooks.
450
+ - `docs/scaffolding.md` - add the `menus` branch to the recipe format reference.
451
+ - `CLAUDE.md` (project) - update the Domma navbar dropdown section to note multi-menu architecture and link to the new docs.
452
+
453
+ ## Out of Scope (Recorded for Later)
454
+
455
+ - **Mega-menu layouts.** Tree depth supports the data; presentation belongs in a follow-up.
456
+ - **Per-menu A/B variants / scheduled menus.** No current use case.
457
+ - **Drag-to-reorder locations.** Registry order is presentational only.
458
+ - **Multi-site menu sharing.** Out of scope until multi-site is.
459
+ - **Built-in `sidebar` slot.** A sidebar slot is shown as a plugin-registration example in section 3, not registered by core. Adding a default sidebar layout + a core-registered `sidebar` slot is a separate spec.
460
+
461
+ ## Open Follow-Ups (Tracked)
462
+
463
+ - **Projects feature** - next brainstorm cycle. Recipe-apply produces a Project; Menus is one of the things a Project owns.
464
+ - **Bundled-menu lifecycle parity** - extend the bundled/plugin metadata machinery (memory: `project_bundled_plugin_system.md`) to cover menus so plugins can install/teardown menus cleanly.
465
+ - **`domma-js` flyout + floating PR** - prerequisite; lands before Menus ships.
466
+ - **Settings UI for `site.json.brand`.** Migration creates the field; an editor in the Settings admin view lets admins update brand text/icon/logo/url without hand-editing JSON. Small follow-up - the same form blueprint pattern as `adminBrand`.