domma-cms 1.6.60 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/CHANGELOG.md +4349 -0
  2. package/README.md +1 -1
  3. package/admin/dist/domma/domma-tools.css +3 -3
  4. package/admin/dist/domma/domma-tools.min.js +3 -3
  5. package/admin/js/lib/effect-defs.js +1 -1
  6. package/admin/js/lib/effects-builder.js +2 -2
  7. package/admin/js/lib/mail-compose.js +1 -0
  8. package/admin/js/lib/page-editor-reference.js +5 -5
  9. package/admin/js/lib/shortcode-tags.js +1 -1
  10. package/admin/js/templates/call-new.html +1 -1
  11. package/admin/js/templates/docs/usage-shortcodes.html +39 -0
  12. package/admin/js/views/call-new.js +1 -1
  13. package/admin/js/views/contacts.js +15 -15
  14. package/admin/js/views/form-submissions.js +2 -2
  15. package/docs/DommaFeatures.md +3005 -0
  16. package/docs/MultiTenant.md +176 -0
  17. package/docs/api-reference.md +1667 -0
  18. package/docs/cache.md +96 -0
  19. package/docs/configuration.md +1811 -0
  20. package/docs/editions-and-licences.md +190 -0
  21. package/docs/getting-started.md +201 -0
  22. package/docs/guides/form-triggers.md +260 -0
  23. package/docs/guides/mongodb-storage-adapter.md +282 -0
  24. package/docs/guides/multi-page-forms.md +121 -0
  25. package/docs/images/scribe-composer.png +0 -0
  26. package/docs/markdown-shortcodes.md +1738 -0
  27. package/docs/pentest-report-2026-04-03.md +181 -0
  28. package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
  29. package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
  30. package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
  31. package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
  32. package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
  33. package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
  34. package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
  35. package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
  36. package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
  37. package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
  38. package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
  39. package/docs/plans/2026-03-05-user-types-design.md +139 -0
  40. package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
  41. package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
  42. package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
  43. package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
  44. package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
  45. package/docs/plans/2026-06-10-core-project-design.md +240 -0
  46. package/docs/plugin-development.md +827 -0
  47. package/docs/recruitment-recipe.md +501 -0
  48. package/docs/scaffolding.md +312 -0
  49. package/docs/screenshots/calculator-floating.png +0 -0
  50. package/docs/screenshots/calculator-standard.png +0 -0
  51. package/docs/screenshots/email-settings.png +0 -0
  52. package/docs/screenshots/messages-count.png +0 -0
  53. package/docs/structure/user-types.md +107 -0
  54. package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
  55. package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
  56. package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
  57. package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
  58. package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
  59. package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
  60. package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
  61. package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
  62. package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
  63. package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
  64. package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
  65. package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
  66. package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
  67. package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
  68. package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
  69. package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
  70. package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
  71. package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
  72. package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
  73. package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
  74. package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
  75. package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
  76. package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
  77. package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
  78. package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
  79. package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
  80. package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
  81. package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
  82. package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
  83. package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
  84. package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
  85. package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
  86. package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
  87. package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
  88. package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
  89. package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
  90. package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
  91. package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
  92. package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
  93. package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
  94. package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
  95. package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
  96. package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
  97. package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
  98. package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
  99. package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
  100. package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
  101. package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
  102. package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
  103. package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
  104. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
  105. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
  106. package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
  107. package/docs/theming.md +320 -0
  108. package/package.json +2 -2
  109. package/plugins/_lib/admin/mail/compose-window.js +3 -1
  110. package/plugins/_lib/admin/mail/identity.js +18 -3
  111. package/plugins/_lib/admin/mail/reader-view.js +32 -3
  112. package/plugins/blog/CLAUDE.md +4 -0
  113. package/plugins/blog/admin/lib/email.js +191 -0
  114. package/plugins/blog/admin/views/comments.js +3 -1
  115. package/plugins/blog/plugin.json +2 -2
  116. package/plugins/blog/plugin.public.js +1 -1
  117. package/plugins/blog/tests/email.test.js +69 -0
  118. package/plugins/booking/CLAUDE.md +4 -0
  119. package/plugins/booking/admin/lib/email.js +191 -0
  120. package/plugins/booking/admin/views/panels.js +7 -3
  121. package/plugins/booking/plugin.json +1 -1
  122. package/plugins/booking/tests/email.test.js +69 -0
  123. package/plugins/free-tier.lock.json +27 -19
  124. package/plugins/shopping-cart/CLAUDE.md +4 -0
  125. package/plugins/shopping-cart/admin/lib/email.js +191 -0
  126. package/plugins/shopping-cart/admin/views/orders.js +11 -3
  127. package/plugins/shopping-cart/plugin.json +2 -2
  128. package/plugins/shopping-cart/tests/email.test.js +69 -0
  129. package/plugins/support/CLAUDE.md +4 -0
  130. package/plugins/support/admin/lib/email.js +191 -0
  131. package/plugins/support/admin/views/desk.js +4 -1
  132. package/plugins/support/admin/views/inbox.js +6 -2
  133. package/plugins/support/plugin.json +2 -2
  134. package/plugins/support/tests/email.test.js +69 -0
  135. package/public/js/celebrations/core/canvas.js +1 -1
  136. package/public/js/celebrations/index.js +1 -1
  137. package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
  138. package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
  139. package/public/js/celebrations/message-attrs.mjs +1 -0
  140. package/public/js/celebrations/themes/anniversary.js +1 -0
  141. package/public/js/celebrations/themes/autumn.js +1 -0
  142. package/public/js/celebrations/themes/birthday.js +1 -0
  143. package/public/js/celebrations/themes/butterfly-flight.js +1 -0
  144. package/public/js/celebrations/themes/calendar.js +1 -0
  145. package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
  146. package/public/js/celebrations/themes/chinese-new-year.js +1 -0
  147. package/public/js/celebrations/themes/christmas.js +1 -1
  148. package/public/js/celebrations/themes/easter.js +1 -0
  149. package/public/js/celebrations/themes/garden-drawing.js +1 -0
  150. package/public/js/celebrations/themes/garden.js +1 -0
  151. package/public/js/celebrations/themes/graduation.js +1 -0
  152. package/public/js/celebrations/themes/greeting.js +1 -0
  153. package/public/js/celebrations/themes/make-it-rain.js +1 -0
  154. package/public/js/celebrations/themes/money-rain.js +1 -0
  155. package/public/js/celebrations/themes/new-year.js +1 -0
  156. package/public/js/celebrations/themes/occasion-drawing.js +1 -0
  157. package/public/js/celebrations/themes/occasions.js +1 -0
  158. package/public/js/celebrations/themes/rain-control.js +1 -0
  159. package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
  160. package/public/js/celebrations/themes/sparkler-font.js +1 -0
  161. package/public/js/celebrations/themes/sparkler-writing.js +1 -0
  162. package/public/js/celebrations/themes/summer-garden.js +1 -0
  163. package/public/js/celebrations/themes/winter.js +1 -0
  164. package/public/js/celebrations/trait-data.mjs +1 -1
  165. package/public/js/effects.js +1 -1
  166. package/scripts/build.js +4 -0
  167. package/server/services/markdown.js +5 -2
  168. package/server/templates/page.html +1 -1
