domma-cms 1.6.60 → 1.7.1

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 (168) hide show
  1. package/CHANGELOG.md +4349 -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/lib/effect-defs.js +1 -1
  6. package/admin/js/lib/effects-builder.js +2 -2
  7. package/admin/js/lib/mail-compose.js +1 -0
  8. package/admin/js/lib/page-editor-reference.js +5 -5
  9. package/admin/js/lib/shortcode-tags.js +1 -1
  10. package/admin/js/templates/call-new.html +1 -1
  11. package/admin/js/templates/docs/usage-shortcodes.html +39 -0
  12. package/admin/js/views/call-new.js +1 -1
  13. package/admin/js/views/contacts.js +15 -15
  14. package/admin/js/views/form-submissions.js +2 -2
  15. package/docs/DommaFeatures.md +3005 -0
  16. package/docs/MultiTenant.md +176 -0
  17. package/docs/api-reference.md +1667 -0
  18. package/docs/cache.md +96 -0
  19. package/docs/configuration.md +1811 -0
  20. package/docs/editions-and-licences.md +190 -0
  21. package/docs/getting-started.md +201 -0
  22. package/docs/guides/form-triggers.md +260 -0
  23. package/docs/guides/mongodb-storage-adapter.md +282 -0
  24. package/docs/guides/multi-page-forms.md +121 -0
  25. package/docs/images/scribe-composer.png +0 -0
  26. package/docs/markdown-shortcodes.md +1738 -0
  27. package/docs/pentest-report-2026-04-03.md +181 -0
  28. package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
  29. package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
  30. package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
  31. package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
  32. package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
  33. package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
  34. package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
  35. package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
  36. package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
  37. package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
  38. package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
  39. package/docs/plans/2026-03-05-user-types-design.md +139 -0
  40. package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
  41. package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
  42. package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
  43. package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
  44. package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
  45. package/docs/plans/2026-06-10-core-project-design.md +240 -0
  46. package/docs/plugin-development.md +827 -0
  47. package/docs/recruitment-recipe.md +501 -0
  48. package/docs/scaffolding.md +312 -0
  49. package/docs/screenshots/calculator-floating.png +0 -0
  50. package/docs/screenshots/calculator-standard.png +0 -0
  51. package/docs/screenshots/email-settings.png +0 -0
  52. package/docs/screenshots/messages-count.png +0 -0
  53. package/docs/structure/user-types.md +107 -0
  54. package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
  55. package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
  56. package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
  57. package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
  58. package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
  59. package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
  60. package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
  61. package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
  62. package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
  63. package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
  64. package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
  65. package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
  66. package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
  67. package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
  68. package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
  69. package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
  70. package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
  71. package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
  72. package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
  73. package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
  74. package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
  75. package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
  76. package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
  77. package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
  78. package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
  79. package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
  80. package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
  81. package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
  82. package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
  83. package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
  84. package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
  85. package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
  86. package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
  87. package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
  88. package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
  89. package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
  90. package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
  91. package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
  92. package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
  93. package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
  94. package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
  95. package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
  96. package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
  97. package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
  98. package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
  99. package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
  100. package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
  101. package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
  102. package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
  103. package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
  104. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
  105. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
  106. package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
  107. package/docs/theming.md +320 -0
  108. package/package.json +2 -2
  109. package/plugins/_lib/admin/mail/compose-window.js +3 -1
  110. package/plugins/_lib/admin/mail/identity.js +18 -3
  111. package/plugins/_lib/admin/mail/reader-view.js +32 -3
  112. package/plugins/blog/CLAUDE.md +4 -0
  113. package/plugins/blog/admin/lib/email.js +191 -0
  114. package/plugins/blog/admin/views/comments.js +3 -1
  115. package/plugins/blog/plugin.json +2 -2
  116. package/plugins/blog/plugin.public.js +1 -1
  117. package/plugins/blog/tests/email.test.js +69 -0
  118. package/plugins/booking/CLAUDE.md +4 -0
  119. package/plugins/booking/admin/lib/email.js +191 -0
  120. package/plugins/booking/admin/views/panels.js +7 -3
  121. package/plugins/booking/plugin.json +1 -1
  122. package/plugins/booking/tests/email.test.js +69 -0
  123. package/plugins/free-tier.lock.json +27 -19
  124. package/plugins/shopping-cart/CLAUDE.md +4 -0
  125. package/plugins/shopping-cart/admin/lib/email.js +191 -0
  126. package/plugins/shopping-cart/admin/views/orders.js +11 -3
  127. package/plugins/shopping-cart/plugin.json +2 -2
  128. package/plugins/shopping-cart/tests/email.test.js +69 -0
  129. package/plugins/support/CLAUDE.md +4 -0
  130. package/plugins/support/admin/lib/email.js +191 -0
  131. package/plugins/support/admin/views/desk.js +4 -1
  132. package/plugins/support/admin/views/inbox.js +6 -2
  133. package/plugins/support/plugin.json +2 -2
  134. package/plugins/support/tests/email.test.js +69 -0
  135. package/public/js/celebrations/core/canvas.js +1 -1
  136. package/public/js/celebrations/index.js +1 -1
  137. package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
  138. package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
  139. package/public/js/celebrations/message-attrs.mjs +1 -0
  140. package/public/js/celebrations/themes/anniversary.js +1 -0
  141. package/public/js/celebrations/themes/autumn.js +1 -0
  142. package/public/js/celebrations/themes/birthday.js +1 -0
  143. package/public/js/celebrations/themes/butterfly-flight.js +1 -0
  144. package/public/js/celebrations/themes/calendar.js +1 -0
  145. package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
  146. package/public/js/celebrations/themes/chinese-new-year.js +1 -0
  147. package/public/js/celebrations/themes/christmas.js +1 -1
  148. package/public/js/celebrations/themes/easter.js +1 -0
  149. package/public/js/celebrations/themes/garden-drawing.js +1 -0
  150. package/public/js/celebrations/themes/garden.js +1 -0
  151. package/public/js/celebrations/themes/graduation.js +1 -0
  152. package/public/js/celebrations/themes/greeting.js +1 -0
  153. package/public/js/celebrations/themes/make-it-rain.js +1 -0
  154. package/public/js/celebrations/themes/money-rain.js +1 -0
  155. package/public/js/celebrations/themes/new-year.js +1 -0
  156. package/public/js/celebrations/themes/occasion-drawing.js +1 -0
  157. package/public/js/celebrations/themes/occasions.js +1 -0
  158. package/public/js/celebrations/themes/rain-control.js +1 -0
  159. package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
  160. package/public/js/celebrations/themes/sparkler-font.js +1 -0
  161. package/public/js/celebrations/themes/sparkler-writing.js +1 -0
  162. package/public/js/celebrations/themes/summer-garden.js +1 -0
  163. package/public/js/celebrations/themes/winter.js +1 -0
  164. package/public/js/celebrations/trait-data.mjs +1 -1
  165. package/public/js/effects.js +1 -1
  166. package/scripts/build.js +4 -0
  167. package/server/services/markdown.js +5 -2
  168. package/server/templates/page.html +1 -1
