domma-cms 1.6.60 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.md +4344 -0
  2. package/README.md +1 -1
  3. package/admin/dist/domma/domma-tools.css +3 -3
  4. package/admin/dist/domma/domma-tools.min.js +3 -3
  5. package/admin/js/lib/effect-defs.js +1 -1
  6. package/admin/js/lib/effects-builder.js +2 -2
  7. package/admin/js/lib/page-editor-reference.js +5 -5
  8. package/admin/js/lib/shortcode-tags.js +1 -1
  9. package/admin/js/templates/docs/usage-shortcodes.html +39 -0
  10. package/docs/DommaFeatures.md +3005 -0
  11. package/docs/MultiTenant.md +176 -0
  12. package/docs/api-reference.md +1667 -0
  13. package/docs/cache.md +96 -0
  14. package/docs/configuration.md +1811 -0
  15. package/docs/editions-and-licences.md +190 -0
  16. package/docs/getting-started.md +201 -0
  17. package/docs/guides/form-triggers.md +260 -0
  18. package/docs/guides/mongodb-storage-adapter.md +282 -0
  19. package/docs/guides/multi-page-forms.md +121 -0
  20. package/docs/images/scribe-composer.png +0 -0
  21. package/docs/markdown-shortcodes.md +1738 -0
  22. package/docs/pentest-report-2026-04-03.md +181 -0
  23. package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
  24. package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
  25. package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
  26. package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
  27. package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
  28. package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
  29. package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
  30. package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
  31. package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
  32. package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
  33. package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
  34. package/docs/plans/2026-03-05-user-types-design.md +139 -0
  35. package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
  36. package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
  37. package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
  38. package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
  39. package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
  40. package/docs/plans/2026-06-10-core-project-design.md +240 -0
  41. package/docs/plugin-development.md +827 -0
  42. package/docs/recruitment-recipe.md +501 -0
  43. package/docs/scaffolding.md +312 -0
  44. package/docs/screenshots/calculator-floating.png +0 -0
  45. package/docs/screenshots/calculator-standard.png +0 -0
  46. package/docs/screenshots/email-settings.png +0 -0
  47. package/docs/screenshots/messages-count.png +0 -0
  48. package/docs/structure/user-types.md +107 -0
  49. package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
  50. package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
  51. package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
  52. package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
  53. package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
  54. package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
  55. package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
  56. package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
  57. package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
  58. package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
  59. package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
  60. package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
  61. package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
  62. package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
  63. package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
  64. package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
  65. package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
  66. package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
  67. package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
  68. package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
  69. package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
  70. package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
  71. package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
  72. package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
  73. package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
  74. package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
  75. package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
  76. package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
  77. package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
  78. package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
  79. package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
  80. package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
  81. package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
  82. package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
  83. package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
  84. package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
  85. package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
  86. package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
  87. package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
  88. package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
  89. package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
  90. package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
  91. package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
  92. package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
  93. package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
  94. package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
  95. package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
  96. package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
  97. package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
  98. package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
  99. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
  100. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
  101. package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
  102. package/docs/theming.md +320 -0
  103. package/package.json +2 -2
  104. package/public/js/celebrations/core/canvas.js +1 -1
  105. package/public/js/celebrations/index.js +1 -1
  106. package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
  107. package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
  108. package/public/js/celebrations/message-attrs.mjs +1 -0
  109. package/public/js/celebrations/themes/anniversary.js +1 -0
  110. package/public/js/celebrations/themes/autumn.js +1 -0
  111. package/public/js/celebrations/themes/birthday.js +1 -0
  112. package/public/js/celebrations/themes/butterfly-flight.js +1 -0
  113. package/public/js/celebrations/themes/calendar.js +1 -0
  114. package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
  115. package/public/js/celebrations/themes/chinese-new-year.js +1 -0
  116. package/public/js/celebrations/themes/christmas.js +1 -1
  117. package/public/js/celebrations/themes/easter.js +1 -0
  118. package/public/js/celebrations/themes/garden-drawing.js +1 -0
  119. package/public/js/celebrations/themes/garden.js +1 -0
  120. package/public/js/celebrations/themes/graduation.js +1 -0
  121. package/public/js/celebrations/themes/greeting.js +1 -0
  122. package/public/js/celebrations/themes/make-it-rain.js +1 -0
  123. package/public/js/celebrations/themes/money-rain.js +1 -0
  124. package/public/js/celebrations/themes/new-year.js +1 -0
  125. package/public/js/celebrations/themes/occasion-drawing.js +1 -0
  126. package/public/js/celebrations/themes/occasions.js +1 -0
  127. package/public/js/celebrations/themes/rain-control.js +1 -0
  128. package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
  129. package/public/js/celebrations/themes/sparkler-font.js +1 -0
  130. package/public/js/celebrations/themes/sparkler-writing.js +1 -0
  131. package/public/js/celebrations/themes/summer-garden.js +1 -0
  132. package/public/js/celebrations/themes/winter.js +1 -0
  133. package/public/js/celebrations/trait-data.mjs +1 -1
  134. package/public/js/effects.js +1 -1
  135. package/scripts/build.js +4 -0
  136. package/server/services/markdown.js +5 -2
  137. package/server/templates/page.html +1 -1
