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.
- package/CHANGELOG.md +4349 -0
- package/README.md +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/js/lib/effect-defs.js +1 -1
- package/admin/js/lib/effects-builder.js +2 -2
- package/admin/js/lib/mail-compose.js +1 -0
- package/admin/js/lib/page-editor-reference.js +5 -5
- package/admin/js/lib/shortcode-tags.js +1 -1
- package/admin/js/templates/call-new.html +1 -1
- package/admin/js/templates/docs/usage-shortcodes.html +39 -0
- package/admin/js/views/call-new.js +1 -1
- package/admin/js/views/contacts.js +15 -15
- package/admin/js/views/form-submissions.js +2 -2
- package/docs/DommaFeatures.md +3005 -0
- package/docs/MultiTenant.md +176 -0
- package/docs/api-reference.md +1667 -0
- package/docs/cache.md +96 -0
- package/docs/configuration.md +1811 -0
- package/docs/editions-and-licences.md +190 -0
- package/docs/getting-started.md +201 -0
- package/docs/guides/form-triggers.md +260 -0
- package/docs/guides/mongodb-storage-adapter.md +282 -0
- package/docs/guides/multi-page-forms.md +121 -0
- package/docs/images/scribe-composer.png +0 -0
- package/docs/markdown-shortcodes.md +1738 -0
- package/docs/pentest-report-2026-04-03.md +181 -0
- package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
- package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
- package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
- package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
- package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
- package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
- package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
- package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
- package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
- package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
- package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
- package/docs/plans/2026-03-05-user-types-design.md +139 -0
- package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
- package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
- package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
- package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
- package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
- package/docs/plans/2026-06-10-core-project-design.md +240 -0
- package/docs/plugin-development.md +827 -0
- package/docs/recruitment-recipe.md +501 -0
- package/docs/scaffolding.md +312 -0
- package/docs/screenshots/calculator-floating.png +0 -0
- package/docs/screenshots/calculator-standard.png +0 -0
- package/docs/screenshots/email-settings.png +0 -0
- package/docs/screenshots/messages-count.png +0 -0
- package/docs/structure/user-types.md +107 -0
- package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
- package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
- package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
- package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
- package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
- package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
- package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
- package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
- package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
- package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
- package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
- package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
- package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
- package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
- package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
- package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
- package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
- package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
- package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
- package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
- package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
- package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
- package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
- package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
- package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
- package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
- package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
- package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
- package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
- package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
- package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
- package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
- package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
- package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
- package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
- package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
- package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
- package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
- package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
- package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
- package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
- package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
- package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
- package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
- package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
- package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
- package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
- package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
- package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
- package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
- package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
- package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
- package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
- package/docs/theming.md +320 -0
- package/package.json +2 -2
- package/plugins/_lib/admin/mail/compose-window.js +3 -1
- package/plugins/_lib/admin/mail/identity.js +18 -3
- package/plugins/_lib/admin/mail/reader-view.js +32 -3
- package/plugins/blog/CLAUDE.md +4 -0
- package/plugins/blog/admin/lib/email.js +191 -0
- package/plugins/blog/admin/views/comments.js +3 -1
- package/plugins/blog/plugin.json +2 -2
- package/plugins/blog/plugin.public.js +1 -1
- package/plugins/blog/tests/email.test.js +69 -0
- package/plugins/booking/CLAUDE.md +4 -0
- package/plugins/booking/admin/lib/email.js +191 -0
- package/plugins/booking/admin/views/panels.js +7 -3
- package/plugins/booking/plugin.json +1 -1
- package/plugins/booking/tests/email.test.js +69 -0
- package/plugins/free-tier.lock.json +27 -19
- package/plugins/shopping-cart/CLAUDE.md +4 -0
- package/plugins/shopping-cart/admin/lib/email.js +191 -0
- package/plugins/shopping-cart/admin/views/orders.js +11 -3
- package/plugins/shopping-cart/plugin.json +2 -2
- package/plugins/shopping-cart/tests/email.test.js +69 -0
- package/plugins/support/CLAUDE.md +4 -0
- package/plugins/support/admin/lib/email.js +191 -0
- package/plugins/support/admin/views/desk.js +4 -1
- package/plugins/support/admin/views/inbox.js +6 -2
- package/plugins/support/plugin.json +2 -2
- package/plugins/support/tests/email.test.js +69 -0
- package/public/js/celebrations/core/canvas.js +1 -1
- package/public/js/celebrations/index.js +1 -1
- package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
- package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
- package/public/js/celebrations/message-attrs.mjs +1 -0
- package/public/js/celebrations/themes/anniversary.js +1 -0
- package/public/js/celebrations/themes/autumn.js +1 -0
- package/public/js/celebrations/themes/birthday.js +1 -0
- package/public/js/celebrations/themes/butterfly-flight.js +1 -0
- package/public/js/celebrations/themes/calendar.js +1 -0
- package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
- package/public/js/celebrations/themes/chinese-new-year.js +1 -0
- package/public/js/celebrations/themes/christmas.js +1 -1
- package/public/js/celebrations/themes/easter.js +1 -0
- package/public/js/celebrations/themes/garden-drawing.js +1 -0
- package/public/js/celebrations/themes/garden.js +1 -0
- package/public/js/celebrations/themes/graduation.js +1 -0
- package/public/js/celebrations/themes/greeting.js +1 -0
- package/public/js/celebrations/themes/make-it-rain.js +1 -0
- package/public/js/celebrations/themes/money-rain.js +1 -0
- package/public/js/celebrations/themes/new-year.js +1 -0
- package/public/js/celebrations/themes/occasion-drawing.js +1 -0
- package/public/js/celebrations/themes/occasions.js +1 -0
- package/public/js/celebrations/themes/rain-control.js +1 -0
- package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
- package/public/js/celebrations/themes/sparkler-font.js +1 -0
- package/public/js/celebrations/themes/sparkler-writing.js +1 -0
- package/public/js/celebrations/themes/summer-garden.js +1 -0
- package/public/js/celebrations/themes/winter.js +1 -0
- package/public/js/celebrations/trait-data.mjs +1 -1
- package/public/js/effects.js +1 -1
- package/scripts/build.js +4 -0
- package/server/services/markdown.js +5 -2
- package/server/templates/page.html +1 -1
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
# Insert Builders - Tabs, Hero, Banner - 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:** Upgrade the Page Editor Insert toolbar with a richer Tabs dialog, a new Hero dialog, and a brand-new
|
|
7
|
+
`[banner]` shortcode end-to-end (server render + CSS + dismiss + toolbar item + insert dialog).
|
|
8
|
+
|
|
9
|
+
**Architecture:** A tiny shared utility (`shortcode-modal.js`) provides UI helpers imported by `page-editor.js`. The
|
|
10
|
+
three insert handlers (`onTabs`, `onHero`, `onBanner`) live inline in `page-editor.js` using `E.modal()` light DOM
|
|
11
|
+
projection. Server-side, `processBannerBlocks()` slots into the existing `parseMarkdown` pipeline and
|
|
12
|
+
`processTabsBlocks()` is extended for new attributes.
|
|
13
|
+
|
|
14
|
+
**Tech Stack:** Fastify 5 ESM, Domma SPA (admin), `E.modal()` light DOM, `markdown.js` shortcode pipeline,
|
|
15
|
+
`public/css/site.css` (Domma CSS custom properties), `public/js/site.js` (Domma event delegation).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## File Map
|
|
20
|
+
|
|
21
|
+
| File | Action | Responsibility |
|
|
22
|
+
|------------------------------------|------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
23
|
+
| `admin/js/lib/shortcode-modal.js` | **Create** | Shared UI helpers: `makeField`, `makeSelect`, `makeTextInput`, `makeCheckbox`, `makeLivePreview` |
|
|
24
|
+
| `admin/js/views/page-editor.js` | **Modify** | Add import; replace `onTabs`; add `onHero`; add `onBanner` |
|
|
25
|
+
| `admin/js/lib/markdown-toolbar.js` | **Modify** | Register `banner-insert` icon; add `banner` insert item; add dispatch; add `onBanner` to handler chain |
|
|
26
|
+
| `server/services/markdown.js` | **Modify** | Update `processTabsBlocks` (bordered/fade/icon); update `processHeroBlocks` (color/min-height); add `processBannerBlocks`; wire into pipeline; add `'banner'` to reserved names |
|
|
27
|
+
| `public/css/site.css` | **Modify** | Add `.dm-banner` base + 5 type variants |
|
|
28
|
+
| `public/js/site.js` | **Modify** | Add `.dm-banner__dismiss` click handler |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Task 1: `shortcode-modal.js` - Shared UI utility
|
|
33
|
+
|
|
34
|
+
**File:** Create `admin/js/lib/shortcode-modal.js`
|
|
35
|
+
|
|
36
|
+
This module provides the styled form primitives used across all three insert dialogs. Without it, each handler
|
|
37
|
+
copy-pastes the same CSS strings and DOM-building helpers.
|
|
38
|
+
|
|
39
|
+
- [ ] **Step 1: Create the file**
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
/**
|
|
43
|
+
* shortcode-modal.js
|
|
44
|
+
* Shared UI helpers for shortcode insert dialogs in the page editor.
|
|
45
|
+
*
|
|
46
|
+
* Exports:
|
|
47
|
+
* makeField(label, control) - labelled field wrapper
|
|
48
|
+
* makeSelect(items) - styled <select>
|
|
49
|
+
* makeTextInput(placeholder, value) - styled <input type="text">
|
|
50
|
+
* makeCheckbox(label) - styled checkbox row
|
|
51
|
+
* makeLivePreview() - { el, update(str) } live shortcode preview box
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
const LABEL_STYLE = 'display:block;font-size:.7rem;font-weight:600;color:var(--dm-text-muted,#aaa);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.25rem;';
|
|
55
|
+
const INPUT_STYLE = '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;box-sizing:border-box;';
|
|
56
|
+
|
|
57
|
+
export function makeField(label, control) {
|
|
58
|
+
const wrap = document.createElement('div');
|
|
59
|
+
const lbl = document.createElement('label');
|
|
60
|
+
lbl.style.cssText = LABEL_STYLE;
|
|
61
|
+
lbl.textContent = label;
|
|
62
|
+
wrap.appendChild(lbl);
|
|
63
|
+
wrap.appendChild(control);
|
|
64
|
+
return wrap;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function makeSelect(items) {
|
|
68
|
+
const sel = document.createElement('select');
|
|
69
|
+
sel.style.cssText = INPUT_STYLE;
|
|
70
|
+
items.forEach(([val, text]) => {
|
|
71
|
+
const opt = document.createElement('option');
|
|
72
|
+
opt.value = val;
|
|
73
|
+
opt.textContent = text;
|
|
74
|
+
sel.appendChild(opt);
|
|
75
|
+
});
|
|
76
|
+
return sel;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function makeTextInput(placeholder = '', value = '') {
|
|
80
|
+
const inp = document.createElement('input');
|
|
81
|
+
inp.type = 'text';
|
|
82
|
+
inp.placeholder = placeholder;
|
|
83
|
+
inp.value = value;
|
|
84
|
+
inp.style.cssText = INPUT_STYLE;
|
|
85
|
+
return inp;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function makeCheckbox(label) {
|
|
89
|
+
const wrap = document.createElement('label');
|
|
90
|
+
wrap.style.cssText = 'display:flex;align-items:center;gap:.5rem;cursor:pointer;';
|
|
91
|
+
const cb = document.createElement('input');
|
|
92
|
+
cb.type = 'checkbox';
|
|
93
|
+
const span = document.createElement('span');
|
|
94
|
+
span.style.cssText = 'font-size:.85em;color:var(--dm-text,#eee);';
|
|
95
|
+
span.textContent = label;
|
|
96
|
+
wrap.appendChild(cb);
|
|
97
|
+
wrap.appendChild(span);
|
|
98
|
+
return wrap;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function makeLivePreview() {
|
|
102
|
+
const el = document.createElement('pre');
|
|
103
|
+
el.style.cssText = 'background:var(--dm-surface-subtle,#111);border:1px solid var(--dm-border,#333);border-radius:4px;padding:.5rem .75rem;font-size:.75em;color:var(--dm-text-muted,#aaa);white-space:pre-wrap;word-break:break-all;margin:0;max-height:120px;overflow:auto;';
|
|
104
|
+
el.textContent = '';
|
|
105
|
+
return {
|
|
106
|
+
el,
|
|
107
|
+
update(str) { el.textContent = str; },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- [ ] **Step 2: Verify file exists**
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
ls /home/darryl/src/js/domma-cms/admin/js/lib/shortcode-modal.js
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Expected: file path printed, no error.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Task 2: `onTabs` - Upgraded insert dialog
|
|
123
|
+
|
|
124
|
+
**File:** Modify `admin/js/views/page-editor.js`
|
|
125
|
+
|
|
126
|
+
The existing `onTabs` handler builds a simple modal. Replace it with a richer version that uses `shortcode-modal.js`
|
|
127
|
+
helpers and adds: bordered style, fade animation, multiple-open toggle, per-tab icon inputs, live preview.
|
|
128
|
+
|
|
129
|
+
- [ ] **Step 1: Add the `shortcode-modal.js` import**
|
|
130
|
+
|
|
131
|
+
Find the imports block at the top of `page-editor.js`. It currently reads:
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
import{api as K}from"../api.js";import{CardBuilder}from"../lib/card-builder.js?v=4";import{attachEditorKeybindings as We,createToolbar as $e,insertAtCursor as ke}from"../lib/markdown-toolbar.js?v=9";import{populateThemeSelect as Ye}from"../lib/themes.js";import{createFoldingManager as je}from"../lib/editor-folding.js";
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Add the import for `shortcode-modal.js` as a **new line immediately after** the existing single-line import block (do
|
|
138
|
+
not insert into the middle of it - the existing block is a minified single line and must stay intact):
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
import { makeField, makeSelect, makeTextInput, makeCheckbox, makeLivePreview } from '../lib/shortcode-modal.js';
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Note: `page-editor.js` uses `?v=N` cache-busting suffixes on some imports during development. Add `?v=1` to
|
|
145
|
+
`shortcode-modal.js` if hot-reloading issues occur.
|
|
146
|
+
|
|
147
|
+
- [ ] **Step 2: Locate the existing `onTabs` handler**
|
|
148
|
+
|
|
149
|
+
Search for `onTabs(` in `page-editor.js`. The handler starts with:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
V.onTabs(e=>{const l=document.createElement("div");l.style.cssText="padding:.25rem 0 .5rem;
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
And ends at the next `}),V.onGrid(` call.
|
|
156
|
+
|
|
157
|
+
- [ ] **Step 3: Replace the `onTabs` handler**
|
|
158
|
+
|
|
159
|
+
Replace the full existing `onTabs(...)` call (from `V.onTabs(` to the matching `}),` before `V.onGrid(`) with:
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
V.onTabs(ta => {
|
|
163
|
+
const body = document.createElement('div');
|
|
164
|
+
body.style.cssText = 'padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem;';
|
|
165
|
+
|
|
166
|
+
// Style
|
|
167
|
+
const styleSelect = makeSelect([
|
|
168
|
+
['', 'Default (underline)'],
|
|
169
|
+
['pills', 'Pills'],
|
|
170
|
+
['bordered', 'Bordered'],
|
|
171
|
+
]);
|
|
172
|
+
body.appendChild(makeField('Style', styleSelect));
|
|
173
|
+
|
|
174
|
+
// Alignment
|
|
175
|
+
const alignSelect = makeSelect([
|
|
176
|
+
['', 'Left'],
|
|
177
|
+
['center', 'Centre'],
|
|
178
|
+
['right', 'Right'],
|
|
179
|
+
]);
|
|
180
|
+
body.appendChild(makeField('Alignment', alignSelect));
|
|
181
|
+
|
|
182
|
+
// Animation
|
|
183
|
+
const animSelect = makeSelect([['', 'None'], ['fade', 'Fade']]);
|
|
184
|
+
body.appendChild(makeField('Animation', animSelect));
|
|
185
|
+
|
|
186
|
+
// Multiple open
|
|
187
|
+
const multipleWrap = makeCheckbox('Allow multiple panels open at once');
|
|
188
|
+
const multipleCb = multipleWrap.querySelector('input');
|
|
189
|
+
body.appendChild(multipleWrap);
|
|
190
|
+
|
|
191
|
+
// Tab list
|
|
192
|
+
const tabListLabel = document.createElement('div');
|
|
193
|
+
tabListLabel.style.cssText = 'display:block;font-size:.7rem;font-weight:600;color:var(--dm-text-muted,#aaa);text-transform:uppercase;letter-spacing:.05em;';
|
|
194
|
+
tabListLabel.textContent = 'Tabs';
|
|
195
|
+
body.appendChild(tabListLabel);
|
|
196
|
+
|
|
197
|
+
const tabRows = document.createElement('div');
|
|
198
|
+
tabRows.style.cssText = 'display:flex;flex-direction:column;gap:.4rem;';
|
|
199
|
+
const tabs = [];
|
|
200
|
+
|
|
201
|
+
function syncRemoveButtons() {
|
|
202
|
+
tabs.forEach(t => {
|
|
203
|
+
t.removeBtn.disabled = tabs.length <= 2;
|
|
204
|
+
t.removeBtn.style.opacity = tabs.length <= 2 ? '0.3' : '1';
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildShortcode() {
|
|
209
|
+
const style = styleSelect.value;
|
|
210
|
+
const align = alignSelect.value;
|
|
211
|
+
const anim = animSelect.value;
|
|
212
|
+
const multi = multipleCb.checked;
|
|
213
|
+
const parts = [];
|
|
214
|
+
if (style) parts.push(`style="${style}"`);
|
|
215
|
+
if (align) parts.push(`align="${align}"`);
|
|
216
|
+
if (anim) parts.push(anim);
|
|
217
|
+
if (multi) parts.push('multiple="true"');
|
|
218
|
+
const attrStr = parts.length ? ' ' + parts.join(' ') : '';
|
|
219
|
+
let out = `[tabs${attrStr}]\n`;
|
|
220
|
+
tabs.forEach((t, i) => {
|
|
221
|
+
const title = t.titleInput.value.trim() || `Tab ${i + 1}`;
|
|
222
|
+
const icon = t.iconInput.value.trim();
|
|
223
|
+
const tabAttr = icon ? ` title="${title}" icon="${icon}"` : ` title="${title}"`;
|
|
224
|
+
out += `[tab${tabAttr}]Tab content here.[/tab]\n`;
|
|
225
|
+
});
|
|
226
|
+
out += '[/tabs]';
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function addTab(title = '') {
|
|
231
|
+
if (tabs.length >= 10) return;
|
|
232
|
+
const row = document.createElement('div');
|
|
233
|
+
row.style.cssText = 'display:flex;gap:.4rem;align-items:center;';
|
|
234
|
+
|
|
235
|
+
const titleInput = makeTextInput('Tab title', title);
|
|
236
|
+
titleInput.style.cssText += 'flex:1;';
|
|
237
|
+
|
|
238
|
+
const iconInput = makeTextInput('icon (optional)');
|
|
239
|
+
iconInput.style.cssText += 'flex:.6;';
|
|
240
|
+
|
|
241
|
+
const removeBtn = document.createElement('button');
|
|
242
|
+
removeBtn.type = 'button';
|
|
243
|
+
removeBtn.textContent = '×';
|
|
244
|
+
removeBtn.style.cssText = 'padding:.3rem .5rem;background:transparent;border:1px solid var(--dm-border,#333);border-radius:4px;color:var(--dm-text-muted,#aaa);cursor:pointer;font-size:.85em;flex-shrink:0;';
|
|
245
|
+
|
|
246
|
+
const entry = { row, titleInput, iconInput, removeBtn };
|
|
247
|
+
tabs.push(entry);
|
|
248
|
+
row.appendChild(titleInput);
|
|
249
|
+
row.appendChild(iconInput);
|
|
250
|
+
row.appendChild(removeBtn);
|
|
251
|
+
tabRows.appendChild(row);
|
|
252
|
+
|
|
253
|
+
removeBtn.addEventListener('click', () => {
|
|
254
|
+
if (tabs.length <= 2) return;
|
|
255
|
+
const idx = tabs.indexOf(entry);
|
|
256
|
+
tabs.splice(idx, 1);
|
|
257
|
+
tabRows.removeChild(row);
|
|
258
|
+
syncRemoveButtons();
|
|
259
|
+
preview.update(buildShortcode());
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
[titleInput, iconInput].forEach(el => el.addEventListener('input', () => preview.update(buildShortcode())));
|
|
263
|
+
syncRemoveButtons();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
addTab('Overview');
|
|
267
|
+
addTab('Details');
|
|
268
|
+
body.appendChild(tabRows);
|
|
269
|
+
|
|
270
|
+
const addBtn = document.createElement('button');
|
|
271
|
+
addBtn.type = 'button';
|
|
272
|
+
addBtn.textContent = '+ Add Tab';
|
|
273
|
+
addBtn.style.cssText = 'padding:.35rem .75rem;background:transparent;border:1px dashed var(--dm-border,#555);border-radius:4px;color:var(--dm-text-muted,#aaa);cursor:pointer;font-size:.85em;align-self:flex-start;';
|
|
274
|
+
addBtn.addEventListener('click', () => { addTab(''); tabs[tabs.length - 1].titleInput.focus(); preview.update(buildShortcode()); });
|
|
275
|
+
body.appendChild(addBtn);
|
|
276
|
+
|
|
277
|
+
// Live preview
|
|
278
|
+
const preview = makeLivePreview();
|
|
279
|
+
preview.update(buildShortcode());
|
|
280
|
+
body.appendChild(makeField('Shortcode preview', preview.el));
|
|
281
|
+
|
|
282
|
+
// Wire all top-level fields to preview
|
|
283
|
+
[styleSelect, alignSelect, animSelect].forEach(el => el.addEventListener('change', () => preview.update(buildShortcode())));
|
|
284
|
+
multipleCb.addEventListener('change', () => preview.update(buildShortcode()));
|
|
285
|
+
|
|
286
|
+
// Insert button
|
|
287
|
+
const insertBtn = document.createElement('button');
|
|
288
|
+
insertBtn.type = 'button';
|
|
289
|
+
insertBtn.className = 'btn btn-primary';
|
|
290
|
+
insertBtn.textContent = 'Insert Tabs';
|
|
291
|
+
body.appendChild(insertBtn);
|
|
292
|
+
|
|
293
|
+
const modal = E.modal({ title: 'Insert Tabs', size: 'sm' });
|
|
294
|
+
modal.element.appendChild(body);
|
|
295
|
+
modal.open();
|
|
296
|
+
requestAnimationFrame(() => tabs[0].titleInput.focus());
|
|
297
|
+
|
|
298
|
+
insertBtn.addEventListener('click', () => {
|
|
299
|
+
modal.close();
|
|
300
|
+
ke(ta, buildShortcode());
|
|
301
|
+
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
302
|
+
ta.focus();
|
|
303
|
+
});
|
|
304
|
+
}),
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
- [ ] **Step 4: Smoke test in browser**
|
|
308
|
+
|
|
309
|
+
1. Open the admin, navigate to any page editor
|
|
310
|
+
2. Click Insert → Tabs
|
|
311
|
+
3. Verify modal opens with Style/Alignment/Animation selects, Multiple checkbox, two tab rows each with title + icon
|
|
312
|
+
fields
|
|
313
|
+
4. Change style to "Pills" → verify live preview updates to `[tabs style="pills"]`
|
|
314
|
+
5. Add a tab → verify third row appears
|
|
315
|
+
6. Remove a tab → verify it disappears (remove button disabled at 2 tabs)
|
|
316
|
+
7. Click Insert Tabs → verify shortcode inserted at cursor in editor
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Task 3: `processTabsBlocks` - Server-side bordered/fade/icon support
|
|
321
|
+
|
|
322
|
+
**File:** Modify `server/services/markdown.js`
|
|
323
|
+
|
|
324
|
+
The insert dialog can now generate `style="bordered"`, `fade`, and per-tab `icon` attributes. Without server-side
|
|
325
|
+
support, they silently render as unstyled tabs with no icons.
|
|
326
|
+
|
|
327
|
+
- [ ] **Step 1: Locate `processTabsBlocks`**
|
|
328
|
+
|
|
329
|
+
Find the function at approximately line 1466. It currently reads:
|
|
330
|
+
|
|
331
|
+
```js
|
|
332
|
+
const pillsClass = attrs.style === 'pills' ? ' tabs-pills' : '';
|
|
333
|
+
const centeredClass = attrs.align === 'center' ? ' tabs-centered' : '';
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
- [ ] **Step 2: Add bordered and fade class support**
|
|
337
|
+
|
|
338
|
+
After the existing class derivations, add:
|
|
339
|
+
|
|
340
|
+
```js
|
|
341
|
+
const borderedClass = attrs.style === 'bordered' ? ' tabs-bordered' : '';
|
|
342
|
+
const fadeClass = 'fade' in attrs ? ' tabs-fade' : '';
|
|
343
|
+
const rightClass = attrs.align === 'right' ? ' tabs-right' : '';
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Update the wrapper div return value to include all new classes:
|
|
347
|
+
|
|
348
|
+
```js
|
|
349
|
+
`<div class="tabs${pillsClass}${borderedClass}${centeredClass}${rightClass}${fadeClass}"${idAttr}>\n` +
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Note: `pills` and `bordered` are mutually exclusive (both use `style=`), so at most one will be non-empty.
|
|
353
|
+
|
|
354
|
+
- [ ] **Step 3: Add per-tab icon support**
|
|
355
|
+
|
|
356
|
+
Inside the `body.replace(/\[tab...` callback, the tab attrs are already parsed via `parseShortcodeAttrs(tabAttr)`. Find
|
|
357
|
+
the line:
|
|
358
|
+
|
|
359
|
+
```js
|
|
360
|
+
const title = tabAttrs.title || `Tab ${tabIdx}`;
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Add icon extraction immediately after:
|
|
364
|
+
|
|
365
|
+
```js
|
|
366
|
+
const icon = tabAttrs.icon || '';
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Then update the `navItems` map to render the icon span before the title text:
|
|
370
|
+
|
|
371
|
+
```js
|
|
372
|
+
const navItems = items.map(t =>
|
|
373
|
+
`<button class="tab-item${t.first ? ' active' : ''}">${t.icon ? `<span data-icon="${escapeAttr(t.icon)}"></span>` : ''}${escapeAttr(t.title)}</button>`
|
|
374
|
+
).join('\n ');
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Update the `items.push(...)` call to include `icon`:
|
|
378
|
+
|
|
379
|
+
```js
|
|
380
|
+
items.push({ title, icon, paneId, bodyHtml, first: tabIdx === 1 });
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
- [ ] **Step 4: Verify via curl**
|
|
384
|
+
|
|
385
|
+
Create a test page or use the existing preview API:
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
curl -s -X POST http://localhost:4096/api/pages/preview \
|
|
389
|
+
-H 'Content-Type: application/json' \
|
|
390
|
+
-H 'Authorization: Bearer YOUR_TOKEN' \
|
|
391
|
+
-d '{"content":"[tabs style=\"bordered\" fade]\n[tab title=\"One\" icon=\"star\"]Hello[/tab]\n[tab title=\"Two\"]World[/tab]\n[/tabs]"}' \
|
|
392
|
+
| grep -o 'tabs[^"]*"'
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Expected: response HTML contains `class="tabs tabs-bordered tabs-fade"` and `data-icon="star"`.
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Task 4: `onHero` - New insert dialog
|
|
400
|
+
|
|
401
|
+
**File:** Modify `admin/js/views/page-editor.js`
|
|
402
|
+
|
|
403
|
+
The toolbar already dispatches `handlers.hero(ta)` but no handler is registered. Add `V.onHero(...)` alongside the other
|
|
404
|
+
handler registrations (near `V.onCard`, `V.onTabs`).
|
|
405
|
+
|
|
406
|
+
- [ ] **Step 1: Locate where to add the handler**
|
|
407
|
+
|
|
408
|
+
Find the line `V.onCard(e=>{CardBuilder.open(...)})` - this is near the end of the `onMount` function. Add
|
|
409
|
+
`V.onHero(...)` immediately before or after `V.onCard(...)`.
|
|
410
|
+
|
|
411
|
+
- [ ] **Step 2: Add the `onHero` handler**
|
|
412
|
+
|
|
413
|
+
```js
|
|
414
|
+
V.onHero(ta => {
|
|
415
|
+
const body = document.createElement('div');
|
|
416
|
+
body.style.cssText = 'padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem;';
|
|
417
|
+
|
|
418
|
+
const titleInput = makeTextInput('Hero title (required)');
|
|
419
|
+
body.appendChild(makeField('Title', titleInput));
|
|
420
|
+
|
|
421
|
+
const taglineInput = makeTextInput('Tagline / subtitle (optional)');
|
|
422
|
+
body.appendChild(makeField('Tagline', taglineInput));
|
|
423
|
+
|
|
424
|
+
const sizeSelect = makeSelect([
|
|
425
|
+
['', 'Default'],
|
|
426
|
+
['sm', 'Small'],
|
|
427
|
+
['lg', 'Large'],
|
|
428
|
+
['full', 'Full screen'],
|
|
429
|
+
]);
|
|
430
|
+
body.appendChild(makeField('Size', sizeSelect));
|
|
431
|
+
|
|
432
|
+
const alignSelect = makeSelect([
|
|
433
|
+
['center', 'Centre'],
|
|
434
|
+
['left', 'Left'],
|
|
435
|
+
['right', 'Right'],
|
|
436
|
+
]);
|
|
437
|
+
body.appendChild(makeField('Alignment', alignSelect));
|
|
438
|
+
|
|
439
|
+
// Background type radio group
|
|
440
|
+
const bgLabel = document.createElement('div');
|
|
441
|
+
bgLabel.style.cssText = 'display:block;font-size:.7rem;font-weight:600;color:var(--dm-text-muted,#aaa);text-transform:uppercase;letter-spacing:.05em;';
|
|
442
|
+
bgLabel.textContent = 'Background';
|
|
443
|
+
body.appendChild(bgLabel);
|
|
444
|
+
|
|
445
|
+
const bgGroup = document.createElement('div');
|
|
446
|
+
bgGroup.style.cssText = 'display:flex;gap:.75rem;';
|
|
447
|
+
const bgOptions = ['None', 'Colour', 'Image'];
|
|
448
|
+
const bgRadios = bgOptions.map(opt => {
|
|
449
|
+
const lbl = document.createElement('label');
|
|
450
|
+
lbl.style.cssText = 'display:flex;align-items:center;gap:.3rem;font-size:.85em;cursor:pointer;color:var(--dm-text,#eee);';
|
|
451
|
+
const rb = document.createElement('input');
|
|
452
|
+
rb.type = 'radio';
|
|
453
|
+
rb.name = 'hero-bg';
|
|
454
|
+
rb.value = opt.toLowerCase();
|
|
455
|
+
if (opt === 'None') rb.checked = true;
|
|
456
|
+
lbl.appendChild(rb);
|
|
457
|
+
lbl.appendChild(document.createTextNode(opt));
|
|
458
|
+
bgGroup.appendChild(lbl);
|
|
459
|
+
return rb;
|
|
460
|
+
});
|
|
461
|
+
body.appendChild(bgGroup);
|
|
462
|
+
|
|
463
|
+
// Colour field (hidden by default)
|
|
464
|
+
const colorInput = makeTextInput('#3b82f6 or any CSS colour');
|
|
465
|
+
const colorField = makeField('Background Colour', colorInput);
|
|
466
|
+
colorField.style.display = 'none';
|
|
467
|
+
body.appendChild(colorField);
|
|
468
|
+
|
|
469
|
+
// Image fields (hidden by default)
|
|
470
|
+
const imageInput = makeTextInput('/media/hero.jpg');
|
|
471
|
+
const imageField = makeField('Image Path', imageInput);
|
|
472
|
+
imageField.style.display = 'none';
|
|
473
|
+
|
|
474
|
+
const overlaySelect = makeSelect([
|
|
475
|
+
['', 'None'],
|
|
476
|
+
['dark', 'Dark'],
|
|
477
|
+
['light', 'Light'],
|
|
478
|
+
['darker', 'Darker'],
|
|
479
|
+
['gradient', 'Gradient'],
|
|
480
|
+
]);
|
|
481
|
+
const overlayField = makeField('Overlay', overlaySelect);
|
|
482
|
+
overlayField.style.display = 'none';
|
|
483
|
+
body.appendChild(imageField);
|
|
484
|
+
body.appendChild(overlayField);
|
|
485
|
+
|
|
486
|
+
const minHeightInput = makeTextInput('e.g. 400px (optional)');
|
|
487
|
+
body.appendChild(makeField('Min Height', minHeightInput));
|
|
488
|
+
|
|
489
|
+
// Show/hide conditional fields
|
|
490
|
+
function syncBgFields() {
|
|
491
|
+
const val = bgRadios.find(r => r.checked)?.value || 'none';
|
|
492
|
+
colorField.style.display = val === 'colour' ? '' : 'none';
|
|
493
|
+
imageField.style.display = val === 'image' ? '' : 'none';
|
|
494
|
+
overlayField.style.display = val === 'image' ? '' : 'none';
|
|
495
|
+
preview.update(buildShortcode());
|
|
496
|
+
}
|
|
497
|
+
bgRadios.forEach(r => r.addEventListener('change', syncBgFields));
|
|
498
|
+
|
|
499
|
+
function buildShortcode() {
|
|
500
|
+
const parts = [];
|
|
501
|
+
const title = titleInput.value.trim();
|
|
502
|
+
if (title) parts.push(`title="${title}"`);
|
|
503
|
+
const tagline = taglineInput.value.trim();
|
|
504
|
+
if (tagline) parts.push(`tagline="${tagline}"`);
|
|
505
|
+
const size = sizeSelect.value;
|
|
506
|
+
if (size) parts.push(`size="${size}"`);
|
|
507
|
+
const align = alignSelect.value;
|
|
508
|
+
if (align && align !== 'center') parts.push(`align="${align}"`);
|
|
509
|
+
|
|
510
|
+
const bgType = bgRadios.find(r => r.checked)?.value || 'none';
|
|
511
|
+
if (bgType === 'colour') {
|
|
512
|
+
const col = colorInput.value.trim();
|
|
513
|
+
if (col) parts.push(`color="${col}"`);
|
|
514
|
+
} else if (bgType === 'image') {
|
|
515
|
+
const img = imageInput.value.trim();
|
|
516
|
+
if (img) parts.push(`image="${img}"`);
|
|
517
|
+
const ov = overlaySelect.value;
|
|
518
|
+
if (ov) parts.push(`overlay="${ov}"`);
|
|
519
|
+
}
|
|
520
|
+
const minH = minHeightInput.value.trim();
|
|
521
|
+
if (minH) parts.push(`min-height="${minH}"`);
|
|
522
|
+
|
|
523
|
+
const attrStr = parts.length ? ' ' + parts.join(' ') : '';
|
|
524
|
+
return `[hero${attrStr}]\nContent here.\n[/hero]`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Live preview
|
|
528
|
+
const preview = makeLivePreview();
|
|
529
|
+
preview.update(buildShortcode());
|
|
530
|
+
body.appendChild(makeField('Shortcode preview', preview.el));
|
|
531
|
+
|
|
532
|
+
const allInputs = [titleInput, taglineInput, colorInput, imageInput, minHeightInput];
|
|
533
|
+
allInputs.forEach(el => el.addEventListener('input', () => preview.update(buildShortcode())));
|
|
534
|
+
[sizeSelect, alignSelect, overlaySelect].forEach(el => el.addEventListener('change', () => preview.update(buildShortcode())));
|
|
535
|
+
|
|
536
|
+
const insertBtn = document.createElement('button');
|
|
537
|
+
insertBtn.type = 'button';
|
|
538
|
+
insertBtn.className = 'btn btn-primary';
|
|
539
|
+
insertBtn.textContent = 'Insert Hero';
|
|
540
|
+
body.appendChild(insertBtn);
|
|
541
|
+
|
|
542
|
+
const modal = E.modal({ title: 'Insert Hero', size: 'sm' });
|
|
543
|
+
modal.element.appendChild(body);
|
|
544
|
+
modal.open();
|
|
545
|
+
requestAnimationFrame(() => titleInput.focus());
|
|
546
|
+
|
|
547
|
+
insertBtn.addEventListener('click', () => {
|
|
548
|
+
modal.close();
|
|
549
|
+
ke(ta, buildShortcode());
|
|
550
|
+
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
551
|
+
ta.focus();
|
|
552
|
+
});
|
|
553
|
+
}),
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
- [ ] **Step 3: Smoke test**
|
|
557
|
+
|
|
558
|
+
1. Click Insert → Hero
|
|
559
|
+
2. Verify modal opens with all fields
|
|
560
|
+
3. Select "Colour" → colour input appears; select "Image" → image + overlay appear; select "None" → both hidden
|
|
561
|
+
4. Fill in title + image path → live preview updates
|
|
562
|
+
5. Click Insert Hero → shortcode appears in editor
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## Task 5: `processHeroBlocks` - Add `color` and `min-height` support
|
|
567
|
+
|
|
568
|
+
**File:** Modify `server/services/markdown.js`
|
|
569
|
+
|
|
570
|
+
The insert dialog can generate `color="..."` (custom background colour) and `min-height="..."`. `processHeroBlocks` at
|
|
571
|
+
line ~2265 must handle these.
|
|
572
|
+
|
|
573
|
+
- [ ] **Step 1: Locate attribute handling in `processHeroBlocks`**
|
|
574
|
+
|
|
575
|
+
Find the block inside the regex callback where `attrs` is used to build inline styles or class names. The function
|
|
576
|
+
currently handles `image`, `overlay`, `align`, `variant`, `size`.
|
|
577
|
+
|
|
578
|
+
- [ ] **Step 2: Add `color` attribute**
|
|
579
|
+
|
|
580
|
+
Find where the `style` attribute string is built for the hero wrapper div (where `image` is applied as
|
|
581
|
+
`background-image`). Add handling for `color`:
|
|
582
|
+
|
|
583
|
+
```js
|
|
584
|
+
const colorStyle = attrs.color ? `background:${attrs.color};` : '';
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Append `colorStyle` to the element's inline style (alongside any existing background-image style). Ensure `color` and
|
|
588
|
+
`image` are mutually exclusive - if both are provided, `image` wins (it's the more specific declaration).
|
|
589
|
+
|
|
590
|
+
- [ ] **Step 3: Add `min-height` attribute**
|
|
591
|
+
|
|
592
|
+
```js
|
|
593
|
+
const minHeightStyle = attrs['min-height'] ? `min-height:${attrs['min-height']};` : '';
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
Include `minHeightStyle` in the wrapper element's inline `style` attribute.
|
|
597
|
+
|
|
598
|
+
- [ ] **Step 4: Verify via curl**
|
|
599
|
+
|
|
600
|
+
```bash
|
|
601
|
+
curl -s -X POST http://localhost:4096/api/pages/preview \
|
|
602
|
+
-H 'Content-Type: application/json' \
|
|
603
|
+
-H 'Authorization: Bearer YOUR_TOKEN' \
|
|
604
|
+
-d '{"content":"[hero title=\"Test\" color=\"#1e293b\" min-height=\"300px\"]\nHello\n[/hero]"}' \
|
|
605
|
+
| grep -o 'style="[^"]*"' | head -3
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
Expected: inline style contains `background:#1e293b` and `min-height:300px`.
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## Task 6: `processBannerBlocks` - New server-side shortcode
|
|
613
|
+
|
|
614
|
+
**File:** Modify `server/services/markdown.js`
|
|
615
|
+
|
|
616
|
+
- [ ] **Step 1: Add `'banner'` to the reserved shortcode names array**
|
|
617
|
+
|
|
618
|
+
Find the array near the top of `markdown.js` that lists reserved shortcode tag names (around line 19, containing
|
|
619
|
+
`'block'`, `'collection'`, `'tabs'`, etc.). Add `'banner'` to the list:
|
|
620
|
+
|
|
621
|
+
```js
|
|
622
|
+
'banner',
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
- [ ] **Step 2: Add the `processBannerBlocks` function**
|
|
626
|
+
|
|
627
|
+
Add this function after `processTextBlocks` (around line 1803) and before the next function:
|
|
628
|
+
|
|
629
|
+
```js
|
|
630
|
+
/**
|
|
631
|
+
* Pre-process [banner] shortcodes before running through marked.
|
|
632
|
+
*
|
|
633
|
+
* Syntax:
|
|
634
|
+
* [banner type="info" title="Note" icon="info" dismissible class="my-class"]
|
|
635
|
+
* Body content with **Markdown** support.
|
|
636
|
+
* [/banner]
|
|
637
|
+
*
|
|
638
|
+
* Supported attributes:
|
|
639
|
+
* type - "info" (default), "success", "warning", "danger", "neutral"
|
|
640
|
+
* title - Optional bold heading
|
|
641
|
+
* icon - Optional Domma icon name
|
|
642
|
+
* dismissible - Boolean flag - renders a dismiss (×) button
|
|
643
|
+
* class - Extra CSS classes on the wrapper
|
|
644
|
+
*
|
|
645
|
+
* @param {string} markdown
|
|
646
|
+
* @returns {string}
|
|
647
|
+
*/
|
|
648
|
+
function processBannerBlocks(markdown) {
|
|
649
|
+
const { scrubbed, restore } = scrubCodeRegions(markdown);
|
|
650
|
+
const processed = scrubbed.replace(
|
|
651
|
+
/\[banner([^\]]*)\]([\s\S]*?)\[\/banner\]/gi,
|
|
652
|
+
(_, attrStr, body) => {
|
|
653
|
+
const attrs = parseShortcodeAttrs(attrStr);
|
|
654
|
+
const type = ['info', 'success', 'warning', 'danger', 'neutral'].includes(attrs.type)
|
|
655
|
+
? attrs.type : 'info';
|
|
656
|
+
const extraClass = attrs.class ? ` ${attrs.class}` : '';
|
|
657
|
+
const dismissible = 'dismissible' in attrs;
|
|
658
|
+
|
|
659
|
+
const iconHtml = attrs.icon
|
|
660
|
+
? `<span class="dm-banner__icon" data-icon="${escapeAttr(attrs.icon)}"></span>`
|
|
661
|
+
: '';
|
|
662
|
+
const titleHtml = attrs.title
|
|
663
|
+
? `<strong class="dm-banner__title">${escapeAttr(attrs.title)}</strong>`
|
|
664
|
+
: '';
|
|
665
|
+
// Safe to call marked.parse() here - the resulting <div class="dm-banner ..."> block
|
|
666
|
+
// passes through the final outer marked.parse() call unchanged, because marked treats
|
|
667
|
+
// block-level HTML elements as HTML blocks and passes them through verbatim (CommonMark spec).
|
|
668
|
+
// This is the same pattern used by processCardBlocks, processTabsBlocks, etc.
|
|
669
|
+
const bodyHtml = marked.parse(body.trim());
|
|
670
|
+
const dismissHtml = dismissible
|
|
671
|
+
? `<button class="dm-banner__dismiss" aria-label="Dismiss">×</button>`
|
|
672
|
+
: '';
|
|
673
|
+
|
|
674
|
+
return (
|
|
675
|
+
`<div class="dm-banner dm-banner--${type}${extraClass}">\n` +
|
|
676
|
+
(iconHtml ? ` ${iconHtml}\n` : '') +
|
|
677
|
+
` <div class="dm-banner__body">\n` +
|
|
678
|
+
(titleHtml ? ` ${titleHtml}\n` : '') +
|
|
679
|
+
` ${bodyHtml.trim()}\n` +
|
|
680
|
+
` </div>\n` +
|
|
681
|
+
(dismissHtml ? ` ${dismissHtml}\n` : '') +
|
|
682
|
+
`</div>\n`
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
);
|
|
686
|
+
return restore(processed);
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
- [ ] **Step 3: Wire into the `parseMarkdown` pipeline**
|
|
691
|
+
|
|
692
|
+
Find these two consecutive lines in `parseMarkdown` (near line 2484):
|
|
693
|
+
|
|
694
|
+
```js
|
|
695
|
+
const withCard = processCardBlocks(withGrid);
|
|
696
|
+
const withSlideover = processSlideoverBlocks(withCard);
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
Insert `processBannerBlocks` between them:
|
|
700
|
+
|
|
701
|
+
```js
|
|
702
|
+
const withCard = processCardBlocks(withGrid);
|
|
703
|
+
const withBanner = processBannerBlocks(withCard);
|
|
704
|
+
const withSlideover = processSlideoverBlocks(withBanner);
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
Also update the pipeline comment at the top of `parseMarkdown` to include `banner`:
|
|
708
|
+
|
|
709
|
+
```
|
|
710
|
+
// → grid → card → banner → slideover → marked → sanitize → afterParse
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
- [ ] **Step 4: Add `aria-label` to the `sanitizeHtml` button allowlist**
|
|
714
|
+
|
|
715
|
+
Find the `sanitizeHtml` config at the bottom of `parseMarkdown`. Locate the `button` entry in `allowedAttributes`:
|
|
716
|
+
|
|
717
|
+
```js
|
|
718
|
+
button: ['type', 'disabled', 'data-action', 'data-entry', 'data-confirm'],
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
Add `'aria-label'` unconditionally:
|
|
722
|
+
|
|
723
|
+
```js
|
|
724
|
+
button: ['type', 'disabled', 'aria-label', 'data-action', 'data-entry', 'data-confirm'],
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
This ensures the dismiss button's accessible label survives sanitisation.
|
|
728
|
+
|
|
729
|
+
- [ ] **Step 5: Verify via curl**
|
|
730
|
+
|
|
731
|
+
```bash
|
|
732
|
+
curl -s -X POST http://localhost:4096/api/pages/preview \
|
|
733
|
+
-H 'Content-Type: application/json' \
|
|
734
|
+
-H 'Authorization: Bearer YOUR_TOKEN' \
|
|
735
|
+
-d '{"content":"[banner type=\"success\" title=\"Done\" icon=\"check-circle\" dismissible]\nAll saved.\n[/banner]"}' \
|
|
736
|
+
| grep -o 'dm-banner[^"]*"'
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
Expected: `dm-banner dm-banner--success"` in output, `dm-banner__icon`, `dm-banner__title`, `dm-banner__dismiss`.
|
|
740
|
+
|
|
741
|
+
---
|
|
742
|
+
|
|
743
|
+
## Task 7: Banner CSS
|
|
744
|
+
|
|
745
|
+
**File:** Modify `public/css/site.css`
|
|
746
|
+
|
|
747
|
+
- [ ] **Step 1: Add banner base and variant styles**
|
|
748
|
+
|
|
749
|
+
Append to `public/css/site.css`:
|
|
750
|
+
|
|
751
|
+
```css
|
|
752
|
+
/* ─── Banner shortcode ──────────────────────────────────────────────────── */
|
|
753
|
+
|
|
754
|
+
.dm-banner {
|
|
755
|
+
position: relative;
|
|
756
|
+
display: flex;
|
|
757
|
+
align-items: flex-start;
|
|
758
|
+
gap: .75rem;
|
|
759
|
+
padding: .85rem 1rem .85rem .9rem;
|
|
760
|
+
border-radius: 6px;
|
|
761
|
+
border-left: 4px solid;
|
|
762
|
+
margin-bottom: 1rem;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
.dm-banner__icon {
|
|
766
|
+
flex-shrink: 0;
|
|
767
|
+
margin-top: .1rem;
|
|
768
|
+
width: 1.15rem;
|
|
769
|
+
height: 1.15rem;
|
|
770
|
+
}
|
|
771
|
+
.dm-banner__icon svg {
|
|
772
|
+
width: 1.15rem;
|
|
773
|
+
height: 1.15rem;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.dm-banner__body {
|
|
777
|
+
flex: 1;
|
|
778
|
+
min-width: 0;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
.dm-banner__title {
|
|
782
|
+
display: block;
|
|
783
|
+
margin-bottom: .2rem;
|
|
784
|
+
font-size: .9em;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.dm-banner__dismiss {
|
|
788
|
+
position: absolute;
|
|
789
|
+
top: .5rem;
|
|
790
|
+
right: .5rem;
|
|
791
|
+
background: none;
|
|
792
|
+
border: none;
|
|
793
|
+
cursor: pointer;
|
|
794
|
+
font-size: 1.1rem;
|
|
795
|
+
line-height: 1;
|
|
796
|
+
padding: .1rem .3rem;
|
|
797
|
+
opacity: .5;
|
|
798
|
+
color: inherit;
|
|
799
|
+
}
|
|
800
|
+
.dm-banner__dismiss:hover { opacity: 1; }
|
|
801
|
+
|
|
802
|
+
/* Type variants */
|
|
803
|
+
.dm-banner--info { background: color-mix(in srgb, var(--dm-accent, #3b82f6) 10%, transparent); border-color: var(--dm-accent, #3b82f6); color: var(--dm-text, #eee); }
|
|
804
|
+
.dm-banner--success { background: color-mix(in srgb, #22c55e 10%, transparent); border-color: #22c55e; color: var(--dm-text, #eee); }
|
|
805
|
+
.dm-banner--warning { background: color-mix(in srgb, #f59e0b 10%, transparent); border-color: #f59e0b; color: var(--dm-text, #eee); }
|
|
806
|
+
.dm-banner--danger { background: color-mix(in srgb, #ef4444 10%, transparent); border-color: #ef4444; color: var(--dm-text, #eee); }
|
|
807
|
+
.dm-banner--neutral { background: var(--dm-surface-subtle, rgba(255,255,255,.05)); border-color: var(--dm-border, #444); color: var(--dm-text-muted, #aaa); }
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
- [ ] **Step 2: Verify in browser**
|
|
811
|
+
|
|
812
|
+
Navigate to a public page, add a `[banner type="warning" title="Warning"]Test[/banner]` shortcode, preview - verify
|
|
813
|
+
banner renders with amber left border and tinted background.
|
|
814
|
+
|
|
815
|
+
---
|
|
816
|
+
|
|
817
|
+
## Task 8: Banner dismiss handler
|
|
818
|
+
|
|
819
|
+
**File:** Modify `public/js/site.js`
|
|
820
|
+
|
|
821
|
+
- [ ] **Step 1: Locate a suitable place in `site.js`**
|
|
822
|
+
|
|
823
|
+
Find the section near the bottom of `site.js` where other DOM event handlers are attached (e.g. accordion collapse,
|
|
824
|
+
slideover wiring).
|
|
825
|
+
|
|
826
|
+
- [ ] **Step 2: Add the dismiss handler**
|
|
827
|
+
|
|
828
|
+
```js
|
|
829
|
+
// Banner dismiss
|
|
830
|
+
$(document).on('click', '.dm-banner__dismiss', function () {
|
|
831
|
+
$(this).closest('.dm-banner').remove();
|
|
832
|
+
});
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
Important: use **unnamespaced** event delegation - `$(document).on('click.ns', ...)` silently fails in Domma.
|
|
836
|
+
|
|
837
|
+
- [ ] **Step 3: Verify**
|
|
838
|
+
|
|
839
|
+
Open a public page with a dismissible banner. Click the × button - banner should disappear with no console errors.
|
|
840
|
+
|
|
841
|
+
---
|
|
842
|
+
|
|
843
|
+
## Task 9: Toolbar - `banner` insert item + icon + handler
|
|
844
|
+
|
|
845
|
+
**File:** Modify `admin/js/lib/markdown-toolbar.js` (1510 lines)
|
|
846
|
+
|
|
847
|
+
Three changes in this file.
|
|
848
|
+
|
|
849
|
+
- [ ] **Step 1: Register the `banner-insert` icon**
|
|
850
|
+
|
|
851
|
+
Find `registerIcons()` (first function in the file). Add the `banner-insert` icon alongside the others:
|
|
852
|
+
|
|
853
|
+
```js
|
|
854
|
+
I.register('banner-insert', {
|
|
855
|
+
viewBox: '0 0 24 24',
|
|
856
|
+
paths: [
|
|
857
|
+
'M3 7a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z',
|
|
858
|
+
'M3 10h18',
|
|
859
|
+
'M7 14h4',
|
|
860
|
+
],
|
|
861
|
+
stroke: 'currentColor', fill: 'none', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
862
|
+
});
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
- [ ] **Step 2: Add `banner` to the insert items array**
|
|
866
|
+
|
|
867
|
+
Find the items array for the Insert dropdown (around line 1261). It starts with `{label: 'Accordion', ...}`. Add
|
|
868
|
+
`Banner` in alphabetical position between `Accordion` and `Badge`:
|
|
869
|
+
|
|
870
|
+
```js
|
|
871
|
+
{label: 'Accordion', icon: 'accordion-insert', action: 'accordion'},
|
|
872
|
+
{label: 'Banner', icon: 'banner-insert', action: 'banner'}, // [banner type="..."]...[/banner]
|
|
873
|
+
{label: 'Badge', icon: 'badge', action: 'badge'},
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
- [ ] **Step 3: Add dispatch in the action switch**
|
|
877
|
+
|
|
878
|
+
Find the `} else if (action === 'accordion') {` block. Add the banner dispatch nearby (alphabetical order is good, but
|
|
879
|
+
any order works):
|
|
880
|
+
|
|
881
|
+
```js
|
|
882
|
+
} else if (action === 'banner') {
|
|
883
|
+
if (handlers.banner) handlers.banner(ta);
|
|
884
|
+
}
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
- [ ] **Step 4: Add `banner` to the `handlers` object initialisation**
|
|
888
|
+
|
|
889
|
+
Find the `const handlers = { link: null, image: null, card: null, ... }` object. Add:
|
|
890
|
+
|
|
891
|
+
```js
|
|
892
|
+
banner: null,
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
- [ ] **Step 5: Add `onBanner` to the returned handler chain**
|
|
896
|
+
|
|
897
|
+
Find where `onAccordion(cb)` is defined in the returned object. Add alongside it:
|
|
898
|
+
|
|
899
|
+
```js
|
|
900
|
+
onBanner(cb) {
|
|
901
|
+
handlers.banner = cb;
|
|
902
|
+
},
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
- [ ] **Step 6: Add `onBanner` to the JSDoc `@returns` block**
|
|
906
|
+
|
|
907
|
+
Find the JSDoc comment above `export function createToolbar(...)`. Add to the returns list:
|
|
908
|
+
|
|
909
|
+
```js
|
|
910
|
+
* onBanner(cb: Function): void,
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
- [ ] **Step 7: Verify in browser**
|
|
914
|
+
|
|
915
|
+
Open the admin page editor. Click Insert - verify "Banner" appears in the dropdown with the new icon. (It won't do
|
|
916
|
+
anything yet - handler wired in Task 10.)
|
|
917
|
+
|
|
918
|
+
---
|
|
919
|
+
|
|
920
|
+
## Task 10: `onBanner` - Insert dialog
|
|
921
|
+
|
|
922
|
+
**File:** Modify `admin/js/views/page-editor.js`
|
|
923
|
+
|
|
924
|
+
- [ ] **Step 1: Add the `onBanner` handler**
|
|
925
|
+
|
|
926
|
+
Add `V.onBanner(...)` alongside the other handler registrations:
|
|
927
|
+
|
|
928
|
+
```js
|
|
929
|
+
V.onBanner(ta => {
|
|
930
|
+
const body = document.createElement('div');
|
|
931
|
+
body.style.cssText = 'padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem;';
|
|
932
|
+
|
|
933
|
+
// Type - segmented button group
|
|
934
|
+
const typeLabel = document.createElement('div');
|
|
935
|
+
typeLabel.style.cssText = 'display:block;font-size:.7rem;font-weight:600;color:var(--dm-text-muted,#aaa);text-transform:uppercase;letter-spacing:.05em;';
|
|
936
|
+
typeLabel.textContent = 'Type';
|
|
937
|
+
body.appendChild(typeLabel);
|
|
938
|
+
|
|
939
|
+
const TYPE_OPTS = [
|
|
940
|
+
{ val: 'info', label: 'Info', dot: '#3b82f6' },
|
|
941
|
+
{ val: 'success', label: 'Success', dot: '#22c55e' },
|
|
942
|
+
{ val: 'warning', label: 'Warning', dot: '#f59e0b' },
|
|
943
|
+
{ val: 'danger', label: 'Danger', dot: '#ef4444' },
|
|
944
|
+
{ val: 'neutral', label: 'Neutral', dot: '#888' },
|
|
945
|
+
];
|
|
946
|
+
|
|
947
|
+
const typeGroup = document.createElement('div');
|
|
948
|
+
typeGroup.style.cssText = 'display:flex;flex-wrap:wrap;gap:.3rem;';
|
|
949
|
+
let selectedType = 'info';
|
|
950
|
+
|
|
951
|
+
const typeBtns = TYPE_OPTS.map(opt => {
|
|
952
|
+
const btn = document.createElement('button');
|
|
953
|
+
btn.type = 'button';
|
|
954
|
+
btn.style.cssText = 'display:flex;align-items:center;gap:.35rem;padding:.3rem .6rem;border:1px solid var(--dm-border,#333);border-radius:4px;background:transparent;color:var(--dm-text,#eee);font-size:.8em;cursor:pointer;';
|
|
955
|
+
const dot = document.createElement('span');
|
|
956
|
+
dot.style.cssText = `width:8px;height:8px;border-radius:50%;background:${opt.dot};flex-shrink:0;`;
|
|
957
|
+
btn.appendChild(dot);
|
|
958
|
+
btn.appendChild(document.createTextNode(opt.label));
|
|
959
|
+
if (opt.val === 'info') btn.style.borderColor = opt.dot;
|
|
960
|
+
btn.addEventListener('click', () => {
|
|
961
|
+
selectedType = opt.val;
|
|
962
|
+
typeBtns.forEach((b, i) => {
|
|
963
|
+
b.style.borderColor = TYPE_OPTS[i].val === selectedType ? TYPE_OPTS[i].dot : 'var(--dm-border,#333)';
|
|
964
|
+
b.style.fontWeight = TYPE_OPTS[i].val === selectedType ? '600' : '';
|
|
965
|
+
});
|
|
966
|
+
preview.update(buildShortcode());
|
|
967
|
+
});
|
|
968
|
+
typeGroup.appendChild(btn);
|
|
969
|
+
return btn;
|
|
970
|
+
});
|
|
971
|
+
body.appendChild(typeGroup);
|
|
972
|
+
|
|
973
|
+
const titleInput = makeTextInput('Banner heading (optional)');
|
|
974
|
+
body.appendChild(makeField('Title', titleInput));
|
|
975
|
+
|
|
976
|
+
const iconInput = makeTextInput('Domma icon name (optional)');
|
|
977
|
+
body.appendChild(makeField('Icon', iconInput));
|
|
978
|
+
|
|
979
|
+
const dismissWrap = makeCheckbox('Dismissible (show × button)');
|
|
980
|
+
const dismissCb = dismissWrap.querySelector('input');
|
|
981
|
+
body.appendChild(dismissWrap);
|
|
982
|
+
|
|
983
|
+
function buildShortcode() {
|
|
984
|
+
const parts = [];
|
|
985
|
+
if (selectedType !== 'info') parts.push(`type="${selectedType}"`);
|
|
986
|
+
const title = titleInput.value.trim();
|
|
987
|
+
if (title) parts.push(`title="${title}"`);
|
|
988
|
+
const icon = iconInput.value.trim();
|
|
989
|
+
if (icon) parts.push(`icon="${icon}"`);
|
|
990
|
+
if (dismissCb.checked) parts.push('dismissible');
|
|
991
|
+
const attrStr = parts.length ? ' ' + parts.join(' ') : '';
|
|
992
|
+
return `[banner${attrStr}]\nBanner content here.\n[/banner]`;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Live preview
|
|
996
|
+
const preview = makeLivePreview();
|
|
997
|
+
preview.update(buildShortcode());
|
|
998
|
+
body.appendChild(makeField('Shortcode preview', preview.el));
|
|
999
|
+
|
|
1000
|
+
[titleInput, iconInput].forEach(el => el.addEventListener('input', () => preview.update(buildShortcode())));
|
|
1001
|
+
dismissCb.addEventListener('change', () => preview.update(buildShortcode()));
|
|
1002
|
+
|
|
1003
|
+
const insertBtn = document.createElement('button');
|
|
1004
|
+
insertBtn.type = 'button';
|
|
1005
|
+
insertBtn.className = 'btn btn-primary';
|
|
1006
|
+
insertBtn.textContent = 'Insert Banner';
|
|
1007
|
+
body.appendChild(insertBtn);
|
|
1008
|
+
|
|
1009
|
+
const modal = E.modal({ title: 'Insert Banner', size: 'sm' });
|
|
1010
|
+
modal.element.appendChild(body);
|
|
1011
|
+
modal.open();
|
|
1012
|
+
requestAnimationFrame(() => titleInput.focus());
|
|
1013
|
+
|
|
1014
|
+
insertBtn.addEventListener('click', () => {
|
|
1015
|
+
modal.close();
|
|
1016
|
+
ke(ta, buildShortcode());
|
|
1017
|
+
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1018
|
+
ta.focus();
|
|
1019
|
+
});
|
|
1020
|
+
}),
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
- [ ] **Step 2: Smoke test end-to-end**
|
|
1024
|
+
|
|
1025
|
+
1. Click Insert → Banner
|
|
1026
|
+
2. Verify modal opens with segmented type buttons, title/icon text inputs, dismissible checkbox, live preview
|
|
1027
|
+
3. Click "Warning" → preview updates to `type="warning"`, button highlights
|
|
1028
|
+
4. Enter title "Heads up" → preview updates
|
|
1029
|
+
5. Tick dismissible → `dismissible` appears in preview
|
|
1030
|
+
6. Click Insert Banner → shortcode inserted in editor
|
|
1031
|
+
7. Preview the page → banner renders with amber border, title, dismiss button (if ticked)
|
|
1032
|
+
8. On the public page, click × → banner disappears
|
|
1033
|
+
|
|
1034
|
+
---
|
|
1035
|
+
|
|
1036
|
+
## Task 11: Final smoke test checklist
|
|
1037
|
+
|
|
1038
|
+
Verify all three features work together without regressions.
|
|
1039
|
+
|
|
1040
|
+
- [ ] **Tabs**: Insert → Tabs → modal opens, all fields work, live preview updates, Insert inserts correctly, preview
|
|
1041
|
+
renders styled tabs (pills/bordered/fade apply)
|
|
1042
|
+
- [ ] **Hero**: Insert → Hero → modal opens, conditional background fields show/hide, Insert inserts correctly, preview
|
|
1043
|
+
renders hero with colour/image background as expected
|
|
1044
|
+
- [ ] **Banner**: Insert → Banner → modal opens, type selector highlights, Insert inserts correctly, public page renders
|
|
1045
|
+
banner, dismiss works
|
|
1046
|
+
- [ ] **Existing Card insert still works** (CardBuilder opens, no JS errors)
|
|
1047
|
+
- [ ] **Existing Accordion insert still works** (no JS errors)
|
|
1048
|
+
- [ ] No console errors on admin load
|
|
1049
|
+
- [ ] No console errors on public page with banner shortcode
|