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.
- package/CHANGELOG.md +4344 -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/page-editor-reference.js +5 -5
- package/admin/js/lib/shortcode-tags.js +1 -1
- package/admin/js/templates/docs/usage-shortcodes.html +39 -0
- 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/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,368 @@
|
|
|
1
|
+
# Components Area - Design Spec
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-04-17
|
|
4
|
+
**Status:** Draft - ready for implementation planning
|
|
5
|
+
**Scope:** New "Components" feature under the admin **Data** section, alongside Collections, Forms, Views, Actions, and Blocks.
|
|
6
|
+
|
|
7
|
+
## 1. Intent
|
|
8
|
+
|
|
9
|
+
Add a first-class **Components** area to Domma CMS. Authors can **collect, edit, and inject** Domma components - Vue-style Custom Elements built with Domma's component factory (`Domma.component(tagName, {…})`) - into public pages.
|
|
10
|
+
|
|
11
|
+
Unlike **Blocks** (static HTML + `{{placeholder}}` substitution), Components ship executable JavaScript: template + reactive `data()` + computed properties + methods + scoped styles + lifecycle hooks. They render on the public site as real Custom Elements with Shadow DOM isolation.
|
|
12
|
+
|
|
13
|
+
### Constraints and decisions
|
|
14
|
+
|
|
15
|
+
| Decision | Choice |
|
|
16
|
+
|---|---|
|
|
17
|
+
| Primary surface | Public site. Admin-side consumption is out of scope for v1 but the design must not preclude it. |
|
|
18
|
+
| Authoring model | Developer-level editor (admin authors write the full component) + seeded shipped library. |
|
|
19
|
+
| On-disk shape | SFC-style single file (`.dmc`) with `<template>`, `<props>`, `<script>`, `<style>` blocks. |
|
|
20
|
+
| Page injection | Shortcode (`[component name="x" …]`) is the page-editor default; raw custom-element tags (`<dm-x …>`) are also accepted. |
|
|
21
|
+
| Runtime loading | Per-page. Renderer scans used components; injects only the scripts needed. |
|
|
22
|
+
| Insertion UX | Slideover with gallery → props form → live iframe preview → insert. |
|
|
23
|
+
| Permissions | New `components` resource in `permissionRegistry`. Super-admin default; loosenable via Roles UI. |
|
|
24
|
+
|
|
25
|
+
## 2. File format: `.dmc`
|
|
26
|
+
|
|
27
|
+
One `.dmc` file per component at `content/components/<name>.dmc`. Companion `<name>.meta.json` holds `{ bundled: true }` for shipped starters (same pattern as blocks).
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
<template>
|
|
31
|
+
<div class="dm-counter">
|
|
32
|
+
<div class="value">{{count}}</div>
|
|
33
|
+
<button data-action="inc">+</button>
|
|
34
|
+
<button data-action="dec">-</button>
|
|
35
|
+
{{#if showReset}}<button data-action="reset">Reset</button>{{/if}}
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<props>
|
|
40
|
+
{
|
|
41
|
+
"initial": { "type": "number", "default": 0, "label": "Initial value" },
|
|
42
|
+
"step": { "type": "number", "default": 1, "label": "Step size" },
|
|
43
|
+
"showReset": { "type": "boolean", "default": false, "label": "Show reset button" }
|
|
44
|
+
}
|
|
45
|
+
</props>
|
|
46
|
+
|
|
47
|
+
<script>
|
|
48
|
+
export default {
|
|
49
|
+
data() { return { count: this.props.initial }; },
|
|
50
|
+
methods: {
|
|
51
|
+
inc() { this.set({ count: this.data.count + this.props.step }); },
|
|
52
|
+
dec() { this.set({ count: this.data.count - this.props.step }); },
|
|
53
|
+
reset() { this.set({ count: this.props.initial }); }
|
|
54
|
+
},
|
|
55
|
+
onMount() {
|
|
56
|
+
this.el.addEventListener('click', e => {
|
|
57
|
+
const action = e.target.dataset.action;
|
|
58
|
+
if (action && this[action]) this[action]();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<style>
|
|
65
|
+
.dm-counter .value { font-size: 2rem; font-weight: 700; }
|
|
66
|
+
.dm-counter button { margin-right: .25rem; }
|
|
67
|
+
</style>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Block semantics
|
|
71
|
+
|
|
72
|
+
| Block | Required | Purpose |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `<template>` | yes | Mustache-style template string passed to `Domma.component`. |
|
|
75
|
+
| `<props>` | yes | Machine-readable JSON props schema. Drives the page-editor props form and validation. |
|
|
76
|
+
| `<script>` | yes | ES module with `export default { data, computed, methods, lifecycle hooks }`. Runtime behaviour. |
|
|
77
|
+
| `<style>` | no | CSS injected into the Custom Element's Shadow DOM. |
|
|
78
|
+
|
|
79
|
+
`<props>` is deliberately separate from any `props:` key inside `<script>`. **`<props>` is the UI contract; `<script>` is the runtime.** If both are present and diverge, compilation emits a warning; `<props>` wins for the editor UI, `<script>` for runtime coercion. Authors are expected to keep them aligned; a future v1.1 compiler macro can generate one from the other.
|
|
80
|
+
|
|
81
|
+
### Name validation
|
|
82
|
+
|
|
83
|
+
Component name regex: `^[a-z][a-z0-9-]*$`. Registered Custom Element tag is always `dm-<name>` - enforces the hyphen required by Custom Elements and reserves a single predictable namespace.
|
|
84
|
+
|
|
85
|
+
## 3. Server pipeline
|
|
86
|
+
|
|
87
|
+
Two new files carry the server work:
|
|
88
|
+
|
|
89
|
+
- **`server/services/components.js`** - file I/O, compiler, cache, plugin hook wiring.
|
|
90
|
+
- **`server/routes/api/components.js`** - HTTP surface.
|
|
91
|
+
|
|
92
|
+
### 3.1 Compiler
|
|
93
|
+
|
|
94
|
+
`compileComponent(source, name) → { js, errors }`:
|
|
95
|
+
|
|
96
|
+
1. **Split blocks** via a single tolerant regex pass. Each of the four block tags is matched with `/<(template|props|script|style)>([\s\S]*?)<\/\1>/g`. A full HTML parser is unwarranted - files are admin-authored and small.
|
|
97
|
+
2. **Validate `<props>` JSON.** Must parse; must be an object; each entry must declare `type ∈ {string, number, boolean, array, object}`.
|
|
98
|
+
3. **Emit a JS module.** The `<script>` body is transformed by stripping its `export default` keyword and capturing the remaining object expression - `export default { … }` becomes `const definition = { … }`. This keeps `<script>` readable by a JS-aware editor (LSP/syntax highlighting) while allowing the compiler to produce a self-contained module that registers the component directly, with no external imports:
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
const template = `…`; // <template> body
|
|
102
|
+
const style = `…`; // <style> body, or empty
|
|
103
|
+
const props = { … }; // parsed <props> JSON
|
|
104
|
+
const definition = { … }; // <script> body with `export default` stripped
|
|
105
|
+
|
|
106
|
+
Domma.component('dm-<name>', { template, style, props, ...definition });
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Compile errors are structured:
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
{ type: 'MISSING_BLOCK' | 'INVALID_PROPS_JSON' | 'SCRIPT_SYNTAX',
|
|
113
|
+
message: '…', line?: number, column?: number }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 3.2 Caching
|
|
117
|
+
|
|
118
|
+
Compiled output is held in an in-memory `Map` keyed by `name`, with entry `{ js, mtime }`. `saveComponent()` invalidates the entry for that name; `deleteComponent()` removes it. No disk cache.
|
|
119
|
+
|
|
120
|
+
### 3.3 API surface
|
|
121
|
+
|
|
122
|
+
Mirrors `/api/blocks` closely.
|
|
123
|
+
|
|
124
|
+
| Method | Route | Auth | Purpose |
|
|
125
|
+
|---|---|---|---|
|
|
126
|
+
| GET | `/api/components` | `components.read` | List: `[{name, size, updatedAt, bundled, props}]`. |
|
|
127
|
+
| GET | `/api/components/:name` | `components.read` | Raw `.dmc` source + parsed `props`. |
|
|
128
|
+
| GET | `/api/components/:name.js` | **none (public)** | Compiled JS module. Served with `Cache-Control` and a content hash. |
|
|
129
|
+
| POST | `/api/components/compile` | `components.update` | Transient compile of a source body. Body: `{ source, name }`. Returns `{ js, errors }`. Used by the editor for live-preview compilation without touching disk. |
|
|
130
|
+
| GET | `/api/components/:name/export` | `components.read` | Download as `.dmcomponent.json` bundle. |
|
|
131
|
+
| POST | `/api/components/import` | `components.update` | Import bundle. 409 on collision unless `overwrite: true`. |
|
|
132
|
+
| PUT | `/api/components/:name` | `components.update` | Upsert. Body: `{ source, bundled }`. **Compile runs before write**; malformed sources never reach disk. |
|
|
133
|
+
| DELETE | `/api/components/:name` | `components.delete` | Delete `.dmc` + companion `.meta.json`. |
|
|
134
|
+
|
|
135
|
+
Admin-side `api.js` helpers wrap these as `api.components.{list, get, compile, put, delete, exportBundle, importBundle}`. The JS method names differ from route verbs intentionally, matching the existing `api.blocks.*` convention.
|
|
136
|
+
|
|
137
|
+
`.dmcomponent.json` export bundle shape (format version 1):
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
{ format: 1, name: 'counter', source: '<full .dmc source>', bundled: false }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 3.4 Plugin hook
|
|
144
|
+
|
|
145
|
+
One new hook in `server/services/hooks.js`: `registerComponent({ name, source })`. Plugins can bundle `.dmc` sources that compile on startup and appear in the library as read-only entries (the list API flags them with `bundled: true, source: 'plugin'`).
|
|
146
|
+
|
|
147
|
+
## 4. Renderer integration
|
|
148
|
+
|
|
149
|
+
### 4.1 Scan pass (added to `server/services/markdown.js`)
|
|
150
|
+
|
|
151
|
+
A new processor `collectComponents(markdown)` runs after the existing shortcode passes. It does two jobs:
|
|
152
|
+
|
|
153
|
+
1. Rewrite `[component name="x" attr="y" /]` shortcodes to `<dm-x attr="y">…</dm-x>` tags (attribute flattening identical to the existing `[block]` and `[form]` processors).
|
|
154
|
+
2. Collect every `<dm-([a-z][a-z0-9-]*)` token encountered in the source into a de-duplicated `Set`.
|
|
155
|
+
|
|
156
|
+
The `Set` is attached to the page object as `page._usedComponents` for the renderer.
|
|
157
|
+
|
|
158
|
+
### 4.2 Sanitiser config
|
|
159
|
+
|
|
160
|
+
Props become kebab-case HTML attributes on the custom element (`<dm-counter initial="5" show-reset="true">`). The sanitiser must allow:
|
|
161
|
+
|
|
162
|
+
1. **Tags:** the `dm-*` tag family. Preferred: wildcard via `ADD_TAGS: ['dm-*']` **if DOMPurify supports it**. If not, the components service exposes `getRegisteredTagNames()` and the sanitiser config is hydrated from it at startup (and invalidated on create/delete).
|
|
163
|
+
2. **Attributes on those tags:** any attribute, because prop names are author-defined and enumerating them at render time has no security benefit. The strategy is a custom `uponSanitizeAttribute` hook that **unconditionally permits attributes whose parent element matches `/^dm-[a-z0-9-]+$/`**, while leaving attribute sanitisation on all other tags untouched. This is safe because a custom element attribute cannot execute JavaScript on its own - `on*` handlers and `javascript:` URLs are still stripped by the hook's default pass for any tag.
|
|
164
|
+
|
|
165
|
+
Both points are flagged as implementation-time verifications (DOMPurify capability checks). Custom-element tags themselves are HTML-safe - the browser will not execute anything until a matching class is `customElements.define`'d - so the allowlist does not open a fresh XSS surface.
|
|
166
|
+
|
|
167
|
+
### 4.3 Script injection (in `server/services/renderer.js`)
|
|
168
|
+
|
|
169
|
+
For each name in `page._usedComponents`, the renderer emits:
|
|
170
|
+
|
|
171
|
+
```html
|
|
172
|
+
<script type="module" src="/api/components/<name>.js"></script>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
These scripts are concatenated into the existing `bodyEndInject` slot, in this order:
|
|
176
|
+
|
|
177
|
+
1. `domma.min.js` (existing)
|
|
178
|
+
2. `site.js` - guarantees `window.Domma.component` is defined (existing)
|
|
179
|
+
3. **Component scripts** (new)
|
|
180
|
+
4. Plugin body-end snippets (existing)
|
|
181
|
+
|
|
182
|
+
### 4.4 Missing-component graceful degradation
|
|
183
|
+
|
|
184
|
+
If a page references a component whose `.dmc` no longer exists:
|
|
185
|
+
|
|
186
|
+
- `/api/components/<name>.js` returns 404.
|
|
187
|
+
- The corresponding `<dm-name>` tag remains inert in the DOM (browser doesn't upgrade an unknown custom element).
|
|
188
|
+
- **Public pages still render cleanly.** The page editor's preview shows an inline warning banner listing unresolved component names.
|
|
189
|
+
|
|
190
|
+
### 4.5 Flash-of-unrendered-custom-element
|
|
191
|
+
|
|
192
|
+
Custom elements upgrade after their defining script runs (deferred module loading introduces a small delay). A single CSS rule in `public/css/site.css` mitigates flash for above-the-fold uses:
|
|
193
|
+
|
|
194
|
+
```css
|
|
195
|
+
:not(:defined) { visibility: hidden; }
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## 5. Admin UI
|
|
199
|
+
|
|
200
|
+
### 5.1 Sidebar entry
|
|
201
|
+
|
|
202
|
+
Under the **Data** heading, immediately after **Blocks**:
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
{ id: 'components', text: 'Components', icon: 'puzzle', url: '#/components',
|
|
206
|
+
section: '#/components', badge: badge(counts.components) }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Icon `puzzle` registered via `I.register` on first mount (same pattern as `blocks.js`). Visibility gated by `components.read`.
|
|
210
|
+
|
|
211
|
+
### 5.2 List view - `admin/js/views/components.js`
|
|
212
|
+
|
|
213
|
+
Route `#/components`. Table columns: **Name / Props / Size / Updated / Actions**. The **Props** column shows a compact pill list of declared prop names (from the parsed `<props>` block). Top bar: **New Component**, **Import**. Per-row actions: **Edit**, **Export**, **Delete**.
|
|
214
|
+
|
|
215
|
+
Pattern follows `admin/js/views/blocks.js` nearly line-for-line.
|
|
216
|
+
|
|
217
|
+
### 5.3 Component editor - `admin/js/views/component-editor.js`
|
|
218
|
+
|
|
219
|
+
Route `#/components/edit/:name` (and `#/components/new` for creation). Full-page view. Layout:
|
|
220
|
+
|
|
221
|
+
- **Header bar:** name, Custom Element tag preview (`<dm-foo>`), Export button, Bundled toggle, Save button.
|
|
222
|
+
- **Left pane:** tabbed code editor with four tabs matching the four SFC blocks (`<template>` / `<props>` / `<script>` / `<style>`). Each tab reuses the block editor's primitives: line numbers, cursor readout, tab-key indent, undo/redo toolbar.
|
|
223
|
+
- **Right pane:** live iframe preview (see section 6) driven by an auto-generated "Preview props" panel below it. Props panel inputs are typed by the `<props>` JSON.
|
|
224
|
+
- **Status bar:** compile indicator. Green + line count when OK; red + expandable error when not. Save is disabled while compile is failing.
|
|
225
|
+
|
|
226
|
+
Debounced recompile and iframe remount at ~300 ms since last keystroke. The editor **posts the draft source to `POST /api/components/compile`** rather than compiling client-side - this keeps the compiler single-sourced on the server and guarantees the preview uses the exact same module the public site would serve on save. Response shape is the same `{ js, errors }` the `PUT` endpoint validates against.
|
|
227
|
+
|
|
228
|
+
### 5.4 Insertion flow - slideover in the page editor
|
|
229
|
+
|
|
230
|
+
A new toolbar button **Insert Component** (alongside Insert Block / Insert Form) opens a Domma slideover (`size="lg"`, `position="right"`). Slideover is preferred over modal because:
|
|
231
|
+
|
|
232
|
+
- Page context remains visible behind the panel.
|
|
233
|
+
- The gallery + props form + preview fit comfortably side-by-side.
|
|
234
|
+
- Multi-stage flow (pick → configure → preview → insert) suits a persistent panel.
|
|
235
|
+
|
|
236
|
+
Stages inside the slideover:
|
|
237
|
+
|
|
238
|
+
1. **Gallery.** Card grid; each card shows a thumbnail (auto-generated screenshot of the default-props preview iframe, cached per component mtime) + name + short description.
|
|
239
|
+
2. **Props form.** After selection, a `F.create()` form is rendered from the `<props>` JSON blueprint.
|
|
240
|
+
3. **Preview.** An iframe instance (same preview bootstrap as the editor) remounts on prop changes.
|
|
241
|
+
4. **Insert.** On confirm, the shortcode form `[component name="<x>" attr="y" /]` is inserted at the cursor. An overflow menu offers "Insert as raw tag" for power users.
|
|
242
|
+
|
|
243
|
+
Caveat: `E.slideover()` may share the `E.modal().setContent()` gotcha (HTML stripped via `textContent`). The iframe is a single DOM node so the light-DOM append pattern (`.element.appendChild(iframe)`) is the safe path. Implementation must verify this behaviour.
|
|
244
|
+
|
|
245
|
+
## 6. Preview sandbox
|
|
246
|
+
|
|
247
|
+
Admin-authored JS runs in both previews (editor + insertion slideover). A buggy `onMount()` or runaway `setInterval()` must not brick the admin session. Solution: **iframe sandbox, same-origin, with `postMessage` protocol.**
|
|
248
|
+
|
|
249
|
+
### 6.1 Bootstrap file
|
|
250
|
+
|
|
251
|
+
A single static document at `admin/preview/component-preview.html`:
|
|
252
|
+
|
|
253
|
+
- Loads Domma CSS + JS (DOMPurify is **not** needed here - the iframe runs only admin-authored, pre-compiled modules; there is no untrusted HTML to sanitise inside the preview surface).
|
|
254
|
+
- Contains a `#mount` div.
|
|
255
|
+
- Listens for `postMessage` from the parent admin window; mounts the compiled component with the supplied tag name and props.
|
|
256
|
+
- Posts ready/error/resize events back.
|
|
257
|
+
|
|
258
|
+
### 6.2 Protocol
|
|
259
|
+
|
|
260
|
+
| Direction | Message | Payload |
|
|
261
|
+
|---|---|---|
|
|
262
|
+
| admin → iframe | `mount` | `{ compiledJs, tagName, props }` |
|
|
263
|
+
| admin → iframe | `updateProps` | `{ props }` - surgical attribute update, no remount |
|
|
264
|
+
| admin → iframe | `remount` | - triggered by template/script/style edits |
|
|
265
|
+
| iframe → admin | `ready` | `{ height }` |
|
|
266
|
+
| iframe → admin | `error` | `{ message, stack, line?, col? }` |
|
|
267
|
+
| iframe → admin | `resize` | `{ height }` |
|
|
268
|
+
|
|
269
|
+
### 6.3 Safety properties
|
|
270
|
+
|
|
271
|
+
- Runaway timers and memory leaks die with the iframe on the next remount.
|
|
272
|
+
- Navigation attempts (`window.location = …`) navigate the **iframe**, not the admin.
|
|
273
|
+
- Uncaught exceptions inside the component surface as structured `error` messages, not as admin-level errors.
|
|
274
|
+
- Same-origin is deliberate: we need convenient `postMessage` and DOM access for auto-sizing. `sandbox="allow-scripts"` is not used because it would complicate communication without adding protection against super-admin-authored code, which we trust at the boundary.
|
|
275
|
+
|
|
276
|
+
### 6.4 Auto-sizing
|
|
277
|
+
|
|
278
|
+
The iframe `ResizeObserver`s its own body and posts `resize` height back. The admin container uses the posted height directly so tall components (e.g., carousels) are not clipped.
|
|
279
|
+
|
|
280
|
+
## 7. Permissions
|
|
281
|
+
|
|
282
|
+
New `components` resource in `server/services/permissionRegistry.js`:
|
|
283
|
+
|
|
284
|
+
```js
|
|
285
|
+
components: {
|
|
286
|
+
read: ['admin', 'super-admin'],
|
|
287
|
+
create: ['super-admin'],
|
|
288
|
+
update: ['super-admin'],
|
|
289
|
+
delete: ['super-admin']
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
- Route guards use `requirePermission('components', 'read'|'create'|'update'|'delete')`.
|
|
294
|
+
- Sidebar entry and page-editor Insert Component button gate on `components.read`.
|
|
295
|
+
- `/api/components/:name.js` is public (no auth) - it's static output served to every page visitor.
|
|
296
|
+
- Because permissions live in the registry rather than being hard-coded, future roles can be granted component authoring via the Roles UI without a code change.
|
|
297
|
+
|
|
298
|
+
## 8. Seeded starter library
|
|
299
|
+
|
|
300
|
+
Five components seeded on first run by `seedDefaultComponents()` (same pattern as `seedDefaultBlocks()` - never overwrites existing files). Each ships with `bundled: true` in its `.meta.json`.
|
|
301
|
+
|
|
302
|
+
| Name | Demonstrates |
|
|
303
|
+
|---|---|
|
|
304
|
+
| `counter` | Minimal stateful component - data, methods, template bindings. |
|
|
305
|
+
| `copy-button` | One-shot action - clipboard API, computed, transient state. |
|
|
306
|
+
| `reveal-panel` | Progressive disclosure - boolean state, `{{#if}}` structural binding, default slot projection. |
|
|
307
|
+
| `testimonial-card` | Pure-presentational with rich props (quote, author, avatar). |
|
|
308
|
+
| `countdown-timer` | Lifecycle - `onMount` / `onUnmount` with `setInterval` cleanup. |
|
|
309
|
+
|
|
310
|
+
These collectively cover every meaningful shape of component (stateful, side-effecting, props-only, lifecycle) and double as living documentation.
|
|
311
|
+
|
|
312
|
+
The seed names (`counter`, `copy-button`, `reveal-panel`, `testimonial-card`, `countdown-timer`) are deliberately distinct from existing files in `content/blocks/` (which currently holds `contact-info`, `contact-card`, `cta-banner`, `enquiry-card`, `feedback-card`, `note-card`, `todo-item`) - even though the namespaces are separate on disk, keeping the human-facing names unambiguous avoids documentation confusion.
|
|
313
|
+
|
|
314
|
+
## 9. YAGNI - explicitly out of scope for v1
|
|
315
|
+
|
|
316
|
+
- Component-to-component composition (`<dm-foo>` using `<dm-bar>` internally). Technically supported by the factory; no dedicated UI or docs until demand surfaces.
|
|
317
|
+
- Named slots in the props schema. Default slot (children) works naturally via Custom Elements.
|
|
318
|
+
- Scoped-styles flag. Component `<style>` already lives in Shadow DOM via the factory - encapsulation is automatic.
|
|
319
|
+
- TypeScript / JSDoc validation of `<script>`. Plain JS; trust model is the admin boundary.
|
|
320
|
+
- Visual `<props>` schema builder. Hand-written JSON for v1; builder deferred to v1.1 if painful.
|
|
321
|
+
- Component usage analytics as a dashboard. Can be derived from grepping `content/pages/`.
|
|
322
|
+
- Live hot-reload of public pages on component edit. Refresh is sufficient for v1.
|
|
323
|
+
- Cross-site component sync beyond the existing export/import bundle.
|
|
324
|
+
- Versioning of component history. Components are code; git is the versioning story. `content/versions/` remains page-scoped.
|
|
325
|
+
|
|
326
|
+
## 10. Risks and open verifications
|
|
327
|
+
|
|
328
|
+
1. **DOMPurify custom-element allowlist.** Depends on DOMPurify's support for wildcard `ADD_TAGS`. Verify during implementation; fall back to explicit name enumeration from the components service if wildcards are unsupported.
|
|
329
|
+
2. **`E.slideover()` interactive-content handling.** May share the modal's HTML-stripping behaviour. Implementation must test that an iframe inserted via `.element.appendChild` survives slideover open/close cycles.
|
|
330
|
+
3. **Shadow DOM theming.** Components inherit via `getThemeVariables()` from Domma's base. Verify site-level CSS custom properties reach component styles consistently.
|
|
331
|
+
4. **Deferred module load timing.** `<script type="module">` tags are deferred. For above-the-fold components, `:not(:defined) { visibility: hidden; }` is applied site-wide to mask the pre-upgrade flash.
|
|
332
|
+
|
|
333
|
+
## 11. Files introduced / modified
|
|
334
|
+
|
|
335
|
+
**New files:**
|
|
336
|
+
|
|
337
|
+
- `content/components/` - seeded `.dmc` files and `.meta.json` companions.
|
|
338
|
+
- `server/services/components.js` - compiler, CRUD, cache, seed.
|
|
339
|
+
- `server/routes/api/components.js` - HTTP routes.
|
|
340
|
+
- `admin/preview/component-preview.html` - iframe bootstrap.
|
|
341
|
+
- `admin/js/views/components.js` - list view.
|
|
342
|
+
- `admin/js/templates/components.html` - list view template.
|
|
343
|
+
- `admin/js/views/component-editor.js` - editor view.
|
|
344
|
+
- `admin/js/templates/component-editor.html` - editor view template.
|
|
345
|
+
|
|
346
|
+
**Modified files:**
|
|
347
|
+
|
|
348
|
+
- `server/server.js` - registers the components route; calls `seedDefaultComponents()` on startup.
|
|
349
|
+
- `server/services/markdown.js` - new `collectComponents()` pass; sanitiser config update.
|
|
350
|
+
- `server/services/renderer.js` - injects per-page component script tags into `bodyEndInject`.
|
|
351
|
+
- `server/services/hooks.js` - adds `registerComponent` hook.
|
|
352
|
+
- `server/services/permissionRegistry.js` - adds `components` resource.
|
|
353
|
+
- `admin/js/views/index.js` - registers `components` + `componentEditor` views.
|
|
354
|
+
- `admin/js/config/sidebar-config.js` - adds the sidebar entry.
|
|
355
|
+
- `admin/js/app.js` - extends `fetchSidebarCounts()` with `api.components.list()`.
|
|
356
|
+
- `admin/js/api.js` - adds `api.components.{list,get,put,delete,exportBundle,importBundle}`.
|
|
357
|
+
- `admin/js/views/page-editor.js` - adds the "Insert Component" toolbar button and slideover wiring.
|
|
358
|
+
- `public/css/site.css` - adds `:not(:defined) { visibility: hidden; }`.
|
|
359
|
+
|
|
360
|
+
## 12. Success criteria
|
|
361
|
+
|
|
362
|
+
1. An authorised user can create a `.dmc` file in the admin, have it compile and appear in the library list.
|
|
363
|
+
2. Embedding the component on a page via the Insert Component slideover results in the component rendering correctly on the public site, with only its own script loaded (not the library as a whole).
|
|
364
|
+
3. A compile error in the editor is visibly surfaced, save is blocked, and the existing disk source is untouched.
|
|
365
|
+
4. Deleting or renaming a referenced component degrades gracefully: public pages still render, the `<dm-x>` tag remains inert, and the page editor preview shows a warning.
|
|
366
|
+
5. Runaway JS in a component's preview does not bring down the admin session.
|
|
367
|
+
6. Export and re-import of `.dmcomponent.json` round-trips the component losslessly.
|
|
368
|
+
7. The five seeded starter components install on a fresh CMS and remain editable.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Effects slideover overhaul - design
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-05-02
|
|
4
|
+
**Status:** Approved (pending implementation plan)
|
|
5
|
+
**Scope:** Phase 2 of the Effects work begun on 2026-05-02 (Phase 1 - `[ticker-tape]` shortcode - already shipped on `main`).
|
|
6
|
+
|
|
7
|
+
## Background
|
|
8
|
+
|
|
9
|
+
The page-editor toolbar currently exposes Domma effects through a flat dropdown menu (`effectsMenuItems` in `admin/js/lib/markdown-toolbar.js`). Each entry inserts a basic snippet at the cursor with no parameter UI and no preview. The reference documentation sits in a separate Effects view (`admin/js/templates/effects.html`).
|
|
10
|
+
|
|
11
|
+
The CMS now supports nine of Domma's ten canvas/JS effects via shortcodes (`reveal`, `breathe`, `pulse`, `scribe`, `reveal`, `scramble`, `counter`, `ripple`, `shake`, `twinkle`, `ticker-tape`) plus four CSS-based ones (`animate`, `ambient`, `firework`, `fireworks`) and the canvas `celebrate`. The dropdown does not scale well as the catalogue grows, and writing effect attributes by hand is error-prone.
|
|
12
|
+
|
|
13
|
+
This spec describes a unified slideover that replaces the dropdown, adds a parameter UI for every effect, and extends the row-reveal injection pattern to other host shortcodes so effects can be applied to a `[card]`, `[hero]`, etc. without an extra wrapper.
|
|
14
|
+
|
|
15
|
+
## Goals
|
|
16
|
+
|
|
17
|
+
- Replace the Effects dropdown with a unified `E.slideover`-based UI that lists all effects, exposes their parameters, and previews their output.
|
|
18
|
+
- Allow effects to be **injected** as flag-attributes on host shortcodes (`[card]`, `[hero]`, `[banner]`, `[col]`, `[button]`) - extending the existing `[row reveal …]` pattern.
|
|
19
|
+
- Support **edit-in-place** of an existing effect block when the cursor is inside one.
|
|
20
|
+
- Keep all current shortcode syntax working unchanged. The new UI is additive; hand-written shortcodes continue to render identically.
|
|
21
|
+
|
|
22
|
+
## Non-goals (YAGNI)
|
|
23
|
+
|
|
24
|
+
- Drag-and-drop reordering of effects in the rail.
|
|
25
|
+
- Effect favourites / recents tracking.
|
|
26
|
+
- A UI for stacking multiple effects on one block. The underlying syntax permits it (e.g. `[card reveal pulse]…[/card]`); users who want this can hand-edit.
|
|
27
|
+
- Keyboard shortcuts beyond `Esc`-to-close (relies on Domma slideover's default close-on-Esc behaviour; verify during implementation).
|
|
28
|
+
- Live render preview for effects that don't translate to a small viewport (full-page `[ticker-tape /]`, `[celebrate]`). Those get a static "this runs full-page on the published site" placeholder.
|
|
29
|
+
|
|
30
|
+
## Decisions taken during brainstorm
|
|
31
|
+
|
|
32
|
+
| # | Question | Decision |
|
|
33
|
+
|---|----------|----------|
|
|
34
|
+
| 1 | Slideover shape | Option **X** - unified slideover with category rail + effect list on the left, parameters + preview on the right. Replaces the dropdown entirely. |
|
|
35
|
+
| 2 | Preview style | **Shortcode preview always (Option A)**, plus a **live render preview where it works (Option C)** as a secondary feature. Shortcode preview is the primary affordance - it supports the copy-paste-and-amend workflow. |
|
|
36
|
+
| 3 | Phase scoping | **All-in-one (Option i)** - slideover overhaul and effect injection ship together. |
|
|
37
|
+
| 4 | Edit-existing | Slideover detects an effect block at the cursor and opens populated for editing; Insert button becomes Update. |
|
|
38
|
+
| 5 | Reference docs | Existing `admin/js/templates/effects.html` reference is **kept** as a hand-syntax cheatsheet alongside the slideover. No content removed. |
|
|
39
|
+
|
|
40
|
+
## User experience
|
|
41
|
+
|
|
42
|
+
### Entry points
|
|
43
|
+
|
|
44
|
+
- **Toolbar** - the existing `sparkles` Effects button (`markdown-toolbar.js`) opens the slideover instead of a dropdown.
|
|
45
|
+
- **Cursor-aware behaviour** - when the slideover opens, it inspects the textarea selection/cursor:
|
|
46
|
+
- Inside an existing effect block (e.g. `[reveal …]…[/reveal]`) → opens on that effect, fields populated, **Insert** button becomes **Update**.
|
|
47
|
+
- Inside a host shortcode (`[card …]`, `[hero …]`, `[banner …]`, `[col …]`, `[button …]`) → opens on the last-used effect with scope set to *Inject into [host]*.
|
|
48
|
+
- Otherwise → opens on the last-used effect (or Reveal as default), scope = *Standalone*.
|
|
49
|
+
|
|
50
|
+
### Slideover anatomy
|
|
51
|
+
|
|
52
|
+
`E.slideover({ title: 'Effects', size: 'lg', position: 'right' })` - Domma's native slideover, not a custom modal.
|
|
53
|
+
|
|
54
|
+
Body content is a two-column grid:
|
|
55
|
+
|
|
56
|
+
**Left rail (~170px wide, sticky):**
|
|
57
|
+
|
|
58
|
+
- Five category headings: Entrance, Animation, Text, Visual, Celebrations.
|
|
59
|
+
- Effects under each. Initial catalogue:
|
|
60
|
+
- **Entrance:** Reveal, Row reveal
|
|
61
|
+
- **Animation:** Breathe, Pulse, Shake, CSS animate
|
|
62
|
+
- **Text:** Scribe, Scramble, Counter
|
|
63
|
+
- **Visual:** Ripple, Twinkle, Ambient bg
|
|
64
|
+
- **Celebrations:** Firework, Fireworks, Celebrate, Ticker tape
|
|
65
|
+
- Active effect highlighted; clicking another effect swaps the right pane without closing the slideover.
|
|
66
|
+
|
|
67
|
+
**Right pane (flex, scrollable):**
|
|
68
|
+
|
|
69
|
+
1. Effect name (`h3`) and one-line description.
|
|
70
|
+
2. **Scope selector** - a two-button toggle:
|
|
71
|
+
- *Standalone block* (always available)
|
|
72
|
+
- *Inject into [host]* - present only if the cursor is inside a host that supports this effect (per the matrix below). When unavailable, an explanatory caption replaces the toggle.
|
|
73
|
+
Default scope: *Inject* if the cursor is inside a compatible host that does not yet have this effect injected; otherwise *Standalone*.
|
|
74
|
+
3. **Form fields** - one input per attribute, two per row where they fit. Field types: `text`, `number`, `select`, `checkbox`, `colour`. Sensible defaults pre-filled from each effect's `attrs[].default`. Fields are declared in `effect-defs.js` (see below).
|
|
75
|
+
4. **Shortcode preview (always visible)** - monospace block, updates live on every field change. When scope is *Inject*, the preview shows the host shortcode with effect flag-attributes embedded; when *Standalone*, it shows the standalone form. Read-only - the textarea cursor is the source of truth for editing.
|
|
76
|
+
5. **Live preview (effects flagged `previewable: true`)** - small render area with a *Replay* button to re-trigger one-shot animations. Effects flagged `previewable: false` (full-page only) show a static placeholder explaining where the effect actually runs.
|
|
77
|
+
6. **Footer** - *Cancel* and *Insert* (or *Update* in edit mode). Submitting writes the generated shortcode to the textarea via the existing `insertAtCursor` helper, then closes the slideover.
|
|
78
|
+
|
|
79
|
+
## Server-side: effect injection
|
|
80
|
+
|
|
81
|
+
### Mechanism
|
|
82
|
+
|
|
83
|
+
Effect injection means that compatible host shortcodes accept effect-flag attributes and emit a host element that is *also* a `dm-fx-*` element. The client-side `public/js/effects.js` matches on the `.dm-fx-*` class - it does not care whether the class lives on a standalone div or on a `[card]` wrapper, so no client-side changes are needed for injection.
|
|
84
|
+
|
|
85
|
+
Each affected host processor in `server/services/markdown.js` is updated once to:
|
|
86
|
+
|
|
87
|
+
1. Detect the presence of any flag-attribute belonging to an injectable effect (e.g. `reveal`, `breathe`, etc.).
|
|
88
|
+
2. Add the matching `dm-fx-<effect>` class to the rendered host element's `class` list, alongside the host's own classes.
|
|
89
|
+
3. Convert effect-prefixed attributes (e.g. `reveal-animation="slide-up"`) to `data-fx-animation="slide-up"` on the same element.
|
|
90
|
+
4. Strip the flag-attribute and prefixed attributes from the host's normal attribute pipeline so they do not leak into output as raw HTML attributes.
|
|
91
|
+
|
|
92
|
+
The existing `processGridBlocks` row-reveal handling stays as-is - it predates this design and uses the same shape (`reveal`, `reveal-animation`, etc. mapped to `dm-fx-reveal` + `data-fx-*`).
|
|
93
|
+
|
|
94
|
+
### Host / effect compatibility matrix
|
|
95
|
+
|
|
96
|
+
| Host | reveal | breathe | pulse | shake | twinkle | ticker-tape | ambient | animate | ripple |
|
|
97
|
+
|------|:------:|:-------:|:-----:|:-----:|:-------:|:-----------:|:-------:|:-------:|:------:|
|
|
98
|
+
| `[card]` | ✓ | ✓ | ✓ | ✓ | ✓ | - | ✓ | ✓ | ✓ |
|
|
99
|
+
| `[hero]` | ✓ | ✓ | ✓ | - | ✓ | ✓ | ✓ | ✓ | - |
|
|
100
|
+
| `[banner]` | ✓ | - | ✓ | ✓ | - | - | ✓ | ✓ | - |
|
|
101
|
+
| `[col]` | ✓ | ✓ | - | - | - | - | - | ✓ | ✓ |
|
|
102
|
+
| `[button]` | - | ✓ | ✓ | ✓ | - | - | - | ✓ | ✓ |
|
|
103
|
+
|
|
104
|
+
`[row]` already supports row-level reveal with its own multi-child stagger logic - left untouched.
|
|
105
|
+
|
|
106
|
+
`scribe`, `scramble`, `counter`, `firework`, `fireworks`, `celebrate` are **always standalone**. They either replace text content (text effects) or are page-level decoration (celebrations) and have no meaningful "host" pairing.
|
|
107
|
+
|
|
108
|
+
### Syntax
|
|
109
|
+
|
|
110
|
+
Mirrors the existing `[row reveal reveal-animation="..."]` shape:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
[card title="Welcome" reveal reveal-animation="slide-up" reveal-duration="600"]
|
|
114
|
+
Card body
|
|
115
|
+
[/card]
|
|
116
|
+
|
|
117
|
+
[hero title="Welcome" twinkle twinkle-count="100" twinkle-colour="#fff"]
|
|
118
|
+
Hero body
|
|
119
|
+
[/hero]
|
|
120
|
+
|
|
121
|
+
[button label="Click me" pulse pulse-scale="1.05"]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Attribute naming pattern: `<effect>` (flag, presence-only) and `<effect>-<attr>` for parameterised values. The flag is required; it's the trigger that tells the host's processor to enter injection mode.
|
|
125
|
+
|
|
126
|
+
## Components and files
|
|
127
|
+
|
|
128
|
+
### New
|
|
129
|
+
|
|
130
|
+
| File | Purpose |
|
|
131
|
+
|------|---------|
|
|
132
|
+
| `admin/js/lib/effect-defs.js` | Declarative effect catalogue. Single source of truth consumed by the slideover. Each entry:<br>`{ name, label, category, description, attrs: [{ name, label, type, default, options?, hint? }], hosts: [...], previewable: boolean }` |
|
|
133
|
+
| `admin/js/lib/effects-builder.js` | Slideover module. Exports `EffectsBuilder.open(textarea, options?)`. Encapsulates: cursor inspection, scope detection, form rendering, shortcode generation, edit-mode handling, live and shortcode preview. Uses helpers from `admin/js/lib/shortcode-modal.js` (`makeField`, `makeSelect`, etc.) for form fields. |
|
|
134
|
+
|
|
135
|
+
### Modified
|
|
136
|
+
|
|
137
|
+
| File | Change |
|
|
138
|
+
|------|--------|
|
|
139
|
+
| `admin/js/lib/markdown-toolbar.js` | Remove `effectsMenuItems` and `openEffectsDropdown`. Effects button stays; its handler delegates to the registered `handlers.effects(ta)` callback (already wired). |
|
|
140
|
+
| `admin/js/views/page-editor.js` | Register `toolbar.onEffects(ta => EffectsBuilder.open(ta, …))`. Add new imports for `effects-builder.js?v=1` and `effect-defs.js?v=1`. Bump the toolbar import's `?v=N` if the toolbar handler signature changes. |
|
|
141
|
+
| `server/services/markdown.js` | Extend `processCardBlocks`, `processHeroBlocks`, `processBannerBlocks`, the `[col]` handling inside `processGridBlocks`, and the button shortcode processor to detect injectable effect-flag attributes and stamp `dm-fx-*` class + `data-fx-*` attributes onto the rendered host element. Add a small shared helper (`extractInjectedEffects(attrs, allowedEffects)`) so each host processor uses the same logic. |
|
|
142
|
+
|
|
143
|
+
### Unchanged
|
|
144
|
+
|
|
145
|
+
- `public/js/effects.js` - no changes needed. Class-based selection finds injected effects automatically.
|
|
146
|
+
- `admin/js/templates/effects.html` - kept as a hand-syntax cheatsheet alongside the slideover. The Ticker tape section added in Phase 1 stays put.
|
|
147
|
+
- `admin/js/views/effects.js` - no changes; settings tab is untouched.
|
|
148
|
+
|
|
149
|
+
## Data flow
|
|
150
|
+
|
|
151
|
+
### Insertion (standalone)
|
|
152
|
+
|
|
153
|
+
1. User clicks Effects button → `handlers.effects(ta)` → `EffectsBuilder.open(ta)`.
|
|
154
|
+
2. Builder inspects `ta.selectionStart` against `ta.value`. No effect block, no host detected → opens with scope = Standalone, default effect = last-used or Reveal.
|
|
155
|
+
3. User adjusts fields. Each field change calls `_rebuildShortcode()` → updates the live shortcode preview pane (Option A) and, if `previewable`, re-mounts the live render preview (Option C).
|
|
156
|
+
4. User clicks Insert. Builder calls `insertAtCursor(ta, generatedShortcode)`, closes slideover.
|
|
157
|
+
|
|
158
|
+
### Insertion (injection)
|
|
159
|
+
|
|
160
|
+
1. Same as above through step 1.
|
|
161
|
+
2. Builder detects the cursor sits inside `[card …]…[/card]`. Opens with scope = *Inject into [card]*, default effect = last-used or Reveal (filtered to effects valid for the detected host).
|
|
162
|
+
3. The generated shortcode rewrites the host's opening tag, adding flag attributes. Preview shows the rewritten host tag.
|
|
163
|
+
4. Insert replaces the host's opening tag in `ta.value` (range computed from cursor position) - not via `insertAtCursor`.
|
|
164
|
+
|
|
165
|
+
### Edit-in-place
|
|
166
|
+
|
|
167
|
+
1. Cursor sits inside `[reveal animation="fade"]…[/reveal]`. Builder identifies the enclosing block by scanning backwards from the cursor for `[reveal` and forwards for `[/reveal]`. The body content between is preserved verbatim.
|
|
168
|
+
2. The opening tag is parsed via `parseShortcodeAttrs` (existing helper exported from `server/services/markdown.js` - but the admin UI re-implements it client-side as a small standalone helper, since admin code does not import server modules).
|
|
169
|
+
3. Form fields populate; button label switches to *Update*.
|
|
170
|
+
4. Update replaces the existing `[reveal …]…[/reveal]` block in `ta.value` with the regenerated form. Selection is restored to the inside of the new block.
|
|
171
|
+
|
|
172
|
+
## Edge cases and behaviours
|
|
173
|
+
|
|
174
|
+
- **Multiple cursors / no selection** - builder uses `ta.selectionStart` only; no multi-selection support.
|
|
175
|
+
- **Cursor inside a code block** - builder still opens but disables Insert with a caption explaining that effects inside fenced code are inert. Not blocking.
|
|
176
|
+
- **Cursor inside a host that already has an effect injected** - edit-mode wins; the existing effect's fields populate, and the user can change it or remove flags by clearing the attribute fields.
|
|
177
|
+
- **Reduced motion preview** - the live render preview honours `prefers-reduced-motion` (skips animation, shows the result frame). The published page already handles this via `effects.js`.
|
|
178
|
+
- **Slideover reopened after navigation** - only the last-used effect *name* is persisted via `S.set('effects-builder-last', name)`; field values reset to defaults each open. This avoids stale numeric values bleeding across unrelated pages.
|
|
179
|
+
|
|
180
|
+
## Risks
|
|
181
|
+
|
|
182
|
+
| Risk | Mitigation |
|
|
183
|
+
|------|------------|
|
|
184
|
+
| Markdown parser performance regression from the new `extractInjectedEffects` pass on five host processors. | The pass operates on already-parsed attribute objects (cheap), not on the raw markdown. No new regex passes. |
|
|
185
|
+
| Effect injection produces invalid output when multiple effects are flagged on one host (e.g. `[card reveal pulse]`). | `extractInjectedEffects` allows multiple effects in principle but the slideover only emits one at a time. Hand-written multiple-effect cases get `dm-fx-reveal dm-fx-pulse` on the same div - verified to work because `effects.js` initialisers are independent. Document this in the cheatsheet. |
|
|
186
|
+
| Live preview of `[scribe]` in the slideover competes with the `scribe-composer.js` slideover already used for the Insert → Scribe path. | The Effects slideover's Scribe form uses simple-mode fields only (speed, cursor, loop). Users who need the action-script composer use Insert → Scribe instead. Cross-link in the Scribe form's description text. |
|
|
187
|
+
| Cache-busters drift if implementation lands across multiple commits. | Bump `?v=N` in the same commit that touches the imported module. Already part of the project convention (see `MEMORY.md`). |
|
|
188
|
+
|
|
189
|
+
## Open follow-ups (not in this spec)
|
|
190
|
+
|
|
191
|
+
- Phase 4: a search/filter input on the slideover's left rail once the catalogue grows past ~20 effects.
|
|
192
|
+
- Phase 4: optional plugin-defined effects appearing in the slideover via a registry call.
|
|
193
|
+
- Phase 4: extending the cheatsheet template to be auto-generated from `effect-defs.js`.
|