domma-cms 1.6.60 → 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 (137) 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/lib/effect-defs.js +1 -1
  6. package/admin/js/lib/effects-builder.js +2 -2
  7. package/admin/js/lib/page-editor-reference.js +5 -5
  8. package/admin/js/lib/shortcode-tags.js +1 -1
  9. package/admin/js/templates/docs/usage-shortcodes.html +39 -0
  10. package/docs/DommaFeatures.md +3005 -0
  11. package/docs/MultiTenant.md +176 -0
  12. package/docs/api-reference.md +1667 -0
  13. package/docs/cache.md +96 -0
  14. package/docs/configuration.md +1811 -0
  15. package/docs/editions-and-licences.md +190 -0
  16. package/docs/getting-started.md +201 -0
  17. package/docs/guides/form-triggers.md +260 -0
  18. package/docs/guides/mongodb-storage-adapter.md +282 -0
  19. package/docs/guides/multi-page-forms.md +121 -0
  20. package/docs/images/scribe-composer.png +0 -0
  21. package/docs/markdown-shortcodes.md +1738 -0
  22. package/docs/pentest-report-2026-04-03.md +181 -0
  23. package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
  24. package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
  25. package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
  26. package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
  27. package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
  28. package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
  29. package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
  30. package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
  31. package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
  32. package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
  33. package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
  34. package/docs/plans/2026-03-05-user-types-design.md +139 -0
  35. package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
  36. package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
  37. package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
  38. package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
  39. package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
  40. package/docs/plans/2026-06-10-core-project-design.md +240 -0
  41. package/docs/plugin-development.md +827 -0
  42. package/docs/recruitment-recipe.md +501 -0
  43. package/docs/scaffolding.md +312 -0
  44. package/docs/screenshots/calculator-floating.png +0 -0
  45. package/docs/screenshots/calculator-standard.png +0 -0
  46. package/docs/screenshots/email-settings.png +0 -0
  47. package/docs/screenshots/messages-count.png +0 -0
  48. package/docs/structure/user-types.md +107 -0
  49. package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
  50. package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
  51. package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
  52. package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
  53. package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
  54. package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
  55. package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
  56. package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
  57. package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
  58. package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
  59. package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
  60. package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
  61. package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
  62. package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
  63. package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
  64. package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
  65. package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
  66. package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
  67. package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
  68. package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
  69. package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
  70. package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
  71. package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
  72. package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
  73. package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
  74. package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
  75. package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
  76. package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
  77. package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
  78. package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
  79. package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
  80. package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
  81. package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
  82. package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
  83. package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
  84. package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
  85. package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
  86. package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
  87. package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
  88. package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
  89. package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
  90. package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
  91. package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
  92. package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
  93. package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
  94. package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
  95. package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
  96. package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
  97. package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
  98. package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
  99. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
  100. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
  101. package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
  102. package/docs/theming.md +320 -0
  103. package/package.json +2 -2
  104. package/public/js/celebrations/core/canvas.js +1 -1
  105. package/public/js/celebrations/index.js +1 -1
  106. package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
  107. package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
  108. package/public/js/celebrations/message-attrs.mjs +1 -0
  109. package/public/js/celebrations/themes/anniversary.js +1 -0
  110. package/public/js/celebrations/themes/autumn.js +1 -0
  111. package/public/js/celebrations/themes/birthday.js +1 -0
  112. package/public/js/celebrations/themes/butterfly-flight.js +1 -0
  113. package/public/js/celebrations/themes/calendar.js +1 -0
  114. package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
  115. package/public/js/celebrations/themes/chinese-new-year.js +1 -0
  116. package/public/js/celebrations/themes/christmas.js +1 -1
  117. package/public/js/celebrations/themes/easter.js +1 -0
  118. package/public/js/celebrations/themes/garden-drawing.js +1 -0
  119. package/public/js/celebrations/themes/garden.js +1 -0
  120. package/public/js/celebrations/themes/graduation.js +1 -0
  121. package/public/js/celebrations/themes/greeting.js +1 -0
  122. package/public/js/celebrations/themes/make-it-rain.js +1 -0
  123. package/public/js/celebrations/themes/money-rain.js +1 -0
  124. package/public/js/celebrations/themes/new-year.js +1 -0
  125. package/public/js/celebrations/themes/occasion-drawing.js +1 -0
  126. package/public/js/celebrations/themes/occasions.js +1 -0
  127. package/public/js/celebrations/themes/rain-control.js +1 -0
  128. package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
  129. package/public/js/celebrations/themes/sparkler-font.js +1 -0
  130. package/public/js/celebrations/themes/sparkler-writing.js +1 -0
  131. package/public/js/celebrations/themes/summer-garden.js +1 -0
  132. package/public/js/celebrations/themes/winter.js +1 -0
  133. package/public/js/celebrations/trait-data.mjs +1 -1
  134. package/public/js/effects.js +1 -1
  135. package/scripts/build.js +4 -0
  136. package/server/services/markdown.js +5 -2
  137. package/server/templates/page.html +1 -1