@@ -0,0 +1,59 @@
1
+ # Sidebar: Tools & Plugins folders
2
+
3
+ **Date:** 2026-06-08
4
+ **Status:** Implemented (dev checkout; pending commit + deploy)
5
+
6
+ ## Goal
7
+
8
+ Group plugin admin nav into two auto-built, system-managed top-level sidebar
9
+ folders:
10
+
11
+ - **Tools** - core plugins (always-on; `CORE_PLUGINS` on the server, currently `analytics`)
12
+ - **Plugins** - optional plugins, led by a **Manage Plugins** link (the old enable/disable page)
13
+
14
+ Layout (top level): Overview · Content · Data · **Tools** · **Plugins** · System · Documentation.
15
+
16
+ ## Decisions
17
+
18
+ - **System-managed, not menu-editable.** Folders are synthesised at render time
19
+ from each plugin's core/optional status - no per-site menu data, no migration,
20
+ correct on every existing site the instant the code deploys. (Consistent with
21
+ how plugin nav items are already render-time injected, not persisted.)
22
+ - **Manage Plugins moves into the Plugins folder**; the standalone System →
23
+ Plugins item is removed from the seed and stripped from already-seeded menus at
24
+ render time so it never shows twice.
25
+ - **Manage Plugins is unconditional** (not derived from the plugin list) so the
26
+ management link survives a `registered-items` fetch failure.
27
+
28
+ ## Components
29
+
30
+ - `admin/js/lib/sidebar-grouping.js` - pure, dependency-free helpers:
31
+ `groupPluginItems`, `stripItemByUrl`, `insertFoldersBeforeSystem`,
32
+ `pruneEmptySynthesisedFolders`. Unit-tested under `node --test`.
33
+ - `admin/js/lib/sidebar-renderer.js` - wires the helpers into `renderAdminSidebar`:
34
+ strip `#/plugins` → group plugin items → insert folders before System →
35
+ permission filter → prune empty synthesised folders.
36
+ - `server/services/plugins.js` - stamps `core: CORE_PLUGINS.has(name)` on each
37
+ bridged `plugin.json → admin.sidebar` item. Flows through
38
+ `GET /api/sidebar/registered-items` unchanged.
39
+ - `config/menus/admin-sidebar.json` - seed no longer carries System → Plugins.
40
+
41
+ Icons (all in Domma's set): Tools `tool`, Plugins `package`, Manage Plugins `sliders`.
42
+
43
+ ## Data flow
44
+
45
+ `plugin.json (core?)` → server stamps `core` on item → `registered-items` API →
46
+ `groupPluginItems` → Tools/Plugins folders → `filterByPermission` →
47
+ `pruneEmptySynthesisedFolders` → render.
48
+
49
+ ## Edge cases
50
+
51
+ - No core plugins → no Tools folder.
52
+ - No optional plugins → Plugins folder still shows (just Manage Plugins).
53
+ - User lacks `plugins` permission and no optional plugins → Plugins folder pruned.
54
+ - Plugin item with an explicit `parent` → keeps nesting there (unchanged).
55
+
56
+ ## Testing
57
+
58
+ - `admin/js/lib/sidebar-grouping.test.js` - 8 cases, all passing.
59
+ - Not browser-verified: the final rendered sidebar needs a real admin login to confirm.
@@ -0,0 +1,192 @@
1
+ # Domma Docs - default documentation project
2
+
3
+ **Date:** 2026-06-25
4
+ **Status:** Design - awaiting review
5
+
6
+ ## Goal
7
+
8
+ Ship a built-in **Domma Docs** project on every site. It is **disabled by
9
+ default**. When a site admin enables it, the full CMS documentation becomes
10
+ **live, browsable pages on the public site** under `/domma-docs/...`. When they
11
+ disable it, those pages disappear from the public site entirely.
12
+
13
+ The documentation is **moved** out of the admin panel and into this project -
14
+ the admin doc screens are retired, and the docs' single home becomes the
15
+ project's public pages.
16
+
17
+ ## Why this works with almost no new mechanism
18
+
19
+ The public-surface kill-switch already exists (commit `d7c255b`):
20
+
21
+ - `server/routes/public.js:112-118` resolves each page's project via
22
+ `getProjectForPage(...)` and returns **404** if that project is disabled -
23
+ regardless of page visibility.
24
+ - `getProjectForPage(urlPath, frontmatterProject)` resolves a page to a project
25
+ by an explicit `project:` in frontmatter, or by longest `rootUrl` prefix.
26
+ - `isProjectEnabled(slug)` returns `false` for a disabled project.
27
+
28
+ So a project with `rootUrl: /domma-docs`, seeded disabled, whose pages are
29
+ tagged `project: domma-docs`, is **automatically** hidden from the public site
30
+ until an admin switches it on. No new routing or guard code is required.
31
+
32
+ ## Non-goals
33
+
34
+ - No public navbar entry for the docs (browsable by URL / landing page; a menu
35
+ item can be added later if wanted).
36
+ - No live/interactive admin tooling carried onto public pages (see Caveats).
37
+ - The repo's own developer docs under `docs/*.md` are untouched - this concerns
38
+ the **admin doc screens** only.
39
+
40
+ ## 1. The project seed
41
+
42
+ New `seedDocsProject()` in `server/services/projects.js`, called in
43
+ `server/server.js` immediately after `seedCoreProject()`. Create-if-absent by
44
+ slug (an existing record is left untouched, so admin edits and the
45
+ enabled/disabled state survive reboots).
46
+
47
+ Seed record:
48
+
49
+ | Field | Value |
50
+ |---------------|-----------------------------------------------------------------------------------------|
51
+ | `slug` | `domma-docs` (immutable) |
52
+ | `name` | `Domma Docs` |
53
+ | `description` | "The Domma CMS handbook - guides, tutorials, component & API reference. Switch on to publish it on your own site." |
54
+ | `icon` | `document` |
55
+ | `rootUrl` | `/domma-docs` |
56
+ | `sortOrder` | `0` (Core is `-1`, so this sorts immediately after Core) |
57
+ | `enabled` | `false` |
58
+ | `protected` | `true` (see Undeletable below) |
59
+
60
+ ## 2. Undeletable (permanent but toggleable)
61
+
62
+ The special-casing that makes Core *always-enabled* and *rootUrl-locked* is keyed
63
+ on the **core slug**, not on the `protected` flag. So `protected: true` can be
64
+ reused purely as "undeletable" for Domma Docs without inheriting Core's other
65
+ locks - Domma Docs stays fully toggleable and its `rootUrl` stays editable.
66
+
67
+ Changes:
68
+
69
+ - `deleteProject(slug)` - refuse when the target's `protected === true` (today it
70
+ refuses only the core slug). Generalises "protected = undeletable" and keeps
71
+ Core safe by the same rule.
72
+ - The untag-all path - refuse for `protected` projects too. Untagging Domma Docs
73
+ would re-home its pages to Core (always-on) and defeat the kill-switch.
74
+ - `server/routes/api/projects.js` - mirror both refusals at the route level
75
+ (consistent with how Core is guarded today).
76
+ - Admin UI - the delete control for a protected project is disabled with a hint
77
+ ("Disable instead of deleting"), matching the Core treatment.
78
+
79
+ ## 3. Documentation as pages
80
+
81
+ Pages live under `content/pages/domma-docs/**.md`, mirroring today's screens but
82
+ unified beneath `/domma-docs`. Every page's frontmatter carries
83
+ `visibility: public` and **`project: domma-docs`** (explicit tag → robust
84
+ resolution *and* files the page under the project in admin).
85
+
86
+ Page tree (one page per current admin screen + landing/group index pages):
87
+
88
+ ```
89
+ /domma-docs index (landing - card grid to the 4 groups)
90
+ /domma-docs/usage group index
91
+ /domma-docs/usage/pages ← usage-pages.html
92
+ /domma-docs/usage/media ← usage-media.html
93
+ /domma-docs/usage/navigation ← usage-navigation.html
94
+ /domma-docs/usage/dconfig ← usage-dconfig.html
95
+ /domma-docs/usage/shortcodes ← usage-shortcodes.html
96
+ /domma-docs/usage/site-settings ← usage-site-settings.html
97
+ /domma-docs/usage/plugins ← usage-plugins.html
98
+ /domma-docs/usage/users-roles ← usage-users-roles.html
99
+ /domma-docs/usage/views ← usage-views.html
100
+ /domma-docs/usage/actions ← usage-actions.html
101
+ /domma-docs/usage/cta-shortcode ← usage-cta-shortcode.html
102
+ /domma-docs/tutorials group index
103
+ /domma-docs/tutorials/crud ← tutorial-crud.html (scaffolder → static)
104
+ /domma-docs/tutorials/plugin ← tutorial-plugin.html
105
+ /domma-docs/tutorials/forms ← tutorial-forms.html
106
+ /domma-docs/components group index ← components-reference.html
107
+ /domma-docs/components/howto ← components-howto.html
108
+ /domma-docs/components/walkthrough ← components-walkthrough.html
109
+ /domma-docs/components/rules ← components-rules.html
110
+ /domma-docs/api group index
111
+ /domma-docs/api/authentication ← api-authentication.html
112
+ /domma-docs/api/pages ← api-pages.html
113
+ /domma-docs/api/settings ← api-settings.html
114
+ /domma-docs/api/layouts ← api-layouts.html
115
+ /domma-docs/api/navigation ← api-navigation.html
116
+ /domma-docs/api/media ← api-media.html
117
+ /domma-docs/api/users ← api-users.html
118
+ /domma-docs/api/plugins ← api-plugins.html
119
+ /domma-docs/api/collections ← api-collections.html
120
+ /domma-docs/api/views ← api-views.html
121
+ /domma-docs/api/actions ← api-actions.html
122
+ ```
123
+
124
+ **Sourcing:** each page body is **ported from the matching admin template's
125
+ inner content** (the `.docs-body` HTML), not re-authored from scratch. Markdown
126
+ pages pass raw HTML through `marked`, so the existing markup, tables, code
127
+ blocks and `data-icon` spans carry over. The admin-only `.view-header`
128
+ back-link strip is dropped from each. This is fast and high-fidelity.
129
+
130
+ ## 4. Public rendering
131
+
132
+ - **CSS** - add the `.docs-body` styling (typography, tables, code) to
133
+ `public/css/site.css` so ported pages read well on the public theme.
134
+ - **Syntax highlighting** - ensure the public page pipeline runs the code-block
135
+ highlighter the admin docs rely on (`Domma.syntax.scan()` equivalent) on
136
+ `/domma-docs/*` pages. Confirm in `public/js/site.js`; wire if absent.
137
+ - **Icons** - already handled (public `site.js` runs `I.scan()`).
138
+
139
+ ## 5. Admin retirement (the "move")
140
+
141
+ The docs leave the admin panel:
142
+
143
+ - Remove the hardcoded **Documentation** section from
144
+ `config/menus/admin-sidebar.json` (and add a one-time migration/strip for
145
+ already-seeded sites, consistent with existing sidebar-migration patterns).
146
+ - Remove the doc/tutorial routes from `admin/js/app.js`.
147
+ - Remove the doc views (`documentation.js`, `doc-pages.js`, `tutorials.js`) and
148
+ their registrations in `views/index.js`. *(`pro-docs.js` is separate - leave
149
+ it.)*
150
+ - Remove `admin/js/templates/docs/*` after their content is ported.
151
+
152
+ Admins reach the docs through the **Projects** sidebar subtree: once Domma Docs
153
+ is enabled it appears like any project (with a "Pages (N)" child); while
154
+ disabled it is hidden, exactly as projects already behave.
155
+
156
+ **Consequence (accepted):** a fresh site, with the project off by default, has
157
+ **no in-admin documentation** until an admin enables Domma Docs - at which point
158
+ the docs are browsed on the public site. This matches "disabled by default".
159
+
160
+ ## Phasing
161
+
162
+ 1. **Engine + pilot** - seed the project (disabled, undeletable, `/domma-docs`);
163
+ add the `/domma-docs` landing page + 2-3 representative pages (a small usage
164
+ page, the large `usage/shortcodes`, one `api/*`). Prove end-to-end on the
165
+ live site: enabled → pages render with correct styling/highlighting/icons;
166
+ disabled → 404.
167
+ 2. **Full migration** - port the remaining screens; add the group index pages.
168
+ 3. **Admin retirement** - remove the admin Documentation section, routes, views
169
+ and templates.
170
+
171
+ ## Caveats
172
+
173
+ - **Interactive tutorials degrade to static.** `tutorial-crud.html` embeds a
174
+ live recipe scaffolder (`#tutorial-scaffolder-mount`); on a public page it
175
+ becomes static reference text. Acceptable for docs; noted so fidelity
176
+ expectations are correct.
177
+ - **`rootUrl` namespace ownership.** With `rootUrl: /domma-docs`, any page under
178
+ `/domma-docs/*` resolves to this project and is gated by its toggle. The
179
+ namespace is owned by the project - intended.
180
+ - **sortOrder tie.** Seeded at `0`; user projects also default to `0` and
181
+ tie-break alphabetically. Core (`-1`) is always first; Domma Docs sits among
182
+ the first projects after it.
183
+
184
+ ## Verification
185
+
186
+ - Unit: `seedDocsProject()` is idempotent (no duplicate on re-boot; existing
187
+ record untouched). `deleteProject('domma-docs')` rejects. Untag-all rejects.
188
+ - Live (browser, not curl): with the project **disabled**, `/domma-docs` and a
189
+ child page return 404. **Enable** it → both render with docs styling, working
190
+ code highlighting and icons. **Disable** again → 404 returns.
191
+ - Admin: Documentation section gone from the sidebar; Domma Docs appears under
192
+ Projects when enabled with the correct page count; delete control disabled.
@@ -0,0 +1,44 @@
1
+ # Domma Charts - Upstream Proposal (stub)
2
+
3
+ **Date:** 2026-06-26
4
+ **Status:** Captured requirement - **not yet brainstormed**. Deferred from the Surveys plugin work.
5
+ **Target repo:** `domma-js` (framework package, served from `node_modules/domma-js/public/dist/`)
6
+ **Seeded by:** [Surveys Pro plugin](./2026-06-26-surveys-pro-plugin-design.md) - its Results view is the first real consumer.
7
+
8
+ ## Why this is a stub, not part of Surveys
9
+
10
+ Charting is **framework furniture**, not a CMS view or a plugin concern. The dashboard, analytics, blog stats, and Surveys all want it. Building it inside one CMS view (or one plugin) would be the wrong home and would force an extraction later. It therefore belongs upstairs in `domma-js`.
11
+
12
+ It is **deliberately not blocking Surveys**: once it lives in `domma-js`, it is a separate repo on a separate release cycle. Surveys ships now with a self-contained CSS-bar fallback behind an isolated seam (see below), and upgrades to the real component with a one-function change whenever Domma Charts lands and `domma-js` is re-published.
13
+
14
+ ## The seam already in place
15
+
16
+ Surveys renders all visualisation through a single function:
17
+
18
+ ```js
19
+ renderDistribution(items /* [{ label, value, pct }] */) -> HTML string
20
+ ```
21
+
22
+ It currently draws lightweight Domma-CSS bars (no external dependency). **The input shape is the contract.** A future `Domma.charts` must accept the same normalised shape so the swap is mechanical:
23
+
24
+ ```js
25
+ // target API (to be designed)
26
+ Domma.charts(el, { type: 'bar', series: [{ label, value, pct }], ... })
27
+ // or alias-consistent with the rest of the framework, e.g. C.create(...)
28
+ ```
29
+
30
+ ## Requirements seeded by the survey Results view
31
+
32
+ The first consumer needs only a modest subset - capture it as the MVP, but design the component for general use:
33
+
34
+ - **Horizontal distribution bars** - label + value + percentage (choice-field results).
35
+ - **Rating distribution** - bars plus a summary average.
36
+ - **Completions over time** - a simple line/area or stepped bar over a date axis.
37
+ - Theme-aware across all ~28 Domma themes (charts must read CSS variables, not hardcode colours).
38
+ - No hard dependency on a third-party charting library if avoidable (keep the framework dependency-free, consistent with the rest of Domma).
39
+
40
+ Explicitly **out of the survey use-case** (so do not let it drive the design narrowly): multi-series legends, tooltips on hover, zoom/pan, real-time streaming. Design for them if cheap; do not over-fit to surveys.
41
+
42
+ ## Next step
43
+
44
+ When picked up: run this through the normal brainstorming → spec → plan flow **in the `domma-js` repo**, using this stub and the survey Results view as the concrete requirements source. On completion, swap `renderDistribution`'s body for the real call and remove the CSS-bar fallback.
@@ -0,0 +1,46 @@
1
+ # Manager Scheduler - Upstream Proposal (stub)
2
+
3
+ **Date:** 2026-06-26
4
+ **Status:** Captured requirement - **not yet brainstormed**. Deferred from the Surveys plugin work.
5
+ **Target repo:** `domma-cms-manager`
6
+ **Seeded by:** [Surveys Pro plugin](./2026-06-26-surveys-pro-plugin-design.md) - its due-work contract is the first real consumer.
7
+
8
+ ## Why this is a stub, not part of Surveys
9
+
10
+ Scheduling - reminders, targeted-email batches, housekeeping - is **cross-cutting platform infrastructure**, not a survey feature. Blog digests, invoice chasers, contact re-engagement, and Surveys all eventually want it. pm2-driven scheduling is the manager's domain. Building a scheduler inside the Surveys plugin would be the same category error as building charts inside one CMS view.
11
+
12
+ It is **deliberately not blocking Surveys**: the plugin already ships a working synchronous path (admin "Send invitations" / "Send reminders" buttons). The scheduler does not *add a capability* to Surveys - it merely *automates calling an endpoint that already exists*. The manager is a separate system on its own cycle.
13
+
14
+ ## Design principle: keep the scheduler generic
15
+
16
+ The scheduler must **not** learn about surveys. The seam is a **generic due-work protocol** any plugin can implement, so the manager stays a dumb poll-and-execute ticker:
17
+
18
+ - **Policy lives in the plugin.** Reminder cadence (`reminderAfterDays`) is set per-campaign in the survey editor, where that UX belongs.
19
+ - **The manager is a generic loop.** It periodically asks each registered plugin endpoint "what work is due?" and "do this due work" - nothing survey-specific.
20
+
21
+ ## The contract already in place (Surveys side)
22
+
23
+ Token-authenticated endpoints the plugin exposes (`dcms_` service token; exact auth header to be confirmed against how the manager already talks to the CMS):
24
+
25
+ ```
26
+ GET /api/plugins/surveys/due-work
27
+ -> outstanding sends past reminderAfterDays for open campaigns
28
+
29
+ POST /api/plugins/surveys/run
30
+ { campaignSlug, audience: 'all' | 'non-responders', reminder: bool }
31
+ -> execute a batch (flips ledger pending->sent / ->bounced)
32
+ ```
33
+
34
+ Generalise this to a registry the scheduler reads: `{ plugin, dueWorkUrl, runUrl }` so adding a future plugin to the scheduler is config, not code.
35
+
36
+ ## Requirements seeded by Surveys
37
+
38
+ - pm2-managed poll loop (interval configurable per registered job).
39
+ - Service-token auth to the CMS; never a user JWT.
40
+ - Idempotent execution: a `run` that overlaps a manual admin send must not double-email (the ledger's `completed`/`sent` state is the guard - the plugin already enforces it, but the scheduler should tolerate empty/no-op `due-work` responses gracefully).
41
+ - Housekeeping jobs (the broader manager goal) share the same poll-and-execute machinery - design the loop generically, not just for "send reminders".
42
+ - Observability: log each poll/run result; surface failures (so a stuck SMTP server is visible, not silent).
43
+
44
+ ## Next step
45
+
46
+ When picked up: run this through the normal brainstorming → spec → plan flow **in the `domma-cms-manager` repo**, using this stub and the survey due-work contract as the first consumer. Surveys needs no change when the scheduler lands - it already exposes the contract.
@@ -0,0 +1,206 @@
1
+ # Surveys (Pro Plugin) - Design Spec
2
+
3
+ **Date:** 2026-06-26
4
+ **Status:** Approved design - ready for implementation planning
5
+ **Repo:** `domma-cms` (this repo)
6
+ **Related upstream work:** [Domma Charts](./2026-06-26-domma-charts-upstream-proposal.md) (`domma-js`), [Manager Scheduler](./2026-06-26-manager-scheduler-upstream-proposal.md) (`domma-cms-manager`)
7
+
8
+ ## 1. Summary
9
+
10
+ A **Pro plugin** that turns the existing Forms system into a targeted survey tool. A *survey* is **not a new form engine** - it is a *campaign wrapper* around an existing Form, plus an *audience* (shared contacts + groups), plus an *invite/token ledger*, plus a *results/stats* layer.
11
+
12
+ The Forms system already:
13
+
14
+ - stores submissions in a Collection whose slug matches the form,
15
+ - runs an actions pipeline (collection / email / webhook) on submit, and
16
+ - accepts an **optional JWT** on the public submit endpoint.
17
+
18
+ So the genuinely new surface area is narrow: an **invitation/token layer** (the hash), a **campaign** concept (this survey → that audience → sent on this date), and **statistics**.
19
+
20
+ Everything new lives under `plugins/surveys/`. **Nothing in `server/` is modified** (it is upstream-replaced on update). Where Surveys needs core behaviour (storing a submission, running submit actions, sending mail) it *calls* the existing services rather than reimplementing them.
21
+
22
+ ## 2. Key decisions (resolved during brainstorming)
23
+
24
+ | Decision | Resolution |
25
+ |---|---|
26
+ | Audience model | Surveys owns shared `survey-contacts` + `survey-groups`; one-way **"Import from Contacts"** bridge from the per-user Contacts plugin. |
27
+ | Storage | Schemas declare `"storage": {"adapter": "mongodb"}` (Pro → Mongo, `cms_`-prefixed); degrade gracefully to `FileAdapter` if no connection. Schema management stays file-based. |
28
+ | Attribution | **Per-survey toggle**: each campaign is created `identified` or `anonymous`. |
29
+ | Survey surface | Plugin-hosted page at `/survey/<slug>` **plus** a `[survey slug]` shortcode for embedding. |
30
+ | Sending | Editor-triggered batch (synchronous, via `email.js`) **plus** a token-authenticated due-work contract for the external scheduler. **No in-process scheduler.** |
31
+ | Statistics | Layered (campaign health + per-question aggregation) **plus** CSV/JSON export. |
32
+ | Charts | Lightweight Domma-CSS bars now, behind an isolated `renderDistribution()` seam; real charting deferred to **Domma Charts** upstream. |
33
+ | Out of v1 (YAGNI) | Per-survey project scoping via `meta.project`; open-tracking pixel (no `opened` status - ledger goes `sent → completed`). |
34
+
35
+ ## 3. Data model
36
+
37
+ Four plugin collections (`schema.json` files under `plugins/surveys/collections/`, mirroring the Contacts plugin layout), all declaring the mongodb adapter with graceful file fallback:
38
+
39
+ ### `survey-contacts`
40
+ Shared, editor-owned recipients. Fields: `email` (required), `name`, plus optional custom fields. **"Import from Contacts"** copies (one-way) from the per-user Contacts plugin's `user-contacts`.
41
+
42
+ ### `survey-groups`
43
+ Named audience groups. Fields: `name`, `description`, `contactIds[]` (membership by contact id).
44
+
45
+ ### `surveys` (the campaign)
46
+ Fields:
47
+
48
+ - `slug` (immutable), `title`, `description`
49
+ - `formSlug` - the underlying Form that supplies the questions and the submission collection
50
+ - `groupIds[]` - targeted audience group(s)
51
+ - `mode` - `identified | anonymous`
52
+ - `status` - `draft | open | closed`
53
+ - `opensAt`, `closesAt` (optional)
54
+ - `reminderAfterDays` - per-campaign reminder cadence (consumed by the due-work contract; see §6)
55
+ - `inviteEmail` - `{ subject, body }` template with `{{link}}`, `{{name}}` tokens
56
+
57
+ ### `survey-invites` (the ledger)
58
+ **One row per (survey × contact).** This is the heart of the new mechanic. Fields:
59
+
60
+ - `surveyId`, `contactId`
61
+ - `tokenHash` - **SHA-256 of the token** (plaintext never stored - identical pattern to `api-tokens`)
62
+ - `status` - `pending | sent | completed | bounced`
63
+ - `sentAt`, `completedAt`
64
+ - `responseId` - the submission row id (identified mode), else null
65
+
66
+ ### Responses
67
+ **Answers reuse the underlying Form's existing submission collection** (slug = `formSlug`). Surveys does **not** create a separate response store.
68
+
69
+ - **Identified mode:** the submission row carries `surveyInviteId` (and therefore resolves to a contact).
70
+ - **Anonymous mode:** the submission row carries **no** contact/invite link - but the ledger still flips that invite to `completed`.
71
+
72
+ One mechanism, two privacy postures: the invite ledger drives response-rate stats and reminder-suppression identically in both modes; only the *persisted link on the response row* differs.
73
+
74
+ ## 4. Taking a survey & the token
75
+
76
+ ### Token
77
+ - A **readable code** per invite (e.g. `XR4K-9QP2`, unambiguous alphabet - no `0/O`, `1/I/L`).
78
+ - Only its **SHA-256 hash** is stored in `survey-invites.tokenHash`.
79
+ - The personalised email link embeds the plaintext: `/survey/<slug>?t=XR4K-9QP2`.
80
+ - The manual-entry box on the hosted page accepts the **same code**. One secret, two entry paths.
81
+
82
+ ### Surface
83
+ - **Hosted page** at `/survey/<slug>` (plugin-served, public). Responsibilities:
84
+ - auto-bind `?t=` from the link,
85
+ - show a "enter your code" box when no/invalid token is present,
86
+ - own the **thank-you / already-completed / closed** states,
87
+ - render the underlying Form (so the form's Domma components initialise via the normal public SSR + `site.js` path).
88
+ - **`[survey slug]` shortcode** for embedding the same experience inside a branded CMS page (registered via `hooks.registerShortcode`).
89
+
90
+ ### Submit flow
91
+ A **plugin** submit endpoint (`POST /api/plugins/surveys/submit/:slug`):
92
+
93
+ 1. Looks up the invite by `tokenHash` (hash the submitted code, match).
94
+ 2. Rejects if: token unknown, invite already `completed`, or survey `closed`/outside `opensAt..closesAt`.
95
+ 3. **Delegates answer storage + the actions pipeline to the core Forms submit path** (calls the existing public form submit handler / forms service - does not reimplement validation or actions).
96
+ 4. Records the invite as `completed` with `completedAt` and `responseId`.
97
+
98
+ **Notifications reuse existing machinery:**
99
+ - "Notify completion" = an **email action already configured on the underlying Form** - reused, not rebuilt.
100
+ - "Notify it's there" = the **campaign invite email** (§6).
101
+
102
+ ## 5. Statistics (layered + export)
103
+
104
+ Per-survey **Results** view:
105
+
106
+ - **Campaign health:** invited / completed / response-rate %; completions over time; **outstanding list** (named in identified mode, count-only in anonymous).
107
+ - **Per-question** (computed live from the form's submission collection):
108
+ - choice fields → distribution bars,
109
+ - rating fields → average + distribution,
110
+ - free-form text → listed responses (not charted).
111
+ - **Export:** raw responses as **CSV / JSON**. Anonymous-mode export is guaranteed contact-free.
112
+
113
+ ### Charts seam (`renderDistribution`)
114
+ All visualisation goes through a single isolated function:
115
+
116
+ ```js
117
+ // returns an HTML string of Domma-CSS bars from a normalised shape
118
+ renderDistribution(items /* [{ label, value, pct }] */) { ... }
119
+ ```
120
+
121
+ It draws lightweight Domma-CSS bars today (e.g. `.dm-progression`-style / grid widths - **no Chart.js, no external dependency**). The input shape is *exactly* what a future `Domma.charts(el, {series})` will accept, so swapping to the real component later is a one-function change. See the [Domma Charts upstream proposal](./2026-06-26-domma-charts-upstream-proposal.md).
122
+
123
+ ## 6. Sending & the due-work contract
124
+
125
+ The plugin owns the *what*; the external scheduler owns the *when*. **No in-process timers.**
126
+
127
+ ### Admin (synchronous)
128
+ - **"Send invitations"** → emails the targeted group via `email.js`; per-recipient result flips ledger `pending → sent` or `→ bounced`.
129
+ - **"Send reminder to non-responders"** → same, auto-excluding anyone `completed`/`bounced`.
130
+
131
+ ### Generic due-work contract (for the scheduler)
132
+ Token-authenticated plugin endpoints - kept **generic**, not survey-specific, so any future plugin can implement the same protocol and the [Manager Scheduler](./2026-06-26-manager-scheduler-upstream-proposal.md) stays a dumb ticker:
133
+
134
+ - `GET /api/plugins/surveys/due-work` → outstanding sends past `reminderAfterDays` for open campaigns.
135
+ - `POST /api/plugins/surveys/run` `{ campaignSlug, audience: 'all' | 'non-responders', reminder: bool }` → execute a batch.
136
+
137
+ Reminder **policy** (cadence) lives **per-campaign in the survey editor**; the scheduler merely polls "what's due?" and "do it". Authentication aligns with the scheme domma-cms-manager already uses against the CMS (a `dcms_` service token - exact header to be confirmed against the manager).
138
+
139
+ ## 7. Permissions & projects
140
+
141
+ - New permission family **`surveys.{read, create, update, delete}`**. Custom roles are **not** back-filled - admin grants via the role editor (same gotcha as Projects/Menus).
142
+ - v1 surveys are **core-project**. Per-survey `meta.project` scoping is deferred (YAGNI).
143
+
144
+ ## 8. Admin UI (Domma-first throughout)
145
+
146
+ Plugin sidebar section **Surveys**. The whole plugin obeys the CLAUDE.md Domma conventions - the Contacts plugin (`plugins/contacts/admin/`) is the reference pattern.
147
+
148
+ - **Views** export `{ templateUrl, async onMount($container) }` with companion `.html` templates; use `$container.find('#id')` + `.get(0)` for native elements.
149
+ - **Tables** (surveys list, contacts, outstanding list) via `T.create` - render functions return **HTML strings** and use **`onclick`**, never `addEventListener` (double-bind gotcha).
150
+ - **Forms** (survey editor, contact editor, audience picker) via `F.create(selector, {blueprint})` - no hand-written `<form>` markup.
151
+ - **UI chrome:** `E.toast` for results; `E.confirm` before destructive sends/deletes; `E.modal` with light-DOM slot projection for any buttons/inputs.
152
+ - **Icons:** `<span data-icon>` + `I.scan()`. Check the registry first; register new SVGs (likely `clipboard-list`, `bar-chart`, `send`) via `I.register` only if missing.
153
+ - **HTTP:** `H.post`/`H.get` for JSON; `fetch().then(r => r.text())` only for HTML templates (the `H.get`-auto-parses-JSON gotcha).
154
+ - **Admin module loading:** rely on the existing `Cache-Control: no-store` header - **no `?v=N`** query strings on admin imports.
155
+
156
+ Views:
157
+ 1. **Surveys list** - `T.create`: title, status, response-rate, actions.
158
+ 2. **Survey editor** - pick/create underlying Form, choose groups, `mode` toggle, open/close dates, `reminderAfterDays`, invite-email template.
159
+ 3. **Audience** - contacts + groups CRUD, **Import from Contacts** button.
160
+ 4. **Results** - health widgets + `renderDistribution` charts + CSV/JSON export.
161
+
162
+ ## 9. Error handling
163
+
164
+ - Invalid / used token on submit → friendly "this link is no longer valid / already completed".
165
+ - Closed or out-of-window survey → "this survey has closed".
166
+ - Duplicate submit (invite already `completed`) → blocked by default.
167
+ - Batch send → **never fails wholesale**; per-recipient bounce tracking; SMTP rejection → `bounced`.
168
+ - Mongo absent → schemas fall back to `FileAdapter` (no crash).
169
+ - Anonymous mode → assert **no `contactId`/`surveyInviteId`** ever reaches the response row or the export.
170
+
171
+ ## 10. Testing
172
+
173
+ - **Unit:** token generation / hash / verify; ledger state transitions; stats aggregation (choice / rating / text); anonymous-mode de-linking (assert no contact link on response); `renderDistribution` output shape.
174
+ - **Integration:** send batch → `pending → sent`; valid-token submit → `completed` + `responseId`; used-token submit → rejected; closed-survey submit → rejected; reminder/due-work query excludes `completed`.
175
+ - **Browser-verified** (never claimed from curl alone - see project memory): hosted page link flow + manual code entry + thank-you/closed states; stats dashboard renders bars; survey editor `F.create` round-trip.
176
+
177
+ ## 11. Plugin structure
178
+
179
+ ```
180
+ plugins/surveys/
181
+ plugin.json manifest (name, displayName, version, description, author, date, icon, admin{sidebar,routes,views})
182
+ plugin.js Fastify plugin: hosted page, submit endpoint, due-work contract, [survey] shortcode
183
+ config.js config defaults
184
+ CLAUDE.md routes, storage, gotchas
185
+ collections/
186
+ survey-contacts/schema.json
187
+ survey-groups/schema.json
188
+ surveys/schema.json
189
+ survey-invites/schema.json
190
+ admin/
191
+ templates/*.html
192
+ views/*.js surveys list, survey editor, audience, results
193
+ lib/
194
+ tokens.js generate / hash / verify readable codes
195
+ ledger.js invite state transitions
196
+ stats.js aggregation + renderDistribution seam
197
+ ```
198
+
199
+ ## 12. Seams to upstream work
200
+
201
+ | Seam (lives here) | Consumed by | Upstream stub |
202
+ |---|---|---|
203
+ | `renderDistribution({label,value,pct}[])` | `Domma.charts` (one-function swap) | [Domma Charts](./2026-06-26-domma-charts-upstream-proposal.md) |
204
+ | `GET /due-work` + `POST /run` generic contract | Manager pm2 poll-and-execute loop | [Manager Scheduler](./2026-06-26-manager-scheduler-upstream-proposal.md) |
205
+
206
+ Both seams ship in v1 so the plugin is self-sufficient; the upstream components upgrade it later without throwing anything away.
@@ -0,0 +1,96 @@
1
+ # Right-click shortcode context menu - design
2
+
3
+ **Date:** 2026-07-02
4
+ **Status:** Implemented (pure logic test-covered; menu UI pending browser verification)
5
+
6
+ ## Goal
7
+
8
+ In the page editor, right-clicking inside the Markdown textarea opens a context
9
+ menu that is aware of the shortcode the cursor sits in. The menu is **hybrid**:
10
+
11
+ - **Inside a tag** - the tag's own attributes at the top (edit them live), then
12
+ `Insert…` and `Wrap in…` submenus.
13
+ - **In plain text** - just `Insert…` (plus `Wrap selection in…` when text is
14
+ selected).
15
+
16
+ ## Decisions (from brainstorming)
17
+
18
+ - **Scope:** hybrid menu (tag options + Insert + Wrap).
19
+ - **Coverage:** all ~28 shortcodes get full attribute schemas.
20
+ - **Architecture:** one central client-side registry is the source of truth.
21
+ - **Edit model:** inline, live in the menu - checkboxes toggle instantly, value
22
+ fields edit in place, selects open a submenu; the shortcode text rewrites as
23
+ you go.
24
+
25
+ ## Components
26
+
27
+ ### `admin/js/lib/shortcode-tags.js` (shared utilities)
28
+ Generalised from the private helpers in `effects-builder.js`:
29
+ - `parseAttrs(attrStr)` - quoted values → strings, bare flags → `true`.
30
+ - `findEnclosingShortcode(textarea, specs)` - innermost tag wrapping the cursor,
31
+ handling both block (`[card]…[/card]`) and self-closing (`[spacer … /]`), plus
32
+ the cursor being literally inside a tag's `[…]` brackets. Returns a span
33
+ `{name, openTagStart, openTagEnd, attrStr, attrs, selfClosing}` or `null`.
34
+ - `serialiseTag(name, attrs, selfClosing)` - rebuild a tag; flags/empties handled;
35
+ `"` escaped.
36
+ - `setAttr(textarea, span, key, value)` - set/remove one attribute, rewrite in
37
+ place, dispatch `input`, return the fresh span.
38
+
39
+ **Robustness:** `selfClosing` is derived from the **actual tag text** (does it end
40
+ in `/]`?), never from the registry hint - so a mis-flagged registry entry can
41
+ never rewrite a paired `[name]…[/name]` opening tag as `[name … /]`.
42
+
43
+ ### `admin/js/lib/shortcode-registry.js` (source of truth)
44
+ `SHORTCODES` - 32 entries (28 top-level shortcodes + child tags `tab`, `item`,
45
+ `col`, `slide`, `event`). Each: `{name, label, category, selfClosing, container,
46
+ wrappable, template, attrs[]}`. Attribute `type` ∈ `text | number | checkbox |
47
+ select | colour | icon | url`; selects carry `options: [[value,label],…]`.
48
+ Attribute names were verified against the `process<X>Blocks` functions in
49
+ `server/services/markdown.js` (code overrides docs). Effects are handled
50
+ separately by `effect-defs.js` and are out of scope here.
51
+
52
+ ### `admin/js/lib/shortcode-context-menu.js` (the menu)
53
+ `attachContextMenu(textarea)` binds a `contextmenu` listener that suppresses the
54
+ browser menu, finds the enclosing shortcode, and renders the menu. Bespoke DOM
55
+ (direct `addEventListener`, no namespaced delegation), a single guarded instance
56
+ reset on **every** close path (outside-click / Esc / scroll) - applying the
57
+ Effects-builder latch lesson. Rows render by attribute type; `Insert…` inserts a
58
+ registry template; `Wrap in…` wraps the selection (grid wraps as
59
+ `[grid][col]…[/col][/grid]`).
60
+
61
+ ### Wiring
62
+ `page-editor.js` imports `attachContextMenu` and calls it on the
63
+ `#markdown-editor` textarea right after the toolbar is created.
64
+
65
+ ## Testing
66
+
67
+ - **Automated (node `--test`):** `shortcode-tags` (parse / find / serialise /
68
+ setAttr, incl. nested, self-closing, and the mis-flag-can't-corrupt case);
69
+ `shortcode-registry` integrity (shape, unique names, coverage, valid attr
70
+ types, and the self-closing-vs-paired-template invariant).
71
+ - **Browser-only (not yet verified):** the `contextmenu` interception, menu
72
+ rendering/positioning, inline editing, submenus, and wrap-on-selection.
73
+
74
+ ## Effects extension (2026-07-03)
75
+
76
+ Effects (`effect-defs.js`, 17) folded into the same menu, full parity:
77
+
78
+ - **Standalone effect tags** (`[reveal]…[/reveal]`, `[counter /]`) are added to the
79
+ detection specs and get inline option editing via the existing row engine.
80
+ - **Injected effects on a host** (`[card reveal reveal-animation="…"]`) render an
81
+ `Effect: <label>` section editing the `<effect>-*` prefixed params, with
82
+ **✕ Remove effect** (drops the flag + all prefixed keys in one rewrite).
83
+ - **Add effect…** submenu injects into a compatible host (`effect.hosts`) or
84
+ inserts a standalone effect template.
85
+
86
+ Shared-util additions: `setAttrs(ta, span, changes)` (multi-attr rewrite) and
87
+ `effectAttrKeys(attrs, name)`. `_attrRow` takes an explicit key so injected
88
+ effects address `<effect>-<attr>`. Integrity guarded by
89
+ `tests/admin/effect-menu-integrity.test.js`.
90
+
91
+ ## Deliberately deferred
92
+
93
+ - Refactoring `effects-builder.js` onto the shared utilities (works today; avoided
94
+ to not risk a regression in an untested UI path).
95
+ - Routing `Insert…` for Embed/Effects to their dedicated builders (currently
96
+ inserts a template; builders remain reachable from the toolbar).