@@ -0,0 +1,578 @@
1
+ # Nav Hidden Items Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or
4
+ > superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
5
+
6
+ **Goal:** Add a per-item "hidden" toggle to the navigation editor so items and footer links can be suppressed from the
7
+ public site without deleting them.
8
+
9
+ **Architecture:** A `hidden: true` flag is stored optionally on each nav item and footer link in their respective config
10
+ files. The admin editor toggles this flag in-memory and saves it via the existing Save button. At render time,
11
+ `renderer.js` strips hidden items before serialising `__CMS_NAV__` into the public page - hidden data never reaches the
12
+ browser.
13
+
14
+ **Tech Stack:** Fastify 5 (ESM) backend, Domma SPA admin (Domma JS framework), CSS in `admin/css/admin.css`.
15
+
16
+ **Spec:** `docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md`
17
+
18
+ ---
19
+
20
+ ## File Map
21
+
22
+ | File | What changes |
23
+ |--------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
24
+ | `server/services/renderer.js` | Add `filterHiddenNavItems()` helper; apply before building `navJson` and `siteJson` |
25
+ | `admin/css/admin.css` | Add `.nav-item-row--hidden` and `.btn-toggle-hidden.active` styles |
26
+ | `admin/js/views/navigation.js` | `flattenItems`, `nestItems`, `footerLinks` init, `syncFromDom`, `syncFooterFromDom`, `renderItems`, `renderFooterLinks`, click handlers |
27
+ | `admin/js/templates/navigation.html` | Add "Actions" label to both `nav-col-action` header spans |
28
+
29
+ ---
30
+
31
+ ## Task 1: Server-side filtering
32
+
33
+ **Files:**
34
+
35
+ - Modify: `server/services/renderer.js`
36
+
37
+ The goal is to strip `hidden: true` items before the nav config is JSON-serialised into the public page. The filter must
38
+ handle:
39
+
40
+ - Top-level items (hidden parent → entire entry gone, children included)
41
+ - Children of visible parents (hidden child → removed from parent's `items` array)
42
+ - Footer links in `site.footer.links`
43
+
44
+ The codebase only supports two levels of nesting, so no recursive traversal is needed.
45
+
46
+ - [ ] **Step 1.1: Open `server/services/renderer.js` and add the filter helper**
47
+
48
+ Add this function before the JSDoc block that documents `renderPage` (the JSDoc starts at line 33 with
49
+ `/** Render a page...`). Do NOT insert it between the JSDoc and the function signature - place it before line 33:
50
+
51
+ ```js
52
+ /**
53
+ * Strip hidden items from the navigation config before public injection.
54
+ * Returns a shallow clone - never mutates the cached config object.
55
+ *
56
+ * @param {object} nav - Raw navigation config from getConfig('navigation')
57
+ * @returns {object} Filtered navigation object safe for public injection
58
+ */
59
+ function filterHiddenNavItems(nav) {
60
+ const items = (nav.items || [])
61
+ .filter(item => !item.hidden)
62
+ .map(item => {
63
+ if (!item.items?.length) return item;
64
+ const visibleChildren = item.items.filter(c => !c.hidden);
65
+ return visibleChildren.length === item.items.length
66
+ ? item
67
+ : { ...item, items: visibleChildren };
68
+ });
69
+ return { ...nav, items };
70
+ }
71
+ ```
72
+
73
+ - [ ] **Step 1.2: Apply the filter in `renderPage`**
74
+
75
+ Find line 101 in `renderer.js`:
76
+ ```js
77
+ navJson: JSON.stringify(navigation).replace(/<\/script>/gi, '<\\/script>'),
78
+ ```
79
+
80
+ Replace it with:
81
+ ```js
82
+ navJson: JSON.stringify(filterHiddenNavItems(navigation)).replace(/<\/script>/gi, '<\\/script>'),
83
+ ```
84
+
85
+ - [ ] **Step 1.3: Filter footer links in `siteJson`**
86
+
87
+ Find the `siteJson` block (lines 102-107). Replace the opening of `Object.assign`:
88
+
89
+ From:
90
+ ```js
91
+ siteJson: JSON.stringify(Object.assign(
92
+ { title: site.title, footer: site.footer, social: site.social || null },
93
+ ```
94
+
95
+ To:
96
+ ```js
97
+ siteJson: JSON.stringify(Object.assign(
98
+ {
99
+ title: site.title,
100
+ footer: site.footer
101
+ ? {
102
+ ...site.footer,
103
+ links: (site.footer.links || []).filter(l => !l.hidden)
104
+ }
105
+ : site.footer,
106
+ social: site.social || null
107
+ },
108
+ ```
109
+
110
+ - [ ] **Step 1.4: Verify server-side filtering manually**
111
+
112
+ Temporarily edit `config/navigation.json` - add `"hidden": true` to one top-level item (e.g. "About"):
113
+
114
+ ```json
115
+ { "text": "About", "url": "/about", "icon": "info", "hidden": true }
116
+ ```
117
+
118
+ Then run (`getConfig` reads fresh from disk on every call - confirmed from `server/config.js` line 30-32:
119
+ `return JSON.parse(fs.readFileSync(...))` - no caching, no restart needed):
120
+ ```bash
121
+ curl -s http://localhost:4096/ | grep -o '__CMS_NAV__ = [^;]*' | head -c 500
122
+ ```
123
+
124
+ Expected: the "About" item is **absent** from the JSON blob in the output.
125
+
126
+ - [ ] **Revert the manual edit to `navigation.json`:**
127
+
128
+ ```bash
129
+ git checkout config/navigation.json
130
+ ```
131
+
132
+ Confirm the file is clean:
133
+ ```bash
134
+ git diff config/navigation.json
135
+ ```
136
+ Expected: no output (no changes).
137
+
138
+ - [ ] **Step 1.5: Commit**
139
+
140
+ ```bash
141
+ git add server/services/renderer.js
142
+ git commit -m "feat: strip hidden nav items/footer links at render time"
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Task 2: Admin CSS
148
+
149
+ **Files:**
150
+
151
+ - Modify: `admin/css/admin.css`
152
+
153
+ - [ ] **Step 2.1: Add hidden-row styles**
154
+
155
+ Find the nav section in `admin/css/admin.css` (around line 423, just after `.nav-item-row--child`). Add:
156
+
157
+ ```css
158
+ .nav-col-action { display: flex; align-items: center; gap: .25rem; }
159
+
160
+ .nav-item-row--hidden { opacity: 0.45; }
161
+ .nav-item-row--hidden .item-text,
162
+ .nav-item-row--hidden .footer-link-text { text-decoration: line-through; }
163
+
164
+ .btn-toggle-hidden { color: var(--text-muted, #888); }
165
+ .btn-toggle-hidden.active { color: var(--color-warning, #f59e0b); }
166
+ ```
167
+
168
+ The `nav-col-action` flex rule is required - the span now holds two buttons and without it they render with no
169
+ controlled spacing. The amber `color-warning` gives the active (item-is-hidden) button state a distinct colour.
170
+
171
+ - [ ] **Step 2.2: Visual regression check**
172
+
173
+ Open `http://localhost:4096/admin/#/navigation` in a browser. Layout should look identical to before - no new buttons
174
+ yet, just verifying the CSS additions don't break anything.
175
+
176
+ - [ ] **Step 2.3: Commit**
177
+
178
+ ```bash
179
+ git add admin/css/admin.css
180
+ git commit -m "feat: add hidden nav row styles"
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Task 3: Update `navigation.js` - data model
186
+
187
+ **Files:**
188
+
189
+ - Modify: `admin/js/views/navigation.js`
190
+
191
+ **Note on `_nextId`:** This module-level counter never resets across view mounts - this is intentional. IDs only need to
192
+ be unique within a session (not persistent), and an ever-incrementing counter guarantees that. Do not reset it.
193
+
194
+ **Note on `.attr()` vs `.data()` - plan overrides spec:** The spec (line 41) suggests `.data('hidden') === 'true'`.
195
+ This plan overrides that with `.attr('data-hidden') === 'true'` because Domma's `.data()` (jQuery-compatible) auto-coerces
196
+ `"true"`/`"false"` attribute strings to actual booleans, making the `=== 'true'` string comparison always false. Using
197
+ `.attr('data-hidden')` returns the raw string and makes the comparison work correctly. The spec is wrong on this specific
198
+ point; follow the plan.
199
+
200
+ **Note on `.data()` for numeric IDs:** The toggle click handlers use `$(this).data('id')` and `$(this).data('idx')` to
201
+ read numeric IDs. `.data()` auto-coercing `"123"` → `123` is harmless (the `parseInt` call is a safe no-op on a
202
+ number). The `.attr()` rule applies specifically to the boolean `data-hidden` attribute.
203
+
204
+ ### 3a - `flattenItems`: read `hidden` from JSON
205
+
206
+ - [ ] **Step 3a.1: Update `flattenItems` (lines 10-20)**
207
+
208
+ Replace the whole function with:
209
+
210
+ ```js
211
+ function flattenItems(items) {
212
+ const flat = [];
213
+ (items || []).forEach(item => {
214
+ const id = _nextId++;
215
+ flat.push({ _id: id, text: item.text || '', url: item.url || '', icon: item.icon || '', hidden: item.hidden || false, parentId: null });
216
+ (item.items || item.children || []).forEach(child => {
217
+ flat.push({ _id: _nextId++, text: child.text || '', url: child.url || '', icon: child.icon || '', hidden: child.hidden || false, parentId: id });
218
+ });
219
+ });
220
+ return flat;
221
+ }
222
+ ```
223
+
224
+ ### 3b - `nestItems`: pass `hidden` through to JSON
225
+
226
+ - [ ] **Step 3b.1: Update `nestItems` (lines 22-33)**
227
+
228
+ Replace the whole function with:
229
+
230
+ ```js
231
+ function nestItems(flat) {
232
+ return flat
233
+ .filter(i => i.parentId === null)
234
+ .map(parent => {
235
+ const children = flat
236
+ .filter(i => i.parentId === parent._id)
237
+ .map(c => ({
238
+ text: c.text,
239
+ url: c.url,
240
+ ...(c.icon && { icon: c.icon }),
241
+ ...(c.hidden && { hidden: true }),
242
+ }));
243
+ const obj = {
244
+ text: parent.text,
245
+ url: parent.url,
246
+ ...(parent.icon && { icon: parent.icon }),
247
+ ...(parent.hidden && { hidden: true }),
248
+ };
249
+ if (children.length) obj.items = children;
250
+ return obj;
251
+ });
252
+ }
253
+ ```
254
+
255
+ ### 3c - `footerLinks` initialisation: preserve `hidden`
256
+
257
+ - [ ] **Step 3c.1: Update the `footerLinks` initialisation in `onMount` (line 54)**
258
+
259
+ Find:
260
+ ```js
261
+ let footerLinks = (site.footer?.links || []).map(l => ({text: l.text || '', url: l.url || ''}));
262
+ ```
263
+
264
+ Replace with:
265
+ ```js
266
+ let footerLinks = (site.footer?.links || []).map(l => ({
267
+ text: l.text || '',
268
+ url: l.url || '',
269
+ ...(l.hidden && { hidden: true }),
270
+ }));
271
+ ```
272
+
273
+ **Why this matters:** Without this, any `hidden: true` flags that already exist in `site.json` will be silently
274
+ stripped when the view mounts. The first save after opening the editor would then write back the links without the
275
+ `hidden` flag, permanently losing the state.
276
+
277
+ ### 3d - `#add-nav-item` and `#add-footer-link` handlers: include `hidden`
278
+
279
+ Edit these in document order (line 136 first, then line 156).
280
+
281
+ - [ ] **Step 3d.1: Add `hidden` to the `#add-footer-link` push (line 136)**
282
+
283
+ Find:
284
+ ```js
285
+ footerLinks.push({text: '', url: ''});
286
+ ```
287
+
288
+ Replace with:
289
+ ```js
290
+ footerLinks.push({ text: '', url: '', hidden: false });
291
+ ```
292
+
293
+ - [ ] **Step 3d.2: Add `hidden: false` to the `#add-nav-item` push (line 156)**
294
+
295
+ Find:
296
+ ```js
297
+ flatItems.push({ _id: _nextId++, text: '', url: '', icon: '', parentId: null });
298
+ ```
299
+
300
+ Replace with:
301
+ ```js
302
+ flatItems.push({ _id: _nextId++, text: '', url: '', icon: '', hidden: false, parentId: null });
303
+ ```
304
+
305
+ *(New items default to visible - `hidden: false` keeps the model consistent so `renderItems`/`renderFooterLinks` never encounter `undefined`.)*
306
+
307
+ ### 3e - `syncFromDom`: read `hidden` from toggle button
308
+
309
+ - [ ] **Step 3e.1: Update `syncFromDom` (lines 56-72)**
310
+
311
+ Use `.attr('data-hidden')` (raw string) not `.data('hidden')` (auto-coerces to boolean). Also scope the selector to
312
+ `#nav-items-list .nav-item-row` so footer link rows (which share the `.nav-item-row` class) are not accidentally
313
+ matched:
314
+
315
+ ```js
316
+ const syncFromDom = () => {
317
+ $container.find('#nav-items-list .nav-item-row').each(function () {
318
+ const id = parseInt($(this).data('id'), 10);
319
+ const item = flatItems.find(i => i._id === id);
320
+ if (!item) return;
321
+ item.text = $(this).find('.item-text').val();
322
+ item.url = $(this).find('.item-url').val();
323
+ item.icon = $(this).find('.item-icon').val();
324
+ item.hidden = $(this).find('.btn-toggle-hidden').attr('data-hidden') === 'true';
325
+ const parentVal = $(this).find('.item-parent').val();
326
+ item.parentId = parentVal ? parseInt(parentVal, 10) : null;
327
+ });
328
+ // Promote children whose parent is now itself a child
329
+ const childIds = new Set(flatItems.filter(i => i.parentId !== null).map(i => i._id));
330
+ flatItems.forEach(i => {
331
+ if (i.parentId !== null && childIds.has(i.parentId)) i.parentId = null;
332
+ });
333
+ };
334
+ ```
335
+
336
+ ### 3f - `syncFooterFromDom`: read `hidden` from toggle button
337
+
338
+ - [ ] **Step 3f.1: Update `syncFooterFromDom` (lines 124-132)**
339
+
340
+ Use `.attr('data-hidden')` (raw string), same reasoning as `syncFromDom`:
341
+
342
+ ```js
343
+ const syncFooterFromDom = () => {
344
+ footerLinks = [];
345
+ $container.find('#footer-links-list .nav-item-row').each(function () {
346
+ const hidden = $(this).find('.btn-toggle-hidden').attr('data-hidden') === 'true';
347
+ const link = {
348
+ text: $(this).find('.footer-link-text').val().trim(),
349
+ url: $(this).find('.footer-link-url').val().trim(),
350
+ };
351
+ if (hidden) link.hidden = true;
352
+ footerLinks.push(link);
353
+ });
354
+ };
355
+ ```
356
+
357
+ **Note on blank+hidden links:** If a footer link has no text or URL but is marked hidden, it will still be dropped by
358
+ the existing `validFooterLinks` filter in the save handler (`footerLinks.filter(l => l.text || l.url)`). This is
359
+ acceptable - a blank link has no value regardless of its hidden state.
360
+
361
+ - [ ] **Step 3f.2: Commit the data-model changes**
362
+
363
+ ```bash
364
+ git add admin/js/views/navigation.js
365
+ git commit -m "feat: propagate hidden flag through nav flattenItems/nestItems/sync"
366
+ ```
367
+
368
+ ---
369
+
370
+ ## Task 4: Update `navigation.js` - render + interaction
371
+
372
+ **Files:**
373
+
374
+ - Modify: `admin/js/views/navigation.js`
375
+
376
+ ### 4a - `renderItems`: add toggle button, apply hidden class
377
+
378
+ - [ ] **Step 4a.1: Replace `renderItems` (lines 74-103)**
379
+
380
+ ```js
381
+ const renderItems = () => {
382
+ const $list = $container.find('#nav-items-list').empty();
383
+ const topLevel = flatItems.filter(i => i.parentId === null);
384
+
385
+ getOrderedItems(flatItems).forEach(item => {
386
+ const isChild = item.parentId !== null;
387
+ const isHidden = item.hidden === true;
388
+ const parentOptions = `<option value="">- top-level -</option>` +
389
+ topLevel
390
+ .filter(t => t._id !== item._id)
391
+ .map(t => `<option value="${t._id}"${t._id === item.parentId ? ' selected' : ''}>${esc(t.text) || '(untitled)'}</option>`)
392
+ .join('');
393
+
394
+ $list.append(`
395
+ <div class="nav-item-row${isChild ? ' nav-item-row--child' : ''}${isHidden ? ' nav-item-row--hidden' : ''}" data-id="${item._id}">
396
+ <span class="nav-col-indent">${isChild ? '↳' : ''}</span>
397
+ <input type="text" class="form-input item-text nav-col-main" value="${esc(item.text)}" placeholder="Label">
398
+ <input type="text" class="form-input item-url nav-col-main" value="${esc(item.url)}" placeholder="/url">
399
+ <input type="text" class="form-input item-icon nav-col-icon" value="${esc(item.icon)}" placeholder="icon">
400
+ <select class="form-select item-parent nav-col-parent">${parentOptions}</select>
401
+ <span class="nav-col-action">
402
+ <button class="btn btn-sm btn-ghost btn-toggle-hidden${isHidden ? ' active' : ''}" data-id="${item._id}" data-hidden="${isHidden}" data-tooltip="${isHidden ? 'Show' : 'Hide'}"><span data-icon="eye-off"></span></button>
403
+ <button class="btn btn-sm btn-danger btn-remove-item" data-id="${item._id}" data-tooltip="Remove"><span data-icon="trash"></span></button>
404
+ </span>
405
+ </div>
406
+ `);
407
+ });
408
+ Domma.icons.scan('#nav-items-list');
409
+ document.querySelectorAll('#nav-items-list [data-tooltip]').forEach(el => {
410
+ E.tooltip(el, {content: el.getAttribute('data-tooltip'), position: 'top'});
411
+ });
412
+ };
413
+ ```
414
+
415
+ ### 4b - `renderFooterLinks`: add toggle button, apply hidden class
416
+
417
+ - [ ] **Step 4b.1: Replace `renderFooterLinks` (lines 105-122)**
418
+
419
+ ```js
420
+ const renderFooterLinks = () => {
421
+ const $list = $container.find('#footer-links-list').empty();
422
+ footerLinks.forEach((link, idx) => {
423
+ const isHidden = link.hidden === true;
424
+ $list.append(`
425
+ <div class="nav-item-row${isHidden ? ' nav-item-row--hidden' : ''}" data-footer-idx="${idx}">
426
+ <input type="text" class="form-input footer-link-text nav-col-main" value="${esc(link.text)}" placeholder="Label">
427
+ <input type="text" class="form-input footer-link-url nav-col-main" value="${esc(link.url)}" placeholder="/url">
428
+ <span class="nav-col-action">
429
+ <button class="btn btn-sm btn-ghost btn-toggle-hidden${isHidden ? ' active' : ''}" data-idx="${idx}" data-hidden="${isHidden}" data-tooltip="${isHidden ? 'Show' : 'Hide'}"><span data-icon="eye-off"></span></button>
430
+ <button class="btn btn-sm btn-danger btn-remove-footer" data-idx="${idx}" data-tooltip="Remove"><span data-icon="trash"></span></button>
431
+ </span>
432
+ </div>
433
+ `);
434
+ });
435
+ Domma.icons.scan('#footer-links-list');
436
+ document.querySelectorAll('#footer-links-list [data-tooltip]').forEach(el => {
437
+ E.tooltip(el, {content: el.getAttribute('data-tooltip'), position: 'top'});
438
+ });
439
+ };
440
+ ```
441
+
442
+ ### 4c - Wire up the toggle click handlers
443
+
444
+ - [ ] **Step 4c.1: Add click handlers alongside the existing `btn-remove-item` / `btn-remove-footer` handlers (after
445
+ line ~165)**
446
+
447
+ Use scoped selectors (`#nav-items-list` / `#footer-links-list`) to prevent cross-list double-firing:
448
+
449
+ ```js
450
+ $container.off('click', '#nav-items-list .btn-toggle-hidden').on('click', '#nav-items-list .btn-toggle-hidden', function (e) {
451
+ e.stopPropagation();
452
+ syncFromDom();
453
+ const id = parseInt($(this).data('id'), 10);
454
+ const item = flatItems.find(i => i._id === id);
455
+ if (item) item.hidden = !item.hidden;
456
+ renderItems();
457
+ });
458
+
459
+ $container.off('click', '#footer-links-list .btn-toggle-hidden').on('click', '#footer-links-list .btn-toggle-hidden', function (e) {
460
+ e.stopPropagation();
461
+ syncFooterFromDom();
462
+ const idx = parseInt($(this).data('idx'), 10);
463
+ if (footerLinks[idx]) footerLinks[idx].hidden = !footerLinks[idx].hidden;
464
+ renderFooterLinks();
465
+ });
466
+ ```
467
+
468
+ - [ ] **Step 4c.2: Commit the render + interaction changes**
469
+
470
+ ```bash
471
+ git add admin/js/views/navigation.js
472
+ git commit -m "feat: add hidden toggle button and visual treatment to nav editor rows"
473
+ ```
474
+
475
+ ---
476
+
477
+ ## Task 5: Update `navigation.html` - column headers
478
+
479
+ **Files:**
480
+
481
+ - Modify: `admin/js/templates/navigation.html`
482
+
483
+ **Note on header label:** The spec suggests "Vis." as a dedicated column label for the toggle. This plan uses "Actions"
484
+ instead because both the eye-off toggle and the trash button share the existing `nav-col-action` span - adding a
485
+ separate narrow "Vis." column would require restructuring the column layout. "Actions" covers both controls and requires
486
+ no layout changes. This is an intentional deviation from the spec's suggested label.
487
+
488
+ - [ ] **Step 5.1: Update the Menu Items header**
489
+
490
+ Find the `nav-items-header` div inside the "Menu Items" card (lines 46-53). Change:
491
+ ```html
492
+ <span class="nav-col-action"></span>
493
+ ```
494
+ To:
495
+ ```html
496
+ <span class="nav-col-action text-muted"><small>Actions</small></span>
497
+ ```
498
+
499
+ - [ ] **Step 5.2: Update the Footer Links header**
500
+
501
+ Find the `nav-items-header` inside the "Footer Links" card (lines 65-70). Change:
502
+ ```html
503
+ <span class="nav-col-action"></span>
504
+ ```
505
+ To:
506
+ ```html
507
+ <span class="nav-col-action text-muted"><small>Actions</small></span>
508
+ ```
509
+
510
+ - [ ] **Step 5.3: Commit**
511
+
512
+ ```bash
513
+ git add admin/js/templates/navigation.html
514
+ git commit -m "feat: label actions column in nav editor headers"
515
+ ```
516
+
517
+ ---
518
+
519
+ ## Task 6: End-to-end verification
520
+
521
+ - [ ] **Step 6.1: Open the Navigation editor**
522
+
523
+ Open `http://localhost:4096/admin/#/navigation`.
524
+
525
+ - Confirm each nav item row has an eye-off icon button to the left of the trash button.
526
+ - Confirm each footer link row has the same.
527
+ - Confirm rows look normal (not dimmed) when hidden is off.
528
+
529
+ - [ ] **Step 6.2: Toggle a nav item hidden**
530
+
531
+ - Click the eye-off button on any nav item (e.g. "About").
532
+ - Row should dim and the label input should gain a strikethrough.
533
+ - Button should turn amber.
534
+
535
+ - [ ] **Step 6.3: Save and verify public navbar**
536
+
537
+ - Click Save. Toast: "Navigation saved."
538
+ - Open `http://localhost:4096/` in the browser - "About" must be absent from the navbar.
539
+ - Open `config/navigation.json` - "About" must still be there with `"hidden": true`.
540
+
541
+ - [ ] **Step 6.4: Toggle back and save**
542
+
543
+ - Return to the editor, click the eye-off button on "About" again.
544
+ - Row should un-dim, button returns to muted colour.
545
+ - Save. Verify "About" reappears on the public navbar.
546
+
547
+ - [ ] **Step 6.5: Test pre-existing hidden state round-trips correctly**
548
+
549
+ - Manually add `"hidden": true` to a footer link in `config/site.json` (under `footer.links`).
550
+ - Reload the Navigation editor (`http://localhost:4096/admin/#/navigation`).
551
+ - The footer link row should render dimmed with its button in amber - confirming `footerLinks` init preserves the
552
+ flag.
553
+ - Save without changing anything - verify the link still has `"hidden": true` in `site.json`.
554
+
555
+ - [ ] **Step 6.6: Test footer link hidden via UI**
556
+
557
+ - Toggle a footer link hidden via the UI. Save. Run:
558
+ ```bash
559
+ curl -s http://localhost:4096/ | grep -o '__CMS_SITE__ = [^;]*' | head -c 600
560
+ ```
561
+ - Confirm the hidden footer link is absent from the `links` array in `__CMS_SITE__`.
562
+
563
+ - [ ] **Step 6.7: Test a parent with hidden children**
564
+
565
+ - Mark a child item (e.g. one under "Resources") hidden. Save.
566
+ - Verify the "Resources" dropdown still appears in the navbar, but the hidden child does not.
567
+
568
+ - [ ] **Step 6.8: Test a hidden parent**
569
+
570
+ - Mark "Resources" (a parent with children) hidden. Save.
571
+ - Verify neither "Resources" nor any of its children appear in the navbar.
572
+
573
+ - [ ] **Step 6.9: Final commit if any tweaks were needed**
574
+
575
+ ```bash
576
+ git add -p
577
+ git commit -m "fix: nav hidden items polish after e2e verification"
578
+ ```