@@ -0,0 +1,1738 @@
1
+ # Markdown Shortcodes
2
+
3
+ Domma CMS extends standard Markdown with shortcodes - special tags you write in page content that the server converts to HTML before rendering. Shortcodes are processed before the Markdown parser runs, so Markdown formatting works inside shortcode bodies.
4
+
5
+ All shortcode attributes accept single or double quotes, and bare flags (e.g. `hover`, `dismissible`) are allowed.
6
+ Most shortcodes are skipped inside fenced code blocks and inline code, so you can show shortcode syntax on a page
7
+ (`[block]` / block short tags and `[banner]` are the exceptions - they are expanded even inside code).
8
+
9
+ ## Expiring any shortcode
10
+
11
+ Every shortcode accepts the following optional expiry attributes. Conditions combine: when any configured limit is reached, the shortcode and its body are hidden from that visitor.
12
+
13
+ ```markdown
14
+ [banner expires-at="2026-12-31T23:59:00Z"]Offer ends this year.[/banner]
15
+ [card expire-after="60" expire-scope="ip"]Shown for an hour after the first view from this IP.[/card]
16
+ [button expire-views="3" expire-scope="ip" /]
17
+ [text expire-views="1" expire-scope="session"]Shown once in this browser session.[/text]
18
+ [card expire-ip="203.0.113.8, 2001:db8::/32"]Only shown to these IP addresses.[/card]
19
+ ```
20
+
21
+ `expires-at` takes an ISO 8601 date and time, including its timezone (for example, `2026-12-31T23:59:00Z`). `expire-after` starts its timer on the visitor's first view. `expire-views` sets the maximum rendered page views. Both counters use the IP address by default; set `expire-scope="session"` to track the browser session instead. `expire-session="true"` is shorthand for showing a shortcode once per browser session. `expire-ip` is an optional allowlist of exact IP addresses or CIDR ranges. Editor previews ignore expiry limits and do not consume views. Expiry state stores hashed visitor identifiers in `content/shortcode-expiry.json`.
22
+
23
+ Use `expiry-id` to change which visitor counter a shortcode uses after changing its position in the page. Without it, each shortcode occurrence has its own counter.
24
+
25
+ ---
26
+
27
+ ## `[card]`
28
+
29
+ Wraps content in a Domma card component.
30
+
31
+ ### Basic usage
32
+
33
+ ```
34
+ [card]
35
+ Card body with **Markdown** support.
36
+ [/card]
37
+ ```
38
+
39
+ ### With a title
40
+
41
+ ```
42
+ [card title="Getting Started"]
43
+ Follow these steps to set up your site.
44
+ [/card]
45
+ ```
46
+
47
+ ### All attributes
48
+
49
+ | Attribute | Type | Description |
50
+ |---|---|---|
51
+ | `title` | string | Card header title. Omit for a body-only card |
52
+ | `subtitle` | string | Optional subtitle below the title |
53
+ | `icon` | string | Domma icon name displayed in the card header |
54
+ | `icon-layout` | string | `"inline"` (default) or `"stacked"` - controls icon/title arrangement |
55
+ | `icon-size` | string | `xs`, `sm`, `md`, `lg`, `xl`, `2xl`, or an explicit size: `48` / `48px`, `3rem`, `2em` |
56
+ | `icon-align` | string | `left`, `center` (or `centre`) or `right` - aligns the icon and title together; works with `icon-layout` and the `icon-top` / `icon-inline` layouts |
57
+ | `footer` | string | Text content for the card footer |
58
+ | `layout` | string | One of 33 ready-made card designs - see [Card layouts](#card-layouts) below |
59
+ | `variant` | string | `clean`, `gradient`, `glass`, `accent`, `dark` or `glow`. Legacy `"primary"` still adds the primary colour theme |
60
+ | `gradient` | string | Gradient name used with `variant="gradient"` (default `indigo`) |
61
+ | `image` | string | Image URL, used by the image-based layouts |
62
+ | `collapsible` | `"true"` | Makes the card body toggle on header click |
63
+ | `hover` | flag | Adds a hover highlight effect; `hover="off"` switches the lift off instead |
64
+ | `font` | string | `serif` or `mono` |
65
+ | `font-size` | string | `sm`, `lg` or `xl` |
66
+ | `borderless` | flag | No border |
67
+ | `shadow` | string | `none`, `md` or `lg` |
68
+ | `rounded` | string | `none`, `xs` (2px), `sm`, `lg` or `full` |
69
+ | `text-align` | string | `center` or `right` |
70
+ | `padding` | string | `compact` or `spacious` |
71
+ | `class` | string | Extra CSS classes added to the card element |
72
+ | `id` | string | HTML `id` attribute on the card element |
73
+
74
+ Effects can also be added straight onto a card as flags - see [Effect injection](#effect-injection).
75
+
76
+ ### Card layouts
77
+
78
+ `layout="…"` picks one of 33 ready-made designs. The page editor's Card builder exposes all of them. Extra
79
+ attributes used by particular layouts are shown in brackets.
80
+
81
+ `basic`, `header-body`, `header-body-footer`, `no-header-footer`, `icon-top`, `icon-inline`, `image-top`,
82
+ `image-overlay`, `thumb-left`, `thumb-right`, `wide-left-image`, `full-bg`, `split-half`, `avatar-profile`,
83
+ `stat-metric` (`delta`, `progress`), `quote-testimonial`, `callout` (`callout-type`), `video-media`,
84
+ `location-map` (`address`), `step-numbered` (`step` - the number on its badge, default 1), `corner-badge` and `badge-band` (`badge`), `glass-gradient-border`,
85
+ `rating-review` (`rating` 0-5), `tag-cloud` (`tags`), `timeline-entry`, `code-snippet` (`lang`), `before-after`,
86
+ `pricing` (`price`, `period`, `href` for its button, `footer` for its label, `[feature]` lines), `feature-comparison`, `activity-feed`, `progress-goal` (`progress`),
87
+ `file-document` (`filename`, `filesize`, `filetype`).
88
+
89
+ A card with no `layout` (or an unknown one) renders the classic header/body/footer card shown below.
90
+
91
+ ```
92
+ [card title="Why us" layout="icon-top" icon="star" variant="glass"]
93
+ Fast, file-based and friendly.
94
+ [/card]
95
+ ```
96
+
97
+ ### Collapsible card
98
+
99
+ ```
100
+ [card title="Frequently Asked Questions" collapsible="true"]
101
+ Content that can be shown or hidden.
102
+ [/card]
103
+ ```
104
+
105
+ ### Card with icon
106
+
107
+ ```
108
+ [card title="Tips & Tricks" icon="lightbulb" icon-layout="inline"]
109
+ Here are some helpful tips.
110
+ [/card]
111
+ ```
112
+
113
+ ### Rich header and footer sub-tags
114
+
115
+ Use `[header]` and `[footer]` sub-tags when you need Markdown-rendered content (bold, links, icons) in the card header
116
+ or footer regions. Sub-tags take precedence over the `title`/`subtitle`/`icon` and `footer` attributes.
117
+
118
+ ```
119
+ [card]
120
+ [header]
121
+ ### Custom Title with **bold** and [a link](/about)
122
+ [/header]
123
+
124
+ Body content here - works exactly as before.
125
+
126
+ [footer]
127
+ [Read more](/about) | [icon name="arrow-right" /]
128
+ [/footer]
129
+ [/card]
130
+ ```
131
+
132
+ Both sub-tags are optional and independent:
133
+
134
+ ```
135
+ [card title="With attribute title"]
136
+ [footer]
137
+ **Note:** This footer uses Markdown, the header still comes from the `title` attribute.
138
+ [/footer]
139
+ [/card]
140
+ ```
141
+
142
+ ```
143
+ [card]
144
+ [header]
145
+ ## Rich Header - no footer on this card
146
+ [/header]
147
+
148
+ Just a body, no footer.
149
+ [/card]
150
+ ```
151
+
152
+ When a sub-tag is present alongside its equivalent attribute (e.g. both `[header]` and `title="..."`), the sub-tag
153
+ always wins and the attribute is ignored.
154
+
155
+ ### Rendered output
156
+
157
+ The shortcode outputs:
158
+
159
+ ```html
160
+ <div class="card mb-4">
161
+ <div class="card-header">
162
+ <div class="card-title">Getting Started</div>
163
+ </div>
164
+ <div class="card-body"><p>Follow these steps…</p></div>
165
+ </div>
166
+ ```
167
+
168
+ ---
169
+
170
+ ## `[box]`
171
+
172
+ A card with no header - a plain container around any content. Without attributes it has **no shadow** and a
173
+ **2px corner radius**.
174
+
175
+ ```
176
+ [box]
177
+ Anything here: text, a [button href="/contact" label="Get in touch" /], a [grid] or a [card].
178
+ [/box]
179
+ ```
180
+
181
+ `shadow` and `rounded` on the tag replace those defaults; every other body-card attribute works as on `[card]`.
182
+
183
+ | Attribute | Type | Description |
184
+ |---|---|---|
185
+ | `shadow` | string | `"none"` (default), `"md"` or `"lg"` |
186
+ | `rounded` | string | `"xs"` (2px, default), `"none"`, `"sm"`, `"lg"` or `"full"` |
187
+ | `padding` | string | `"compact"` or `"spacious"` |
188
+ | `borderless` | flag | No border |
189
+ | `hover` | string | `"off"` stops the box lifting when the pointer is over it (boxes and cards lift by default). Also `false`, `none`, `no`, `0` |
190
+ | `color` (or `colour`) | string | A theme colour - `primary`, `secondary`, `success`, `info`, `warning`, `danger` - or a hex colour (`#0f766e`, `#fa0`). Replaces `variant` |
191
+ | `variant` | string | As on `[card]` (`clean`, `glass`, `accent`, `dark`, `glow`, `gradient`, `primary`) |
192
+ | `footer` | string | Text for a footer strip |
193
+ | `class` / `id` | string | On the box element (it also carries `card` and `dm-box`) |
194
+
195
+ **Colours follow the theme.** A plain box takes its background, border and text from the theme's card tokens, so
196
+ it matches every theme, light or dark, and changes with the theme switcher. `color="success"` (and the other theme
197
+ colour names) tints the box with that theme colour over the card background - a pale tint on a light theme, a deep
198
+ one on a dark theme - with the theme's text colour on top; links in it take the tone, pulled towards the text
199
+ colour so they stay readable.
200
+
201
+ ```
202
+ [box color="warning"]
203
+ **Heads up:** the office is closed on Friday.
204
+ [/box]
205
+
206
+ [box color="#0f766e" rounded="lg"]
207
+ Our own brand teal, whatever the theme.
208
+ [/box]
209
+ ```
210
+
211
+ A hex colour is yours and is kept on every theme: the box gets black or white text, whichever reads better on it,
212
+ and headings, links, code and the footer follow that text colour. A mid-grey such as `#777` cannot reach 4.5:1
213
+ with either, so pick a lighter or darker shade for body text. Anything else in `color` (a colour name like `red`,
214
+ `rgb(…)`) is ignored and the box is drawn plain. `variant="dark"` stays a dark panel on every theme, with light
215
+ text, links, code and footer on it.
216
+
217
+ A header is never drawn: `title`, `subtitle`, `icon`, `image` and `layout` are ignored. Effects that go on a
218
+ `[card]` (`reveal`, `pulse`, …) go on a `[box]` too. A `[card]` can sit inside a `[box]` and a `[box]` inside a
219
+ `[card]`; a `[box]` cannot hold another `[box]`. `rounded="xs"` is also available on `[card]`.
220
+
221
+ ---
222
+
223
+ ## `[grid]` / `[row]` with `[col]`
224
+
225
+ Lay out content in multi-column grids using Domma's native CSS Grid system.
226
+
227
+ ### Grid (explicit column count)
228
+
229
+ ```
230
+ [grid cols="2" gap="3"]
231
+ [col]
232
+ Left column content with **Markdown**.
233
+ [/col]
234
+ [col]
235
+ Right column content.
236
+ [/col]
237
+ [/grid]
238
+ ```
239
+
240
+ ### Grid with a spanning column
241
+
242
+ ```
243
+ [grid cols="3" gap="2"]
244
+ [col span="2"]Wide column (spans 2 of 3)[/col]
245
+ [col]Narrow column[/col]
246
+ [/grid]
247
+ ```
248
+
249
+ ### Row (auto-layout)
250
+
251
+ ```
252
+ [row gap="2"]
253
+ [col]Column A[/col]
254
+ [col]Column B[/col]
255
+ [col]Column C[/col]
256
+ [/row]
257
+ ```
258
+
259
+ ### `[grid]` attributes
260
+
261
+ | Attribute | Type | Description |
262
+ |---|---|---|
263
+ | `cols` | number | Number of equal columns. By default the grid stacks on small screens (one column on mobile, then more at the `md`/`lg` breakpoints) |
264
+ | `responsive` | `"false"` | Turn off mobile stacking - always use exactly `cols` columns (`grid-cols-N`) |
265
+ | `gap` | number | Gap size token (`gap-N`) |
266
+ | `class` | string | Extra CSS classes on the grid wrapper |
267
+ | `fullwidth` | `"true"` | Adds `grid-breakout` to break out of the content column |
268
+ | `id` | string | HTML `id` attribute |
269
+
270
+ ### `[row]` attributes
271
+
272
+ | Attribute | Type | Description |
273
+ |--------------------|--------|-----------------------------------------------------------------------------------------------|
274
+ | `gap` | number | Gap size token (`gap-N`) |
275
+ | `class` | string | Extra CSS classes on the row wrapper |
276
+ | `id` | string | HTML `id` attribute |
277
+ | `reveal` | flag | Enables scroll-triggered reveal animations on child columns |
278
+ | `reveal-mode` | string | `"stagger"` (default) or `"sequence"` |
279
+ | `reveal-animation` | string | Animation type: `slide-up`, `slide-down`, `slide-left`, `slide-right`, `fade`, `zoom`, `flip` |
280
+ | `reveal-duration` | number | Animation duration in ms (default: `400`) |
281
+ | `reveal-stagger` | number | Delay between each child in ms (default: `60`) |
282
+ | `reveal-delay` | number | Initial delay before first animation in ms (default: `0`) |
283
+ | `reveal-direction` | string | `"ltr"` (default) or `"rtl"` |
284
+
285
+ ### Row with staggered reveal
286
+
287
+ Columns animate into view one by one as the row scrolls into the viewport:
288
+
289
+ ```
290
+ [row gap="4" reveal reveal-mode="stagger" reveal-animation="slide-up" reveal-duration="400" reveal-stagger="60"]
291
+ [col]First to appear[/col]
292
+ [col]Second to appear[/col]
293
+ [col]Third to appear[/col]
294
+ [/row]
295
+ ```
296
+
297
+ ### Row with fade and right-to-left direction
298
+
299
+ ```
300
+ [row gap="3" reveal reveal-animation="fade" reveal-direction="rtl"]
301
+ [col]Appears third[/col]
302
+ [col]Appears second[/col]
303
+ [col]Appears first[/col]
304
+ [/row]
305
+ ```
306
+
307
+ ### Row with zoom and delay
308
+
309
+ ```
310
+ [row gap="4" reveal reveal-animation="zoom" reveal-delay="200" reveal-stagger="100"]
311
+ [col]Zooms in after 200ms[/col]
312
+ [col]Zooms in after 300ms[/col]
313
+ [/row]
314
+ ```
315
+
316
+ ### `[col]` attributes
317
+
318
+ | Attribute | Type | Description |
319
+ |---|---|---|
320
+ | `span` | number | Number of columns to span (`col-span-N`) |
321
+ | `id` | string | HTML `id` attribute |
322
+
323
+ `[col]` also accepts injected effects (`reveal`, `breathe`, `animate`, `ripple`) - see [Effect injection](#effect-injection).
324
+
325
+ ### Rendered output
326
+
327
+ ```html
328
+ <div class="grid grid-cols-2 gap-3">
329
+ <div>Left column content…</div>
330
+ <div>Right column content.</div>
331
+ </div>
332
+ ```
333
+
334
+ Note: `[col]` inside `[grid]` renders as a plain `<div>` (no `.col` class). The `.col` compatibility layer is not used.
335
+
336
+ ---
337
+
338
+ ## `[slideover]`
339
+
340
+ Renders a trigger button that opens a Domma slideover panel when clicked.
341
+
342
+ ### Basic usage
343
+
344
+ ```
345
+ [slideover title="More Information" trigger="Read more"]
346
+ This content appears in a side panel.
347
+
348
+ You can use **Markdown**, [cards](#), and other shortcodes here.
349
+ [/slideover]
350
+ ```
351
+
352
+ ### All attributes
353
+
354
+ | Attribute | Default | Description |
355
+ |---|---|---|
356
+ | `title` | `""` | Title shown in the slideover header |
357
+ | `trigger` | `"Open"` | Label for the trigger button |
358
+ | `size` | `"md"` | Panel width: `"sm"`, `"md"`, `"lg"` |
359
+ | `position` | `"right"` | Which side the panel slides in from: `"right"` or `"left"` |
360
+
361
+ ### Rendered output
362
+
363
+ ```html
364
+ <button class="btn btn-link dm-so-trigger" data-so-target="dm-so-1">Read more</button>
365
+ <div class="dm-so-content" id="dm-so-1" style="display:none"
366
+ data-so-title="More Information"
367
+ data-so-size="md"
368
+ data-so-position="right">
369
+ <!-- Markdown body rendered here -->
370
+ </div>
371
+ ```
372
+
373
+ The public `site.js` wires `.dm-so-trigger` click events to open the corresponding slideover via `E.slideover()`.
374
+
375
+ ### Nesting shortcodes inside slideover
376
+
377
+ `[card]`, `[grid]`, and other shortcodes are fully supported inside a slideover body:
378
+
379
+ ```
380
+ [slideover title="Resources" trigger="View resources"]
381
+ [grid cols="2"]
382
+ [col]
383
+ [card title="Guide"]Quick start guide.[/card]
384
+ [/col]
385
+ [col]
386
+ [card title="API"]API reference.[/card]
387
+ [/col]
388
+ [/grid]
389
+ [/slideover]
390
+ ```
391
+
392
+ ---
393
+
394
+ ## `[dconfig]`
395
+
396
+ Embeds declarative page-specific behaviour configuration using Domma's `$.setup()` system. No JavaScript is required.
397
+
398
+ ### Basic usage
399
+
400
+ ```
401
+ [dconfig]
402
+ {
403
+ "#my-button": {
404
+ "events": {
405
+ "click": {
406
+ "target": "#my-panel",
407
+ "toggleClass": "hidden"
408
+ }
409
+ }
410
+ }
411
+ }
412
+ [/dconfig]
413
+ ```
414
+
415
+ This wires a click on `#my-button` to toggle the `hidden` class on `#my-panel`, entirely through configuration.
416
+
417
+ ### How it works
418
+
419
+ 1. The server validates the JSON and base64-encodes it into a hidden `<div>`:
420
+
421
+ ```html
422
+ <div class="dm-page-config" style="display:none" data-config="eyIjbXktYnV..."></div>
423
+ ```
424
+
425
+ 2. `site.js` decodes and merges all `dm-page-config` blocks on the page (inline blocks merge with any `dconfig` set in the page editor's frontmatter - inline wins on key conflicts).
426
+
427
+ 3. `$.setup(merged)` is called, which wires all event handlers declaratively.
428
+
429
+ ### Multiple blocks
430
+
431
+ You can use multiple `[dconfig]` blocks on one page. All valid blocks are merged:
432
+
433
+ ```
434
+ [dconfig]
435
+ { "#btn-a": { "events": { "click": { "target": "#section-a", "toggleClass": "active" } } } }
436
+ [/dconfig]
437
+
438
+ [dconfig]
439
+ { "#btn-b": { "events": { "click": { "target": "#section-b", "toggleClass": "active" } } } }
440
+ [/dconfig]
441
+ ```
442
+
443
+ ### Notes
444
+
445
+ - Invalid JSON is silently dropped (no output, no error)
446
+ - The `[dconfig]` shortcode is processed first in the pipeline, before all other shortcodes
447
+ - `$.setup()` config can also be set per-page in the page editor's **DConfig** card (stored as `dconfig` in frontmatter)
448
+ - Window-level `__CMS_DCONFIG__` (from frontmatter) and inline `dm-page-config` divs are merged at page load, with inline blocks taking precedence
449
+
450
+ ---
451
+
452
+ ## Additional Shortcodes
453
+
454
+ The following shortcodes are also available in page Markdown:
455
+
456
+ ### `[tabs]` / `[tab]`
457
+
458
+ ```
459
+ [tabs style="pills"]
460
+ [tab title="First Tab"]First tab content.[/tab]
461
+ [tab title="Second Tab"]Second tab content.[/tab]
462
+ [/tabs]
463
+ ```
464
+
465
+ Attributes on `[tabs]`: `style` (`"pills"`, `"bordered"`, or default underline), `align` (`"center"` or `"right"`),
466
+ `fade` (flag - fade between panes), `id`.
467
+ Attributes on `[tab]`: `title`, `icon` (Domma icon name shown before the title).
468
+
469
+ ---
470
+
471
+ ### `[accordion]` / `[item]`
472
+
473
+ ```
474
+ [accordion]
475
+ [item title="Question 1"]Answer to question 1.[/item]
476
+ [item title="Question 2"]Answer to question 2.[/item]
477
+ [/accordion]
478
+ ```
479
+
480
+ Attributes on `[accordion]`: `multiple="true"` (allow multiple items open simultaneously), `id`.
481
+ Attributes on `[item]`: `title`.
482
+
483
+ ---
484
+
485
+ ### `[carousel]` / `[slide]`
486
+
487
+ ```
488
+ [carousel autoplay="true" interval="5000" animation="fade"]
489
+ [slide title="Slide 1" image="/media/photo.jpg"]Caption text.[/slide]
490
+ [slide title="Slide 2"]Text-only slide.[/slide]
491
+ [/carousel]
492
+ ```
493
+
494
+ `[carousel]` attributes: `autoplay`, `interval` (ms), `loop`, `animation` (`"slide"`, `"fade"`, or `"crossfade"`), `animation-duration` (ms, default 500), `animation-easing` (any CSS timing function - `ease`, `linear`, `ease-in-out`, `cubic-bezier(...)`), `height-mode` (`crop` or `average`), `height` (optional fixed height in pixels), `arrow-position` (`top`, `middle` or `bottom`; default `middle`), `background`, `arrow-background`, `arrow-color`, `id`. The colour attributes accept CSS colours or theme tokens such as `primary`, `accent`, `text` and `surface`; `background` colours the carousel area, while the two arrow attributes set the navigation button background and icon colour.
495
+
496
+ Image carousels use one height across all slides and align images to the top with crop-to-fill scaling. The default `height-mode="crop"` uses the shortest image's height after scaling every image to the carousel width. `height-mode="average"` uses `(shortest scaled image height + tallest scaled image height) / 2`. Both recalculate when the carousel width changes. `height="420"` sets a fixed height instead. Text-only carousels keep their natural content height.
497
+ `[slide]` attributes: `title`, `image` (URL). In the page editor, the image button beside each slide (and Browse in the right-click menu) picks a picture from Media or uploads one.
498
+
499
+ `fade` swaps opacity per slide; `crossfade` overlaps the outgoing and incoming slides for a smoother dissolve.
500
+
501
+ ---
502
+
503
+ ### `[hero]`
504
+
505
+ ```
506
+ [hero title="Welcome" tagline="Build something great" size="lg" variant="gradient-blue"]
507
+ Optional body content with **Markdown** support.
508
+ [/hero]
509
+ ```
510
+
511
+ Attributes: `title`, `tagline`, `size` (`sm`, `lg`, `xl`, `full`), `variant` (see below), `image`, `overlay` (`light`, `dark`,
512
+ `darker`, `gradient`, `gradient-reverse`), `align` (`center`, `left`; content alignment),
513
+ `bg-x` (`left`, `center`, `right`) and `bg-y` (`top`, `center`, `bottom`) position the background image; both default to `center`.
514
+ `bg-fit` (`contain` to show the whole image, `cover` to fill the area with cropping). Extra large (`xl`) is at least 75vh. All sizes fill the background area by default; Show whole image is an explicit option. `fullwidth="true"` independently makes the hero span the viewport. `bg` (background colour CSS value),
515
+ `color` (background colour; takes precedence over `bg`), `min-height` (a number with `px`, `em`, `rem`, `vh`, `vw`
516
+ or `%`, e.g. `"60vh"`), `fullwidth="true"` (break out of page container to span full viewport width),
517
+ `twinkle` (flag - adds particle overlay), `twinkle-count` (number of particles),
518
+ `twinkle-colour` (particle colour CSS value), `blobs` (flag - adds ambient blob background),
519
+ `blobs-type` (blob animation type; default: `float-blobs`), `class`, `id`.
520
+
521
+ `[hero]` also accepts injected effects (`reveal`, `breathe`, `pulse`, `twinkle`, `ticker-tape`, `butterflies`,
522
+ `strobe`, `ambient`, `animate`) - see [Effect injection](#effect-injection).
523
+
524
+ **Gradient variants:**
525
+
526
+ | Variant | Description |
527
+ |--------------------------------------------------------|------------------------|
528
+ | `gradient-purple` | Pastel lavender wash |
529
+ | `gradient-blue` | Pastel sky blue |
530
+ | `gradient-green` | Pastel mint green |
531
+ | `gradient-sunset` | Pastel amber/gold |
532
+ | `gradient-ocean` | Pastel sky/teal |
533
+ | `gradient-rose` | Pastel rose pink |
534
+ | `gradient-forest` | Pastel fresh green |
535
+ | `gradient-night` | Muted slate dark |
536
+ | `gradient-ocean-light` / `gradient-ocean-dark` | Ocean theme pastel |
537
+ | `gradient-forest-light` / `gradient-forest-dark` | Forest theme pastel |
538
+ | `gradient-sunset-light` / `gradient-sunset-dark` | Sunset theme pastel |
539
+ | `gradient-royal-light` / `gradient-royal-dark` | Royal theme pastel |
540
+ | `gradient-lemon-light` / `gradient-lemon-dark` | Lemon theme pastel |
541
+ | `gradient-silver-light` / `gradient-silver-dark` | Silver theme pastel |
542
+ | `gradient-charcoal-light` / `gradient-charcoal-dark` | Charcoal theme pastel |
543
+ | `gradient-christmas-light` / `gradient-christmas-dark` | Christmas theme pastel |
544
+ | `gradient-unicorn-light` / `gradient-unicorn-dark` | Unicorn theme pastel |
545
+ | `gradient-dreamy-light` / `gradient-dreamy-dark` | Dreamy theme pastel |
546
+ | `gradient-grayve-light` / `gradient-grayve-dark` | Grayve theme pastel |
547
+ | `gradient-mint-light` / `gradient-mint-dark` | Mint theme pastel |
548
+ | `gradient-wedding-light` / `gradient-wedding-dark` | Wedding theme pastel |
549
+
550
+ ---
551
+
552
+ ### `[table]`
553
+
554
+ Wraps a GFM Markdown table with Domma table classes and a responsive container:
555
+
556
+ ```
557
+ [table striped="true" caption="Sales Data"]
558
+ | Product | Price |
559
+ | ------- | ----: |
560
+ | Widget | $9.99 |
561
+ [/table]
562
+ ```
563
+
564
+ Attributes: `striped="true"`, `bordered="true"`, `compact="true"`, `caption`, `class`, `id`.
565
+
566
+ ---
567
+
568
+ ### `[badge]`
569
+
570
+ Inline badge - renders a styled `<span>`:
571
+
572
+ ```
573
+ [badge variant="success"]New[/badge]
574
+ [badge variant="danger" outline pill]Deprecated[/badge]
575
+ [badge variant="warning" size="small"]Beta[/badge]
576
+ ```
577
+
578
+ Self-closing (empty badge, useful as a colour dot):
579
+
580
+ ```
581
+ [badge variant="primary" pill /]
582
+ ```
583
+
584
+ Attributes: `variant` (`primary`, `secondary`, `success`, `danger`, `warning`, `info`, `light`, `dark`), `pill` (flag), `outline` (flag), `size` (`"small"` or `"large"`).
585
+
586
+ ---
587
+
588
+ ### `[countdown]`
589
+
590
+ Self-closing. Renders a countdown timer initialised client-side by `E.timer()`:
591
+
592
+ ```
593
+ [countdown to="2026-12-31" format="DD:HH:mm:ss" /]
594
+ [countdown duration="300" format="mm:ss" /]
595
+ ```
596
+
597
+ Attributes: `to` (ISO date - counts down to it), `duration` (seconds), `format` (`mm:ss`, `HH:mm:ss`, `DD:HH:mm:ss`),
598
+ `id`. Without a `format`, a countdown of an hour or more shows `HH:mm:ss`.
599
+
600
+ ---
601
+
602
+ ### `[timeline]` / `[event]`
603
+
604
+ ```
605
+ [timeline layout="vertical" theme="modern"]
606
+ [event title="Launch" date="2025-01-15" status="completed" icon="rocket"]
607
+ Project launched successfully.
608
+ [/event]
609
+ [event title="V2 Release" date="2026-06-01" status="planned"]
610
+ Next major release.
611
+ [/event]
612
+ [/timeline]
613
+ ```
614
+
615
+ `[timeline]` attributes: `layout` (`vertical`, `centred`, `horizontal`), `theme` (`minimal`, `corporate`, `modern`), `mode` (`timeline`, `roadmap`), `class`, `id`.
616
+ `[event]` attributes: `title`, `date`, `status` (`planned`, `in-progress`, `completed`, `blocked`), `icon` (kept on the
617
+ item as `data-event-icon`; the timeline's markers show the status icon).
618
+
619
+ ---
620
+
621
+ ### `[listgroup]` / `[item]`
622
+
623
+ Renders a Domma list group. Each `[item]` body takes inline Markdown.
624
+
625
+ ```
626
+ [listgroup variant="flush" size="sm" itemvariant="info" action="true"]
627
+ [item]**First** item[/item]
628
+ [item]Second item with a [link](/about)[/item]
629
+ [/listgroup]
630
+ ```
631
+
632
+ `[listgroup]` attributes: `variant` (`flush`, `numbered` or `horizontal`), `size` (`sm` or `lg`), `itemvariant`
633
+ (`primary`, `secondary`, `success`, `danger`, `warning` or `info` - colours every item), `action="true"` (hover/click
634
+ styling on items). `[item]` takes no attributes. The same styling is available for collections with
635
+ `[collection display="listgroup"]`.
636
+
637
+ ---
638
+
639
+ ### `[banner]`
640
+
641
+ An alert-style message strip with an optional icon, title and dismiss button. The body is Markdown.
642
+
643
+ ```
644
+ [banner type="warning" title="Maintenance" icon="alert-triangle" dismissible]
645
+ The site will be read-only on **Sunday** from 8am.
646
+ [/banner]
647
+ ```
648
+
649
+ | Attribute | Description |
650
+ |---|---|
651
+ | `type` | `info` (default), `success`, `warning`, `danger` or `neutral` |
652
+ | `title` | Bold title shown above the body |
653
+ | `icon` | Domma icon name shown at the left |
654
+ | `dismissible` | Flag - adds a close button |
655
+ | `class` | Extra CSS classes on the banner |
656
+
657
+ `[banner]` also accepts injected effects (`reveal`, `pulse`, `shake`, `ambient`, `animate`) - see
658
+ [Effect injection](#effect-injection). It is processed late (after `[card]` and `[grid]`), so it cannot be nested inside
659
+ a card, grid, tab or similar container body - place it at the top level of the page, or inside `[center]` or
660
+ `[slideover]`.
661
+
662
+ ---
663
+
664
+ ### `[text]`
665
+
666
+ Wraps inline text in a `<span>` with custom styles. All attributes are optional.
667
+
668
+ ```
669
+ [text size="xl" bold italic color="primary"]Styled text[/text]
670
+ [text font="Georgia" decoration="underline" spacing="wide"]Classic[/text]
671
+ [text size="2em" color="#ff6600"]Custom size and colour[/text]
672
+ ```
673
+
674
+ ### Attributes
675
+
676
+ | Attribute | Values | Notes |
677
+ |--------------|---------------------------------------------------------------------------------------------|---------------------------------------------------------------------|
678
+ | `size` | `xs` `sm` `base` `lg` `xl` `2xl` `3xl` `4xl` or raw CSS (e.g. `"1.5em"`) | Maps to `font-size` |
679
+ | `point-size` | A positive number, e.g. `"14"` | Sets `font-size` in points (`14pt`) |
680
+ | `weight` | `thin` `light` `normal` `medium` `semibold` `bold` `extrabold` `black` | Maps to `font-weight` |
681
+ | `bold` | flag | Shorthand for `font-weight:700`. Overrides `weight` if both present |
682
+ | `italic` | flag | `font-style:italic` |
683
+ | `color` | Token (`primary` `secondary` `muted` `danger` `success` `warning` `info`) or any CSS colour | Maps to `color` (`colour` also works) |
684
+ | `font` | `Georgia` `Arial` `Verdana` `Courier New` `Times New Roman` `Trebuchet MS` | Web-safe fonts only |
685
+ | `transform` | `upper` `lower` `capitalize` `none` | Maps to `text-transform` |
686
+ | `decoration` | `underline` `line-through` `none` | Maps to `text-decoration` |
687
+ | `spacing` | `tight` `normal` `wide` `wider` or raw CSS (e.g. `"0.1em"`) | Maps to `letter-spacing` |
688
+ | `align` | Any `text-align` value (`left` `center` `right` `justify`) | Makes the span `display:block` and sets `text-align` |
689
+ | `style` | Raw CSS declarations | Appended to the generated inline style |
690
+ | `class` | CSS class name(s) | Added to the `<span>` element |
691
+ | `id` | Element ID | Added to the `<span>` element |
692
+
693
+ ### Notes
694
+
695
+ - `align` turns the span into a block so the alignment takes effect. For centring larger blocks of content, `[center]`
696
+ or a grid/card is usually a better fit.
697
+ - The Page Editor's Insert menu has a **Text** entry that opens the shortcode form for it - every attribute in this
698
+ table, with a live preview of the tag it will insert. The same form is reachable by right-clicking an existing
699
+ `[text]` tag and choosing **Edit Text…**.
700
+
701
+ ---
702
+
703
+ ### `[spacer]`
704
+
705
+ Self-closing. Inserts a vertical gap:
706
+
707
+ ```
708
+ [spacer /]
709
+ [spacer size="48" /]
710
+ [spacer size="24" class="mt-4" /]
711
+ ```
712
+
713
+ Attributes: `size` (px height; defaults to `layoutOptions.spacerSize` from `config/site.json`), `class`.
714
+
715
+ ---
716
+
717
+ ### `[contents]`
718
+
719
+ Self-closing. This page's own settings for its **page contents** - the list a layout with a sidebar
720
+ (With Sidebar) builds from the page's headings - over the layout's (**Layouts → Page contents
721
+ settings**). It draws nothing itself; the first `[contents]` on a page counts.
722
+
723
+ ```
724
+ [contents numbered="true" headings="h2h3" title="On this page" /]
725
+ [contents side="right" collapsible="true" collapsed="true" /]
726
+ [contents floating="true" float-anchor="BR" movable="true" /]
727
+ [contents hidden="true" /]
728
+ ```
729
+
730
+ Attributes: `side` (left / right), `width` (160-480 px), `sticky`, `floating`, `float-anchor` (TL, TR,
731
+ BL, BR, L, R), `movable`, `hidden`, `collapsible`, `collapsed`, `autohide`, `scroll-spy`, `headings`
732
+ (auto - H2s, else H3s - / h2 / h3 / h2h3), `numbered`, `title`, `exclude` (headings to leave out, comma
733
+ separated), `hover`, `selected` (the menus' highlight effects), `accent`, `font`, `font-size`,
734
+ `font-weight`. `true` / `false` switch a setting on or off. Floating wins over autohide, and both over
735
+ collapse. The older front matter `sidebarSide` still works (the tag wins over it).
736
+
737
+ ---
738
+
739
+ ### `[navbar]`
740
+
741
+ Self-closing. This page's own navbar settings, over the navbar menu's - for this page only. It draws
742
+ nothing itself.
743
+
744
+ ```
745
+ [navbar collapsed="true" /]
746
+ [navbar autohide="true" width="200" /]
747
+ [navbar orientation="horizontal" position="sticky" hide-on-scroll="true" /]
748
+ [navbar position="floating" float-anchor="TR" float-collapsible="true" /]
749
+ ```
750
+
751
+ Every Behaviour setting of a menu is an attribute: `position`, `orientation`, `side`, `align`,
752
+ `variant`, `hover-open`, `hover`, `selected`, `accent`, `class`, `font`, `font-size`, `font-weight`,
753
+ `letter-spacing`, `icon-size`, `width`, `collapsible`, `collapsed`, `autohide`, `hide-on-scroll`, and the
754
+ floating panel's `float-anchor`, `float-x`, `float-y`, `float-width`, `float-radius`, `float-shadow`,
755
+ `float-accent`, `float-opacity`, `float-title`, `float-collapsible`, `float-collapsed`, `float-movable`,
756
+ `float-resizable`. `true` / `false` switch a setting on or off (so a page can turn off what the menu
757
+ turns on); an empty value clears it back to the default. The first `[navbar]` on a page counts. The
758
+ result is checked like a saved menu - an override that would not validate is ignored whole.
759
+
760
+ In the page editor it is under Insert → Layout; right-clicking it offers one-click actions (collapse,
761
+ autohide or keep the rail open on this page, hide the bar on scroll, remove the override) and the most
762
+ used settings, with the rest in **Edit…**.
763
+
764
+ ---
765
+
766
+ ### `[theme-switcher]`
767
+
768
+ Self-closing. The site's theme dots, placed by the page:
769
+
770
+ ```
771
+ [theme-switcher /]
772
+ [theme-switcher position="bottom-left" offset-x="24" offset-y="24" /]
773
+ [theme-switcher themes="charcoal-dark,charcoal-light" /]
774
+ ```
775
+
776
+ Attributes:
777
+
778
+ - `position` - `inline` (the default: where the tag stands, inside the text if you like) or pinned to
779
+ the window: `top-left`, `top-right`, `left-center`, `right-center`, `bottom-left`, `bottom-right`
780
+ - `offset-x`, `offset-y` - distance from the window's edges in px when pinned (0-400, default 32)
781
+ - `themes` - a comma list narrowing the themes chosen under **Theme > Theme switcher**; the site's
782
+ list when absent or when none of the names match
783
+
784
+ It works with the floating switcher off. On its page it takes the floating switcher's place, so a
785
+ page never shows two; a switcher in a menu (`{"type": "theme-switcher"}`) is kept.
786
+
787
+ ---
788
+
789
+ ### `[center]`
790
+
791
+ ```
792
+ [center]This content is centred.[/center]
793
+ [center class="my-section"]More centred content.[/center]
794
+ ```
795
+
796
+ Attributes: `class`.
797
+
798
+ ---
799
+
800
+ ### `[icon]`
801
+
802
+ Self-closing. Renders a Domma icon inline:
803
+
804
+ ```
805
+ [icon name="arrow-right" /]
806
+ [icon name="star" size="24" color="#f59e0b" /]
807
+ ```
808
+
809
+ Attributes: `name` (required - Domma icon name), `size` (px), `color` (CSS colour), `class`.
810
+
811
+ ---
812
+
813
+ ### `[embed]`
814
+
815
+ Self-closing. Embeds a video or an external page. The `url` is auto-detected and rendered as the right element:
816
+
817
+ - **YouTube / Vimeo** links → a privacy-friendly responsive `<iframe>` (`youtube-nocookie.com` / `player.vimeo.com`).
818
+ - **Self-hosted video** (`.mp4`, `.webm`, `.ogg` - e.g. an uploaded `/media/…` file) → a native `<video>` player with controls.
819
+ - **Any other URL** → a generic `<iframe>`, but **only if its host is in the allow-list** at `config/embeds.json`; otherwise a visible "not allowed" placeholder is shown.
820
+
821
+ ```
822
+ [embed url="https://youtu.be/dQw4w9WgXcQ" /]
823
+ [embed url="/media/showreel.mp4" ratio="16:9" title="Our showreel" /]
824
+ [embed url="https://www.google.com/maps/embed?pb=…" width="640" align="center" /]
825
+ [embed url="/media/hero.mp4" autoplay muted loop /]
826
+ ```
827
+
828
+ | Attribute | Description |
829
+ |------------|-----------------------------------------------------------------------------------|
830
+ | `url` | YouTube/Vimeo link, self-hosted video path, or generic iframe URL (**required**) |
831
+ | `ratio` | Aspect ratio - `16:9` (default), `4:3`, `1:1`, `21:9`, `9:16`, `3:4`, or `W:H` |
832
+ | `title` | Accessibility title on the frame **and** a visible caption underneath |
833
+ | `width` | Max width in px or % (e.g. `640`, `80%`) |
834
+ | `align` | `left`, `center`, or `right` (when `width` is set) |
835
+ | `autoplay` | Flag - starts playback automatically (forces `muted`, per browser policy) |
836
+ | `loop` | Flag - loops playback |
837
+ | `muted` | Flag - starts muted |
838
+
839
+ **Security:** generic iframes are gated by a host allow-list. Add trusted hosts to `config/embeds.json`:
840
+
841
+ ```json
842
+ { "allowedHosts": ["www.google.com", "codepen.io", "open.spotify.com"] }
843
+ ```
844
+
845
+ The allow-list is enforced twice - at render time (the placeholder) and again in the HTML sanitiser
846
+ (`allowedIframeHostnames`) - so a host that is not listed can never be framed. YouTube and Vimeo are always allowed.
847
+
848
+ The editor's **Insert → Embed** menu opens a builder with a URL field, a "browse self-hosted video" picker, and live
849
+ shortcode preview.
850
+
851
+ ---
852
+
853
+ ### `[form]`
854
+
855
+ Self-closing. Embeds a CMS-managed form by slug. Forms are configured under **Forms** in the sidebar.
856
+
857
+ ```
858
+ [form name="contact" /]
859
+ [form name="newsletter" class="mt-4" id="newsletter-form" /]
860
+ ```
861
+
862
+ | Attribute | Description |
863
+ |-----------------|--------------------------------------|
864
+ | `name` / `slug` | Form slug (required) |
865
+ | `class` | Extra CSS classes on the wrapper div |
866
+ | `id` | `id` attribute on the wrapper div |
867
+
868
+ The form's own CSS classes (set in the form builder: Settings, Submit Button and each field) go on the `<form>`, the submit button and each field's wrapper; `class` here is for this placement's wrapper only.
869
+
870
+ ---
871
+
872
+ ### `[collection]`
873
+
874
+ Self-closing. Renders a CMS collection inline on a public page. Supports table, cards, list, accordion, timeline, and
875
+ block display modes, with structured filtering and per-user scoping.
876
+
877
+ ```
878
+ [collection slug="enquiries" display="table" /]
879
+ [collection slug="team" display="cards" columns="3" title-field="name" /]
880
+ [collection slug="products" display="list" limit="10" sort="price" order="asc" /]
881
+ [collection slug="jobs" where_location="London" where_salary_gte="50000" /]
882
+ [collection slug="applications" scope="mine" display="cards" /]
883
+ ```
884
+
885
+ | Attribute | Description |
886
+ |------------------|--------------------------------------------------------------------------------------------------------------|
887
+ | `slug` | Required. The collection slug |
888
+ | `display` | `table` (default), `cards`, `list`, `accordion`, `timeline`, `carousel`, `listgroup`, or `block` |
889
+ | `fields` | Comma-separated field keys to show. Defaults to all |
890
+ | `limit` | Maximum number of entries to display (`0` or unset = no limit) |
891
+ | `sort` | Field key to sort by (default `createdAt`) |
892
+ | `order` | `asc` or `desc` (default `desc`) |
893
+ | `title-field` | Field used as the card/list title (`cards` + `list` modes) |
894
+ | `columns` | Grid columns for `cards` display (2-4, default 3) |
895
+ | `search` | `true`/`false` - enable search in table mode (default `true`) |
896
+ | `sortable` | `true`/`false` - enable column sorting in table mode (default `true`) |
897
+ | `exportable` | `true`/`false` - show CSV export button in table mode (default `false`) |
898
+ | `page-size` | Rows per page in table mode (default 25) |
899
+ | `empty` | Text shown when there are no entries |
900
+ | `where` | Legacy single-attribute filter - `"field=value,field2=value2"`. Equality only, AND-combined |
901
+ | `where_<field>` | Modern per-field filter - see **Structured filtering** below |
902
+ | `scope` | `mine` to render only entries created by the current user (per-user hydration; requires sign-in) |
903
+ | `cta` | Action slug: enables per-entry CTA buttons |
904
+ | `cta-label` | CTA button label (default: `"Run"`) |
905
+ | `cta-icon` | CTA button icon name |
906
+ | `cta-style` | CTA button variant (default: `primary`) |
907
+ | `cta-confirm` | CTA confirmation prompt text |
908
+ | `transitions` | Bare flag: lazy-loads available state-machine transitions per row and renders them as buttons (interactive Browser only; works with `scope="mine"`) |
909
+ | `class` | Extra CSS classes on the wrapper div |
910
+ | `item-class` | Your own CSS classes on every entry's wrapper (cards, list, accordion, timeline, carousel, listgroup and block; not table rows, which are drawn in the browser) |
911
+ | `id` | `id` attribute on the wrapper div |
912
+
913
+ Table display uses Domma Table - search, sort, pagination, column selector, and CSV export are all built in. Cards and
914
+ list are static.
915
+
916
+ #### Timeline, carousel, and listgroup modes
917
+
918
+ `display="timeline"` renders entries as a Domma progression. Field mapping: `title-field` (default `title`),
919
+ plus optional `date-field`, `status-field`, `icon-field`, and `body-field` (Markdown). Layout attrs: `layout`
920
+ (`vertical` | `centred` | `horizontal`), `theme` (`minimal` | `corporate` | `modern`), `mode`
921
+ (`timeline` | `roadmap`). The public site JS hydrates the server markup into the full Domma progression
922
+ component (markers, connectors, status icons).
923
+
924
+ `display="carousel"` renders one slide per entry. It also accepts `height-mode` and `height` with the same sizing rules as `[carousel]`. Field mapping: `image-field`, `title-field`, `body-field`
925
+ (Markdown). The `[carousel]` behaviour and colour attrs pass through: `autoplay`, `interval`, `loop`, `animation`,
926
+ `animation-duration`, `animation-easing`, `arrow-position`, `background`, `arrow-background`, `arrow-color`.
927
+
928
+ `display="listgroup"` renders one list-group item per entry. Field mapping: `title-field`, `body-field`
929
+ (inline Markdown). Styling attrs match `[listgroup]`: `variant` (`flush` | `numbered` | `horizontal`),
930
+ `size` (`sm` | `lg`), `itemvariant`, `action="true"`.
931
+
932
+ Both `carousel` and `listgroup` also accept `block="<name>"` - each entry is then rendered through that
933
+ reusable block template (same `{{field}}` placeholders as `display="block"`) instead of the field mapping,
934
+ so any custom per-entry design works inside a carousel slide or list row.
935
+
936
+ #### Right-click search & filter (all displays)
937
+
938
+ Every collection-rendered display on the public site is filterable in place: visitors right-click any
939
+ entry to get a context menu with quick filters on the clicked value (`=`, `≠`, and `>`/`<` for
940
+ numeric/date values), a free-text search across the displayed fields, and removable filter chips above
941
+ the display. The cursor changes to a custom filter cursor (arrow + funnel badge) over filterable
942
+ content - a custom SVG cursor because the native `context-menu` cursor is unsupported on Windows. Only the fields the
943
+ display actually shows are exposed - hidden fields never reach the page. No configuration needed;
944
+ it works for cards, list, accordion, timeline, carousel, listgroup, block, and table modes on both
945
+ `[collection]` and `[view]`.
946
+
947
+ This menu owns the display it sits on: it is registered as an exclusive context menu, so another
948
+ right-click menu placed on the page cannot take over a collection display and quietly remove filter,
949
+ sort and export from it. Right-clicking outside any collection reaches whatever other menu the page
950
+ defines, as normal.
951
+
952
+ The same menu also offers **Sort…**, **Group by…**, **Copy…** (visible entries as CSV/JSON/Markdown),
953
+ **Export…** (a download, gated by the collection's `export` permissions - see
954
+ `docs/configuration.md`), **Print**, and **Copy link to this view**. Availability of Sort, Group and
955
+ Export/copy varies by display type:
956
+
957
+ | Display | Filter | Sort | Group | Export / copy |
958
+ |-------------------------------------------|--------|------|-------|----------------|
959
+ | cards, list, listgroup, accordion, block | yes | yes | yes | yes |
960
+ | table | yes | yes | - | yes |
961
+ | timeline | yes | yes | - | yes |
962
+ | carousel | yes | yes | - | yes |
963
+
964
+ Grouping inserts a heading element between sibling entries, which only works where entries are
965
+ rendered as direct DOM siblings - true of the five toggle-based display types (cards, list,
966
+ listgroup, accordion, block), which filter by toggling a hidden class on each `[data-entry-id]`.
967
+ The other three manage their own rendering and have nowhere for an injected heading to go:
968
+
969
+ - **table** - rows are re-rendered by `T.create` and paginated; an injected heading row would fight
970
+ pagination and the table's own re-render
971
+ - **timeline** - driven by the Progression instance's `.setItems()`, which takes a flat item list,
972
+ not section headers
973
+ - **carousel** - slides are a sequence, and entries live inside `.carousel-track` rather than as
974
+ direct siblings of the wrapper
975
+
976
+ The **Group by…** row is hidden, not disabled, on the three unsupported types.
977
+
978
+ Sort, filters and grouping all persist in the URL hash (`#dmf=…`, keyed per display), so a
979
+ filtered/sorted/grouped view survives a reload and can be bookmarked or shared - opening the link
980
+ re-applies filters, sort, group and chips. Existing anchor fragments are preserved alongside the
981
+ filter segment. **Copy link to this view** (in the same menu) writes the current state to the hash
982
+ and copies the resulting URL to the clipboard.
983
+
984
+ ```
985
+ [collection slug="events" display="timeline" title-field="title" date-field="date" body-field="summary" /]
986
+ [collection slug="testimonials" display="carousel" image-field="photo" title-field="name" body-field="quote" autoplay="true" /]
987
+ [collection slug="faqs" display="listgroup" variant="flush" title-field="question" body-field="answer" /]
988
+ [collection slug="team" display="carousel" block="profile-card" /]
989
+ ```
990
+
991
+ #### Structured filtering
992
+
993
+ Add a filter by writing `where_<field>` or `where_<field>_<op>` attributes. Operators:
994
+
995
+ | Suffix | Meaning | Example |
996
+ |-------------|------------------------------------|------------------------------------------|
997
+ | (none) | equal | `where_location="London"` |
998
+ | `_eq` | equal (explicit) | `where_status_eq="open"` |
999
+ | `_ne` | not equal | `where_status_ne="closed"` |
1000
+ | `_gt` | greater than | `where_salary_gt="40000"` |
1001
+ | `_gte` | greater than or equal | `where_salary_gte="50000"` |
1002
+ | `_lt` | less than | `where_salary_lt="100000"` |
1003
+ | `_lte` | less than or equal | `where_postedAt_lte="2026-06-01"` |
1004
+ | `_in` | value in comma-separated list | `where_type_in="full-time,contract"` |
1005
+ | `_nin` | value not in list | `where_status_nin="archived,withdrawn"` |
1006
+ | `_contains` | substring match (case-insensitive) | `where_title_contains="engineer"` |
1007
+ | `_starts` | starts-with (case-insensitive) | `where_title_starts="Senior"` |
1008
+ | `_ends` | ends-with (case-insensitive) | `where_email_ends="@example.com"` |
1009
+ | `_exists` | field is present and non-null | `where_remote_exists="true"` |
1010
+
1011
+ Predicates AND together. For OR or NOT, define a saved view (`content/views/`) and use `[view slug="…"]`.
1012
+
1013
+ Two pseudo-fields are also recognised: `createdBy` (matches `meta.createdBy`) and `id` (matches the top-level entry id).
1014
+ Dot-paths into nested data work - `where_address.city_eq="London"` matches `entry.data.address.city`.
1015
+
1016
+ The same DSL is honoured by:
1017
+
1018
+ - The collection list API: `GET /api/collections/jobs/entries?filter[location]=London&filter[salary_gte]=50000`
1019
+ - The public read API: `GET /api/collections/jobs/public?filter[location]=London`
1020
+
1021
+ Filtering is pushed down to the storage adapter - MongoDB sees a native `$gte` query, File mode does an in-memory pass.
1022
+
1023
+ #### Interactive Collection Browser
1024
+
1025
+ Add any of `searchable`, `sortable`, `filterable="…"`, or `paginate` to flip a `[collection]` block from a static
1026
+ render into a fully interactive **Collection Browser** - search box, sort dropdown, schema-derived filter rail,
1027
+ and pagination, all wired up automatically.
1028
+
1029
+ ```
1030
+ [collection slug="jobs"
1031
+ display="cards" columns="3"
1032
+ searchable
1033
+ filterable="location,type,salary,tags"
1034
+ sortable
1035
+ page-size="9"
1036
+ empty="No matching jobs." /]
1037
+ ```
1038
+
1039
+ The browser is **opt-in**: existing blocks without these attributes still render statically and look exactly as
1040
+ before. Once any interactive attr is present, the server emits a hydration shell with the schema + initial data
1041
+ and `public/js/collection-browser.js` takes over.
1042
+
1043
+ | Attribute | Description |
1044
+ |----------------------|------------------------------------------------------------------------------------------------------------|
1045
+ | `searchable` | Render a free-text search box (substring match across all fields) |
1046
+ | `sortable` | Bare flag: sort by any field. Or `sortable="title,salary,postedAt"` to restrict the dropdown |
1047
+ | `filterable` | Comma-separated field names - each becomes a filter control in the rail |
1048
+ | `paginate` | Bare flag: enable pagination even without other interactive features |
1049
+ | `pagination` | `pages` (default) - prev/next pager. `scroll` - infinite scroll via IntersectionObserver |
1050
+ | `page-size` | Entries per page (default `12`) |
1051
+ | `mode` | `client` (default) - ship all data inline, filter in browser. `server` - fetch each page from the API |
1052
+ | `max-client-entries` | In client mode, the cap before auto-downgrading to server mode (default `1000`) |
1053
+ | `exportable` | Bare flag: show an "Export CSV" button in the header |
1054
+ | `no-saved-searches` | Bare flag: hide the saved-searches dropdown (it's on by default) |
1055
+ | `no-url-sync` | Bare flag: don't sync filter/sort/search/page to the URL query string (URL sync is on by default) |
1056
+
1057
+ **Filter controls are derived from each field's `type` in the collection schema:**
1058
+
1059
+ | Field type | Control | Operator |
1060
+ |---------------------------------|--------------------------------------------------------|----------|
1061
+ | `text`, `textarea`, `email` | Text input | `_contains` |
1062
+ | `number` | Min/max input pair | `_gte` / `_lte` |
1063
+ | `date`, `datetime` | From/to date picker | `_gte` / `_lte` |
1064
+ | `select`, `chooser` | Single dropdown (options from schema or distinct values) | `_eq` |
1065
+ | `multiselect`, `tags`, `tag` | Multi-pick checkbox group | `_in` |
1066
+ | `boolean`, `checkbox` | Any / Yes / No dropdown | `_eq` |
1067
+
1068
+ For fields without `options` in the schema, the browser **auto-discovers distinct values** from the current dataset
1069
+ - so `filterable="location"` on a plain text field still gives you a useful dropdown.
1070
+
1071
+ **Author-baked filters compose with user filters.** A `where_*` attribute is locked-in - the user can narrow further
1072
+ but never widen past it:
1073
+
1074
+ ```
1075
+ [collection slug="jobs"
1076
+ searchable filterable="location,salary"
1077
+ where_status="open" /]
1078
+ ```
1079
+
1080
+ The job board only ever shows open roles; the user picks from London/Berlin/Remote and adjusts the salary range
1081
+ within that pool.
1082
+
1083
+ **URL state sync** (default on) pushes the user's choices to `?cb_<id>_search=…&cb_<id>_filter[location]=London&…`
1084
+ via `history.replaceState`. Deep-linking, sharing filtered views, and the browser back button all work without a
1085
+ full page reload. Multiple browsers on one page use distinct id-prefixed param names.
1086
+
1087
+ **`mode="server"`** is the right choice for large collections that won't fit comfortably in a single page payload.
1088
+ Every search/sort/filter/page change becomes a `GET /api/collections/:slug/public?filter[…]=…&page=…` round-trip;
1089
+ the storage adapter (Mongo or File) handles the heavy lifting. Client mode is faster for small datasets but ships
1090
+ all entries inline.
1091
+
1092
+ ##### Power-user features (all on by default unless noted)
1093
+
1094
+ - **Facet counts** - each multi-pick chip and single-select option shows how many entries would match if that
1095
+ option were toggled with the user's other current filters. Standard faceted-search behaviour so users see
1096
+ what's available before clicking. **Client mode only** - server mode would need a separate aggregate endpoint;
1097
+ the page-bounded total there would mislead.
1098
+ - **Mobile filter drawer** - on viewports under 768 px a "Filters (n)" button replaces the rail; tapping it
1099
+ slides the rail in from the left with a dimmed overlay. The badge counts active conditions.
1100
+ - **Saved searches** - header dropdown that captures the current state under a user-chosen name. Stored in
1101
+ browser `localStorage` keyed per-browser id (`cb_saved_<id>`). Disable with `no-saved-searches`.
1102
+ - **CSV export** - adds a button in the header (`exportable` attr). Client mode builds the file from the
1103
+ current filtered set in JS; server mode hits the existing `/api/collections/:slug/export?format=csv&filter=…`
1104
+ endpoint with the active filter applied.
1105
+ - **Relevance sort** - when the search box has a term AND the user hasn't manually picked a sort, results are
1106
+ ordered by match count (with a 3× boost on matches in `title-field`). Switching the sort dropdown to anything
1107
+ explicit takes over.
1108
+ - **Keyboard shortcuts** - `/` focuses the first browser's search box; `←` / `→` pages backwards/forwards on
1109
+ the browser containing the focused element. Disabled while you're typing in an input.
1110
+ - **Infinite scroll** (`pagination="scroll"`) - replaces the pager with a sentinel that loads more on scroll.
1111
+ Subsequent pages append rather than replace; filter/sort changes reset to page 1 normally.
1112
+ - **Server-mode debounce + abort** - in-flight fetches are cancelled when a new state change supersedes them, so
1113
+ fast typing in the search box doesn't fight for response order.
1114
+ - **Polished controls** - multiselect renders as toggleable chips with counts; numeric ranges add a dual-handle
1115
+ slider above the min/max inputs (when bounds can be derived from the dataset).
1116
+
1117
+ ##### State-machine transitions
1118
+
1119
+ Set `transitions` on an interactive `[collection]` block (one with `searchable`,
1120
+ `sortable`, `filterable` or `paginate`) and each row sprouts buttons for
1121
+ the actions that legally apply to that row's current state - based on the
1122
+ `transition: { field, from, to }` config on action definitions.
1123
+
1124
+ ```
1125
+ [collection slug="applications" display="cards"
1126
+ title-field="jobId"
1127
+ searchable transitions /]
1128
+ ```
1129
+
1130
+ `transitions` has no effect on a static `[collection]`. On a `scope="mine"` block it works the same once
1131
+ the block is interactive (see [Per-user scoping](#per-user-scoping---scopemine)): each row is one of the
1132
+ viewer's own, and the server refuses a transition on any other row.
1133
+
1134
+ Behind the scenes each row's transitions are fetched independently via
1135
+ `GET /api/actions/transitions?collection=applications&entryId=…`, which
1136
+ returns only the actions whose:
1137
+
1138
+ - `transition.from` includes the row's current value of `transition.field`, **and**
1139
+ - `access.roles` admits the requesting user - any listed role is enough, and every more senior role
1140
+ inherits it (the page-visibility ladder; a role name the site does not have admits only the level-0 role), **and**
1141
+ - the action's `access.rowLevel` rule, if any, passes for that row
1142
+
1143
+ That means a candidate viewing their application in `status: submitted` sees
1144
+ a "Withdraw" button, but the same UI viewed after the application moves to
1145
+ `status: rejected` shows no actions - there's nothing they can do from a
1146
+ terminal state. No client-side conditionals; the server's transition rules
1147
+ are the only source of truth.
1148
+
1149
+ Action-side configuration looks like:
1150
+
1151
+ ```json
1152
+ {
1153
+ "slug": "withdraw-application",
1154
+ "title": "Withdraw",
1155
+ "collection": "applications",
1156
+ "transition": {
1157
+ "field": "status",
1158
+ "from": ["submitted", "reviewing"],
1159
+ "to": "withdrawn"
1160
+ },
1161
+ "access": { "roles": ["candidate"], "rowLevel": { "ownerField": "createdBy" } },
1162
+ "steps": [
1163
+ { "type": "updateField", "config": { "field": "status", "value": "withdrawn" } }
1164
+ ]
1165
+ }
1166
+ ```
1167
+
1168
+ Execute-time validation rejects illegal moves with HTTP 409 - so an action
1169
+ URL someone copies and pastes after the state has changed gets a clean
1170
+ "Cannot apply X - current status is Y" message rather than mutating data
1171
+ out from under the rules.
1172
+
1173
+ ##### Interactive saved views
1174
+
1175
+ `[view slug="…"]` shortcodes can opt into the Browser the same way:
1176
+
1177
+ ```
1178
+ [view slug="employer-applications" searchable sortable page-size="20" /]
1179
+ ```
1180
+
1181
+ Because saved views may aggregate across collections (joins, projections), they don't expose a filter rail by
1182
+ default - the view itself owns its filter logic. But search, sort, pagination, saved searches, and CSV export all
1183
+ work. This is how you give users an interactive UI over a complex cross-collection query without re-inventing the
1184
+ join in shortcode syntax.
1185
+
1186
+ #### Per-user scoping - `scope="mine"`
1187
+
1188
+ Add `scope="mine"` to render only entries created by the current signed-in user. The page is still cached per role,
1189
+ but the scoped block is replaced with a hydration placeholder that fetches the user's data client-side
1190
+ (`POST /api/collections/render-scope`). This keeps caching efficient while making per-user "My Applications" /
1191
+ "My Bookmarks" / "My Orders" patterns trivial.
1192
+
1193
+ ```
1194
+ [collection slug="applications" scope="mine" display="cards" title-field="jobTitle" /]
1195
+ ```
1196
+
1197
+ Anonymous visitors see a "Please sign in" prompt where the block would render. The endpoint never trusts the client
1198
+ to declare which user they are - the JWT is verified server-side and `createdBy = <user.id>` is injected before
1199
+ re-running the render pipeline.
1200
+
1201
+ The collection does not need `api.read` enabled for `scope="mine"` to work - by definition the user can only see
1202
+ their own entries.
1203
+
1204
+ Make the block interactive (`searchable`, `sortable`, `filterable` or `paginate`) and it hydrates as the Collection
1205
+ Browser instead of a static list - still only the viewer's rows, with paging fetched as `scope=mine` - and
1206
+ `transitions` then works on it:
1207
+
1208
+ ```
1209
+ [collection slug="applications" scope="mine" display="cards" title-field="jobId" paginate transitions /]
1210
+ ```
1211
+
1212
+ Everything that block does is scoped on the server: the transition list (`GET /api/actions/transitions?…&scope=mine`)
1213
+ offers nothing on another user's row, and running a transition or `cta` from it sends `scope: "mine"`, so
1214
+ `POST /api/actions/:slug/public` answers 403 (`Row-level access denied`) for a row the viewer did not create - before
1215
+ the state check, so an illegal move on their own row is still 409. The hard boundary against someone calling the
1216
+ action directly is the action's own `access` (its roles and `rowLevel`), so give an action meant for "my rows"
1217
+ `"rowLevel": {"mode": "owner"}` as well.
1218
+
1219
+ ---
1220
+
1221
+ ### `[block]`
1222
+
1223
+ Renders a single block template with static inline data. Use this for one-off content where creating a full Collection
1224
+ would be unnecessary.
1225
+
1226
+ **Syntax:**
1227
+
1228
+ ```
1229
+ [block template="post-card" title="My Post" author="Jane Doe" excerpt="A short intro." /]
1230
+ ```
1231
+
1232
+ **Attributes:**
1233
+
1234
+ | Attribute | Required | Description |
1235
+ |------------|----------|------------------------------------------------------------------------------------------|
1236
+ | `template` | Yes | Name of the block template (files in `content/blocks/`) |
1237
+ | `*` | No | Any other attribute is passed as data for `{{placeholder}}` substitution in the template |
1238
+
1239
+ **Placeholders in the template:**
1240
+
1241
+ | Placeholder | Behaviour |
1242
+ |---|---|
1243
+ | `{{field}}` | Value is HTML-escaped before substitution |
1244
+ | `{{{field}}}` | Value is inserted **raw**, without escaping - use only for trusted HTML |
1245
+ | `{{field:FORMAT}}` | Formats the value as a date, e.g. `{{date:DD MMM YYYY}}`. Tokens: `YYYY YY MMMM MMM MM M DD D dddd ddd HH H hh h mm m ss s A a`. A value that is not a valid date is output unformatted |
1246
+
1247
+ **Notes:**
1248
+
1249
+ - If the named template does not exist, the shortcode is silently removed.
1250
+ - Missing placeholder values render as empty strings.
1251
+ - `class` and `id` attributes are applied to the wrapping `<div class="dm-static-block">`.
1252
+ - If the block has companion CSS (`content/blocks/<name>.css`), it is emitted once alongside the block, scoped to
1253
+ `[data-block="<name>"]`.
1254
+ - To create or edit block templates, go to **Data > Blocks**.
1255
+
1256
+ **Short tags:** every block can also be used with its own name as the tag. These two lines are equivalent:
1257
+
1258
+ ```
1259
+ [block template="contact-card" full_name="Jane Doe" /]
1260
+ [contact-card full_name="Jane Doe" /]
1261
+ ```
1262
+
1263
+ Short tags are self-closing only, and only work for block names made of lowercase letters, digits and hyphens that
1264
+ do not clash with a built-in shortcode name (a block called `card` or `menu`, for example, can only be used through
1265
+ `[block template="…"]`).
1266
+
1267
+ **Example:**
1268
+
1269
+ ```
1270
+ [block template="contact-card"
1271
+ full_name="Jane Doe"
1272
+ phone_number="555-1234"
1273
+ email_address="jane@example.com" /]
1274
+ ```
1275
+
1276
+ ---
1277
+
1278
+ ### `[component]`
1279
+
1280
+ Mounts a custom web component (a `.dmc` component from **Data > Components**, stored in
1281
+ `content/components/<name>.dmc`). The shortcode is rewritten to the component's custom-element tag, so these two lines
1282
+ are equivalent:
1283
+
1284
+ ```
1285
+ [component name="counter" initial="5" /]
1286
+ <dm-counter initial="5"></dm-counter>
1287
+ ```
1288
+
1289
+ `name` is required (lowercase letters, digits and hyphens); every other attribute is passed to the element as a prop.
1290
+ Only the scripts for components actually used on the page are loaded, and a tag for a component that does not exist
1291
+ is stripped. Components are processed first, before any other shortcode, so they can be written inside any container.
1292
+
1293
+ ---
1294
+
1295
+ ### `[menu]`
1296
+
1297
+ Self-closing. Renders any menu (by slug or by mapped location) inline as a nested `<nav>` of `<ul>`s. Menus are defined in `config/menus/<slug>.json` and mapped to slots in `config/menu-locations.json` (see `docs/configuration.md`).
1298
+
1299
+ ```
1300
+ [menu slug="legal" /]
1301
+ [menu location="footer-primary" /]
1302
+ [menu slug="legal" variant="inline" class="prose-nav" /]
1303
+ [menu slug="legal" depth="2" /]
1304
+ [menu slug="main" orientation="horizontal" /]
1305
+ ```
1306
+
1307
+ | Attribute | Description |
1308
+ |---------------|------------------------------------------------------------------------------------------|
1309
+ | `slug` | Render this menu by slug. One of `slug` or `location` is required |
1310
+ | `location` | Render whichever menu is currently mapped to this slot |
1311
+ | `variant` | Optional render variant override |
1312
+ | `class` | Extra CSS class on the wrapper, after the menu's own `class` (if any) |
1313
+ | `depth` | Cap render depth (integer ≥ 1); items deeper than this are dropped from the output |
1314
+ | `orientation` | `horizontal` or `vertical` for this placement only, overriding the menu's own setting |
1315
+ | `float` | `no` renders this placement inline even when the menu is floating in its slot |
1316
+
1317
+ Items with `visibility` set are filtered per requesting user, and items with `hidden: true` are always dropped - both happen server-side per request.
1318
+
1319
+ `location` honours **menu bindings**: if another menu is bound to that slot for the page being rendered, the shortcode renders the bound menu, exactly as the navbar does. `slug` names a menu directly and is never redirected.
1320
+
1321
+ Orientation adds a `dm-menu--horizontal` / `dm-menu--vertical` class to the wrapper. A menu that sets no orientation gets neither class and renders as a plain nested list, as it always has.
1322
+
1323
+ A menu's `highlight` block (hover and current-page item effects - see `docs/configuration.md`) travels with it here: the wrapper carries `data-menu-hover` / `data-menu-active` and the item pointing at the page being rendered is marked `is-active` + `aria-current="page"`. There is no shortcode attribute for it - the effect belongs to the menu, so every placement of it matches.
1324
+
1325
+ **Output:**
1326
+
1327
+ ```html
1328
+ <nav class="dm-menu dm-menu--legal" data-menu="legal" data-menu-hover="underline">
1329
+ <ul>
1330
+ <li><a href="/privacy" class="is-active" aria-current="page">Privacy</a></li>
1331
+ <li>
1332
+ <a href="/legal">Legal</a>
1333
+ <ul>
1334
+ <li><a href="/legal/terms">Terms</a></li>
1335
+ </ul>
1336
+ </li>
1337
+ </ul>
1338
+ </nav>
1339
+ ```
1340
+
1341
+ ---
1342
+
1343
+ ### `[view]`
1344
+
1345
+ Self-closing. Renders a saved View with full filtering, pagination, and display options. Works on every
1346
+ storage adapter: MongoDB-backed collections run native aggregations; file-backed collections run the
1347
+ built-in pipeline evaluator, which supports all ten allow-listed stages (`$match`, `$lookup`, `$sort`,
1348
+ `$project`, `$unwind`, `$limit`, `$skip`, `$count`, `$addFields`, `$group`) with two pragmatic limits:
1349
+ `$addFields`/computed `$project` values accept literals and `$field` references (not operator
1350
+ expressions), and `$lookup` supports the equality form only. Unsupported constructs fail with a clear
1351
+ error in the view preview rather than returning wrong results.
1352
+
1353
+ ```
1354
+ [view slug="products" display="table" limit="50" /]
1355
+ [view slug="team" display="cards" columns="3" title-field="name" /]
1356
+ ```
1357
+
1358
+ | Attribute | Description |
1359
+ |---------------|---------------------------------------------------|
1360
+ | `slug` | Required. The **view** slug (a saved View, not a collection) |
1361
+ | `display` | `table`, `cards`, `list`, `accordion`, `timeline`, `carousel`, `listgroup` or `block`. Defaults to the display mode stored on the View, then `table` |
1362
+ | `limit` | Maximum number of entries to display (default 25) |
1363
+ | `fields` | Comma-separated field keys to show |
1364
+ | `title-field` | Field used as the card/list title |
1365
+ | `columns` | Grid columns for `cards` display (2-4, default 3) |
1366
+ | `empty` | Text shown when there are no entries |
1367
+ | `block` | Block template for `display="block"` (falls back to the View's stored block), `carousel` or `listgroup` |
1368
+ | `action` | Adds a CTA button per row that runs this CMS Action; styled with `cta-label`, `cta-style`, `cta-icon`, `cta-confirm` |
1369
+ | `class` / `item-class` | Your own CSS classes on the wrapper / on every entry, as on `[collection]` |
1370
+
1371
+ The display-specific attributes are the same as on `[collection]` (for example `body-field`, `date-field`,
1372
+ `layout`, `theme` and `mode` for timelines, `image-field` for carousels, `variant`/`size` for list groups).
1373
+ Adding `searchable`, `sortable`, `filterable` or `paginate` switches to the interactive browser shell (search, sort
1374
+ and pagination over the View's results, with no filter rail).
1375
+
1376
+ ---
1377
+
1378
+ ### `[button]`
1379
+
1380
+ Renders a styled Domma button as a link (`<a class="btn">`). Supports both wrapping and self-closing forms.
1381
+
1382
+ ```
1383
+ [button href="/contact" variant="primary"]Get in touch[/button]
1384
+ [button href="/download" variant="success" icon="download" size="sm" label="Download" /]
1385
+ [button href="https://example.com" variant="outline" target="_blank"]Visit site[/button]
1386
+ ```
1387
+
1388
+ | Attribute | Description |
1389
+ |--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
1390
+ | `href` | Link destination (required; defaults to `#`) |
1391
+ | `label` | Button text - used in self-closing form; body text is used in wrapping form |
1392
+ | `variant` | `primary` (default), `secondary`, `success`, `danger`, `warning`, `info`, `outline`, `ghost`, `link`, `outline-success`, `outline-danger`, `outline-warning`, `outline-info` |
1393
+ | `size` | `"sm"` → `.btn-sm` \| `"lg"` → `.btn-lg` |
1394
+ | `icon` | Domma icon name rendered before the label |
1395
+ | `icon-after` | Domma icon name rendered after the label |
1396
+ | `target` | Link target, e.g. `"_blank"` |
1397
+ | `class` | Extra CSS classes appended to `.btn` |
1398
+ | `id` | HTML `id` attribute |
1399
+
1400
+ `[button]` also accepts injected effects (`breathe`, `pulse`, `shake`, `animate`, `ripple`) - see
1401
+ [Effect injection](#effect-injection).
1402
+
1403
+ ---
1404
+
1405
+ ### `[link]`
1406
+
1407
+ Renders a plain anchor with optional icon, target, and class. Prefer this over raw HTML when you need any of these
1408
+ extras; use standard Markdown `[text](url)` for simple inline links.
1409
+
1410
+ ```
1411
+ [link href="/about"]About us[/link]
1412
+ [link href="https://example.com" target="_blank" icon-after="external-link"]Visit site[/link]
1413
+ [link href="/about" icon="arrow-right" label="About us" /]
1414
+ ```
1415
+
1416
+ | Attribute | Description |
1417
+ |--------------|---------------------------------------------------------------------------|
1418
+ | `href` | Link destination (required; defaults to `#`) |
1419
+ | `label` | Link text - used in self-closing form; body text is used in wrapping form |
1420
+ | `target` | Link target, e.g. `"_blank"` |
1421
+ | `class` | CSS classes on the `<a>` |
1422
+ | `icon` | Domma icon name rendered before the text |
1423
+ | `icon-after` | Domma icon name rendered after the text |
1424
+ | `id` | HTML `id` attribute |
1425
+
1426
+ ---
1427
+
1428
+ ### `[cta]`
1429
+
1430
+ Renders an action-trigger button linked to a CMS Action:
1431
+
1432
+ ```
1433
+ [cta action="send-welcome" entry="abc123" style="primary" icon="mail"]Send Welcome Email[/cta]
1434
+ [cta action="archive" entry="abc123" confirm="Archive this record?" /]
1435
+ ```
1436
+
1437
+ Attributes: `action` (required - action slug), `entry` (entry ID), `style` (btn variant), `icon`, `size`, `confirm` (confirmation prompt text), `label` (self-closing form only).
1438
+
1439
+ ---
1440
+
1441
+ ### `[demo]`
1442
+
1443
+ Embeds a demo page from this site in an iframe. Self-closing.
1444
+
1445
+ ```markdown
1446
+ [demo src="elements/accordion" /]
1447
+ [demo src="effects/ambient" height="600px" title="Ambient Effect" /]
1448
+ [demo src="widget" base="/public/showcase" /]
1449
+ ```
1450
+
1451
+ | Attribute | Default | Notes |
1452
+ |-----------|---------|-------|
1453
+ | `src` | required | Path under `base`, without the `.html`. Nothing renders without it |
1454
+ | `height` | `500px` | Any CSS length |
1455
+ | `title` | none | Renders a caption bar above the frame |
1456
+ | `base` | `/public/demos` | Must be an absolute path on this site |
1457
+
1458
+ The frame is **same-origin only**. A `src` carrying a scheme (`https:`), a
1459
+ protocol-relative prefix (`//`), or a `base` that is not an absolute path is
1460
+ refused and nothing is rendered - use `[embed]` for third-party content, which
1461
+ is gated by the host allow-list in `config/embeds.json`.
1462
+
1463
+ Was the `demo-viewer` plugin until 0.67.0.
1464
+
1465
+ ## Effects Shortcodes
1466
+
1467
+ The following shortcodes are provided by the built-in Effects system.
1468
+
1469
+ | Shortcode | Description |
1470
+ |----------------------------------------------|--------------------------------------------------------|
1471
+ | `[reveal animation="fade"]...[/reveal]` | Fade/slide/zoom on scroll |
1472
+ | `[breathe]...[/breathe]` | Continuous gentle scale loop |
1473
+ | `[pulse]...[/pulse]` | Repeating scale pulse |
1474
+ | `[shake]...[/shake]` | One-shot shake |
1475
+ | `[scribe speed="50"]...[/scribe]` | Typewriter - simple mode (text typed letter by letter) |
1476
+ | `[scramble]...[/scramble]` | Character-scramble reveal |
1477
+ | `[counter to="100" /]` | Animated number counter (self-closing) |
1478
+ | `[ripple]...[/ripple]` | Click-triggered ripple |
1479
+ | `[twinkle count="50"]...[/twinkle]` | Particle/star overlay |
1480
+ | `[ticker-tape palette="festive" /]` | Falling ticker-tape strips - self-closing for full-page, wrapping form for container-scoped |
1481
+ | `[butterflies palette="meadow" /]` | Flapping butterflies wander and rise - self-closing for full-page, wrapping form for container-scoped |
1482
+ | `[strobe preset="club" /]` | Sweeping light beams (club/concert/police/searchlight/scanner/mood) - self-closing for full-page, wrapping form for container-scoped |
1483
+ | `[animate type="fade-in-up"]...[/animate]` | CSS-only animation (no plugin needed) |
1484
+ | `[ambient type="aurora"]...[/ambient]` | CSS-only animated background |
1485
+ | `[firework type="burst" colour="rainbow" /]` | CSS firework (self-closing) |
1486
+ | `[fireworks]...[/fireworks]` | Fireworks display container |
1487
+ | `[celebrate theme="auto" /]` | Seasonal canvas celebration (self-closing). `auto` picks whichever seasonal theme is active by date |
1488
+ | `[christmas /]`, `[halloween /]`, `[st-patricks /]`, `[valentines /]`, `[guy-fawkes /]`, `[st-andrews /]`, `[st-davids /]`, `[st-georges /]`, `[new-year /]`, `[easter /]`, `[summer-garden /]`, `[chinese-new-year /]`, `[winter /]`, `[autumn /]`, `[graduation /]`, `[birthday /]`, `[anniversary /]`, `[greeting /]`, `[make-it-rain /]`, `[money-rain /]` | Named celebration tags - shorthand for `[celebrate theme="…" /]` that force that theme. Other `[celebrate]` attributes (e.g. `intensity`) still apply |
1489
+ | `[christmas snow-only="true" /]` | Just the falling snow - every decoration off. Or switch pieces off one at a time: `trees`, `wreaths`, `northstars`, `snowmen`, `sleigh`, `elf`, `train`, `firework`, `robin` = `"off"` (names are not case-sensitive; `north-stars` works too) |
1490
+
1491
+ ### Winter and Make it rain
1492
+
1493
+ ```text
1494
+ [winter intensity="light" /]
1495
+ [winter pine="off" icicle="off" /]
1496
+ [make-it-rain intensity="heavy" duration="8000" /]
1497
+ ```
1498
+
1499
+ Winter offers snowflakes, frosted pines, snowdrifts, icicles and twinkling winter stars. Its switches are `snowflake`, `pine`, `snowdrift`, `icicle` and `star`. `falling-only="true"` leaves just snow. In automatic mode it fills December-February, with the existing holidays taking priority. Manual selection works at any time of year.
1500
+
1501
+ Make it rain is a standalone weather effect with raindrops and ground splashes, available at any time. It accepts `light`, `medium` or `heavy` intensity. `duration` is milliseconds, defaults to 8000 and is capped at 600000; `0` keeps it running until the page closes or the effect is destroyed. It never activates automatically by date. Both effects respect reduced motion and keep the underlying page clickable.
1502
+
1503
+ Summer Garden and Easter use the existing illustrated butterflies, meadow colours, wandering upward flight and natural wingbeats. Their `butterfly="off"` switch still works independently of bees and other decorations.
1504
+
1505
+ ### Sparkler greetings and Chinese New Year
1506
+
1507
+ New Year writes Happy New Year in the sky; Christmas writes Merry Christmas; Chinese New Year writes 新年快乐 (Happy New Year). The greeting is traced stroke by stroke behind a glowing tip, sheds sparks, holds, fades and repeats. English greetings wrap onto two balanced lines on smaller screens. Switch it off independently:
1508
+
1509
+ ```text
1510
+ [new-year sparkler-message="off" /]
1511
+ [christmas sparkler-message="off" /]
1512
+ [chinese-new-year dragon="off" lantern="off" /]
1513
+ ```
1514
+
1515
+ Optional lettering, colour and year settings appear in the editor's Celebrate controls and on each named celebration. Both styles work in both colours:
1516
+
1517
+ ```text
1518
+ [christmas sparkler-style="cursive" sparkler-colour="silver" /]
1519
+ [new-year sparkler-style="cursive" sparkler-colour="gold" /]
1520
+ [new-year sparkler-greeting="best-wishes" sparkler-year="2027" sparkler-style="cursive" sparkler-colour="silver" /]
1521
+ ```
1522
+
1523
+ The final example writes **Best wishes for 2027!**.
1524
+
1525
+ | Parameter | Default | Values |
1526
+ |---|---|---|
1527
+ | `message` | Theme greeting | Custom text, up to 120 characters; `{year}` inserts `sparkler-year` |
1528
+ | `sparkler-style` | `stroke` | `stroke`, `cursive` |
1529
+ | `sparkler-colour` | `gold` | `gold`, `silver`; includes the lettering, tip, sparks and shimmer. `sparkler-color` is also accepted |
1530
+ | `sparkler-greeting` | `default` | `default` (seasonal message), `best-wishes` |
1531
+ | `sparkler-year` | Current year | 1-9999, used by Best wishes and `{year}` in custom messages |
1532
+
1533
+ Chinese New Year's seasonal message uses Chinese stroke lettering in either colour. The optional Best wishes greeting is in English. `sparkler-message="off"`, `falling-only="true"` and Christmas's `snow-only="true"` hide the entire greeting regardless of style or colour.
1534
+
1535
+ Chinese New Year includes a wingless, flowing Chinese dragon, red lanterns, red and gold confetti, fireworks and the sparkler greeting. Its switches are `confetti`, `dragon`, `lantern`, `firework` and `sparkler-message`. Its automatic window starts on Chinese New Year and lasts seven local calendar days, taking priority over overlapping celebrations. Dates for 2020-2050 use the Hong Kong Observatory's published calendar, with the runtime Chinese calendar as a fallback outside that range.
1536
+
1537
+ Summer Garden's bees visit enabled flower heads and pause before flying to the next one. If flowers are off, the bees keep flying. Easter's bunny makes short hops along the lower edge, resting and turning before reaching the edge. The scene's pause and reduced-motion controls apply to these animations too.
1538
+
1539
+ ### New Year, Easter and Summer Garden
1540
+
1541
+ Choose any of these in Insert → Effects → Celebrate, or the page editor's Celebrations submenu. Their own switches also appear when editing a named shortcode.
1542
+
1543
+ ```text
1544
+ [celebrate theme="new-year" intensity="medium" firework="off" /]
1545
+ [easter intensity="light" bunny="off" /]
1546
+ [summer-garden bee="off" ladybird="off" /]
1547
+ [summer-garden falling-only="true" /]
1548
+ ```
1549
+
1550
+ | Theme | Individual switches (`"off"` hides an element) | Automatic dates |
1551
+ |---|---|---|
1552
+ | `new-year` | `confetti`, `streamer`, `firework`, `balloon`, `midnight-clock` | 31 December through 2 January |
1553
+ | `easter` | `blossom`, `flower`, `butterfly`, `easter-egg`, `bunny` | Tuesday before Gregorian Easter through Easter Monday, calculated each year |
1554
+ | `summer-garden` | `blossom`, `flower`, `butterfly`, `bee`, `ladybird` | 1 June through 31 August (UK summer) |
1555
+
1556
+ Dates include both endpoints in the visitor's local calendar. New Year takes over from Christmas on 31 December. Easter takes priority over any overlapping patron saint celebration. Explicit theme selection works at any time of year, including for gardens in other climates. All three support `light`, `medium` and `heavy` intensity and respect the site's reduced-motion setting.
1557
+
1558
+ ### `[scribe]` parameters
1559
+
1560
+ ![Scribe composer with global settings and an action timeline](images/scribe-composer.png)
1561
+
1562
+ Both simple text and action scripts accept the same settings. The editors emit an explicit reduced-motion setting so the checkbox remains effective even when the site default differs. Insert → Effects exposes the simple form; Insert → Scribe exposes these settings plus the action timeline.
1563
+
1564
+ | Parameter | Default | Values / behavior |
1565
+ |---|---|---|
1566
+ | `mode` | `typewriter` | `typewriter`, `word`, `sentence`; typing and deletion operate on that unit |
1567
+ | `effect` | `none` | `none`, `fade`, `bounce`, `glow`; default for simple text and render/redraw actions |
1568
+ | `speed` | `50` | Non-negative milliseconds per typed unit; `0` renders immediately |
1569
+ | `delete-speed` | `30` | Non-negative milliseconds per deleted unit |
1570
+ | `cursor` | `true` | Show the cursor |
1571
+ | `cursor-char` | `\|` | Custom cursor text; ignored for the underline cursor |
1572
+ | `cursor-type` | `caret` | `caret`, `block`, `underline` |
1573
+ | `cursor-blink` | `true` | Blink while idle; cursor remains steady during typing/deletion |
1574
+ | `loop` | `false` | `false` once, `true` forever, or a positive integer for total plays (e.g. `3`) |
1575
+ | `loop-delay` | `1000` | Non-negative milliseconds between plays; the last phrase stays visible |
1576
+ | `pause-on-hover` | `false` | Pause and continue on hover |
1577
+ | `auto-start` | `true` | `false` waits for Play; playback controls appear automatically |
1578
+ | `respect-motion-preference` | Site Effects setting | `true` shows the first phrase without motion when reduced motion is preferred; `false` permits typing |
1579
+ | `controls` | `false` | Show Play, Pause, and Replay buttons |
1580
+
1581
+ ```text
1582
+ [scribe mode="word" effect="bounce" speed="140" cursor-type="underline" cursor-blink="false" loop="3" loop-delay="1800" pause-on-hover="true" controls="true"]
1583
+ Build smoother experiences, one word at a time.
1584
+ [/scribe]
1585
+ ```
1586
+
1587
+ Render and redraw actions inherit `effect` unless they specify their own value; `effect="none"` explicitly disables the default entrance effect for that action. Callback integrations use bubbling DOM events on the Scribe element: `scribe:start`, `scribe:complete`, `scribe:character`, `scribe:render`, `scribe:undo`, and `scribe:loop`. `event.detail.value` carries the unit/text/loop count and `event.detail.index` carries the character's unit index. JavaScript callbacks remain available through the Domma JS API.
1588
+
1589
+ ### `[scribe]` script mode
1590
+
1591
+ `[scribe]` supports a sequenced script mode using `[render]`, `[wait]`, and `[undo]` sub-tags:
1592
+
1593
+ ```
1594
+ [scribe loop="true" loop-delay="2000" delete-speed="20"]
1595
+ [render]We build things that matter.[/render]
1596
+ [wait]1200[/wait]
1597
+ [undo all="true" /]
1598
+ [render]We design for humans.[/render]
1599
+ [wait]1200[/wait]
1600
+ [undo all="true" /]
1601
+ [/scribe]
1602
+ ```
1603
+
1604
+ Use `[render]...[/render]` to type text, `[wait]Ms[/wait]` to pause, and `[undo /]` to delete (`[undo all="true" /]`
1605
+ clears everything typed so far, `[undo count="3" /]` removes the last three units (characters, words, or sentences according to `mode`)). `[redraw]...[/redraw]` is
1606
+ shorthand for `[undo all="true" /]` followed by `[render]...[/render]`. `[render]` and `[redraw]` accept an
1607
+ `effect="…"` attribute that is passed to the typewriter for that step. `loop="true"` replays the sequence. Only
1608
+ `[render]`, `[wait]`, `[undo]` and `[redraw]` are parsed inside a scripted `[scribe]` block - plain text between
1609
+ actions is ignored.
1610
+
1611
+ ```
1612
+ [scribe loop="true"]
1613
+ [render]We build things that matter.[/render]
1614
+ [wait]1200[/wait]
1615
+ [redraw]We design for humans.[/redraw]
1616
+ [/scribe]
1617
+ ```
1618
+
1619
+ ### Effect injection
1620
+
1621
+ Instead of wrapping a tag in an effect shortcode, you can add the effect as a flag on the host tag. Effect parameters
1622
+ use the effect name as a prefix: `reveal-animation="zoom"` becomes the reveal's `animation` setting.
1623
+
1624
+ ```
1625
+ [card title="Why us" reveal reveal-animation="zoom" reveal-duration="600"]
1626
+ Fast, file-based and friendly.
1627
+ [/card]
1628
+
1629
+ [button href="/contact" pulse]Get in touch[/button]
1630
+ ```
1631
+
1632
+ | Host tag | Effects that can be injected |
1633
+ |---|---|
1634
+ | `[card]`, `[box]` | `reveal`, `breathe`, `pulse`, `shake`, `twinkle`, `ambient`, `animate`, `ripple` |
1635
+ | `[hero]` | `reveal`, `breathe`, `pulse`, `twinkle`, `ticker-tape`, `butterflies`, `strobe`, `ambient`, `animate` |
1636
+ | `[banner]` | `reveal`, `pulse`, `shake`, `ambient`, `animate` |
1637
+ | `[col]` | `reveal`, `breathe`, `animate`, `ripple` |
1638
+ | `[button]` | `breathe`, `pulse`, `shake`, `animate`, `ripple` |
1639
+
1640
+ ---
1641
+
1642
+ ## Processing Order
1643
+
1644
+ Shortcodes are processed in this order (see `parseMarkdown()` in `server/services/markdown.js`), which determines
1645
+ nesting behaviour:
1646
+
1647
+ 1. `markdown:beforeParse` plugin transforms
1648
+ 2. `[component]` / `<dm-*>` component tags
1649
+ 3. `[collection]`
1650
+ 4. `[view]`
1651
+ 5. `[block]` and block short tags
1652
+ 6. `[menu]`
1653
+ 7. `[dconfig]`
1654
+ 8. Effects shortcodes (including the named celebration tags)
1655
+ 9. Plugin shortcodes (registered by enabled plugins)
1656
+ 10. `[tabs]` / `[tab]`
1657
+ 11. `[accordion]` / `[item]`
1658
+ 12. `[carousel]` / `[slide]`
1659
+ 13. `[countdown]`
1660
+ 14. `[timeline]` / `[event]`
1661
+ 15. `[listgroup]` / `[item]`
1662
+ 16. `[spacer]`
1663
+ 17. `[theme-switcher]`, `[navbar]`, `[contents]`
1664
+ 18. `[center]`
1665
+ 19. `[icon]`
1666
+ 20. `[demo]`
1667
+ 21. `[embed]`
1668
+ 22. `[form]`
1669
+ 23. `[hero]`
1670
+ 24. `[table]`
1671
+ 25. `[badge]`
1672
+ 26. `[text]`
1673
+ 27. `[button]`
1674
+ 28. `[link]`
1675
+ 29. `[cta]`
1676
+ 30. `[grid]` / `[row]` / `[col]`
1677
+ 31. `[card]` / `[box]`
1678
+ 32. `[banner]`
1679
+ 33. `[slideover]`
1680
+ 34. Markdown → HTML (marked)
1681
+ 35. HTML sanitisation
1682
+ 36. `markdown:afterParse` plugin transforms
1683
+
1684
+ Container tags render their own bodies as they go: effect wrappers, `[tabs]`, `[accordion]`, `[carousel]`,
1685
+ `[timeline]`, `[hero]`, `[grid]`/`[col]`, most `[card]` layouts and `[slideover]` run the inline tags (`[badge]`,
1686
+ `[text]`, `[button]`, `[link]`, `[cta]`) and `[grid]`/`[card]` over their body before Markdown, so those work inside
1687
+ all of them. `[center]` leaves its body as Markdown for the later stages, so anything processed after it works
1688
+ inside it. Tags processed before a container (components, collections, views, blocks, menus) are already HTML by
1689
+ the time the container sees them, so they can be nested anywhere. `[banner]` runs after `[card]` and `[grid]`, so
1690
+ it cannot be nested inside a container that has already rendered its body. Do not nest `[slideover]` inside another `[slideover]`, or a `[box]`
1691
+ inside another `[box]`.
1692
+
1693
+ ### Optional Christmas elf scenes
1694
+
1695
+ `[christmas peeing-elf="true" thieving-elf="true" /]` enables two optional scenes. Both are off when omitted. The clothed peeing elf walks in, pauses and leaves; the thief needs a visible Christmas tree with an available gift, takes that exact present, then escapes carrying it. Each scene is limited to one active elf. `snow-only="true"` disables both, even alongside an explicit opt-in.
1696
+
1697
+ The same attributes work on `[celebrate theme="christmas" /]`. Regular walking elves remain a separate decoration controlled by `elf="off"`.
1698
+
1699
+ ### Standalone celebration characters
1700
+
1701
+ Render a single character inline, at any time of year:
1702
+
1703
+ ```markdown
1704
+ [celebrate character="mooning-leprechaun"]
1705
+ [celebrate character="mooning-elf" size="large" /]
1706
+ [celebrate character="elf" size="200" animate="false" /]
1707
+ [celebrate character="leprechaun" size="small" facing="left" /]
1708
+ ```
1709
+
1710
+ Sizes describe displayed height: `small` = 96px, `medium` = 160px (default),
1711
+ `large` = 240px, `xlarge` = 360px, or a pixel height between 16 and 1024.
1712
+ Characters retain their proportions and fit smaller containers. `animate="false"`
1713
+ shows a still pose; reduced-motion preferences also stop animation.
1714
+
1715
+ Available characters: `elf`, `peeing-elf`, `thieving-elf`, `mooning-elf`,
1716
+ `leprechaun`, `mooning-leprechaun`, `bagpiper`, `knight`, `dragon`,
1717
+ `welsh-dragon`, `cupid`, `robin`, `snowman`.
1718
+
1719
+ A character runs independently of the theme, date and seasonal particles.
1720
+ For the full seasonal overlay, `[christmas mooning-elf="true" /]` or
1721
+ `[st-patricks mooning-leprechaun="true" /]` opts into a walk-in, wiggle and
1722
+ walk-out scene. Both mooners are off by default.
1723
+
1724
+
1725
+ ### Custom celebration messages and occasions
1726
+
1727
+ ```text
1728
+ [celebrate message="Best wishes for {year}!" sparkler-year="2027" sparkler-style="cursive" sparkler-colour="silver" /]
1729
+ [graduation message="Congratulations, Chloé!" sparkler-style="cursive" /]
1730
+ [birthday message="Happy Birthday, Sarah!" /]
1731
+ [anniversary message="Happy Anniversary, Mum & Dad!" /]
1732
+ [autumn /]
1733
+ [money-rain duration="8000" /]
1734
+ ```
1735
+
1736
+ `message` replaces the scene greeting, up to 120 characters. It supports names, punctuation and Latin accents with either lettering style and gold or silver. `{year}` uses `sparkler-year`. Without a theme, a message uses the standalone `greeting` scene. It is also available on Christmas, New Year, Chinese New Year, Graduation, Birthday and Anniversary. The Effects editor handles escaping quotes and square brackets; in hand-written shortcodes use `&quot;`, `&#91;` and `&#93;` for these characters.
1737
+
1738
+ Graduation tosses caps and diplomas; Birthday includes cake, balloons and confetti; Anniversary includes rings and hearts. Autumn has falling leaves, acorns and mushrooms, and runs automatically September-November after specific holidays. Money Rain is the separately named banknote effect; Make it rain is actual rain.