domma-cms 0.13.7 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/css/admin.css +1 -1
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +2 -2
- package/admin/js/config/sidebar-config.js +1 -1
- package/admin/js/lib/markdown-toolbar.js +24 -18
- package/admin/js/lib/scribe-composer.js +4 -0
- package/admin/js/lib/simple-editor.js +49 -0
- package/admin/js/templates/block-editor.html +76 -18
- package/admin/js/templates/blocks.html +18 -8
- package/admin/js/templates/component-editor.html +141 -0
- package/admin/js/templates/components.html +18 -0
- package/admin/js/views/block-editor-enhance.js +1 -0
- package/admin/js/views/block-editor.js +8 -8
- package/admin/js/views/blocks.js +11 -4
- package/admin/js/views/component-editor.js +28 -0
- package/admin/js/views/components.js +11 -0
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/page-editor.js +6 -6
- package/admin/js/views/pages.js +5 -2
- package/config/site.json +1 -1
- package/package.json +2 -2
- package/public/css/site.css +1 -1
- package/server/routes/api/blocks.js +128 -60
- package/server/routes/api/components.js +115 -0
- package/server/routes/api/pages.js +135 -132
- package/server/routes/api/versions.js +16 -0
- package/server/server.js +6 -0
- package/server/services/blocks.js +387 -284
- package/server/services/components.js +653 -0
- package/server/services/content.js +334 -334
- package/server/services/hooks.js +28 -0
- package/server/services/markdown.js +2836 -2629
- package/server/services/permissionRegistry.js +13 -0
- package/server/services/renderer.js +10 -2
- package/server/services/versions.js +37 -0
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
2
|
<h1><span data-icon="box"></span> <span id="block-editor-title">New Block</span></h1>
|
|
3
|
-
<div style="display:flex;gap:.5rem;">
|
|
3
|
+
<div style="display:flex;gap:.5rem;align-items:center;">
|
|
4
|
+
<select id="block-quick-switch" class="form-input form-input--sm"
|
|
5
|
+
title="Switch to another block"
|
|
6
|
+
style="display:none;max-width:260px;">
|
|
7
|
+
<option value="">Switch to…</option>
|
|
8
|
+
</select>
|
|
4
9
|
<a href="#/blocks" class="btn btn-ghost btn-sm">
|
|
5
10
|
<span data-icon="arrow-left"></span> All Blocks
|
|
6
11
|
</a>
|
|
@@ -56,7 +61,7 @@
|
|
|
56
61
|
</button>
|
|
57
62
|
<span class="editor-toolbar-sep"></span>
|
|
58
63
|
<button type="button" class="btn btn-ghost btn-sm" data-action="select-all" title="Select All (Ctrl+A)">
|
|
59
|
-
<span data-icon="
|
|
64
|
+
<span data-icon="maximise-2"></span>
|
|
60
65
|
</button>
|
|
61
66
|
<button type="button" class="btn btn-ghost btn-sm" data-action="format" title="Format HTML">
|
|
62
67
|
<span data-icon="code"></span> Format
|
|
@@ -67,10 +72,10 @@
|
|
|
67
72
|
<span id="block-cursor-pos"
|
|
68
73
|
style="font-size:.75rem;color:var(--dm-text-muted,#888);margin-right:.5rem;">Ln 1, Col 1</span>
|
|
69
74
|
<span class="editor-toolbar-sep"></span>
|
|
70
|
-
<button type="button" class="editor-view-btn
|
|
75
|
+
<button type="button" class="editor-view-btn" data-mode="write" title="Edit only">
|
|
71
76
|
<span data-icon="edit-2"></span>
|
|
72
77
|
</button>
|
|
73
|
-
<button type="button" class="editor-view-btn" data-mode="split" title="Split view">
|
|
78
|
+
<button type="button" class="editor-view-btn active" data-mode="split" title="Split view">
|
|
74
79
|
<span data-icon="columns"></span>
|
|
75
80
|
</button>
|
|
76
81
|
<button type="button" class="editor-view-btn" data-mode="preview" title="Preview only">
|
|
@@ -80,14 +85,14 @@
|
|
|
80
85
|
</div>
|
|
81
86
|
|
|
82
87
|
<!-- Editor body — split pane -->
|
|
83
|
-
<div id="block-editor-body" class="editor-body editor-mode-
|
|
88
|
+
<div id="block-editor-body" class="editor-body editor-mode-split" style="min-height:460px;">
|
|
84
89
|
|
|
85
90
|
<!-- Write pane -->
|
|
86
91
|
<div class="editor-pane editor-pane--write" style="flex-direction:row;overflow:hidden;">
|
|
87
92
|
<div id="block-line-numbers" class="editor-line-numbers">1</div>
|
|
88
93
|
<textarea id="block-content" class="editor-textarea"
|
|
89
94
|
spellcheck="false"
|
|
90
|
-
placeholder="<div class="card mb-3"> <div class="card-body"> <h3
|
|
95
|
+
placeholder="<div class="card mb-3"> <div class="card-body"> <h3>{{title}}</h3> <p>{{message}}</p> </div> </div>"></textarea>
|
|
91
96
|
</div>
|
|
92
97
|
|
|
93
98
|
<div class="editor-divider"></div>
|
|
@@ -112,6 +117,34 @@
|
|
|
112
117
|
</div>
|
|
113
118
|
</div>
|
|
114
119
|
|
|
120
|
+
<!-- Custom CSS -->
|
|
121
|
+
<div class="card card-collapsible mb-3">
|
|
122
|
+
<div class="card-header">
|
|
123
|
+
<h2>Custom CSS</h2>
|
|
124
|
+
<span class="card-collapse-icon" data-icon="chevron-down"></span>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="card-body">
|
|
127
|
+
<p style="font-size:.85rem;color:var(--dm-text-muted,#888);margin:0 0 .5rem;line-height:1.5;">
|
|
128
|
+
Your CSS is automatically scoped to
|
|
129
|
+
<code class="dm-code-inline" id="block-css-scope-hint" style="font-size:.9em;">[data-block="…"]</code>.
|
|
130
|
+
Use CSS nesting for child rules.
|
|
131
|
+
<strong>Note:</strong>
|
|
132
|
+
<code class="dm-code-inline" style="font-size:.9em;">@keyframes</code>,
|
|
133
|
+
<code class="dm-code-inline" style="font-size:.9em;">@font-face</code>, and
|
|
134
|
+
<code class="dm-code-inline" style="font-size:.9em;">@import</code>
|
|
135
|
+
cannot be scoped — use site-wide Custom CSS in Settings for those.
|
|
136
|
+
</p>
|
|
137
|
+
<textarea id="block-css"
|
|
138
|
+
class="editor-textarea"
|
|
139
|
+
spellcheck="false"
|
|
140
|
+
style="font-family:var(--dm-font-mono,monospace);min-height:240px;width:100%;"
|
|
141
|
+
placeholder="/* Example:
|
|
142
|
+
color: var(--dm-primary);
|
|
143
|
+
h2 { font-size: 1.5rem; }
|
|
144
|
+
.highlight { background: yellow; } */"></textarea>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
115
148
|
<!-- Placeholder Reference -->
|
|
116
149
|
<div class="card card-collapsible mb-3 card-collapsed">
|
|
117
150
|
<div class="card-header">
|
|
@@ -119,7 +152,7 @@
|
|
|
119
152
|
<span class="card-collapse-icon" data-icon="chevron-down"></span>
|
|
120
153
|
</div>
|
|
121
154
|
<div class="card-body" style="font-size:.85rem;line-height:1.8;">
|
|
122
|
-
<p>Use <code class="dm-code-inline"
|
|
155
|
+
<p>Use <code class="dm-code-inline">{{fieldName}}</code> to insert entry field values. Values are HTML-escaped
|
|
123
156
|
automatically to prevent XSS.</p>
|
|
124
157
|
<table class="table" style="max-width:520px;margin:.75rem 0;">
|
|
125
158
|
<thead>
|
|
@@ -130,33 +163,58 @@
|
|
|
130
163
|
</thead>
|
|
131
164
|
<tbody>
|
|
132
165
|
<tr>
|
|
133
|
-
<td><code class="dm-code-inline"
|
|
134
|
-
<td>Any entry field — e.g. <code class="dm-code-inline"
|
|
135
|
-
<code class="dm-code-inline"
|
|
166
|
+
<td><code class="dm-code-inline">{{fieldName}}</code></td>
|
|
167
|
+
<td>Any entry field — e.g. <code class="dm-code-inline">{{title}}</code>, <code class="dm-code-inline">{{rating}}</code>,
|
|
168
|
+
<code class="dm-code-inline">{{message}}</code></td>
|
|
136
169
|
</tr>
|
|
137
170
|
<tr>
|
|
138
|
-
<td><code class="dm-code-inline"
|
|
171
|
+
<td><code class="dm-code-inline">{{_id}}</code></td>
|
|
139
172
|
<td>Entry UUID</td>
|
|
140
173
|
</tr>
|
|
141
174
|
<tr>
|
|
142
|
-
<td><code class="dm-code-inline"
|
|
143
|
-
<td>Creation timestamp (ISO 8601)</td>
|
|
175
|
+
<td><code class="dm-code-inline">{{_createdAt}}</code></td>
|
|
176
|
+
<td>Creation timestamp (ISO 8601 by default)</td>
|
|
177
|
+
</tr>
|
|
178
|
+
<tr>
|
|
179
|
+
<td><code class="dm-code-inline">{{_updatedAt}}</code></td>
|
|
180
|
+
<td>Last-updated timestamp (ISO 8601 by default)</td>
|
|
144
181
|
</tr>
|
|
145
182
|
<tr>
|
|
146
|
-
<td><code class="dm-code-inline"
|
|
147
|
-
<td>
|
|
183
|
+
<td><code class="dm-code-inline">{{field:FORMAT}}</code></td>
|
|
184
|
+
<td>Any placeholder with a <code class="dm-code-inline">:FORMAT</code> suffix
|
|
185
|
+
applies moment-style date formatting when the value parses as a date.
|
|
186
|
+
E.g. <code class="dm-code-inline">{{_createdAt:DD MMM YYYY}}</code> →
|
|
187
|
+
<em>05 Jan 2026</em>, or
|
|
188
|
+
<code class="dm-code-inline">{{_createdAt:HH:mm}}</code> →
|
|
189
|
+
<em>14:30</em>.
|
|
190
|
+
</td>
|
|
148
191
|
</tr>
|
|
149
192
|
</tbody>
|
|
150
193
|
</table>
|
|
194
|
+
<p style="margin-top:.5rem;"><strong>Format tokens:</strong>
|
|
195
|
+
<code class="dm-code-inline">YYYY</code> <code class="dm-code-inline">YY</code>
|
|
196
|
+
<code class="dm-code-inline">MMMM</code> <code class="dm-code-inline">MMM</code>
|
|
197
|
+
<code class="dm-code-inline">MM</code> <code class="dm-code-inline">M</code>
|
|
198
|
+
<code class="dm-code-inline">DD</code> <code class="dm-code-inline">D</code>
|
|
199
|
+
<code class="dm-code-inline">dddd</code> <code class="dm-code-inline">ddd</code>
|
|
200
|
+
<code class="dm-code-inline">HH</code> <code class="dm-code-inline">H</code>
|
|
201
|
+
<code class="dm-code-inline">hh</code> <code class="dm-code-inline">h</code>
|
|
202
|
+
<code class="dm-code-inline">mm</code> <code class="dm-code-inline">m</code>
|
|
203
|
+
<code class="dm-code-inline">ss</code> <code class="dm-code-inline">s</code>
|
|
204
|
+
<code class="dm-code-inline">A</code> <code class="dm-code-inline">a</code>
|
|
205
|
+
— any literal character that isn't one of these tokens passes through unchanged
|
|
206
|
+
(so <code class="dm-code-inline">DD/MM/YYYY</code>, <code class="dm-code-inline">HH:mm</code>,
|
|
207
|
+
and <code class="dm-code-inline">dddd D MMMM YYYY</code> all work).
|
|
208
|
+
</p>
|
|
151
209
|
<p><strong>Use in a shortcode:</strong></p>
|
|
152
210
|
<pre class="dm-code-block">[collection slug="feedback" display="block" block="feedback-card" /]
|
|
153
211
|
[view slug="my-view" display="block" block="feedback-card" /]</pre>
|
|
154
212
|
<p style="margin-top:.75rem;"><strong>Example block template:</strong></p>
|
|
155
213
|
<pre class="dm-code-block"><div class="card mb-3">
|
|
156
214
|
<div class="card-body">
|
|
157
|
-
<h3>
|
|
158
|
-
<p>
|
|
159
|
-
<small>Rating:
|
|
215
|
+
<h3>{{name}}</h3>
|
|
216
|
+
<p>{{message}}</p>
|
|
217
|
+
<small>Rating: {{rating}} &mdash; {{_createdAt}}</small>
|
|
160
218
|
</div>
|
|
161
219
|
</div></pre>
|
|
162
220
|
</div>
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="box"></span> Blocks</h1>
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<
|
|
1
|
+
<div class="view-header">
|
|
2
|
+
<h1><span data-icon="box"></span> Blocks</h1>
|
|
3
|
+
<div style="display:flex;gap:.5rem;align-items:center;">
|
|
4
|
+
<button id="import-block-btn" class="btn btn-ghost"
|
|
5
|
+
title="Import a .dmblock.json bundle">
|
|
6
|
+
<span data-icon="upload"></span> Import
|
|
7
|
+
</button>
|
|
8
|
+
<a href="#/blocks/new" class="btn btn-primary">
|
|
9
|
+
<span data-icon="plus"></span> New Block
|
|
10
|
+
</a>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<!-- Hidden file picker driven by the Import button -->
|
|
15
|
+
<input id="import-block-file" type="file" accept=".dmblock.json,application/json"
|
|
16
|
+
style="display:none;">
|
|
17
|
+
|
|
18
|
+
<div id="blocks-table-container"></div>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<div class="view-header">
|
|
2
|
+
<h1><span data-icon="component"></span> <span id="component-editor-title">New Component</span></h1>
|
|
3
|
+
<div style="display:flex;gap:.5rem;align-items:center;">
|
|
4
|
+
<select id="component-quick-switch" class="form-input form-input--sm"
|
|
5
|
+
title="Switch to another component"
|
|
6
|
+
style="display:none;max-width:260px;">
|
|
7
|
+
<option value="">Switch to…</option>
|
|
8
|
+
</select>
|
|
9
|
+
<a href="#/components" class="btn btn-ghost btn-sm">
|
|
10
|
+
<span data-icon="arrow-left"></span> All Components
|
|
11
|
+
</a>
|
|
12
|
+
<button id="export-component-btn" class="btn btn-ghost btn-sm" style="display:none;" title="Export as .dmcomponent.json">
|
|
13
|
+
<span data-icon="download"></span> Export
|
|
14
|
+
</button>
|
|
15
|
+
<button id="save-component-btn" class="btn btn-primary">
|
|
16
|
+
<span data-icon="save"></span> Save Component
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- Name + meta -->
|
|
22
|
+
<div class="card card-collapsible mb-3">
|
|
23
|
+
<div class="card-header">
|
|
24
|
+
<h2>Component Name</h2>
|
|
25
|
+
<span class="card-collapse-icon" data-icon="chevron-down"></span>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="card-body" style="max-width:520px;">
|
|
28
|
+
<label class="form-label">Name <span style="color:var(--dm-danger,#f87171);">*</span></label>
|
|
29
|
+
<input id="component-name" type="text" class="form-input" placeholder="e.g. pricing-table">
|
|
30
|
+
<small class="text-muted">Registered as <code id="component-tag-hint"><dm-…></code>. Lowercase letters, digits, and hyphens. Cannot be changed after creation.</small>
|
|
31
|
+
<div class="mt-3">
|
|
32
|
+
<label class="form-check-label" title="Included in fresh installs via the seed script">
|
|
33
|
+
<input id="component-bundled" type="checkbox" class="form-check"> Bundled
|
|
34
|
+
</label>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<!-- Editor body — tabs on the left, preview on the right -->
|
|
40
|
+
<div class="card card-collapsible editor-card mb-3">
|
|
41
|
+
<div class="card-header">
|
|
42
|
+
<h2>Source</h2>
|
|
43
|
+
<span class="card-collapse-icon" data-icon="chevron-down"></span>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="card-body p-0">
|
|
46
|
+
|
|
47
|
+
<!-- Status / compile bar -->
|
|
48
|
+
<div id="component-status-bar" style="
|
|
49
|
+
display:flex;align-items:center;justify-content:space-between;gap:.75rem;
|
|
50
|
+
padding:.45rem .75rem;
|
|
51
|
+
border-bottom:1px solid var(--dm-border,rgba(255,255,255,.08));
|
|
52
|
+
background:var(--dm-surface-subtle,rgba(0,0,0,.05));
|
|
53
|
+
font-size:.78rem;">
|
|
54
|
+
<div>
|
|
55
|
+
<span id="component-compile-indicator"
|
|
56
|
+
style="display:inline-block;min-width:.7rem;height:.7rem;border-radius:50%;background:#aaa;margin-right:.4rem;"></span>
|
|
57
|
+
<span id="component-compile-status">Compiling…</span>
|
|
58
|
+
</div>
|
|
59
|
+
<span id="component-cursor-pos" style="display:none;"></span>
|
|
60
|
+
</div>
|
|
61
|
+
<pre id="component-compile-errors" hidden style="
|
|
62
|
+
margin:0;
|
|
63
|
+
padding:.75rem;
|
|
64
|
+
background:rgba(201,42,42,.08);
|
|
65
|
+
color:#c92a2a;
|
|
66
|
+
border-bottom:1px solid var(--dm-border,rgba(255,255,255,.08));
|
|
67
|
+
white-space:pre-wrap;
|
|
68
|
+
font-size:.78rem;
|
|
69
|
+
font-family:var(--dm-font-mono,monospace);"></pre>
|
|
70
|
+
|
|
71
|
+
<div style="display:flex;min-height:460px;">
|
|
72
|
+
|
|
73
|
+
<!-- Left: tabbed source editors -->
|
|
74
|
+
<div style="flex:1.4;display:flex;flex-direction:column;border-right:1px solid var(--dm-border,rgba(255,255,255,.08));">
|
|
75
|
+
|
|
76
|
+
<!-- Tab bar -->
|
|
77
|
+
<div id="component-tab-bar" role="tablist"
|
|
78
|
+
style="display:flex;gap:.15rem;padding:.25rem .5rem 0;border-bottom:1px solid var(--dm-border,rgba(255,255,255,.08));">
|
|
79
|
+
<button type="button" class="component-tab active" data-tab="template" role="tab" aria-selected="true"><template></button>
|
|
80
|
+
<button type="button" class="component-tab" data-tab="props" role="tab"><props></button>
|
|
81
|
+
<button type="button" class="component-tab" data-tab="script" role="tab"><script></button>
|
|
82
|
+
<button type="button" class="component-tab" data-tab="style" role="tab"><style></button>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<!-- One simple-editor mount per tab. CSS Grid stacks all four
|
|
86
|
+
panes in the same 1×1 cell so switching tabs is just a
|
|
87
|
+
`hidden` attribute flip. Vertical resize handle lets users
|
|
88
|
+
drag the editor as tall as they want. -->
|
|
89
|
+
<div id="component-editor-surface"
|
|
90
|
+
style="height:min(72vh, 860px);min-height:360px;resize:vertical;overflow:hidden;display:grid;grid-template-columns:1fr;grid-template-rows:1fr;">
|
|
91
|
+
<div id="component-src-template" class="component-tab-pane" data-pane="template"
|
|
92
|
+
style="grid-column:1/2;grid-row:1/2;min-height:0;overflow:hidden;"></div>
|
|
93
|
+
<div id="component-src-props" class="component-tab-pane" data-pane="props" hidden
|
|
94
|
+
style="grid-column:1/2;grid-row:1/2;min-height:0;overflow:hidden;"></div>
|
|
95
|
+
<div id="component-src-script" class="component-tab-pane" data-pane="script" hidden
|
|
96
|
+
style="grid-column:1/2;grid-row:1/2;min-height:0;overflow:hidden;"></div>
|
|
97
|
+
<div id="component-src-style" class="component-tab-pane" data-pane="style" hidden
|
|
98
|
+
style="grid-column:1/2;grid-row:1/2;min-height:0;overflow:hidden;"></div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<!-- Right: iframe preview + props panel -->
|
|
103
|
+
<div style="flex:1;display:flex;flex-direction:column;min-width:260px;">
|
|
104
|
+
|
|
105
|
+
<div style="padding:.4rem .75rem;font-size:.72rem;letter-spacing:.05em;text-transform:uppercase;color:var(--dm-text-muted,#888);border-bottom:1px solid var(--dm-border,rgba(255,255,255,.08));">
|
|
106
|
+
Live preview
|
|
107
|
+
</div>
|
|
108
|
+
<iframe id="component-preview-iframe"
|
|
109
|
+
src="/admin/preview/component-preview.html"
|
|
110
|
+
style="flex:1;min-height:200px;border:0;background:var(--dm-surface,#fff);"></iframe>
|
|
111
|
+
|
|
112
|
+
<div style="padding:.4rem .75rem;font-size:.72rem;letter-spacing:.05em;text-transform:uppercase;color:var(--dm-text-muted,#888);border-top:1px solid var(--dm-border,rgba(255,255,255,.08));border-bottom:1px solid var(--dm-border,rgba(255,255,255,.08));">
|
|
113
|
+
Preview props
|
|
114
|
+
</div>
|
|
115
|
+
<div id="component-preview-props-panel" style="padding:.75rem;overflow-y:auto;max-height:260px;">
|
|
116
|
+
<p class="text-muted" style="font-size:.8rem;margin:0;">Declare props in the <code><props></code> tab to populate this panel.</p>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<style>
|
|
125
|
+
.component-tab {
|
|
126
|
+
background:transparent;border:none;padding:.4rem .75rem;
|
|
127
|
+
font-size:.8rem;color:var(--dm-text-muted,#aaa);cursor:pointer;
|
|
128
|
+
border-bottom:2px solid transparent;margin-bottom:-1px;
|
|
129
|
+
font-family:var(--dm-font-mono,monospace);
|
|
130
|
+
}
|
|
131
|
+
.component-tab.active {
|
|
132
|
+
color:var(--dm-text,inherit);
|
|
133
|
+
border-bottom-color:var(--dm-primary,#5aa8ff);
|
|
134
|
+
font-weight:600;
|
|
135
|
+
}
|
|
136
|
+
.component-tab:hover:not(.active) { color:var(--dm-text,inherit); }
|
|
137
|
+
|
|
138
|
+
/* Layout-critical rules are inline on the elements above so they survive any
|
|
139
|
+
templater/sanitiser that may swallow <style> blocks. This one is cosmetic. */
|
|
140
|
+
.component-tab-pane[hidden] { display:none; }
|
|
141
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<div class="view-header">
|
|
2
|
+
<h1><span data-icon="component"></span> Components</h1>
|
|
3
|
+
<div style="display:flex;gap:.5rem;align-items:center;">
|
|
4
|
+
<button id="import-component-btn" class="btn btn-ghost"
|
|
5
|
+
title="Import a .dmcomponent.json bundle">
|
|
6
|
+
<span data-icon="upload"></span> Import
|
|
7
|
+
</button>
|
|
8
|
+
<a href="#/components/new" class="btn btn-primary">
|
|
9
|
+
<span data-icon="plus"></span> New Component
|
|
10
|
+
</a>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<!-- Hidden file picker driven by the Import button -->
|
|
15
|
+
<input id="import-component-file" type="file" accept=".dmcomponent.json,application/json"
|
|
16
|
+
style="display:none;">
|
|
17
|
+
|
|
18
|
+
<div id="components-table-container"></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{blockEditorView as r}from"./block-editor.js?v=7";import{createSimpleEditor as u}from"../lib/simple-editor.js";const p=r.onMount;r.onMount=async function(e){await p.call(this,e);const n=e.find(".editor-pane--write").get(0),l=e.find("#block-line-numbers").get(0),t=e.find("#block-content").get(0);if(!n||!t||n.dataset.dmEnhanced==="1")return;n.dataset.dmEnhanced="1",l&&(l.style.display="none"),t.style.display="none",n.style.display="block",n.style.position="relative",n.style.overflow="hidden";const i=document.createElement("div");i.style.cssText="width:100%;height:100%;",n.appendChild(i);const a=u(i,{initialValue:t.value,onChange:o=>{t.value!==o&&(t.value=o,t.dispatchEvent(new Event("input",{bubbles:!0})))}}),d=Object.getPrototypeOf(t),c=Object.getOwnPropertyDescriptor(d,"value");Object.defineProperty(t,"value",{configurable:!0,get(){return c.get.call(this)},set(o){c.set.call(this,o),a.getValue()!==o&&a.setValue(o??"")}});const s=e.find(".card.editor-card").get(0);s&&(s.style.height="min(78vh, 900px)",s.style.minHeight="520px"),g(e)};const f={icon:"box","facebook-icon":"facebook","instagram-icon":"instagram"};function g(e){const n=e.get?e.get(0):e;if(!n)return;const l=n.querySelectorAll("input[data-placeholder]");for(const t of l){const i=(t.dataset.placeholder||"").toLowerCase(),a=f[i];a&&(t.value&&!/^Sample /i.test(t.value)||(t.value=a,t.dispatchEvent(new Event("input",{bubbles:!0}))))}}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import{api as
|
|
2
|
-
`).length;
|
|
3
|
-
`),
|
|
4
|
-
`);
|
|
5
|
-
`,
|
|
6
|
-
`,
|
|
7
|
-
`+
|
|
8
|
-
`),i=e.selectionStart;e.value=l,e.selectionStart=e.selectionEnd=Math.min(i,l.length),e.dispatchEvent(new Event("input"))}const
|
|
1
|
+
import{api as g}from"../api.js";let u=null,f=!1;function h(){f=!0;const e=document.getElementById("block-quick-switch");e&&(e.disabled=!0)}function M(){f=!1;const e=document.getElementById("block-quick-switch");e&&(e.disabled=!1)}function x(e,t){if(e==null||e==="")return null;const n=e instanceof Date?e:new Date(e);if(isNaN(n.getTime()))return null;const s=(o,c=2)=>String(o).padStart(c,"0"),a=["January","February","March","April","May","June","July","August","September","October","November","December"],l=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],i={YYYY:()=>n.getFullYear(),YY:()=>s(n.getFullYear()%100),MMMM:()=>a[n.getMonth()],MMM:()=>a[n.getMonth()].slice(0,3),MM:()=>s(n.getMonth()+1),M:()=>n.getMonth()+1,DD:()=>s(n.getDate()),D:()=>n.getDate(),dddd:()=>l[n.getDay()],ddd:()=>l[n.getDay()].slice(0,3),HH:()=>s(n.getHours()),H:()=>n.getHours(),hh:()=>s((n.getHours()+11)%12+1),h:()=>(n.getHours()+11)%12+1,mm:()=>s(n.getMinutes()),m:()=>n.getMinutes(),ss:()=>s(n.getSeconds()),s:()=>n.getSeconds(),A:()=>n.getHours()<12?"AM":"PM",a:()=>n.getHours()<12?"am":"pm"};return t.replace(/YYYY|YY|MMMM|MMM|MM|M|DD|D|dddd|ddd|HH|H|hh|h|mm|m|ss|s|A|a/g,o=>String(i[o]()))}export const blockEditorView={templateUrl:"/admin/js/templates/block-editor.html",async onMount(e){I.register("edit-2",{viewBox:"0 0 24 24",path:"M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5z",stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),requestAnimationFrame(()=>{document.querySelectorAll('[data-icon="edit-2"]').forEach(a=>{if(a.tagName.toLowerCase()==="svg")return;const l=I.render("edit-2",{size:parseInt(a.dataset.iconSize,10)||24});l&&a.replaceWith(l)})}),u=null,f=!1;const t=window.location.hash.match(/\/blocks\/edit\/([^/?#]+)/);t&&(u=decodeURIComponent(t[1]));const n=e.find("#block-name").get(0),s=e.find("#block-content").get(0);if(u){e.find("#block-editor-title").text("Edit Block"),n&&(n.value=u,n.disabled=!0);try{const a=await g.blocks.get(u);s&&(s.value=a.content||""),e.find("#block-bundled").prop("checked",!!a.bundled),e.find("#block-css").val(a.css||"");try{const l=await g.blocks.list(),i=e.find("#block-quick-switch").get(0);i&&Array.isArray(l)&&l.length&&(l.slice().sort((o,c)=>o.name.localeCompare(c.name)).forEach(o=>{const c=document.createElement("option");c.value=o.name,c.textContent=o.name,o.name===u&&(c.selected=!0),i.appendChild(c)}),i.style.display="",i.addEventListener("change",function(){const o=this.value;o&&o!==u&&!f&&R.navigate(`/blocks/edit/${encodeURIComponent(o)}`)}))}catch{}}catch(a){E.toast(a.message||"Block not found.",{type:"error"}),R.navigate("/blocks");return}}s&&(C(s,e),$(s,e)),e.find("#save-block-btn").off("click").on("click",async()=>{await H(e)}),Domma.icons.scan()}};function C(e,t){const n=t.find("#block-line-numbers").get(0),s=t.find("#block-cursor-pos").get(0);function a(){const o=e.value.split(`
|
|
2
|
+
`).length;n.textContent=Array.from({length:o},(c,d)=>d+1).join(`
|
|
3
|
+
`),n.scrollTop=e.scrollTop}a(),e.addEventListener("input",a),e.addEventListener("scroll",()=>{n.scrollTop=e.scrollTop});function l(){if(!s)return;const o=e.value.slice(0,e.selectionStart).split(`
|
|
4
|
+
`);s.textContent=`Ln ${o.length}, Col ${o[o.length-1].length+1}`}e.addEventListener("keyup",l),e.addEventListener("click",l),e.addEventListener("keydown",o=>{if(o.key==="Tab"){o.preventDefault();const c=e.selectionStart,d=e.selectionEnd;if(!o.shiftKey)e.value=e.value.slice(0,c)+" "+e.value.slice(d),e.selectionStart=e.selectionEnd=c+2;else{const r=e.value.lastIndexOf(`
|
|
5
|
+
`,c-1)+1,m=e.value.slice(r,r+2),p=m===" "?2:m[0]===" "?1:0;p>0&&(e.value=e.value.slice(0,r)+e.value.slice(r+p),e.selectionStart=e.selectionEnd=Math.max(r,c-p))}e.dispatchEvent(new Event("input"))}if(o.key==="Enter"){o.preventDefault();const c=e.selectionStart,d=e.value.lastIndexOf(`
|
|
6
|
+
`,c-1)+1,r=e.value.slice(d,c).match(/^(\s*)/)[1];e.value=e.value.slice(0,c)+`
|
|
7
|
+
`+r+e.value.slice(e.selectionEnd),e.selectionStart=e.selectionEnd=c+1+r.length,e.dispatchEvent(new Event("input"))}});const i=t.find("#block-editor-toolbar").get(0);i&&i.addEventListener("click",o=>{const c=o.target.closest("[data-action]");c&&(L(c.dataset.action,e),e.focus())})}function L(e,t){switch(e){case"undo":document.execCommand("undo");break;case"redo":document.execCommand("redo");break;case"cut":t.selectionStart!==t.selectionEnd&&(navigator.clipboard.writeText(t.value.slice(t.selectionStart,t.selectionEnd)).catch(()=>{}),document.execCommand("cut"));break;case"copy":t.selectionStart!==t.selectionEnd&&navigator.clipboard.writeText(t.value.slice(t.selectionStart,t.selectionEnd)).catch(()=>{});break;case"paste":navigator.clipboard.readText().then(n=>{const s=t.selectionStart;t.value=t.value.slice(0,s)+n+t.value.slice(t.selectionEnd),t.selectionStart=t.selectionEnd=s+n.length,t.dispatchEvent(new Event("input"))}).catch(()=>E.toast("Use Ctrl+V to paste.",{type:"info"}));break;case"select-all":t.select();break;case"format":D(t);break}}function D(e){const t=e.value.match(/(<[^>]+>|[^<]+)/g)||[],n=[];let s=0;const a=" ";for(const o of t){const c=o.trim();if(c)if(c.startsWith("</"))s=Math.max(0,s-1),n.push(a.repeat(s)+c);else if(c.startsWith("<")&&!c.startsWith("<!")&&!c.endsWith("/>")){n.push(a.repeat(s)+c);const d=(c.match(/^<(\w+)/)||[])[1]||"";T.has(d.toLowerCase())||s++}else n.push(a.repeat(s)+c)}const l=n.join(`
|
|
8
|
+
`),i=e.selectionStart;e.value=l,e.selectionStart=e.selectionEnd=Math.min(i,l.length),e.dispatchEvent(new Event("input"))}const T=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]);function $(e,t){const n=t.find("#block-editor-body").get(0),s=t.find("#block-sample-data").get(0),a=t.find("#block-preview-output");t.find("[data-mode]").each(function(){this.addEventListener("click",()=>{t.find("[data-mode]").each(function(){this.classList.remove("active")}),this.classList.add("active"),n.className=`editor-body editor-mode-${this.dataset.mode}`,this.dataset.mode!=="write"&&l()})}),e.addEventListener("input",()=>{S(s,e),l(),h()});{const i=t.find("#block-css").get(0);i&&i.addEventListener("input",()=>{l(),h()});const o=t.find("#block-name").get(0);o&&o.addEventListener("input",()=>{l(),h()});const c=t.find("#block-bundled").get(0);c&&c.addEventListener("change",h)}e.value.trim()&&(S(s,e),l());function l(){if(n.classList.contains("editor-mode-write"))return;const i=e.value.replace(/\{\{([\w_]+)(?::([^{}]+))?\}\}/g,(p,b,k)=>{const y=s.querySelector(`[data-placeholder="${CSS.escape(b)}"]`)?.value??`[${b}]`;if(k){const w=x(y,k);if(w!==null)return v(w)}return v(y)}),o=(t.find("#block-name").val()||"preview").trim()||"preview",c=v(o),d=t.find("#block-css").val()||"",r=t.find("#block-css-scope-hint").get(0);r&&(r.textContent=`[data-block="${o}"]`);const m=d.trim()?`<style>[data-block="${c}"] { ${String(d).replace(/<\/style/gi,"<\\/style")} }</style>`:"";a.html(m+`<div data-block="${c}">${i}</div>`,{safe:!1}),Domma.icons.scan(a.get(0))}s._renderPreview=l}function S(e,t){const n=q(t.value),s=new Set([...e.querySelectorAll("[data-placeholder]")].map(a=>a.dataset.placeholder));if(n.size>0&&e.querySelector("p.text-muted")?.remove(),n.forEach(a=>{s.has(a)||A(e,a)}),e.querySelectorAll("[data-placeholder]").forEach(a=>{n.has(a.dataset.placeholder)||a.closest(".block-sample-row")?.remove()}),n.size===0&&!e.querySelector("p.text-muted")){const a=document.createElement("p");a.className="text-muted",a.style.cssText="font-size:.8rem;margin:0;",a.textContent="No {{placeholders}} detected in template.",e.appendChild(a)}}function A(e,t){const n=document.createElement("div");n.className="block-sample-row",n.style.cssText="display:flex;align-items:center;gap:.5rem;margin-bottom:.35rem;";const s=document.createElement("label");s.style.cssText="font-size:.75rem;color:var(--dm-text-muted,#888);white-space:nowrap;min-width:90px;text-align:right;flex-shrink:0;",s.textContent=`{{${t}}}`;const a=document.createElement("input");a.type="text",a.className="form-input form-input--sm",a.style.flex="1",a.dataset.placeholder=t,a.placeholder=`Sample ${t}`,a.value=Y(t),a.addEventListener("input",()=>{e._renderPreview&&e._renderPreview()}),n.appendChild(s),n.appendChild(a),e.appendChild(n)}function Y(e){const t=e.toLowerCase();return t==="_id"?"a1b2c3d4-e5f6-7890-abcd-ef1234567890":t==="_createdat"?new Date().toISOString():t==="_updatedat"?new Date().toISOString():t.includes("email")?"user@example.com":t.includes("phone")?"+44 7700 900000":t.includes("name")?"Jane Smith":t.includes("title")?"Sample Title":t.includes("message")||t.includes("content")||t.includes("description")?"This is a sample value for preview purposes.":t.includes("rating")?"excellent":t.includes("status")?"active":t.includes("priority")?"high":t.includes("date")?new Date().toLocaleDateString():t.includes("tag")?"tag1, tag2":t.includes("subject")?"Sample Subject":t.includes("category")?"general":`Sample ${e}`}function q(e){const t=new Set;for(const[,n]of e.matchAll(/\{\{([\w_]+)(?::[^{}]+)?\}\}/g))t.add(n);return t}function v(e){return String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}async function H(e){const t=e.find("#block-name").get(0),n=e.find("#block-content").get(0),s=(t?.value||"").trim(),a=n?.value??"";if(!s){E.toast("Block name is required.",{type:"warning"});return}if(!/^[a-z0-9][a-z0-9-]*$/.test(s)){E.toast("Name must start with a letter or digit and contain only lowercase letters, digits, and hyphens.",{type:"warning"});return}const l=!!e.find("#block-bundled").is(":checked"),i=e.find("#save-block-btn").get(0);i&&(i.disabled=!0);try{await g.blocks.put(s,{content:a,bundled:l,css:e.find("#block-css").val()||""}),M(),E.toast(u?"Block updated.":"Block created.",{type:"success"}),u||(u=s,R.navigate(`/blocks/edit/${encodeURIComponent(s)}`))}catch(o){E.toast(o.message||"Failed to save block.",{type:"error"})}finally{i&&(i.disabled=!1)}}
|
package/admin/js/views/blocks.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import{api as
|
|
2
|
-
<a href="#/blocks/edit/${
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import{api as s}from"../api.js";function l(t){return String(t??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function p(t){for(;t.firstChild;)t.removeChild(t.firstChild)}export const blocksView={templateUrl:"/admin/js/templates/blocks.html",async onMount(t){m(),await i(t),k(t),Domma.icons.scan(t.get(0))}};let d=!1;function m(){d||(d=!0,I.register("download",{viewBox:"0 0 24 24",paths:["M12 3v12","M7 10l5 5 5-5","M5 19h14"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("upload",{viewBox:"0 0 24 24",paths:["M12 21V9","M7 14l5-5 5 5","M5 5h14"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("plus",{viewBox:"0 0 24 24",paths:["M12 5v14","M5 12h14"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}))}async function i(t){const o=t.find("#blocks-table-container").get(0);if(!o)return;let a=[];try{a=await s.blocks.list()}catch(e){p(o);const r=document.createElement("p");r.className="text-muted",r.textContent=`Failed to load blocks: ${e.message}`,o.appendChild(r);return}T.create(o,{data:a,emptyMessage:'No blocks yet. Click "New Block" to create your first template.',columns:[{key:"name",title:"Name",render:e=>`<a href="#/blocks/edit/${l(e)}">${l(e)}.html</a>`},{key:"size",title:"Size",render:e=>`${e} B`},{key:"updatedAt",title:"Updated",render:e=>e?new Date(e).toLocaleString():"\u2014"},{key:"name",title:"Actions",render:e=>{const r=l(e);return`<div style="display:flex;gap:.4rem;justify-content:flex-end;">
|
|
2
|
+
<a href="#/blocks/edit/${r}" class="btn btn-sm btn-primary">
|
|
3
|
+
<span data-icon="edit"></span> Edit
|
|
4
|
+
</a>
|
|
5
|
+
<button class="btn btn-sm btn-secondary js-export-block" data-name="${r}" title="Export as .dmblock.json">
|
|
6
|
+
<span data-icon="download"></span> Export
|
|
7
|
+
</button>
|
|
8
|
+
<button class="btn btn-sm btn-danger js-delete-block" data-name="${r}">
|
|
9
|
+
<span data-icon="trash"></span> Delete
|
|
10
|
+
</button>
|
|
11
|
+
</div>`}}]}),o.querySelectorAll(".js-delete-block").forEach(e=>{e.addEventListener("click",async()=>{await u(e.dataset.name,t)})}),o.querySelectorAll(".js-export-block").forEach(e=>{e.addEventListener("click",async()=>{try{await s.blocks.exportBundle(e.dataset.name)}catch(r){E.toast(r.message||"Export failed.",{type:"error"})}})}),Domma.icons.scan(o)}async function u(t,o){if(await E.confirm(`Delete block "${t}"? This cannot be undone.`))try{await s.blocks.delete(t),E.toast("Block deleted.",{type:"success"}),await i(o)}catch(e){E.toast(e.message||"Failed to delete block.",{type:"error"})}}function k(t){const o=t.find("#import-block-btn").get(0),a=t.find("#import-block-file").get(0);!o||!a||(o.addEventListener("click",()=>a.click()),a.addEventListener("change",async()=>{const e=a.files?.[0];if(!e)return;a.value="";let r;try{const n=await e.text();r=JSON.parse(n)}catch{E.toast("Not a valid .dmblock.json file (could not parse JSON).",{type:"error"});return}if(!r||typeof r!="object"||!r.name||typeof r.html!="string"){E.toast("Bundle is missing a name or html field.",{type:"error"});return}try{const n=await s.blocks.importBundle(r);E.toast(`Imported "${n.name}".`,{type:"success"}),await i(t)}catch(n){if(n.code==="CONFLICT"){if(!await E.confirm(`A block named "${n.name}" already exists. Overwrite it with the imported version?`)){E.toast("Import cancelled.",{type:"info"});return}try{const c=await s.blocks.importBundle(r,{overwrite:!0});E.toast(`Overwrote "${c.name}".`,{type:"success"}),await i(t)}catch(c){E.toast(c.message||"Import failed.",{type:"error"})}return}E.toast(n.message||"Import failed.",{type:"error"})}}))}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import{api as c}from"../api.js";import{createSimpleEditor as S}from"../lib/simple-editor.js";let i=null,b=null,d=null,l={};const p={};export const componentEditorView={templateUrl:"/admin/js/templates/component-editor.html",async onMount(e){i=null,d=null,l={};for(const t of Object.keys(p))p[t]?.destroy?.(),delete p[t];const n=window.location.hash.match(/\/components\/edit\/([^/?#]+)/);n&&(i=decodeURIComponent(n[1])),O(e),await T(e),L(e),_(e),await q(e);const o=e.find("#component-preview-iframe").get(0);o.contentDocument?.readyState==="complete"?h(e):o.addEventListener("load",()=>h(e),{once:!0}),Domma.icons.scan(e.get(0))}};async function T(e){const n=e.find("#component-name").get(0),o=e.find("#component-tag-hint").get(0),t=e.find("#component-editor-title").get(0),s=e.find("#export-component-btn").get(0),a=()=>{const r=(n.value||"").trim()||"\u2026";o.textContent=`<dm-${r}>`};if(i){t.textContent="Edit Component",n.value=i,n.disabled=!0,s&&(s.style.display="");try{const r=await c.components.get(i);x(e,r.source),e.find("#component-bundled").prop("checked",!!r.bundled)}catch(r){E.toast(r.message||"Component not found.",{type:"error"}),R.navigate("/components");return}}else t.textContent="New Component",x(e,V);a(),n.addEventListener("input",a)}const V=`<template>
|
|
2
|
+
<div>Hello, <span>{{name}}</span>.</div>
|
|
3
|
+
</template>
|
|
4
|
+
<props>
|
|
5
|
+
{
|
|
6
|
+
"name": { "type": "string", "default": "world", "label": "Name" }
|
|
7
|
+
}
|
|
8
|
+
</props>
|
|
9
|
+
<script>
|
|
10
|
+
export default {
|
|
11
|
+
data() { return {}; }
|
|
12
|
+
};
|
|
13
|
+
<\/script>
|
|
14
|
+
<style>
|
|
15
|
+
div { font-family: system-ui, sans-serif; }
|
|
16
|
+
</style>
|
|
17
|
+
`;function L(e){const n=e.find(".component-tab").get()||[],o=e.find(".component-tab-pane").get()||[];for(const t of n)t.addEventListener("click",()=>{for(const s of n)s.classList.toggle("active",s===t);for(const s of o)s.hidden=s.dataset.pane!==t.dataset.tab})}function I(e){const n=o=>{const t=e.match(new RegExp(`<${o}>([\\s\\S]*?)</${o}>`));return t?t[1].replace(/^\n/,"").replace(/\n$/,""):""};return{template:n("template"),props:n("props"),script:n("script"),style:n("style")}}function j(e){return[`<template>
|
|
18
|
+
${e.template}
|
|
19
|
+
</template>`,`<props>
|
|
20
|
+
${e.props}
|
|
21
|
+
</props>`,`<script>
|
|
22
|
+
${e.script}
|
|
23
|
+
<\/script>`,e.style.trim()?`<style>
|
|
24
|
+
${e.style}
|
|
25
|
+
</style>`:""].filter(Boolean).join(`
|
|
26
|
+
`)+`
|
|
27
|
+
`}function x(e,n){const o=I(n);p.template?.setValue(o.template),p.props?.setValue(o.props),p.script?.setValue(o.script),p.style?.setValue(o.style)}function w(e){return j({template:p.template?.getValue()??"",props:p.props?.getValue()??"",script:p.script?.getValue()??"",style:p.style?.getValue()??""})}const M=["template","props","script","style"];function O(e){const n=()=>{b&&clearTimeout(b),b=setTimeout(()=>h(e),300)};for(const o of M){const t=e.find(`#component-src-${o}`).get(0);t&&(p[o]=S(t,{initialValue:"",onChange:n}))}}async function h(e){const n=(e.find("#component-name").val()||"").trim()||"preview",o=w(e);f(e,"compiling");let t;try{t=await c.components.compile(n,o)}catch(s){f(e,"error",s.message||"Compile request failed.");return}if(t.errors?.length){const s=t.errors.map(a=>`[${a.type}] ${a.message}`).join(`
|
|
28
|
+
`);f(e,"error",s),d=null;return}d=t.js,f(e,"ok"),B(e),A(e,{compiledJs:d,tagName:`dm-${n}`,props:l})}let u=null;function A(e,n){const o=e.find("#component-preview-iframe").get(0);if(!o)return;if(!o.dataset.dmMounted){o.dataset.dmMounted="true",v(e,{type:"mount",payload:n});return}u=n;const t=()=>{o.removeEventListener("load",t),u&&(v(e,{type:"mount",payload:u}),u=null)};o.addEventListener("load",t),o.src=o.src}function f(e,n,o=""){const t=e.find("#component-compile-indicator").get(0),s=e.find("#component-compile-status").get(0),a=e.find("#component-compile-errors").get(0),r=e.find("#save-component-btn").get(0);n==="compiling"?(t.style.background="#888",s.textContent="Compiling\u2026",a.hidden=!0,r&&(r.disabled=!0)):n==="ok"?(t.style.background="#3bb273",s.textContent="Compile OK",a.hidden=!0,r&&(r.disabled=!1)):(t.style.background="#c92a2a",s.textContent="Compile failed \u2014 see below",a.hidden=!1,a.textContent=o,r&&(r.disabled=!0))}function N(e){for(;e.firstChild;)e.removeChild(e.firstChild)}function k(e,n,o=!1){N(e);const t=document.createElement("p");if(t.className="text-muted",t.style.cssText="font-size:.8rem;margin:0;",o){t.appendChild(document.createTextNode("Invalid "));const s=document.createElement("code");s.textContent="<props>",t.appendChild(s),t.appendChild(document.createTextNode(" JSON \u2014 fix the tab above to see the props panel."))}else t.textContent=n;e.appendChild(t)}function B(e){const n=e.find("#component-preview-props-panel").get(0);if(!n)return;let o={};try{o=JSON.parse(p.props?.getValue()||"{}")}catch{k(n,"",!0);return}const t={};N(n);const s=Object.entries(o);if(s.length===0){k(n,"No props declared."),l={};return}for(const[a,r]of s){const m=document.createElement("div");m.style.cssText="display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem;";const y=document.createElement("label");y.style.cssText="font-size:.75rem;color:var(--dm-text-muted,#aaa);min-width:120px;",y.textContent=r.label||a,m.appendChild(y);const g=P(r,a,()=>{l[a]=F(g,r),v(e,{type:"update",payload:{props:l}})}),C=a in l?l[a]:r.default!==void 0?r.default:U(r.type);J(g,r,C),t[a]=C,m.appendChild(g),n.appendChild(m)}l=t}function P(e,n,o){let t;return e.type==="number"?(t=document.createElement("input"),t.type="number",t.className="form-input form-input--sm"):e.type==="boolean"?(t=document.createElement("input"),t.type="checkbox",t.className="form-check"):e.type==="array"||e.type==="object"?(t=document.createElement("textarea"),t.rows=2,t.className="form-input form-input--sm",t.style.fontFamily="var(--dm-font-mono, monospace)"):(t=document.createElement("input"),t.type="text",t.className="form-input form-input--sm"),t.style.flex="1",t.dataset.propName=n,t.addEventListener("input",o),t.addEventListener("change",o),t}function F(e,n){if(n.type==="number")return e.value===""?null:Number(e.value);if(n.type==="boolean")return!!e.checked;if(n.type==="array"||n.type==="object")try{return JSON.parse(e.value||"null")}catch{return null}return e.value}function J(e,n,o){n.type==="boolean"?e.checked=!!o:n.type==="array"||n.type==="object"?e.value=JSON.stringify(o??null):e.value=o??""}function U(e){return e==="number"?0:e==="boolean"?!1:e==="array"?[]:e==="object"?{}:""}function v(e,n){const o=e.find("#component-preview-iframe").get(0);o?.contentWindow&&o.contentWindow.postMessage(n,"*")}function _(e){const n=e.find("#save-component-btn").get(0),o=e.find("#export-component-btn").get(0);n&&n.addEventListener("click",()=>z(e)),o&&o.addEventListener("click",async()=>{if(i)try{await c.components.exportBundle(i)}catch(t){E.toast(t.message||"Export failed.",{type:"error"})}})}async function z(e){const n=(e.find("#component-name").val()||"").trim();if(!n){E.toast("Component name is required.",{type:"warning"});return}if(!/^[a-z][a-z0-9-]*$/.test(n)){E.toast("Name must start with a letter and contain only lowercase letters, digits, and hyphens.",{type:"warning"});return}const o=w(e),t=!!e.find("#component-bundled").is(":checked"),s=e.find("#save-component-btn").get(0);s&&(s.disabled=!0);try{await c.components.put(n,{source:o,bundled:t}),E.toast(i?"Component updated.":"Component created.",{type:"success"}),i||(i=n,R.navigate(`/components/edit/${encodeURIComponent(n)}`))}catch(a){E.toast(a.message||"Failed to save component.",{type:"error"})}finally{s&&(s.disabled=!1)}}async function q(e){if(i)try{const n=await c.components.list(),o=e.find("#component-quick-switch").get(0);if(!o||!n?.length)return;const t=[...n].sort((s,a)=>s.name.localeCompare(a.name));for(const s of t){const a=document.createElement("option");a.value=s.name,a.textContent=s.name,s.name===i&&(a.selected=!0),o.appendChild(a)}o.style.display="",o.addEventListener("change",()=>{const s=o.value;s&&s!==i&&R.navigate(`/components/edit/${encodeURIComponent(s)}`)})}catch{}}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import{api as i}from"../api.js";function c(t){return String(t??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function m(t){for(;t.firstChild;)t.removeChild(t.firstChild)}export const componentsView={templateUrl:"/admin/js/templates/components.html",async onMount(t){u(),await d(t),y(t),Domma.icons.scan(t.get(0))}};let p=!1;function u(){p||(p=!0,I.register("download",{viewBox:"0 0 24 24",paths:["M12 3v12","M7 10l5 5 5-5","M5 19h14"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("upload",{viewBox:"0 0 24 24",paths:["M12 21V9","M7 14l5-5 5 5","M5 5h14"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("plus",{viewBox:"0 0 24 24",paths:["M12 5v14","M5 12h14"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}))}async function d(t){const r=t.find("#components-table-container").get(0);if(!r)return;let a=[];try{a=await i.components.list()}catch(e){m(r);const n=document.createElement("p");n.className="text-muted",n.textContent=`Failed to load components: ${e.message}`,r.appendChild(n);return}T.create(r,{data:a,emptyMessage:'No components yet. Click "New Component" to create your first one.',columns:[{key:"name",title:"Name",render:(e,n)=>{const o=n.origin==="plugin"?' <span class="badge badge-secondary" title="Provided by a plugin \u2014 read-only">plugin</span>':"";return`<a href="#/components/edit/${c(e)}">${c(e)}.dmc</a>${o}`}},{key:"props",title:"Props",render:e=>{const n=Object.keys(e||{});return n.length===0?'<span class="text-muted">\u2014</span>':n.map(o=>`<span class="badge badge-secondary" style="margin-right:.25rem;">${c(o)}</span>`).join("")}},{key:"size",title:"Size",render:e=>`${e} B`},{key:"updatedAt",title:"Updated",render:e=>e?new Date(e).toLocaleString():"\u2014"},{key:"name",title:"Actions",render:(e,n)=>{const o=c(e),s=n.origin==="plugin"?"":`<button class="btn btn-sm btn-danger js-delete-component" data-name="${o}">
|
|
2
|
+
<span data-icon="trash"></span> Delete
|
|
3
|
+
</button>`;return`<div style="display:flex;gap:.4rem;justify-content:flex-end;">
|
|
4
|
+
<a href="#/components/edit/${o}" class="btn btn-sm btn-primary">
|
|
5
|
+
<span data-icon="edit"></span> Edit
|
|
6
|
+
</a>
|
|
7
|
+
<button class="btn btn-sm btn-secondary js-export-component" data-name="${o}" title="Export as .dmcomponent.json">
|
|
8
|
+
<span data-icon="download"></span> Export
|
|
9
|
+
</button>
|
|
10
|
+
${s}
|
|
11
|
+
</div>`}}]}),r.querySelectorAll(".js-delete-component").forEach(e=>{e.addEventListener("click",async()=>{await f(e.dataset.name,t)})}),r.querySelectorAll(".js-export-component").forEach(e=>{e.addEventListener("click",async()=>{try{await i.components.exportBundle(e.dataset.name)}catch(n){E.toast(n.message||"Export failed.",{type:"error"})}})}),Domma.icons.scan(r)}async function f(t,r){if(await E.confirm(`Delete component "${t}"? This cannot be undone.`))try{await i.components.delete(t),E.toast("Component deleted.",{type:"success"}),await d(r)}catch(e){E.toast(e.message||"Failed to delete component.",{type:"error"})}}function y(t){const r=t.find("#import-component-btn").get(0),a=t.find("#import-component-file").get(0);!r||!a||(r.addEventListener("click",()=>a.click()),a.addEventListener("change",async()=>{const e=a.files?.[0];if(!e)return;a.value="";let n;try{const o=await e.text();n=JSON.parse(o)}catch{E.toast("Not a valid .dmcomponent.json file (could not parse JSON).",{type:"error"});return}if(!n||typeof n!="object"||!n.name||typeof n.source!="string"){E.toast("Bundle is missing a name or source field.",{type:"error"});return}try{const o=await i.components.importBundle(n);E.toast(`Imported "${o.name}".`,{type:"success"}),await d(t)}catch(o){if(o.code==="CONFLICT"){if(!await E.confirm(`A component named "${o.name}" already exists. Overwrite it with the imported version?`)){E.toast("Import cancelled.",{type:"info"});return}try{const s=await i.components.importBundle(n,{overwrite:!0});E.toast(`Overwrote "${s.name}".`,{type:"success"}),await d(t)}catch(s){E.toast(s.message||"Import failed.",{type:"error"})}return}E.toast(o.message||"Import failed.",{type:"error"})}}))}
|
package/admin/js/views/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js?v=
|
|
1
|
+
import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js?v=5";import{settingsView as t}from"./settings.js";import{navigationView as e}from"./navigation.js";import{notificationsView as m}from"./notifications.js";import{layoutsView as s}from"./layouts.js";import{mediaView as f}from"./media.js";import{loginView as p}from"./login.js";import{usersView as w}from"./users.js";import{userEditorView as n}from"./user-editor.js";import{pluginsView as c}from"./plugins.js";import{documentationView as V}from"./documentation.js";import{tutorialsView as l}from"./tutorials.js";import{apiReferenceView as a}from"./api-reference.js";import{collectionsView as d}from"./collections.js";import{collectionEditorView as E}from"./collection-editor.js";import{collectionEntriesView as u}from"./collection-entries.js";import{formsView as g}from"./forms.js";import{formEditorView as v}from"./form-editor.js";import{formSubmissionsView as b}from"./form-submissions.js";import{viewsListView as k}from"./views-list.js";import{viewEditorView as y}from"./view-editor.js";import{viewPreviewView as L}from"./view-preview.js";import{actionsListView as P}from"./actions-list.js";import{actionEditorView as h}from"./action-editor.js";import{proDocsView as D}from"./pro-docs.js";import{blocksView as R}from"./blocks.js?v=3";import{blockEditorView as S}from"./block-editor.js?v=7";import"./block-editor-enhance.js?v=2";import{componentsView as x}from"./components.js?v=3";import{componentEditorView as j}from"./component-editor.js?v=7";import{myProfileView as q}from"./my-profile.js";import{rolesView as z}from"./roles.js";import{roleEditorView as A}from"./role-editor.js";import{effectsView as B}from"./effects.js";export const views={dashboard:o,pages:i,pageEditor:r,settings:t,navigation:e,layouts:s,media:f,login:p,users:w,userEditor:n,plugins:c,documentation:V,tutorials:l,apiReference:a,collections:d,collectionEditor:E,collectionEntries:u,forms:g,formEditor:v,formSubmissions:b,viewsList:k,viewEditor:y,viewPreview:L,actionsList:P,actionEditor:h,proDocs:D,blocks:R,blockEditor:S,components:x,componentEditor:j,myProfile:q,roles:z,roleEditor:A,effects:B,notifications:m};
|