domma-cms 0.25.14 → 0.25.16

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 (45) hide show
  1. package/admin/js/app.js +2 -2
  2. package/admin/js/templates/api-reference.html +68 -1233
  3. package/admin/js/templates/docs/api-actions.html +146 -0
  4. package/admin/js/templates/docs/api-authentication.html +202 -0
  5. package/admin/js/templates/docs/api-collections.html +347 -0
  6. package/admin/js/templates/docs/api-layouts.html +123 -0
  7. package/admin/js/templates/docs/api-media.html +158 -0
  8. package/admin/js/templates/docs/api-navigation.html +99 -0
  9. package/admin/js/templates/docs/api-pages.html +214 -0
  10. package/admin/js/templates/docs/api-plugins.html +136 -0
  11. package/admin/js/templates/docs/api-settings.html +148 -0
  12. package/admin/js/templates/docs/api-users.html +189 -0
  13. package/admin/js/templates/docs/api-views.html +142 -0
  14. package/admin/js/templates/docs/components-howto.html +175 -0
  15. package/admin/js/templates/docs/components-reference.html +259 -0
  16. package/admin/js/templates/docs/components-rules.html +167 -0
  17. package/admin/js/templates/docs/components-walkthrough.html +167 -0
  18. package/admin/js/templates/docs/tutorial-crud.html +344 -0
  19. package/admin/js/templates/docs/tutorial-forms.html +93 -0
  20. package/admin/js/templates/docs/tutorial-plugin.html +234 -0
  21. package/admin/js/templates/docs/usage-actions.html +122 -0
  22. package/admin/js/templates/docs/usage-cta-shortcode.html +124 -0
  23. package/admin/js/templates/docs/usage-dconfig.html +148 -0
  24. package/admin/js/templates/docs/usage-media.html +25 -0
  25. package/admin/js/templates/docs/usage-navigation.html +31 -0
  26. package/admin/js/templates/docs/usage-pages.html +68 -0
  27. package/admin/js/templates/docs/usage-plugins.html +36 -0
  28. package/admin/js/templates/docs/usage-shortcodes.html +810 -0
  29. package/admin/js/templates/docs/usage-site-settings.html +52 -0
  30. package/admin/js/templates/docs/usage-users-roles.html +84 -0
  31. package/admin/js/templates/docs/usage-views.html +105 -0
  32. package/admin/js/templates/documentation.html +60 -1522
  33. package/admin/js/templates/tutorials.html +46 -659
  34. package/admin/js/views/api-reference.js +1 -1
  35. package/admin/js/views/doc-pages.js +1 -0
  36. package/admin/js/views/documentation.js +1 -1
  37. package/admin/js/views/index.js +1 -1
  38. package/admin/js/views/tutorials.js +1 -1
  39. package/config/menus/admin-sidebar.json +76 -6
  40. package/package.json +1 -1
  41. package/public/js/site.js +1 -1
  42. package/server/server.js +3 -1
  43. package/server/services/menus.js +18 -0
  44. package/server/services/renderer.js +2 -0
  45. package/server/services/sidebar-migration.js +145 -3
