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,545 @@
1
+ # [text] Shortcode Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:
4
+ > executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
5
+
6
+ **Goal:** Add a `[text]` shortcode that renders a styled `<span>` with inline CSS, accessible via a modal in the Page
7
+ Editor Insert menu.
8
+
9
+ **Architecture:** Server-side processor `processTextBlocks()` in `markdown.js` handles rendering; the Insert menu entry
10
+ and modal live entirely in `markdown-toolbar.js`; `docs/markdown-shortcodes.md` is updated to document the shortcode.
11
+
12
+ **Tech Stack:** Fastify 5 (ESM), Domma SPA admin (`E.modal`, `insertAtCursor`), `sanitize-html`, `gray-matter` +
13
+ `marked`.
14
+
15
+ **Spec:** `docs/superpowers/specs/2026-03-25-text-shortcode-design.md`
16
+
17
+ ---
18
+
19
+ ## File Map
20
+
21
+ | File | Change |
22
+ |------------------------------------|----------------------------------------------------------------------------------------------------------|
23
+ | `server/services/markdown.js` | Add `processTextBlocks()` function + wire into pipeline between `withBadge` and `withButton` |
24
+ | `admin/js/lib/markdown-toolbar.js` | Register `text-style` icon + add Insert menu entry + add `action === 'text'` branch in `executeAction()` |
25
+ | `docs/markdown-shortcodes.md` | Add `[text]` section |
26
+
27
+ ---
28
+
29
+ ## Task 1: `processTextBlocks()` - server-side shortcode processor
30
+
31
+ **Files:**
32
+
33
+ - Modify: `server/services/markdown.js`
34
+
35
+ ### Background
36
+
37
+ `processBadgeBlocks` (just above the insertion point) is the closest model - it also outputs a `<span>`, uses
38
+ `scrubCodeRegions`/`restore`, and handles flag attributes via regex rather than `parseShortcodeAttrs`.
39
+
40
+ Two subtleties:
41
+
42
+ - `bold` and `italic` are **flag** attributes - use `/\bbold\b/i.test(attrStr)` for consistency with
43
+ `processBadgeBlocks` (`pill`, `outline`). While `parseShortcodeAttrs` Pass 2 does capture standalone flags as `true`,
44
+ the regex approach matches existing convention and avoids edge-case ambiguity.
45
+ - `bold` flag takes precedence over `weight` attribute when both are present
46
+
47
+ - [ ] **Step 1: Add `processTextBlocks` function to `markdown.js`**
48
+
49
+ Find the closing `}` of `processBadgeBlocks` (search for the line `return restore(result);` followed by `}` in that
50
+ function) and add the following function immediately after it:
51
+
52
+ ```js
53
+ /**
54
+ * Pre-process [text] shortcodes before running through marked.
55
+ *
56
+ * Syntax (paired):
57
+ * [text size="xl" bold italic color="primary" font="Georgia"]Your text[/text]
58
+ *
59
+ * Outputs a <span> with all resolved styles inlined.
60
+ *
61
+ * @param {string} markdown
62
+ * @returns {string}
63
+ */
64
+ function processTextBlocks(markdown) {
65
+ const {scrubbed, restore} = scrubCodeRegions(markdown);
66
+
67
+ const SIZE_MAP = {
68
+ xs: '.75rem', sm: '.875rem', base: '1rem', lg: '1.125rem',
69
+ xl: '1.25rem', '2xl': '1.5rem', '3xl': '1.875rem', '4xl': '2.25rem',
70
+ };
71
+ const WEIGHT_MAP = {
72
+ thin: '100', light: '300', normal: '400', medium: '500',
73
+ semibold: '600', bold: '700', extrabold: '800', black: '900',
74
+ };
75
+ const SPACING_MAP = {
76
+ tight: '-0.05em', normal: '0em', wide: '0.05em', wider: '0.1em',
77
+ };
78
+ const COLOR_TOKENS = {
79
+ primary: 'var(--dm-color-primary)', secondary: 'var(--dm-color-secondary)',
80
+ muted: 'var(--dm-text-muted)', danger: 'var(--dm-color-danger)',
81
+ success: 'var(--dm-color-success)', warning: 'var(--dm-color-warning)',
82
+ info: 'var(--dm-color-info)',
83
+ };
84
+ const FONT_MAP = {
85
+ 'Georgia': 'Georgia,serif',
86
+ 'Arial': 'Arial,sans-serif',
87
+ 'Verdana': 'Verdana,sans-serif',
88
+ 'Courier New': "'Courier New',monospace",
89
+ 'Times New Roman': "'Times New Roman',serif",
90
+ 'Trebuchet MS': "'Trebuchet MS',sans-serif",
91
+ };
92
+ const TRANSFORM_MAP = {
93
+ upper: 'uppercase', lower: 'lowercase', capitalize: 'capitalize', none: 'none',
94
+ };
95
+ const DECORATION_MAP = {
96
+ underline: 'underline', 'line-through': 'line-through', none: 'none',
97
+ };
98
+
99
+ function buildText(attrStr, inner) {
100
+ const attrs = parseShortcodeAttrs(attrStr || '');
101
+ const styles = [];
102
+
103
+ if (attrs.size) {
104
+ styles.push(`font-size:${SIZE_MAP[attrs.size] || attrs.size}`);
105
+ }
106
+
107
+ // bold flag takes precedence over weight attr
108
+ const isBold = /\bbold\b/i.test(attrStr);
109
+ if (isBold) {
110
+ styles.push('font-weight:700');
111
+ } else if (attrs.weight && WEIGHT_MAP[attrs.weight]) {
112
+ styles.push(`font-weight:${WEIGHT_MAP[attrs.weight]}`);
113
+ }
114
+
115
+ if (/\bitalic\b/i.test(attrStr)) {
116
+ styles.push('font-style:italic');
117
+ }
118
+
119
+ if (attrs.color) {
120
+ styles.push(`color:${COLOR_TOKENS[attrs.color] || attrs.color}`);
121
+ }
122
+
123
+ if (attrs.font && FONT_MAP[attrs.font]) {
124
+ styles.push(`font-family:${FONT_MAP[attrs.font]}`);
125
+ }
126
+
127
+ if (attrs.transform && TRANSFORM_MAP[attrs.transform]) {
128
+ styles.push(`text-transform:${TRANSFORM_MAP[attrs.transform]}`);
129
+ }
130
+
131
+ if (attrs.decoration && DECORATION_MAP[attrs.decoration]) {
132
+ styles.push(`text-decoration:${DECORATION_MAP[attrs.decoration]}`);
133
+ }
134
+
135
+ if (attrs.spacing) {
136
+ styles.push(`letter-spacing:${SPACING_MAP[attrs.spacing] || attrs.spacing}`);
137
+ }
138
+
139
+ const styleAttr = styles.length ? ` style="${styles.join(';')}"` : '';
140
+ const classAttr = attrs.class ? ` class="${escapeAttr(attrs.class)}"` : '';
141
+ const idAttr = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
142
+
143
+ return `<span${styleAttr}${classAttr}${idAttr}>${inner.trim()}</span>`;
144
+ }
145
+
146
+ const result = scrubbed.replace(
147
+ /\[text([^\]]*)\]([\s\S]*?)\[\/text\]/gi,
148
+ (_, attrStr, body) => buildText(attrStr, body)
149
+ );
150
+
151
+ return restore(result);
152
+ }
153
+ ```
154
+
155
+ - [ ] **Step 2: Wire `processTextBlocks` into the parse pipeline**
156
+
157
+ In the same file, find these two adjacent lines (around the `withBadge`/`withButton` section):
158
+
159
+ ```js
160
+ const withBadge = processBadgeBlocks(withTable);
161
+ const withButton = processButtonBlocks(withBadge);
162
+ ```
163
+
164
+ Replace with:
165
+
166
+ ```js
167
+ const withBadge = processBadgeBlocks(withTable);
168
+ const withText = processTextBlocks(withBadge);
169
+ const withButton = processButtonBlocks(withText);
170
+ ```
171
+
172
+ - [ ] **Step 3: Verify the processor works**
173
+
174
+ The server is always running on port 4096. Create a quick test page using the API - or just edit an existing page's
175
+ content via the admin - to contain:
176
+
177
+ ```
178
+ [text size="xl" bold italic color="primary"]Hello styled world[/text]
179
+ ```
180
+
181
+ Then curl the public page and check the rendered HTML contains:
182
+
183
+ ```
184
+ <span style="font-size:1.25rem;font-weight:700;font-style:italic;color:var(--dm-color-primary);">Hello styled world</span>
185
+ ```
186
+
187
+ Also verify these edge cases render correctly:
188
+
189
+ - `[text]Plain text[/text]` → `<span>Plain text</span>` (no style attr)
190
+ - `[text bold weight="light"]Text[/text]` → `font-weight:700` only (bold wins)
191
+ - `[text size="2em"]Big[/text]` → `font-size:2em` (raw value passthrough)
192
+ - Shortcode inside a fenced code block should be left untouched (scrubCodeRegions)
193
+
194
+ - [ ] **Step 4: Commit**
195
+
196
+ ```bash
197
+ git add server/services/markdown.js
198
+ git commit -m "feat: add [text] shortcode server-side processor"
199
+ ```
200
+
201
+ ---
202
+
203
+ ## Task 2: Insert menu entry + modal - `markdown-toolbar.js`
204
+
205
+ **Files:**
206
+
207
+ - Modify: `admin/js/lib/markdown-toolbar.js`
208
+
209
+ ### Background
210
+
211
+ All insert-action modals live directly inside `executeAction()` in `markdown-toolbar.js` - there is no `onText` callback
212
+ and no `page-editor.js` change. The modal uses `E.modal` (Domma global), `insertAtCursor` (imported in the same file),
213
+ and the same `makeRow`/`makeSelect`/`makeInput` helper pattern seen in other actions.
214
+
215
+ The live preview uses a `<span>` inside a wrapping `<div>` - styles are applied directly to the `<span>` to match the
216
+ actual rendered element (inline context).
217
+
218
+ - [ ] **Step 1: Register the `text-style` icon**
219
+
220
+ Inside `registerIcons()`, add after the last `I.register(...)` call:
221
+
222
+ ```js
223
+ I.register('text-style', {
224
+ viewBox: '0 0 24 24',
225
+ paths: [
226
+ 'M4 7V5h16v2',
227
+ 'M9 19h6',
228
+ 'M12 5v14',
229
+ 'M5 12h14',
230
+ ],
231
+ stroke: 'currentColor', fill: 'none', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round'
232
+ });
233
+ ```
234
+
235
+ - [ ] **Step 2: Add the Insert menu entry**
236
+
237
+ Find the Insert dropdown items array (search for `{label: 'Tabs', icon:`). Add the `Text` entry between `Tabs` and
238
+ `Timeline`:
239
+
240
+ ```js
241
+ {label: 'Tabs', icon: 'layout-list', action: 'tabs'},
242
+ {label: 'Text', icon: 'text-style', action: 'text'}, // ← add this line
243
+ {label: 'Timeline', icon: 'activity', action: 'timeline'},
244
+ ```
245
+
246
+ - [ ] **Step 3: Add the `text` action handler in `executeAction()`**
247
+
248
+ Find the closing branch before the final `}` of `executeAction` - it ends with:
249
+
250
+ ```js
251
+ } else if (action === 'help') {
252
+ if (handlers.help) handlers.help(ta);
253
+ }
254
+ }
255
+ ```
256
+
257
+ Insert the `text` branch before the `help` branch:
258
+
259
+ ```js
260
+ } else if (action === 'text') {
261
+ const start = ta.selectionStart;
262
+ const end = ta.selectionEnd;
263
+ const selected = ta.value.substring(start, end) || '';
264
+
265
+ const labelStyle = 'display:block;font-size:.7rem;font-weight:600;color:var(--dm-text-muted,#aaa);text-transform:uppercase;letter-spacing:.05em;';
266
+ 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;';
267
+
268
+ function makeRow(labelText, control) {
269
+ const row = document.createElement('div');
270
+ const lbl = document.createElement('label');
271
+ lbl.style.cssText = labelStyle;
272
+ lbl.textContent = labelText;
273
+ row.appendChild(lbl);
274
+ row.appendChild(control);
275
+ return row;
276
+ }
277
+
278
+ function makeSelect(options) {
279
+ const sel = document.createElement('select');
280
+ sel.style.cssText = inputStyle;
281
+ [['', '- default -'], ...options].forEach(([val, text]) => {
282
+ const opt = document.createElement('option');
283
+ opt.value = val;
284
+ opt.textContent = text;
285
+ sel.appendChild(opt);
286
+ });
287
+ return sel;
288
+ }
289
+
290
+ function makeTextInput(placeholder, value) {
291
+ const inp = document.createElement('input');
292
+ inp.type = 'text';
293
+ inp.placeholder = placeholder || '';
294
+ inp.value = value || '';
295
+ inp.style.cssText = inputStyle;
296
+ return inp;
297
+ }
298
+
299
+ function makeCheckbox(labelText) {
300
+ const wrap = document.createElement('label');
301
+ wrap.style.cssText = 'display:flex;align-items:center;gap:.5rem;cursor:pointer;';
302
+ const cb = document.createElement('input');
303
+ cb.type = 'checkbox';
304
+ const span = document.createElement('span');
305
+ span.style.cssText = 'font-size:.85em;color:var(--dm-text,#eee);';
306
+ span.textContent = labelText;
307
+ wrap.appendChild(cb);
308
+ wrap.appendChild(span);
309
+ return {wrap, cb};
310
+ }
311
+
312
+ const wrapper = document.createElement('div');
313
+ wrapper.style.cssText = 'padding:1rem;display:flex;flex-direction:column;gap:.75rem;';
314
+
315
+ const contentInput = makeTextInput('Text to style…', selected);
316
+ wrapper.appendChild(makeRow('Content', contentInput));
317
+
318
+ const sizeSelect = makeSelect([
319
+ ['xs','xs - 0.75rem'], ['sm','sm - 0.875rem'], ['base','base - 1rem'],
320
+ ['lg','lg - 1.125rem'], ['xl','xl - 1.25rem'], ['2xl','2xl - 1.5rem'],
321
+ ['3xl','3xl - 1.875rem'], ['4xl','4xl - 2.25rem'],
322
+ ]);
323
+ wrapper.appendChild(makeRow('Size', sizeSelect));
324
+
325
+ const weightSelect = makeSelect([
326
+ ['thin','Thin (100)'], ['light','Light (300)'], ['normal','Normal (400)'],
327
+ ['medium','Medium (500)'], ['semibold','Semibold (600)'], ['bold','Bold (700)'],
328
+ ['extrabold','Extrabold (800)'], ['black','Black (900)'],
329
+ ]);
330
+ wrapper.appendChild(makeRow('Weight', weightSelect));
331
+
332
+ const {wrap: boldWrap, cb: boldCb} = makeCheckbox('Bold (shorthand - overrides Weight)');
333
+ wrapper.appendChild(boldWrap);
334
+
335
+ const {wrap: italicWrap, cb: italicCb} = makeCheckbox('Italic');
336
+ wrapper.appendChild(italicWrap);
337
+
338
+ const colorInput = makeTextInput('e.g. primary, #ff0000, rgb(255,0,0)');
339
+ wrapper.appendChild(makeRow('Colour', colorInput));
340
+
341
+ const fontSelect = makeSelect([
342
+ ['Georgia','Georgia (serif)'], ['Arial','Arial (sans-serif)'],
343
+ ['Verdana','Verdana (sans-serif)'], ['Courier New','Courier New (monospace)'],
344
+ ['Times New Roman','Times New Roman (serif)'], ['Trebuchet MS','Trebuchet MS (sans-serif)'],
345
+ ]);
346
+ wrapper.appendChild(makeRow('Font', fontSelect));
347
+
348
+ const transformSelect = makeSelect([
349
+ ['upper','Uppercase'], ['lower','Lowercase'], ['capitalize','Capitalise'], ['none','None'],
350
+ ]);
351
+ wrapper.appendChild(makeRow('Transform', transformSelect));
352
+
353
+ const decorationSelect = makeSelect([
354
+ ['underline','Underline'], ['line-through','Line-through'], ['none','None'],
355
+ ]);
356
+ wrapper.appendChild(makeRow('Decoration', decorationSelect));
357
+
358
+ const spacingSelect = makeSelect([
359
+ ['tight','Tight'], ['normal','Normal'], ['wide','Wide'], ['wider','Wider'],
360
+ ]);
361
+ wrapper.appendChild(makeRow('Spacing', spacingSelect));
362
+
363
+ const classInput = makeTextInput('Optional CSS class');
364
+ wrapper.appendChild(makeRow('CSS Class', classInput));
365
+
366
+ const idInput = makeTextInput('Optional ID');
367
+ wrapper.appendChild(makeRow('ID', idInput));
368
+
369
+ // Live preview - <span> inside <div> for accurate inline-context representation
370
+ const previewWrap = document.createElement('div');
371
+ previewWrap.style.cssText = 'padding:.75rem;background:var(--dm-surface-subtle,#111);border-radius:4px;min-height:2.5rem;display:flex;align-items:center;';
372
+ const previewSpan = document.createElement('span');
373
+ previewSpan.textContent = selected || 'Preview text';
374
+ previewWrap.appendChild(previewSpan);
375
+ wrapper.appendChild(makeRow('Preview', previewWrap));
376
+
377
+ const COLOR_TOKENS_PREV = {
378
+ primary:'var(--dm-color-primary)',secondary:'var(--dm-color-secondary)',
379
+ muted:'var(--dm-text-muted)',danger:'var(--dm-color-danger)',
380
+ success:'var(--dm-color-success)',warning:'var(--dm-color-warning)',info:'var(--dm-color-info)',
381
+ };
382
+ const SIZE_MAP_PREV = {xs:'.75rem',sm:'.875rem',base:'1rem',lg:'1.125rem',xl:'1.25rem','2xl':'1.5rem','3xl':'1.875rem','4xl':'2.25rem'};
383
+ const WEIGHT_MAP_PREV = {thin:'100',light:'300',normal:'400',medium:'500',semibold:'600',bold:'700',extrabold:'800',black:'900'};
384
+ const SPACING_MAP_PREV = {tight:'-0.05em',normal:'0em',wide:'0.05em',wider:'0.1em'};
385
+ const FONT_MAP_PREV = {'Georgia':'Georgia,serif','Arial':'Arial,sans-serif','Verdana':'Verdana,sans-serif','Courier New':"'Courier New',monospace",'Times New Roman':"'Times New Roman',serif",'Trebuchet MS':"'Trebuchet MS',sans-serif"};
386
+ const TRANSFORM_MAP_PREV = {upper:'uppercase',lower:'lowercase',capitalize:'capitalize',none:'none'};
387
+ const DECORATION_MAP_PREV = {underline:'underline','line-through':'line-through',none:'none'};
388
+
389
+ function updatePreview() {
390
+ const styles = [];
391
+ if (sizeSelect.value) styles.push(`font-size:${SIZE_MAP_PREV[sizeSelect.value] || sizeSelect.value}`);
392
+ if (boldCb.checked) {
393
+ styles.push('font-weight:700');
394
+ } else if (weightSelect.value) {
395
+ styles.push(`font-weight:${WEIGHT_MAP_PREV[weightSelect.value] || weightSelect.value}`);
396
+ }
397
+ if (italicCb.checked) styles.push('font-style:italic');
398
+ const col = colorInput.value.trim();
399
+ if (col) styles.push(`color:${COLOR_TOKENS_PREV[col] || col}`);
400
+ if (fontSelect.value) styles.push(`font-family:${FONT_MAP_PREV[fontSelect.value]}`);
401
+ if (transformSelect.value) styles.push(`text-transform:${TRANSFORM_MAP_PREV[transformSelect.value]}`);
402
+ if (decorationSelect.value) styles.push(`text-decoration:${DECORATION_MAP_PREV[decorationSelect.value]}`);
403
+ if (spacingSelect.value) styles.push(`letter-spacing:${SPACING_MAP_PREV[spacingSelect.value]}`);
404
+ previewSpan.style.cssText = styles.join(';');
405
+ previewSpan.textContent = contentInput.value || 'Preview text';
406
+ }
407
+
408
+ boldCb.addEventListener('change', () => {
409
+ weightSelect.disabled = boldCb.checked;
410
+ if (boldCb.checked) weightSelect.value = '';
411
+ updatePreview();
412
+ });
413
+ [sizeSelect, weightSelect, fontSelect, transformSelect, decorationSelect, spacingSelect].forEach(el => {
414
+ el.addEventListener('change', updatePreview);
415
+ });
416
+ [colorInput, contentInput].forEach(el => {
417
+ el.addEventListener('input', updatePreview);
418
+ });
419
+ italicCb.addEventListener('change', updatePreview);
420
+
421
+ const insertBtn = document.createElement('button');
422
+ insertBtn.type = 'button';
423
+ insertBtn.className = 'btn btn-primary';
424
+ insertBtn.textContent = 'Insert';
425
+ wrapper.appendChild(insertBtn);
426
+
427
+ const modal = E.modal({title: 'Style Text', size: 'sm'});
428
+ modal.element.appendChild(wrapper);
429
+ modal.open();
430
+
431
+ insertBtn.addEventListener('click', () => {
432
+ const content = contentInput.value;
433
+ if (!content) return;
434
+ const parts = [];
435
+ if (sizeSelect.value) parts.push(`size="${sizeSelect.value}"`);
436
+ if (boldCb.checked) {
437
+ parts.push('bold');
438
+ } else if (weightSelect.value) {
439
+ parts.push(`weight="${weightSelect.value}"`);
440
+ }
441
+ if (italicCb.checked) parts.push('italic');
442
+ if (colorInput.value.trim()) parts.push(`color="${colorInput.value.trim()}"`);
443
+ if (fontSelect.value) parts.push(`font="${fontSelect.value}"`);
444
+ if (transformSelect.value) parts.push(`transform="${transformSelect.value}"`);
445
+ if (decorationSelect.value) parts.push(`decoration="${decorationSelect.value}"`);
446
+ if (spacingSelect.value) parts.push(`spacing="${spacingSelect.value}"`);
447
+ if (classInput.value.trim()) parts.push(`class="${classInput.value.trim()}"`);
448
+ if (idInput.value.trim()) parts.push(`id="${idInput.value.trim()}"`);
449
+ const attrStr = parts.length ? ' ' + parts.join(' ') : '';
450
+ const snippet = `[text${attrStr}]${content}[/text]`;
451
+ modal.close();
452
+ insertAtCursor(ta, snippet);
453
+ ta.dispatchEvent(new Event('input', {bubbles: true}));
454
+ ta.focus();
455
+ });
456
+ // end of action === 'text' branch
457
+ }
458
+ ```
459
+
460
+ - [ ] **Step 4: Verify in the browser**
461
+
462
+ Open the page editor in the admin. Click the Insert (`+`) dropdown. Confirm **Text** appears between Tabs and Timeline
463
+ with the `text-style` icon.
464
+
465
+ Click Text → confirm the "Style Text" modal opens with all controls. Test:
466
+
467
+ - Select a size, enable Bold, add a colour token (`primary`) → preview updates in real time
468
+ - Enable Bold → confirm Weight select is disabled
469
+ - Type content → confirm preview text updates
470
+ - Click Insert → confirm the correct shortcode is inserted at the cursor, modal closes
471
+
472
+ Also verify that selecting text in the editor before opening the modal pre-fills the Content field.
473
+
474
+ - [ ] **Step 5: Commit**
475
+
476
+ ```bash
477
+ git add admin/js/lib/markdown-toolbar.js
478
+ git commit -m "feat: add [text] shortcode insert modal to Page Editor"
479
+ ```
480
+
481
+ ---
482
+
483
+ ## Task 3: Document `[text]` in `docs/markdown-shortcodes.md`
484
+
485
+ **Files:**
486
+
487
+ - Modify: `docs/markdown-shortcodes.md`
488
+
489
+ - [ ] **Step 1: Add the `[text]` section**
490
+
491
+ Find the `## [badge]` section and add the following section immediately before it (text sorts before badge
492
+ alphabetically, but place it wherever the doc's existing ordering convention dictates - check if sections are
493
+ alphabetical or by category):
494
+
495
+ ```markdown
496
+ ## `[text]`
497
+
498
+ Wraps inline text in a `<span>` with custom styles. All attributes are optional.
499
+
500
+ ```
501
+
502
+ [text size="xl" bold italic color="primary"]Styled text[/text]
503
+ [text font="Georgia" decoration="underline" spacing="wide"]Classic[/text]
504
+ [text size="2em" color="#ff6600"]Custom size and colour[/text]
505
+
506
+ ```
507
+
508
+ ### Attributes
509
+
510
+ | Attribute | Values | Notes |
511
+ |---|---|---|
512
+ | `size` | `xs` `sm` `base` `lg` `xl` `2xl` `3xl` `4xl` or raw CSS (e.g. `"1.5em"`) | Maps to `font-size` |
513
+ | `weight` | `thin` `light` `normal` `medium` `semibold` `bold` `extrabold` `black` | Maps to `font-weight` |
514
+ | `bold` | flag | Shorthand for `font-weight:700`. Overrides `weight` if both present |
515
+ | `italic` | flag | `font-style:italic` |
516
+ | `color` | Token (`primary` `secondary` `muted` `danger` `success` `warning` `info`) or any CSS colour | Maps to `color` |
517
+ | `font` | `Georgia` `Arial` `Verdana` `Courier New` `Times New Roman` `Trebuchet MS` | Web-safe fonts only |
518
+ | `transform` | `upper` `lower` `capitalize` `none` | Maps to `text-transform` |
519
+ | `decoration` | `underline` `line-through` `none` | Maps to `text-decoration` |
520
+ | `spacing` | `tight` `normal` `wide` `wider` or raw CSS (e.g. `"0.1em"`) | Maps to `letter-spacing` |
521
+ | `class` | CSS class name(s) | Added to the `<span>` element |
522
+ | `id` | Element ID | Added to the `<span>` element |
523
+
524
+ ### Notes
525
+
526
+ - `align` is intentionally not supported - `text-align` has no effect on inline elements. Use `[center]` for centring or a grid/card for other layout alignment.
527
+ - The Page Editor Insert menu includes a **Text** entry that opens a configuration modal with live preview.
528
+ ```
529
+
530
+ - [ ] **Step 2: Commit**
531
+
532
+ ```bash
533
+ git add docs/markdown-shortcodes.md
534
+ git commit -m "docs: document [text] shortcode"
535
+ ```
536
+
537
+ ---
538
+
539
+ ## Done
540
+
541
+ At this point:
542
+
543
+ - `[text size="xl" bold italic]Hello[/text]` renders correctly in public pages
544
+ - The Page Editor Insert → Text menu item opens the modal, live preview works, Insert button writes the shortcode
545
+ - `docs/markdown-shortcodes.md` is updated