@@ -0,0 +1,635 @@
1
+ # Accordion Insert Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Add an "Accordion" entry to the Page Editor Insert dropdown that generates static accordion markup or a collection-bound accordion shortcode.
6
+
7
+ **Architecture:** Three self-contained layers - server-side collection renderer (markdown.js), toolbar registration (markdown-toolbar.js), and the insert dialog (page-editor.js). Each layer is independently deliverable. The server side adds `display="accordion"` to the existing `[collection]` shortcode; the toolbar and dialog follow patterns already established by Tabs and Collection inserts.
8
+
9
+ **Tech Stack:** Fastify 5 (ESM), Domma SPA (admin), `marked` for Markdown-to-HTML, `api.collections.get()` for schema fetching.
10
+
11
+ ---
12
+
13
+ ## File Map
14
+
15
+ | Action | File | Responsibility |
16
+ |--------|------|----------------|
17
+ | Modify | `server/services/markdown.js` | Add `renderCollectionAccordion` helper + `display="accordion"` branch in `processCollectionBlocks` |
18
+ | Modify | `admin/js/lib/markdown-toolbar.js` | Register `accordion-insert` icon, add `accordion` to handlers + `executeAction` + Insert dropdown + `onAccordion` export |
19
+ | Modify | `admin/js/views/page-editor.js` | Implement `toolbar.onAccordion(...)` modal dialog |
20
+
21
+ ---
22
+
23
+ ## Task 1: Server - `renderCollectionAccordion` helper
24
+
25
+ **Files:**
26
+ - Modify: `server/services/markdown.js` (after `renderCollectionList`, before `processViewBlocks`)
27
+
28
+ - [ ] **Step 1: Add `renderCollectionAccordion` after `renderCollectionList`**
29
+
30
+ Find the closing brace of `renderCollectionList` (the function that returns `'dm-collection-display dm-collection-list'` HTML). Immediately after it, add:
31
+
32
+ ```js
33
+ /**
34
+ * Render a collection as a Domma accordion.
35
+ * @param {object[]} entries
36
+ * @param {string} titleField - entry data field to use as header text
37
+ * @param {string} bodyField - entry data field to use as body content (Markdown)
38
+ * @param {boolean} multiple - allow multiple panels open simultaneously
39
+ * @param {string} emptyMsg
40
+ * @returns {string}
41
+ */
42
+ function renderCollectionAccordion(entries, titleField, bodyField, multiple, emptyMsg) {
43
+ if (!entries.length) {
44
+ return `<div class="dm-collection-display dm-collection-empty"><p>${escapeHtmlText(emptyMsg)}</p></div>`;
45
+ }
46
+
47
+ const multiAttr = multiple ? ' data-multi="true"' : '';
48
+ const items = entries.map(e => {
49
+ const title = escapeHtmlText(String(e.data?.[titleField] ?? e.id ?? '(untitled)'));
50
+ const bodyVal = String(e.data?.[bodyField] ?? '');
51
+ const bodyHtml = marked.parse(bodyVal);
52
+ return (
53
+ `<div class="accordion-item">\n` +
54
+ ` <h3 class="accordion-header"><button class="accordion-button" type="button">${title}` +
55
+ `<span class="accordion-icon" data-icon="chevron-down"></span></button></h3>\n` +
56
+ ` <div class="accordion-body"><div class="accordion-content">${bodyHtml}</div></div>\n` +
57
+ `</div>`
58
+ );
59
+ }).join('\n');
60
+
61
+ return `<div class="dm-collection-display accordion"${multiAttr}>\n${items}\n</div>`;
62
+ }
63
+ ```
64
+
65
+ - [ ] **Step 2: Add `display === 'accordion'` branch in `processCollectionBlocks`**
66
+
67
+ In `processCollectionBlocks`, find the block:
68
+ ```js
69
+ if (display === 'cards') {
70
+ replacement = renderCollectionCards(...);
71
+ } else if (display === 'list') {
72
+ replacement = renderCollectionList(...);
73
+ } else if (display === 'block') {
74
+ ```
75
+
76
+ Add a new branch **before** the `else if (display === 'block')` line:
77
+
78
+ ```js
79
+ } else if (display === 'accordion') {
80
+ const titleField = attrs['title-field'] || 'title';
81
+ const bodyField = attrs['body-field'] || 'description';
82
+ const multiple = attrs.multiple === 'true';
83
+ replacement = renderCollectionAccordion(entries, titleField, bodyField, multiple, emptyMsg);
84
+ ```
85
+
86
+ - [ ] **Step 3: Verify with curl**
87
+
88
+ ```bash
89
+ # Requires a collection with slug "faq" to exist locally.
90
+ # Add [collection slug="faq" display="accordion" title-field="title" body-field="description" /]
91
+ # to content/pages/index.md temporarily, then:
92
+ curl -s http://localhost:4096/ | grep -A5 'class="accordion"'
93
+ # Expected: <div class="dm-collection-display accordion">
94
+ # Remove the test shortcode afterwards.
95
+ ```
96
+
97
+ - [ ] **Step 4: Commit**
98
+
99
+ ```bash
100
+ git add server/services/markdown.js
101
+ git commit -m "feat: add display=\"accordion\" mode to [collection] shortcode"
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Task 2: Toolbar - icon, action, handler registration
107
+
108
+ **Files:**
109
+ - Modify: `admin/js/lib/markdown-toolbar.js`
110
+
111
+ - [ ] **Step 1: Register `accordion-insert` icon**
112
+
113
+ At the end of `registerIcons()`, before its closing brace, add:
114
+
115
+ ```js
116
+ I.register('accordion-insert', {
117
+ viewBox: '0 0 24 24',
118
+ paths: [
119
+ 'M3 4h18',
120
+ 'M3 8h18',
121
+ 'M8 11l4 4 4-4',
122
+ 'M3 16h18',
123
+ 'M3 20h18',
124
+ ],
125
+ stroke: 'currentColor', fill: 'none', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round'
126
+ });
127
+ ```
128
+
129
+ - [ ] **Step 2: Add `accordion` to handlers object**
130
+
131
+ In the `handlers` object inside `createToolbar`, add:
132
+
133
+ ```js
134
+ accordion: null,
135
+ ```
136
+
137
+ (alongside `tabs: null`, `form: null`, etc.)
138
+
139
+ - [ ] **Step 3: Add fallback in `executeAction`**
140
+
141
+ After the `else if (action === 'tabs')` branch, add:
142
+
143
+ ```js
144
+ } else if (action === 'accordion') {
145
+ if (handlers.accordion) {
146
+ handlers.accordion(ta);
147
+ } else {
148
+ const start = ta.selectionStart;
149
+ const snippet = `[accordion]\n[item title="Item 1"]Content here.[/item]\n[item title="Item 2"]Content here.[/item]\n[/accordion]\n`;
150
+ ta.value = ta.value.substring(0, start) + snippet + ta.value.substring(start);
151
+ ta.selectionStart = ta.selectionEnd = start + snippet.length;
152
+ ta.dispatchEvent(new Event('input', {bubbles: true}));
153
+ ta.focus();
154
+ }
155
+ ```
156
+
157
+ - [ ] **Step 4: Add to Insert dropdown items**
158
+
159
+ In the `buttons` array, find the Insert dropdown `items` array. After the `{label: 'Tabs', ...}` entry, add:
160
+
161
+ ```js
162
+ {label: 'Accordion', icon: 'accordion-insert', action: 'accordion'},
163
+ ```
164
+
165
+ - [ ] **Step 5: Export `onAccordion`**
166
+
167
+ In the `return { ... }` block at the end of `createToolbar`, after `onTabs(cb) { ... }`, add:
168
+
169
+ ```js
170
+ onAccordion(cb) {
171
+ handlers.accordion = cb;
172
+ },
173
+ ```
174
+
175
+ - [ ] **Step 6: Smoke-test in browser**
176
+
177
+ Open the page editor. Open the Insert dropdown. Confirm "Accordion" appears in the list with a stacked-lines icon. Click it - should insert a 2-item `[accordion]` snippet (the fallback, since the handler hasn't been wired yet).
178
+
179
+ - [ ] **Step 7: Commit**
180
+
181
+ ```bash
182
+ git add admin/js/lib/markdown-toolbar.js
183
+ git commit -m "feat: add Accordion to Insert toolbar dropdown"
184
+ ```
185
+
186
+ ---
187
+
188
+ ## Task 3: Dialog - `toolbar.onAccordion` implementation
189
+
190
+ **Files:**
191
+ - Modify: `admin/js/views/page-editor.js`
192
+
193
+ - [ ] **Step 1: Wire the handler after `toolbar.onTabs`**
194
+
195
+ Find `toolbar.onTabs((ta) => {` in `page-editor.js`. After the closing `});` of that block, add the full handler below.
196
+
197
+ The handler follows the identical light-DOM modal pattern used by `onTabs` and `onCollection`:
198
+ - `E.modal({title, size})` creates the modal shell
199
+ - `modal.element.appendChild(wrapper)` projects the DOM into the modal body
200
+ - `modal.open()` shows it
201
+
202
+ ```js
203
+ // Accordion shortcode dialog
204
+ toolbar.onAccordion(async (ta) => {
205
+ const collections = await api.collections.list().catch(() => []);
206
+
207
+ const wrapper = document.createElement('div');
208
+ wrapper.style.cssText = 'padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem;';
209
+
210
+ const labelStyle = 'display:block;font-size:.85em;font-weight:600;margin-bottom:.25rem;color:var(--dm-text-muted,#aaa);text-transform:uppercase;letter-spacing:.05em;';
211
+ const inputStyle = 'width:100%;padding:.4rem .6rem;background:var(--dm-input-bg,#1a1a1a);border:1px solid var(--dm-border,#333);border-radius:4px;color:var(--dm-text,#eee);font-size:.9em;';
212
+
213
+ // --- Mode toggle ---
214
+ const toggleRow = document.createElement('div');
215
+ toggleRow.style.cssText = 'display:flex;gap:.5rem;';
216
+ const staticBtn = document.createElement('button');
217
+ staticBtn.type = 'button';
218
+ staticBtn.className = 'btn btn-sm btn-primary';
219
+ staticBtn.textContent = 'Static';
220
+ const collBtn = document.createElement('button');
221
+ collBtn.type = 'button';
222
+ collBtn.className = 'btn btn-sm btn-ghost';
223
+ collBtn.textContent = 'Collection';
224
+ toggleRow.appendChild(staticBtn);
225
+ toggleRow.appendChild(collBtn);
226
+ wrapper.appendChild(toggleRow);
227
+
228
+ let mode = 'static'; // 'static' | 'collection'
229
+
230
+ // --- Static panel ---
231
+ const staticPanel = document.createElement('div');
232
+ staticPanel.style.cssText = 'display:flex;flex-direction:column;gap:.5rem;';
233
+
234
+ const itemRows = [];
235
+
236
+ function makeItemRow(n) {
237
+ const row = document.createElement('div');
238
+ row.style.cssText = 'display:flex;align-items:center;gap:.5rem;';
239
+ const lbl = document.createElement('label');
240
+ lbl.style.cssText = 'font-size:.85em;color:var(--dm-text-muted,#aaa);white-space:nowrap;';
241
+ lbl.textContent = `Item ${n}`;
242
+ const inp = document.createElement('input');
243
+ inp.type = 'text';
244
+ inp.placeholder = 'Item title';
245
+ inp.style.cssText = inputStyle;
246
+ row.appendChild(lbl);
247
+ row.appendChild(inp);
248
+ return {row, input: inp};
249
+ }
250
+
251
+ // Start with 2 rows
252
+ [1, 2].forEach(n => {
253
+ const r = makeItemRow(n);
254
+ itemRows.push(r);
255
+ staticPanel.appendChild(r.row);
256
+ });
257
+
258
+ const addItemBtn = document.createElement('button');
259
+ addItemBtn.type = 'button';
260
+ addItemBtn.className = 'btn btn-ghost btn-sm';
261
+ addItemBtn.textContent = '+ Add Item';
262
+ addItemBtn.addEventListener('click', () => {
263
+ const r = makeItemRow(itemRows.length + 1);
264
+ itemRows.push(r);
265
+ staticPanel.insertBefore(r.row, addItemBtn);
266
+ });
267
+ staticPanel.appendChild(addItemBtn);
268
+
269
+ const staticMultiLabel = document.createElement('label');
270
+ staticMultiLabel.style.cssText = 'display:flex;align-items:center;gap:.5rem;font-size:.9em;cursor:pointer;';
271
+ const staticMultiCheck = document.createElement('input');
272
+ staticMultiCheck.type = 'checkbox';
273
+ staticMultiLabel.appendChild(staticMultiCheck);
274
+ staticMultiLabel.appendChild(document.createTextNode('Allow multiple open'));
275
+ staticPanel.appendChild(staticMultiLabel);
276
+
277
+ wrapper.appendChild(staticPanel);
278
+
279
+ // --- Collection panel (hidden by default) ---
280
+ const collPanel = document.createElement('div');
281
+ collPanel.style.cssText = 'display:none;flex-direction:column;gap:.5rem;';
282
+
283
+ // Collection select
284
+ const collRow = document.createElement('div');
285
+ const collLabel = document.createElement('label');
286
+ collLabel.style.cssText = labelStyle;
287
+ collLabel.textContent = 'Collection';
288
+ const collSelect = document.createElement('select');
289
+ collSelect.style.cssText = inputStyle;
290
+ if (!collections.length) {
291
+ const opt = document.createElement('option');
292
+ opt.value = '';
293
+ opt.textContent = 'No collections found';
294
+ collSelect.appendChild(opt);
295
+ } else {
296
+ collections.forEach(c => {
297
+ const opt = document.createElement('option');
298
+ opt.value = c.slug;
299
+ opt.textContent = c.title || c.slug;
300
+ collSelect.appendChild(opt);
301
+ });
302
+ }
303
+ collRow.appendChild(collLabel);
304
+ collRow.appendChild(collSelect);
305
+ collPanel.appendChild(collRow);
306
+
307
+ // Title field select
308
+ const titleFieldRow = document.createElement('div');
309
+ const titleFieldLabel = document.createElement('label');
310
+ titleFieldLabel.style.cssText = labelStyle;
311
+ titleFieldLabel.textContent = 'Title field';
312
+ const titleFieldSelect = document.createElement('select');
313
+ titleFieldSelect.style.cssText = inputStyle;
314
+ titleFieldRow.appendChild(titleFieldLabel);
315
+ titleFieldRow.appendChild(titleFieldSelect);
316
+ collPanel.appendChild(titleFieldRow);
317
+
318
+ // Body field select
319
+ const bodyFieldRow = document.createElement('div');
320
+ const bodyFieldLabel = document.createElement('label');
321
+ bodyFieldLabel.style.cssText = labelStyle;
322
+ bodyFieldLabel.textContent = 'Body field';
323
+ const bodyFieldSelect = document.createElement('select');
324
+ bodyFieldSelect.style.cssText = inputStyle;
325
+ bodyFieldRow.appendChild(bodyFieldLabel);
326
+ bodyFieldRow.appendChild(bodyFieldSelect);
327
+ collPanel.appendChild(bodyFieldRow);
328
+
329
+ // Populate field selects from schema
330
+ async function loadFields(slug) {
331
+ titleFieldSelect.textContent = '';
332
+ bodyFieldSelect.textContent = '';
333
+ try {
334
+ const schema = await api.collections.get(slug);
335
+ const fields = schema.fields || [];
336
+ if (!fields.length) {
337
+ const none = document.createElement('option');
338
+ none.value = '';
339
+ none.textContent = '- none -';
340
+ titleFieldSelect.appendChild(none.cloneNode(true));
341
+ bodyFieldSelect.appendChild(none);
342
+ return;
343
+ }
344
+ fields.forEach((f, i) => {
345
+ const makeOpt = () => {
346
+ const o = document.createElement('option');
347
+ o.value = f.name;
348
+ o.textContent = f.label || f.name;
349
+ return o;
350
+ };
351
+ titleFieldSelect.appendChild(makeOpt());
352
+ bodyFieldSelect.appendChild(makeOpt());
353
+ // Pre-select 'title' / 'description' by name if present
354
+ if (f.name === 'title') titleFieldSelect.value = 'title';
355
+ if (f.name === 'description') bodyFieldSelect.value = 'description';
356
+ });
357
+ // Fallback: first field for title, second for body
358
+ if (!titleFieldSelect.value && fields[0]) titleFieldSelect.value = fields[0].name;
359
+ if (!bodyFieldSelect.value && fields[1]) bodyFieldSelect.value = fields[1].name;
360
+ } catch {
361
+ const err = document.createElement('option');
362
+ err.value = '';
363
+ err.textContent = '- unknown -';
364
+ titleFieldSelect.appendChild(err.cloneNode(true));
365
+ bodyFieldSelect.appendChild(err);
366
+ }
367
+ }
368
+
369
+ if (collections.length) loadFields(collections[0].slug);
370
+ collSelect.addEventListener('change', () => loadFields(collSelect.value));
371
+
372
+ // Limit
373
+ const limitRow = document.createElement('div');
374
+ const limitLabel = document.createElement('label');
375
+ limitLabel.style.cssText = labelStyle;
376
+ limitLabel.textContent = 'Limit (optional)';
377
+ const limitInput = document.createElement('input');
378
+ limitInput.type = 'number';
379
+ limitInput.placeholder = 'All';
380
+ limitInput.style.cssText = inputStyle;
381
+ limitRow.appendChild(limitLabel);
382
+ limitRow.appendChild(limitInput);
383
+ collPanel.appendChild(limitRow);
384
+
385
+ const collMultiLabel = document.createElement('label');
386
+ collMultiLabel.style.cssText = 'display:flex;align-items:center;gap:.5rem;font-size:.9em;cursor:pointer;';
387
+ const collMultiCheck = document.createElement('input');
388
+ collMultiCheck.type = 'checkbox';
389
+ collMultiLabel.appendChild(collMultiCheck);
390
+ collMultiLabel.appendChild(document.createTextNode('Allow multiple open'));
391
+ collPanel.appendChild(collMultiLabel);
392
+
393
+ wrapper.appendChild(collPanel);
394
+
395
+ // --- Toggle logic ---
396
+ staticBtn.addEventListener('click', () => {
397
+ mode = 'static';
398
+ staticBtn.className = 'btn btn-sm btn-primary';
399
+ collBtn.className = 'btn btn-sm btn-ghost';
400
+ staticPanel.style.display = 'flex';
401
+ collPanel.style.display = 'none';
402
+ });
403
+ collBtn.addEventListener('click', () => {
404
+ mode = 'collection';
405
+ collBtn.className = 'btn btn-sm btn-primary';
406
+ staticBtn.className = 'btn btn-sm btn-ghost';
407
+ collPanel.style.display = 'flex';
408
+ staticPanel.style.display = 'none';
409
+ });
410
+
411
+ // --- Insert button ---
412
+ const insertBtn = document.createElement('button');
413
+ insertBtn.type = 'button';
414
+ insertBtn.className = 'btn btn-primary';
415
+ insertBtn.textContent = 'Insert Accordion';
416
+ wrapper.appendChild(insertBtn);
417
+
418
+ const modal = E.modal({title: 'Insert Accordion', size: 'sm'});
419
+ modal.element.appendChild(wrapper);
420
+ modal.open();
421
+
422
+ requestAnimationFrame(() => itemRows[0].input.focus());
423
+
424
+ insertBtn.addEventListener('click', () => {
425
+ modal.close();
426
+ let snippet;
427
+
428
+ if (mode === 'static') {
429
+ const multiAttr = staticMultiCheck.checked ? ' multiple="true"' : '';
430
+ snippet = `[accordion${multiAttr}]\n`;
431
+ itemRows.forEach((r, i) => {
432
+ const title = r.input.value.trim() || `Item ${i + 1}`;
433
+ snippet += `[item title="${title}"]Content here.[/item]\n`;
434
+ });
435
+ snippet += '[/accordion]';
436
+ } else {
437
+ const slug = collSelect.value;
438
+ if (!slug) return;
439
+ const titleF = titleFieldSelect.value;
440
+ const bodyF = bodyFieldSelect.value;
441
+ let attrs = `slug="${slug}" display="accordion"`;
442
+ if (titleF) attrs += ` title-field="${titleF}"`;
443
+ if (bodyF) attrs += ` body-field="${bodyF}"`;
444
+ const limit = limitInput.value.trim();
445
+ if (limit) attrs += ` limit="${limit}"`;
446
+ if (collMultiCheck.checked) attrs += ' multiple="true"';
447
+ snippet = `[collection ${attrs} /]`;
448
+ }
449
+
450
+ insertAtCursor(ta, snippet);
451
+ ta.dispatchEvent(new Event('input', {bubbles: true}));
452
+ ta.focus();
453
+ });
454
+ });
455
+ ```
456
+
457
+ - [ ] **Step 2: Smoke-test static mode**
458
+
459
+ Open page editor → Insert → Accordion. Dialog appears. Static mode is active. Change item titles. Click "Insert Accordion". Verify the textarea receives:
460
+ ```
461
+ [accordion]
462
+ [item title="Your title"]Content here.[/item]
463
+ ...
464
+ [/accordion]
465
+ ```
466
+
467
+ - [ ] **Step 3: Smoke-test collection mode**
468
+
469
+ Click "Collection" toggle. Collection select populates. Change collection - title/body field selects update. Click "Insert Accordion". Verify the textarea receives:
470
+ ```
471
+ [collection slug="..." display="accordion" title-field="..." body-field="..." /]
472
+ ```
473
+
474
+ - [ ] **Step 4: Commit**
475
+
476
+ ```bash
477
+ git add admin/js/views/page-editor.js
478
+ git commit -m "feat: implement Accordion insert dialog with static and collection modes"
479
+ ```
480
+
481
+ ---
482
+
483
+ ## Task 4: Help panel entry
484
+
485
+ **Files:**
486
+ - Modify: `admin/js/views/page-editor.js` (inside the help slideover builder, `panelComponents` section)
487
+
488
+ The help slideover (opened via the `?` toolbar button) documents every shortcode. Add an Accordion section alongside the existing "Interactive Components" entry so editors can discover the collection-bound syntax.
489
+
490
+ - [ ] **Step 1: Add Accordion section in the help slideover**
491
+
492
+ Find the section that appends Accordion to `section3b` (the interactive components section, which already shows the static `[accordion]` snippet). After that existing snippet, add:
493
+
494
+ ```js
495
+ section3b.appendChild(makeSnippet(
496
+ 'Accordion - collection-bound',
497
+ '[collection slug="faq" display="accordion" title-field="title" body-field="description" /]',
498
+ 'Renders collection entries as accordion items. Use Insert → Accordion for a guided dialog.'
499
+ ));
500
+ ```
501
+
502
+ - [ ] **Step 2: Commit**
503
+
504
+ ```bash
505
+ git add admin/js/views/page-editor.js
506
+ git commit -m "docs: add collection-bound accordion example to editor help panel"
507
+ ```
508
+
509
+ ---
510
+
511
+ ## Task 5: Insert Collection - add Block display option with block picker
512
+
513
+ **Files:**
514
+ - Modify: `admin/js/views/page-editor.js` (inside `toolbar.onCollection(...)`)
515
+
516
+ The existing Insert Collection dialog has `table`, `cards`, `list` display options. The server already supports `display="block"` (renders entries through a reusable HTML block template), but the UI never exposed it. This task adds `block` to the display select and shows a block-name picker when it is chosen.
517
+
518
+ The shortcode generated: `[collection slug="..." display="block" block="my-block" cols="3" /]`
519
+
520
+ - [ ] **Step 1: Fetch blocks alongside collections at dialog open**
521
+
522
+ In `toolbar.onCollection(async (ta) => {`, change the opening line from:
523
+
524
+ ```js
525
+ const collections = await api.collections.list().catch(() => []);
526
+ ```
527
+
528
+ to:
529
+
530
+ ```js
531
+ const [collections, blocks] = await Promise.all([
532
+ api.collections.list().catch(() => []),
533
+ api.blocks.list().catch(() => []),
534
+ ]);
535
+ ```
536
+
537
+ - [ ] **Step 2: Add `block` to the display select options**
538
+
539
+ Find:
540
+ ```js
541
+ ['table', 'cards', 'list'].forEach(d => {
542
+ ```
543
+
544
+ Change to:
545
+ ```js
546
+ ['table', 'cards', 'list', 'block'].forEach(d => {
547
+ ```
548
+
549
+ - [ ] **Step 3: Add block name row (shown only when display = "block")**
550
+
551
+ After the `colsRow` block (which ends with `wrapper.appendChild(colsRow);`), add:
552
+
553
+ ```js
554
+ // Block name (block display only)
555
+ const blockRow = document.createElement('div');
556
+ blockRow.style.display = 'none';
557
+ const blockLabel = document.createElement('label');
558
+ blockLabel.style.cssText = labelStyle;
559
+ blockLabel.textContent = 'Block template';
560
+ const blockSelect = document.createElement('select');
561
+ blockSelect.style.cssText = inputStyle;
562
+ if (!blocks.length) {
563
+ const opt = document.createElement('option');
564
+ opt.value = '';
565
+ opt.textContent = 'No block templates found';
566
+ blockSelect.appendChild(opt);
567
+ } else {
568
+ blocks.forEach(b => {
569
+ const opt = document.createElement('option');
570
+ opt.value = b.name ?? b;
571
+ opt.textContent = b.name ?? b;
572
+ blockSelect.appendChild(opt);
573
+ });
574
+ }
575
+ blockRow.appendChild(blockLabel);
576
+ blockRow.appendChild(blockSelect);
577
+ wrapper.appendChild(blockRow);
578
+ ```
579
+
580
+ - [ ] **Step 4: Update `toggleTableRows` to handle block and cols visibility**
581
+
582
+ Find the existing `toggleTableRows` function:
583
+ ```js
584
+ const toggleTableRows = () => {
585
+ const isTable = dispSelect.value === 'table';
586
+ tableRows.forEach(r => {
587
+ r.style.display = isTable ? '' : 'none';
588
+ });
589
+ colsRow.style.display = dispSelect.value === 'cards' ? '' : 'none';
590
+ };
591
+ ```
592
+
593
+ Replace with:
594
+ ```js
595
+ const toggleTableRows = () => {
596
+ const val = dispSelect.value;
597
+ const isTable = val === 'table';
598
+ tableRows.forEach(r => {
599
+ r.style.display = isTable ? '' : 'none';
600
+ });
601
+ colsRow.style.display = (val === 'cards' || val === 'block') ? '' : 'none';
602
+ blockRow.style.display = val === 'block' ? '' : 'none';
603
+ };
604
+ ```
605
+
606
+ - [ ] **Step 5: Update snippet generation for block display**
607
+
608
+ Find the `insertBtn.addEventListener('click', ...)` handler. Find this line:
609
+ ```js
610
+ if (display === 'cards') snippet += ` columns="${colsInput.value}"`;
611
+ ```
612
+
613
+ After it, add:
614
+ ```js
615
+ if (display === 'block') {
616
+ const blockName = blockSelect.value;
617
+ if (blockName) snippet += ` block="${blockName}"`;
618
+ const cols = colsInput.value.trim();
619
+ if (cols) snippet += ` cols="${cols}"`;
620
+ }
621
+ ```
622
+
623
+ - [ ] **Step 6: Smoke-test**
624
+
625
+ Open Insert → Collection. Confirm "Block" appears in the Display dropdown. Select it - "Block template" select appears (populated from `api.blocks.list()`), "Columns" row also visible. Click Insert. Verify snippet:
626
+ ```
627
+ [collection slug="..." display="block" block="my-block" cols="3" /]
628
+ ```
629
+
630
+ - [ ] **Step 7: Commit**
631
+
632
+ ```bash
633
+ git add admin/js/views/page-editor.js
634
+ git commit -m "feat: add Block display option to Insert Collection dialog"
635
+ ```