@@ -0,0 +1,259 @@
1
+ <div class="view-header">
2
+ <h1><span data-icon="component"></span> Components — Reference</h1>
3
+ <a href="#/components" class="btn btn-ghost btn-sm"><span data-icon="component"></span> Manage Components</a>
4
+ </div>
5
+
6
+ <div class="row">
7
+ <div class="col-12">
8
+ <div class="docs-body">
9
+
10
+ <nav class="alert alert-info" style="display:flex;gap:1rem;flex-wrap:wrap;align-items:center;">
11
+ <strong>Components docs:</strong>
12
+ <a href="#/docs/components">Reference</a>
13
+ <a href="#/docs/components-howto">How-To</a>
14
+ <a href="#/docs/components-walkthrough">Walkthrough</a>
15
+ <a href="#/docs/components-rules">Rules</a>
16
+ </nav>
17
+
18
+ <p class="lead">
19
+ Components are small, reusable custom elements you author in one file and drop into any page.
20
+ Each one is a single <code>.dmc</code> file with up to four sections — <code>&lt;template&gt;</code>,
21
+ <code>&lt;props&gt;</code>, <code>&lt;script&gt;</code> and <code>&lt;style&gt;</code> — and the CMS
22
+ compiles it to a self-contained browser module that registers a
23
+ <code>&lt;dm-<em>name</em>&gt;</code> custom element. No build step, no framework wiring.
24
+ </p>
25
+
26
+ <p>Manage them under <a href="#/components">Data → Components</a>. This page is the complete
27
+ reference; for guided learning start with the
28
+ <a href="#/docs/components-walkthrough">Walkthrough</a> or the
29
+ <a href="#/docs/components-howto">How-To</a> recipes.</p>
30
+
31
+ <hr>
32
+
33
+ <h2>The <code>.dmc</code> file format</h2>
34
+
35
+ <p>A component is a single file made of named blocks, much like a single-file component in other
36
+ frameworks. Three blocks are <strong>required</strong> (<code>&lt;template&gt;</code>,
37
+ <code>&lt;props&gt;</code>, <code>&lt;script&gt;</code>); <code>&lt;style&gt;</code> is optional.
38
+ Block order does not matter.</p>
39
+
40
+ <pre class="code-block"><code>&lt;template&gt;
41
+ &lt;div class="dm-greeting"&gt;Hello, {{name}}!&lt;/div&gt;
42
+ &lt;/template&gt;
43
+
44
+ &lt;props&gt;
45
+ {
46
+ "name": { "type": "string", "default": "world", "label": "Name to greet" }
47
+ }
48
+ &lt;/props&gt;
49
+
50
+ &lt;script&gt;
51
+ export default {};
52
+ &lt;/script&gt;
53
+
54
+ &lt;style&gt;
55
+ .dm-greeting { font-weight: 600; }
56
+ &lt;/style&gt;</code></pre>
57
+
58
+ <p>Saving compiles the source <em>before</em> it is written to disk — a malformed component never
59
+ reaches the filesystem. The compiler reports precise, per-block errors (see
60
+ <a href="#/docs/components-rules">Rules</a> for every error code).</p>
61
+
62
+ <hr>
63
+
64
+ <h2><code>&lt;template&gt;</code> — markup</h2>
65
+
66
+ <p>Plain HTML plus a Handlebars-style expression syntax. Data comes from the component's
67
+ <code>props</code> and reactive <code>data()</code> state. Supported syntax:</p>
68
+
69
+ <table class="table table-sm">
70
+ <thead>
71
+ <tr><th>Syntax</th><th>Purpose</th></tr>
72
+ </thead>
73
+ <tbody>
74
+ <tr><td><code>{{ value }}</code></td><td>Interpolate a prop or state value (HTML-escaped).</td></tr>
75
+ <tr><td><code>{{#if cond}}…{{/if}}</code></td><td>Render the block only when <code>cond</code> is truthy.</td></tr>
76
+ <tr><td><code>{{#unless cond}}…{{/unless}}</code></td><td>Render the block only when <code>cond</code> is falsy.</td></tr>
77
+ <tr><td><code>{{#each list}}…{{/each}}</code></td><td>Repeat the block for each item. Inside, <code>{{.}}</code> is the item (for primitives), <code>{{@index}}</code>, <code>{{@first}}</code> and <code>{{@last}}</code> are available; object items expose their own keys.</td></tr>
78
+ <tr><td><code>{{#with obj}}…{{/with}}</code></td><td>Scope the block to an object's keys.</td></tr>
79
+ <tr><td><code>&lt;slot&gt;&lt;/slot&gt;</code></td><td>Project the light-DOM content placed between the element's tags.</td></tr>
80
+ </tbody>
81
+ </table>
82
+
83
+ <pre class="code-block"><code>&lt;template&gt;
84
+ &lt;ul class="dm-list"&gt;
85
+ {{#each items}}
86
+ &lt;li class="{{#if @first}}first{{/if}}"&gt;{{@index}}. {{.}}&lt;/li&gt;
87
+ {{/each}}
88
+ &lt;/ul&gt;
89
+ {{#if title}}&lt;h4&gt;{{title}}&lt;/h4&gt;{{/if}}
90
+ &lt;slot&gt;&lt;/slot&gt;
91
+ &lt;/template&gt;</code></pre>
92
+
93
+ <hr>
94
+
95
+ <h2><code>&lt;props&gt;</code> — typed inputs</h2>
96
+
97
+ <p>A JSON <strong>object</strong> (not an array). Each key is a prop name; each value declares a
98
+ <code>type</code> and may add a <code>default</code> and a <code>label</code> (shown in the editor's
99
+ preview panel). The type is validated at compile time.</p>
100
+
101
+ <p>Valid types: <code>string</code>, <code>number</code>, <code>boolean</code>, <code>array</code>,
102
+ <code>object</code>.</p>
103
+
104
+ <pre class="code-block"><code>{
105
+ "initial": { "type": "number", "default": 0, "label": "Initial value" },
106
+ "step": { "type": "number", "default": 1, "label": "Step size" },
107
+ "showReset": { "type": "boolean", "default": false, "label": "Show reset button" },
108
+ "tags": { "type": "array", "default": [], "label": "Tag list" }
109
+ }</code></pre>
110
+
111
+ <p>Props are passed as HTML attributes where the value is always a string; the runtime coerces it to
112
+ the declared type (so <code>step="2"</code> arrives as the number <code>2</code>). Read props inside
113
+ <code>onMount()</code> and methods via <code>this.props</code> — see the script API below.</p>
114
+
115
+ <hr>
116
+
117
+ <h2><code>&lt;script&gt;</code> — behaviour</h2>
118
+
119
+ <p>An ES module with a single <code>export default { … }</code> object describing the component. The
120
+ export is the only requirement; every key inside it is optional.</p>
121
+
122
+ <pre class="code-block"><code>export default {
123
+ // Reactive state. Called WITHOUT `this` — do not read props here.
124
+ data() { return { count: 0 }; },
125
+
126
+ // Methods are bound to the component context (`this`).
127
+ methods: {
128
+ inc() { this.set({ count: this.data.count + this.props.step }); }
129
+ },
130
+
131
+ // Lifecycle — bound to `this`; props and the element are available here.
132
+ onMount() { this.set({ count: this.props.initial }); },
133
+ onUnmount() { /* clean up timers, listeners, etc. */ }
134
+ };</code></pre>
135
+
136
+ <table class="table table-sm">
137
+ <thead>
138
+ <tr><th>Key</th><th>What it is</th></tr>
139
+ </thead>
140
+ <tbody>
141
+ <tr><td><code>data()</code></td><td>Returns the initial reactive state object. Called with no <code>this</code> binding — <strong>props are not available here</strong>; initialise from props in <code>onMount()</code>.</td></tr>
142
+ <tr><td><code>methods</code></td><td>Object of functions bound to the component context.</td></tr>
143
+ <tr><td><code>onMount()</code></td><td>Lifecycle hook fired when the element is attached to the DOM. Props and the element node are available.</td></tr>
144
+ <tr><td><code>onUnmount()</code></td><td>Lifecycle hook fired when the element is removed — clear intervals/listeners here.</td></tr>
145
+ </tbody>
146
+ </table>
147
+
148
+ <p><strong>Inside methods and lifecycle hooks</strong>, <code>this</code> exposes:</p>
149
+
150
+ <table class="table table-sm">
151
+ <thead>
152
+ <tr><th>Member</th><th>Description</th></tr>
153
+ </thead>
154
+ <tbody>
155
+ <tr><td><code>this.props</code></td><td>The (type-coerced) props the element was used with.</td></tr>
156
+ <tr><td><code>this.data</code></td><td>The current reactive state snapshot.</td></tr>
157
+ <tr><td><code>this.set({ … })</code></td><td>Patch reactive state; the template re-renders with the new values.</td></tr>
158
+ <tr><td><code>this.el</code></td><td>The custom-element DOM node. Its rendered markup lives in <code>this.el.shadowRoot</code> — attach event listeners there.</td></tr>
159
+ </tbody>
160
+ </table>
161
+
162
+ <p>Domma's browser globals (<code>$</code>, <code>H</code>, <code>S</code>, <code>E</code>, <code>D</code>,
163
+ <code>_</code>, …) are available inside the script just as on any page, so a component can fetch data,
164
+ read storage, or pop a toast.</p>
165
+
166
+ <hr>
167
+
168
+ <h2><code>&lt;style&gt;</code> — scoped CSS</h2>
169
+
170
+ <p>Optional. CSS here is scoped to the component's shadow DOM, so selectors can't leak out and the
171
+ page's styles can't leak in. By convention components prefix their root class with
172
+ <code>dm-<em>name</em></code> for clarity, but scoping is automatic either way.</p>
173
+
174
+ <pre class="code-block"><code>&lt;style&gt;
175
+ .dm-counter { text-align: center; padding: 1rem; }
176
+ .dm-counter .value { font-size: 2.5rem; font-weight: 700; }
177
+ &lt;/style&gt;</code></pre>
178
+
179
+ <hr>
180
+
181
+ <h2>Using a component on a page</h2>
182
+
183
+ <p>Two equivalent ways, both server-rendered into the page so the right component module is auto-injected:</p>
184
+
185
+ <h3>Shortcode (Markdown)</h3>
186
+ <pre class="code-block"><code>[component name="counter" initial="10" step="2" showReset="true" /]</code></pre>
187
+
188
+ <h3>Custom element (HTML)</h3>
189
+ <pre class="code-block"><code>&lt;dm-counter initial="10" step="2" show-reset="true"&gt;&lt;/dm-counter&gt;</code></pre>
190
+
191
+ <p>Both pass <code>{initial, step, showReset}</code> to the component. Note the attribute casing:
192
+ camelCase props map to <code>kebab-case</code> attributes on the raw tag
193
+ (<code>showReset</code> → <code>show-reset</code>); the shortcode accepts the prop name as written.
194
+ When the page is rendered the server collects every <code>dm-*</code> tag used and injects one
195
+ <code>&lt;script type="module" src="/api/components/<em>name</em>.js"&gt;</code> at the end of the
196
+ body, which registers the element via <code>Domma.component()</code>.</p>
197
+
198
+ <p>To project content into a component's <code>&lt;slot&gt;</code>, use the custom-element form with
199
+ children:</p>
200
+ <pre class="code-block"><code>&lt;dm-reveal-panel heading="Click to reveal"&gt;
201
+ &lt;p&gt;Hidden content appears here when toggled.&lt;/p&gt;
202
+ &lt;/dm-reveal-panel&gt;</code></pre>
203
+
204
+ <hr>
205
+
206
+ <h2>Storage, REST API &amp; bundles</h2>
207
+
208
+ <p>Source files live in <code>content/components/<em>name</em>.dmc</code>, with an optional sidecar
209
+ <code>content/components/<em>name</em>.meta.json</code> holding the <code>bundled</code> flag and
210
+ <code>project</code> tag. The REST surface (all under <code>/api/components</code>):</p>
211
+
212
+ <table class="table table-sm">
213
+ <thead>
214
+ <tr><th>Method &amp; path</th><th>Auth</th><th>Purpose</th></tr>
215
+ </thead>
216
+ <tbody>
217
+ <tr><td><code>GET /api/components</code></td><td>read</td><td>List components with metadata + parsed props.</td></tr>
218
+ <tr><td><code>GET /api/components/:name</code></td><td>read</td><td>Raw <code>.dmc</code> source + parsed props.</td></tr>
219
+ <tr><td><code>GET /api/components/:name.js</code></td><td>public</td><td>Compiled browser module (what pages load).</td></tr>
220
+ <tr><td><code>POST /api/components/compile</code></td><td>update</td><td>Transient compile for the live preview (nothing written).</td></tr>
221
+ <tr><td><code>PUT /api/components/:name</code></td><td>update</td><td>Create or update (compiles first; errors returned, file untouched on failure).</td></tr>
222
+ <tr><td><code>DELETE /api/components/:name</code></td><td>delete</td><td>Delete source + sidecar.</td></tr>
223
+ <tr><td><code>GET /api/components/:name/export</code></td><td>read</td><td>Download a <code>.dmcomponent.json</code> bundle.</td></tr>
224
+ <tr><td><code>POST /api/components/import</code></td><td>update</td><td>Import a bundle (<code>?overwrite=true</code> to replace).</td></tr>
225
+ </tbody>
226
+ </table>
227
+
228
+ <p>A bundle is a small JSON envelope you can move between sites (Export / Import buttons on the
229
+ <a href="#/components">Components</a> page):</p>
230
+ <pre class="code-block"><code>{ "format": 1, "name": "counter", "source": "&lt;template&gt;…", "bundled": false }</code></pre>
231
+
232
+ <h3>Bundled &amp; plugin components</h3>
233
+ <p>The <code>bundled</code> flag marks a component as part of fresh-install seed data. Plugins may
234
+ also contribute components via the <code>registerComponent()</code> hook — these appear in the list
235
+ with a <span class="badge badge-secondary">plugin</span> badge, are served from memory, and are
236
+ <strong>read-only</strong> from the admin (you can't edit or delete them, and you can't save a
237
+ component whose name a plugin already owns).</p>
238
+
239
+ <hr>
240
+
241
+ <h2>The five seeded starter components</h2>
242
+ <p>Fresh installs ship with these (never overwritten if you've edited them) — open any in
243
+ <a href="#/components">Components</a> to read the full source:</p>
244
+ <ul>
245
+ <li><code>counter</code> — state, methods and lifecycle in one small example</li>
246
+ <li><code>copy-button</code> — an async method using the clipboard API</li>
247
+ <li><code>reveal-panel</code> — conditional rendering plus a <code>&lt;slot&gt;</code></li>
248
+ <li><code>testimonial-card</code> — presentational only (empty <code>export default {}</code>)</li>
249
+ <li><code>countdown-timer</code> — an interval started in <code>onMount</code> and cleared in <code>onUnmount</code></li>
250
+ </ul>
251
+
252
+ <hr>
253
+ <p class="text-muted" style="font-size:.9rem;">
254
+ Next: <a href="#/docs/components-howto">How-To recipes →</a>
255
+ </p>
256
+
257
+ </div>
258
+ </div>
259
+ </div>
@@ -0,0 +1,167 @@
1
+ <div class="view-header">
2
+ <h1><span data-icon="component"></span> Components — Rules</h1>
3
+ <a href="#/docs/components" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> Reference</a>
4
+ </div>
5
+
6
+ <div class="row">
7
+ <div class="col-12">
8
+ <div class="docs-body">
9
+
10
+ <nav class="alert alert-info" style="display:flex;gap:1rem;flex-wrap:wrap;align-items:center;">
11
+ <strong>Components docs:</strong>
12
+ <a href="#/docs/components">Reference</a>
13
+ <a href="#/docs/components-howto">How-To</a>
14
+ <a href="#/docs/components-walkthrough">Walkthrough</a>
15
+ <a href="#/docs/components-rules">Rules</a>
16
+ </nav>
17
+
18
+ <p class="lead">The constraints the compiler enforces, and the handful of behaviours that commonly
19
+ trip people up. Everything here is validated at save time — a component that breaks a rule never
20
+ reaches disk.</p>
21
+
22
+ <hr>
23
+
24
+ <h2>Naming</h2>
25
+ <ul>
26
+ <li>Names must match <code>^[a-z][a-z0-9-]*$</code> — start with a lowercase letter, then lowercase
27
+ letters, digits and hyphens.</li>
28
+ <li>Valid: <code>counter</code>, <code>copy-button</code>, <code>price-tag-2</code>. Invalid:
29
+ <code>Counter</code>, <code>1counter</code>, <code>my_component</code>, <code>../etc/passwd</code>.</li>
30
+ <li>The name becomes the element tag <code>&lt;dm-<em>name</em>&gt;</code> and is <strong>permanent</strong> —
31
+ it can't be changed after creation. To rename, create a new component and delete the old one.</li>
32
+ </ul>
33
+
34
+ <hr>
35
+
36
+ <h2>Required blocks</h2>
37
+ <ul>
38
+ <li><code>&lt;template&gt;</code>, <code>&lt;props&gt;</code> and <code>&lt;script&gt;</code> are
39
+ required. A missing one fails with <code>MISSING_BLOCK</code>.</li>
40
+ <li><code>&lt;style&gt;</code> is optional and defaults to empty.</li>
41
+ <li>Blocks are matched by their tags and may appear in any order.</li>
42
+ </ul>
43
+
44
+ <hr>
45
+
46
+ <h2>Props must be a typed JSON object</h2>
47
+ <ul>
48
+ <li>The <code>&lt;props&gt;</code> body must be valid JSON and an <strong>object</strong> (not an
49
+ array, not a bare value) — otherwise <code>INVALID_PROPS_JSON</code>.</li>
50
+ <li>Every prop must declare a <code>type</code> that is one of <code>string</code>,
51
+ <code>number</code>, <code>boolean</code>, <code>array</code>, <code>object</code>. Anything else
52
+ (or a missing type) fails validation.</li>
53
+ <li><code>default</code> and <code>label</code> are optional. <code>label</code> only affects the
54
+ editor's preview panel.</li>
55
+ </ul>
56
+
57
+ <hr>
58
+
59
+ <h2>The script must <code>export default</code> an object</h2>
60
+ <ul>
61
+ <li>The <code>&lt;script&gt;</code> body must be exactly one <code>export default { … };</code>
62
+ statement — the compiler strips that prefix and embeds the object. No <code>export default</code>
63
+ means <code>SCRIPT_MISSING_EXPORT</code>.</li>
64
+ <li>Use an object expression — not a class, not multiple exports. An empty
65
+ <code>export default {};</code> is valid for a purely presentational component.</li>
66
+ </ul>
67
+
68
+ <hr>
69
+
70
+ <h2>Gotchas worth memorising</h2>
71
+
72
+ <div class="alert alert-warning">
73
+ <strong>1. <code>data()</code> can't see props.</strong> It's called with no <code>this</code>
74
+ binding, so <code>this.props</code> is unavailable there. Initialise prop-derived state inside
75
+ <code>onMount()</code> (which <em>is</em> bound) using <code>this.set(...)</code>.
76
+ </div>
77
+
78
+ <div class="alert alert-warning">
79
+ <strong>2. Boolean coercion is string-based.</strong> Attribute values are strings. A boolean prop
80
+ is true only for the string <code>"true"</code>; <code>showReset="false"</code> is still a non-empty
81
+ string and coerces to <code>true</code>. To switch a boolean off, set it to <code>"true"</code> or
82
+ omit the attribute entirely.
83
+ </div>
84
+
85
+ <div class="alert alert-warning">
86
+ <strong>3. Attribute casing.</strong> On the raw custom element, camelCase props are written as
87
+ <code>kebab-case</code> attributes — <code>showReset</code> becomes <code>show-reset</code>. The
88
+ <code>[component]</code> shortcode accepts the prop name as written.
89
+ </div>
90
+
91
+ <div class="alert alert-warning">
92
+ <strong>4. Array / object props are JSON in the attribute.</strong> Pass them as a JSON string —
93
+ e.g. <code>items='["a","b"]'</code> — and the runtime parses them to the declared type.
94
+ </div>
95
+
96
+ <div class="alert alert-warning">
97
+ <strong>5. Listen on the shadow root.</strong> A component's rendered markup lives in
98
+ <code>this.el.shadowRoot</code>. Attach delegated listeners there (one listener, switch on
99
+ <code>data-action</code>) rather than to <code>document</code>.
100
+ </div>
101
+
102
+ <div class="alert alert-warning">
103
+ <strong>6. Clean up in <code>onUnmount()</code>.</strong> Anything you start in <code>onMount</code>
104
+ — intervals, timeouts, external listeners — must be torn down in <code>onUnmount</code> so it
105
+ doesn't leak when the element is removed.
106
+ </div>
107
+
108
+ <hr>
109
+
110
+ <h2>Plugin-owned components are read-only</h2>
111
+ <ul>
112
+ <li>Components contributed by a plugin show a <span class="badge badge-secondary">plugin</span>
113
+ badge, are served from memory, and <strong>cannot be edited or deleted</strong> from the admin
114
+ (attempting it returns <code>PLUGIN_OWNED</code>).</li>
115
+ <li>You also can't save a disk component whose name a plugin already owns — pick a different name.</li>
116
+ </ul>
117
+
118
+ <hr>
119
+
120
+ <h2>Import / export constraints</h2>
121
+ <ul>
122
+ <li>A bundle must have a numeric <code>format</code> no newer than the CMS supports, plus
123
+ <code>name</code> and <code>source</code> strings — otherwise <code>INVALID_BUNDLE</code>.</li>
124
+ <li>Importing over an existing name returns <code>CONFLICT</code> unless you confirm the overwrite
125
+ (the Import button prompts you).</li>
126
+ <li>Imported source is compiled like any save, so a broken bundle is rejected with the same errors.</li>
127
+ </ul>
128
+
129
+ <hr>
130
+
131
+ <h2>Rendering &amp; safety notes</h2>
132
+ <ul>
133
+ <li><code>{{ }}</code> interpolation is HTML-escaped, so prop values can't inject markup.</li>
134
+ <li>The markdown sanitiser keeps a live allowlist of <code>dm-*</code> tags, refreshed whenever you
135
+ save or delete a component — a component you just created is immediately usable in page content.
136
+ <code>on*</code> handler attributes and <code>javascript:</code> URLs are still stripped globally.</li>
137
+ <li>Saving recompiles and invalidates the cached module, so edits take effect on the next page load
138
+ without a server restart.</li>
139
+ </ul>
140
+
141
+ <hr>
142
+
143
+ <h2>Compiler error codes at a glance</h2>
144
+ <table class="table table-sm">
145
+ <thead>
146
+ <tr><th>Code</th><th>Meaning</th></tr>
147
+ </thead>
148
+ <tbody>
149
+ <tr><td><code>INVALID_NAME</code></td><td>Name doesn't match the naming rule.</td></tr>
150
+ <tr><td><code>MISSING_BLOCK</code></td><td>A required <code>&lt;template&gt;</code> / <code>&lt;props&gt;</code> / <code>&lt;script&gt;</code> block is absent.</td></tr>
151
+ <tr><td><code>INVALID_PROPS_JSON</code></td><td>Props aren't a JSON object, or a prop has an invalid/missing type.</td></tr>
152
+ <tr><td><code>SCRIPT_MISSING_EXPORT</code></td><td>The script has no <code>export default</code> object.</td></tr>
153
+ <tr><td><code>PLUGIN_OWNED</code></td><td>Tried to edit/delete (or shadow) a plugin-contributed component.</td></tr>
154
+ <tr><td><code>INVALID_BUNDLE</code> / <code>CONFLICT</code></td><td>Import bundle malformed, or a name collision needs overwrite confirmation.</td></tr>
155
+ </tbody>
156
+ </table>
157
+
158
+ <hr>
159
+ <p class="text-muted" style="font-size:.9rem;">
160
+ Back to <a href="#/docs/components">Reference</a> &nbsp;·&nbsp;
161
+ <a href="#/docs/components-howto">How-To</a> &nbsp;·&nbsp;
162
+ <a href="#/docs/components-walkthrough">Walkthrough</a>
163
+ </p>
164
+
165
+ </div>
166
+ </div>
167
+ </div>
@@ -0,0 +1,167 @@
1
+ <div class="view-header">
2
+ <h1><span data-icon="component"></span> Components — Walkthrough</h1>
3
+ <a href="#/components/new" class="btn btn-primary btn-sm"><span data-icon="plus"></span> New Component</a>
4
+ </div>
5
+
6
+ <div class="row">
7
+ <div class="col-12">
8
+ <div class="docs-body">
9
+
10
+ <nav class="alert alert-info" style="display:flex;gap:1rem;flex-wrap:wrap;align-items:center;">
11
+ <strong>Components docs:</strong>
12
+ <a href="#/docs/components">Reference</a>
13
+ <a href="#/docs/components-howto">How-To</a>
14
+ <a href="#/docs/components-walkthrough">Walkthrough</a>
15
+ <a href="#/docs/components-rules">Rules</a>
16
+ </nav>
17
+
18
+ <p class="lead">
19
+ We'll build one component end-to-end: a <strong>star rating</strong> widget you can drop onto any
20
+ page with <code>[component name="star-rating" max="5" value="3" /]</code>. By the end you'll have
21
+ used every section of a <code>.dmc</code> file, reactive state, an event listener, a custom event,
22
+ and the live preview. Follow along in a real <strong>New Component</strong> editor.
23
+ </p>
24
+
25
+ <hr>
26
+
27
+ <h2>Step 1 — Create the component</h2>
28
+ <p>Open <a href="#/components">Data → Components</a> → <strong>New Component</strong> and name it
29
+ <code>star-rating</code>. Remember: lowercase + hyphens, and the name is permanent because it
30
+ becomes the element tag <code>&lt;dm-star-rating&gt;</code>. The four source tabs
31
+ (<code>&lt;template&gt;</code>, <code>&lt;props&gt;</code>, <code>&lt;script&gt;</code>,
32
+ <code>&lt;style&gt;</code>) are on the left; a live preview is on the right.</p>
33
+
34
+ <hr>
35
+
36
+ <h2>Step 2 — Declare the props</h2>
37
+ <p>Start with the inputs, because they shape everything else. A rating needs a maximum number of
38
+ stars and a current value. Put this in the <code>&lt;props&gt;</code> tab:</p>
39
+ <pre class="code-block"><code>{
40
+ "max": { "type": "number", "default": 5, "label": "Number of stars" },
41
+ "value": { "type": "number", "default": 0, "label": "Current rating" },
42
+ "readonly": { "type": "boolean", "default": false, "label": "Read-only (display only)" }
43
+ }</code></pre>
44
+ <p>These appear in the preview's <em>Preview props</em> panel, so you can poke values as you build.</p>
45
+
46
+ <hr>
47
+
48
+ <h2>Step 3 — Write the template</h2>
49
+ <p>We render <code>max</code> stars and mark each as filled if its index is below the current rating.
50
+ Because <code>{{#each}}</code> needs a list, we'll build a <code>stars</code> array in state
51
+ (next step) where each item knows whether it's <code>on</code>. For now, the markup:</p>
52
+ <pre class="code-block"><code>&lt;template&gt;
53
+ &lt;div class="dm-stars" role="img" aria-label="{{value}} of {{max}}"&gt;
54
+ {{#each stars}}
55
+ &lt;button class="star {{#if on}}on{{/if}}" data-action="rate" data-index="{{n}}"&gt;★&lt;/button&gt;
56
+ {{/each}}
57
+ &lt;/div&gt;
58
+ &lt;/template&gt;</code></pre>
59
+ <p>Each star is a button carrying its 1-based position in <code>data-index</code>, so a single click
60
+ handler can read which star was pressed.</p>
61
+
62
+ <hr>
63
+
64
+ <h2>Step 4 — Add the script (state + behaviour)</h2>
65
+ <p>Three jobs: build the <code>stars</code> array from <code>value</code>, rebuild it whenever the
66
+ rating changes, and handle clicks. Remember <code>data()</code> can't see props, so we seed state
67
+ in <code>onMount()</code>.</p>
68
+ <pre class="code-block"><code>&lt;script&gt;
69
+ export default {
70
+ data() { return { stars: [], value: 0 }; },
71
+
72
+ methods: {
73
+ // Build the star list for a given rating.
74
+ render(value) {
75
+ const max = this.props.max;
76
+ const stars = [];
77
+ for (let n = 1; n &lt;= max; n++) stars.push({ n, on: n &lt;= value });
78
+ this.set({ stars, value });
79
+ },
80
+ rate(n) {
81
+ if (this.props.readonly) return;
82
+ this.render(n);
83
+ // Tell the outside world the rating changed.
84
+ this.el.dispatchEvent(new CustomEvent('rating-change', {
85
+ detail: { value: n }, bubbles: true
86
+ }));
87
+ }
88
+ },
89
+
90
+ onMount() {
91
+ this.render(this.props.value);
92
+ this.el.shadowRoot.addEventListener('click', (e) =&gt; {
93
+ const btn = e.target.closest('[data-action="rate"]');
94
+ if (btn) this.rate(Number(btn.dataset.index));
95
+ });
96
+ }
97
+ };
98
+ &lt;/script&gt;</code></pre>
99
+ <p>Note the patterns from the <a href="#/docs/components">Reference</a>: one delegated listener on
100
+ <code>shadowRoot</code>, <code>this.set()</code> to re-render, <code>this.props</code> for inputs,
101
+ and a bubbling <code>CustomEvent</code> so a page can react to the chosen rating.</p>
102
+
103
+ <hr>
104
+
105
+ <h2>Step 5 — Style it (scoped)</h2>
106
+ <pre class="code-block"><code>&lt;style&gt;
107
+ .dm-stars { display: inline-flex; gap: .15rem; }
108
+ .dm-stars .star {
109
+ background: none; border: 0; cursor: pointer;
110
+ font-size: 1.5rem; line-height: 1; color: #d0d0d0; padding: 0;
111
+ }
112
+ .dm-stars .star.on { color: #f5b301; }
113
+ &lt;/style&gt;</code></pre>
114
+ <p>These selectors are scoped to the component, so <code>.star</code> won't collide with anything else
115
+ on the page.</p>
116
+
117
+ <hr>
118
+
119
+ <h2>Step 6 — Watch the preview, then save</h2>
120
+ <p>As you typed, the editor recompiled and re-mounted the component in the preview iframe. Toggle
121
+ <code>readonly</code> and change <code>value</code> in the <em>Preview props</em> panel to sanity-check
122
+ both modes. When it looks right, hit <strong>Save Component</strong> — the source compiles before it
123
+ is written, so a typo surfaces as a clear error rather than a broken page.</p>
124
+
125
+ <hr>
126
+
127
+ <h2>Step 7 — Use it on a page</h2>
128
+ <p>Display-only, in any Markdown page:</p>
129
+ <pre class="code-block"><code>[component name="star-rating" max="5" value="4" readonly="true" /]</code></pre>
130
+ <p>Interactive, reacting to the custom event with a little page script:</p>
131
+ <pre class="code-block"><code>&lt;dm-star-rating max="5" value="0" id="r1"&gt;&lt;/dm-star-rating&gt;
132
+ &lt;script&gt;
133
+ document.getElementById('r1').addEventListener('rating-change', (e) =&gt; {
134
+ console.log('User picked', e.detail.value);
135
+ });
136
+ &lt;/script&gt;</code></pre>
137
+
138
+ <hr>
139
+
140
+ <h2>Step 8 — Share it</h2>
141
+ <p>Back on <a href="#/components">Components</a>, the <span data-icon="download"></span> Export button
142
+ gives you <code>star-rating.dmcomponent.json</code> — import that on another Domma site to reuse the
143
+ widget verbatim.</p>
144
+
145
+ <hr>
146
+
147
+ <h2>What you used</h2>
148
+ <ul>
149
+ <li><strong>Props</strong> with types + defaults, coerced from attributes</li>
150
+ <li><strong>Template</strong> interpolation, <code>{{#each}}</code> and <code>{{#if}}</code></li>
151
+ <li><strong>State</strong> via <code>data()</code> + <code>this.set()</code>, seeded from props in <code>onMount()</code></li>
152
+ <li>A single delegated <strong>shadow-root listener</strong> and <code>data-*</code> dispatch</li>
153
+ <li>A bubbling <strong>CustomEvent</strong> for page-level integration</li>
154
+ <li><strong>Scoped CSS</strong>, the live preview, and export/import</li>
155
+ </ul>
156
+
157
+ <p>For the rules that keep components well-behaved — naming, the <code>data()</code>-can't-see-props
158
+ gotcha, boolean coercion, plugin-owned names — read <a href="#/docs/components-rules">Rules</a>.</p>
159
+
160
+ <hr>
161
+ <p class="text-muted" style="font-size:.9rem;">
162
+ Next: <a href="#/docs/components-rules">Rules &amp; gotchas →</a>
163
+ </p>
164
+
165
+ </div>
166
+ </div>
167
+ </div>