domma-cms 0.1.0 → 0.2.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 +78 -1
- package/admin/js/api.js +32 -0
- package/admin/js/app.js +24 -7
- package/admin/js/config/sidebar-config.js +8 -0
- package/admin/js/templates/collection-editor.html +80 -0
- package/admin/js/templates/collection-entries.html +36 -0
- package/admin/js/templates/collections.html +12 -0
- package/admin/js/templates/documentation.html +136 -0
- package/admin/js/templates/navigation.html +26 -4
- package/admin/js/templates/page-editor.html +91 -85
- package/admin/js/templates/settings.html +433 -172
- package/admin/js/views/collection-editor.js +487 -0
- package/admin/js/views/collection-entries.js +484 -0
- package/admin/js/views/collections.js +153 -0
- package/admin/js/views/dashboard.js +14 -6
- package/admin/js/views/index.js +9 -3
- package/admin/js/views/login.js +3 -2
- package/admin/js/views/navigation.js +77 -11
- package/admin/js/views/page-editor.js +207 -25
- package/admin/js/views/pages.js +14 -6
- package/admin/js/views/settings.js +137 -2
- package/admin/js/views/users.js +10 -7
- package/bin/cli.js +37 -10
- package/config/auth.json +2 -1
- package/config/content.json +1 -0
- package/config/navigation.json +14 -4
- package/config/plugins.json +0 -18
- package/config/presets.json +4 -8
- package/config/site.json +44 -3
- package/package.json +6 -2
- package/plugins/domma-effects/admin/templates/domma-effects.html +92 -3
- package/plugins/domma-effects/plugin.js +125 -0
- package/plugins/domma-effects/public/inject-body.html +19 -0
- package/plugins/example-analytics/admin/views/analytics.js +2 -2
- package/plugins/example-analytics/plugin.json +8 -0
- package/plugins/example-analytics/stats.json +15 -1
- package/plugins/form-builder/admin/templates/form-editor.html +19 -6
- package/plugins/form-builder/admin/views/form-editor.js +634 -9
- package/plugins/form-builder/admin/views/form-submissions.js +4 -4
- package/plugins/form-builder/admin/views/forms-list.js +5 -5
- package/plugins/form-builder/data/forms/consent.json +104 -0
- package/plugins/form-builder/data/forms/contacts.json +66 -0
- package/plugins/form-builder/data/submissions/consent.json +13 -0
- package/plugins/form-builder/data/submissions/contacts.json +26 -0
- package/plugins/form-builder/plugin.js +62 -11
- package/plugins/form-builder/plugin.json +12 -16
- package/plugins/form-builder/public/form-logic-engine.js +568 -0
- package/plugins/form-builder/public/inject-body.html +88 -6
- package/plugins/form-builder/public/inject-head.html +16 -0
- package/plugins/form-builder/public/package.json +1 -0
- package/public/css/site.css +113 -0
- package/public/js/btt.js +90 -0
- package/public/js/cookie-consent.js +61 -0
- package/public/js/site.js +129 -34
- package/scripts/build.js +129 -0
- package/scripts/seed.js +517 -7
- package/server/routes/api/collections.js +301 -0
- package/server/routes/api/settings.js +66 -2
- package/server/server.js +19 -15
- package/server/services/collections.js +430 -0
- package/server/services/content.js +11 -2
- package/server/services/hooks.js +109 -0
- package/server/services/markdown.js +500 -149
- package/server/services/plugins.js +6 -1
- package/server/services/renderer.js +73 -7
- package/server/templates/page.html +38 -3
- package/plugins/back-to-top/admin/templates/back-to-top-settings.html +0 -55
- package/plugins/back-to-top/admin/views/back-to-top-settings.js +0 -44
- package/plugins/back-to-top/config.js +0 -10
- package/plugins/back-to-top/plugin.js +0 -24
- package/plugins/back-to-top/plugin.json +0 -36
- package/plugins/back-to-top/public/inject-body.html +0 -105
- package/plugins/cookie-consent/admin/templates/cookie-consent-settings.html +0 -113
- package/plugins/cookie-consent/admin/views/cookie-consent-settings.js +0 -73
- package/plugins/cookie-consent/config.js +0 -30
- package/plugins/cookie-consent/plugin.js +0 -24
- package/plugins/cookie-consent/plugin.json +0 -36
- package/plugins/cookie-consent/public/inject-body.html +0 -69
- package/plugins/custom-css/admin/templates/custom-css.html +0 -17
- package/plugins/custom-css/admin/views/custom-css.js +0 -35
- package/plugins/custom-css/config.js +0 -1
- package/plugins/custom-css/data/custom.css +0 -0
- package/plugins/custom-css/plugin.js +0 -63
- package/plugins/custom-css/plugin.json +0 -32
- package/plugins/custom-css/public/inject-head.html +0 -1
- package/plugins/form-builder/data/forms/contact.json +0 -52
- package/plugins/form-builder/data/submissions/contact.json +0 -14
package/admin/js/views/index.js
CHANGED
|
@@ -15,6 +15,9 @@ import {userEditorView} from './user-editor.js';
|
|
|
15
15
|
import {pluginsView} from './plugins.js';
|
|
16
16
|
import {documentationView} from './documentation.js';
|
|
17
17
|
import {tutorialsView} from './tutorials.js';
|
|
18
|
+
import {collectionsView} from './collections.js';
|
|
19
|
+
import {collectionEditorView} from './collection-editor.js';
|
|
20
|
+
import {collectionEntriesView} from './collection-entries.js';
|
|
18
21
|
|
|
19
22
|
export const views = {
|
|
20
23
|
dashboard: dashboardView,
|
|
@@ -27,7 +30,10 @@ export const views = {
|
|
|
27
30
|
login: loginView,
|
|
28
31
|
users: usersView,
|
|
29
32
|
userEditor: userEditorView,
|
|
30
|
-
plugins:
|
|
31
|
-
documentation:
|
|
32
|
-
tutorials:
|
|
33
|
+
plugins: pluginsView,
|
|
34
|
+
documentation: documentationView,
|
|
35
|
+
tutorials: tutorialsView,
|
|
36
|
+
collections: collectionsView,
|
|
37
|
+
collectionEditor: collectionEditorView,
|
|
38
|
+
collectionEntries: collectionEntriesView
|
|
33
39
|
};
|
package/admin/js/views/login.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - needsSetup = true → show setup form → onboarding wizard (site → theme → done)
|
|
7
7
|
* - needsSetup = false → show login form → redirect to dashboard
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import {api, isAuthenticated, setAuthData} from '../api.js';
|
|
10
10
|
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
// Blueprints
|
|
@@ -65,7 +65,8 @@ const THEMES = [
|
|
|
65
65
|
{ id: 'lemon-light', label: 'Lemon', dark: false, primary: '#d4ac0d', bg: '#fffef0' },
|
|
66
66
|
{ id: 'silver-dark', label: 'Silver', dark: true, primary: '#95a5a6', bg: '#1c1e20' },
|
|
67
67
|
{ id: 'silver-light', label: 'Silver', dark: false, primary: '#7f8c8d', bg: '#f2f3f4' },
|
|
68
|
-
|
|
68
|
+
{id: 'grayve-dark', label: 'Grayve', dark: true, primary: '#00bcd4', bg: '#1a1e21'},
|
|
69
|
+
{id: 'grayve-light', label: 'Grayve', dark: false, primary: '#00838f', bg: '#ffffff'},
|
|
69
70
|
];
|
|
70
71
|
|
|
71
72
|
export const loginView = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Navigation View
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import {api} from '../api.js';
|
|
5
5
|
|
|
6
6
|
let _nextId = 1;
|
|
7
7
|
|
|
@@ -46,8 +46,12 @@ export const navigationView = {
|
|
|
46
46
|
templateUrl: '/admin/js/templates/navigation.html',
|
|
47
47
|
|
|
48
48
|
async onMount($container) {
|
|
49
|
-
|
|
49
|
+
let [nav, site] = await Promise.all([
|
|
50
|
+
api.navigation.get().catch(() => ({brand: {}, items: []})),
|
|
51
|
+
api.settings.get().catch(() => ({}))
|
|
52
|
+
]);
|
|
50
53
|
let flatItems = flattenItems(nav.items);
|
|
54
|
+
let footerLinks = (site.footer?.links || []).map(l => ({text: l.text || '', url: l.url || ''}));
|
|
51
55
|
|
|
52
56
|
const syncFromDom = () => {
|
|
53
57
|
$container.find('.nav-item-row').each(function () {
|
|
@@ -98,10 +102,54 @@ export const navigationView = {
|
|
|
98
102
|
});
|
|
99
103
|
};
|
|
100
104
|
|
|
105
|
+
const renderFooterLinks = () => {
|
|
106
|
+
const $list = $container.find('#footer-links-list').empty();
|
|
107
|
+
footerLinks.forEach((link, idx) => {
|
|
108
|
+
$list.append(`
|
|
109
|
+
<div class="nav-item-row" data-footer-idx="${idx}">
|
|
110
|
+
<input type="text" class="form-input footer-link-text nav-col-main" value="${esc(link.text)}" placeholder="Label">
|
|
111
|
+
<input type="text" class="form-input footer-link-url nav-col-main" value="${esc(link.url)}" placeholder="/url">
|
|
112
|
+
<span class="nav-col-action">
|
|
113
|
+
<button class="btn btn-sm btn-danger btn-remove-footer" data-idx="${idx}" data-tooltip="Remove"><span data-icon="trash"></span></button>
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
`);
|
|
117
|
+
});
|
|
118
|
+
Domma.icons.scan('#footer-links-list');
|
|
119
|
+
document.querySelectorAll('#footer-links-list [data-tooltip]').forEach(el => {
|
|
120
|
+
E.tooltip(el, {content: el.getAttribute('data-tooltip'), position: 'top'});
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const syncFooterFromDom = () => {
|
|
125
|
+
footerLinks = [];
|
|
126
|
+
$container.find('#footer-links-list .nav-item-row').each(function () {
|
|
127
|
+
footerLinks.push({
|
|
128
|
+
text: $(this).find('.footer-link-text').val().trim(),
|
|
129
|
+
url: $(this).find('.footer-link-url').val().trim()
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
$container.find('#add-footer-link').on('click', () => {
|
|
135
|
+
syncFooterFromDom();
|
|
136
|
+
footerLinks.push({text: '', url: ''});
|
|
137
|
+
renderFooterLinks();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
$container.off('click', '.btn-remove-footer').on('click', '.btn-remove-footer', function () {
|
|
141
|
+
syncFooterFromDom();
|
|
142
|
+
const idx = parseInt($(this).data('idx'), 10);
|
|
143
|
+
footerLinks.splice(idx, 1);
|
|
144
|
+
renderFooterLinks();
|
|
145
|
+
});
|
|
146
|
+
|
|
101
147
|
$container.find('#field-brand-text').val(nav.brand?.text || '');
|
|
102
148
|
$container.find('#field-brand-url').val(nav.brand?.url || '/');
|
|
149
|
+
$container.find('#field-brand-icon').val(nav.brand?.icon || '');
|
|
103
150
|
$container.find('#field-nav-variant').val(nav.variant || 'dark');
|
|
104
151
|
renderItems();
|
|
152
|
+
renderFooterLinks();
|
|
105
153
|
|
|
106
154
|
$container.find('#add-nav-item').on('click', () => {
|
|
107
155
|
syncFromDom();
|
|
@@ -124,28 +172,46 @@ export const navigationView = {
|
|
|
124
172
|
|
|
125
173
|
$container.find('#save-nav-btn').on('click', async () => {
|
|
126
174
|
syncFromDom();
|
|
175
|
+
syncFooterFromDom();
|
|
127
176
|
const nested = nestItems(
|
|
128
177
|
flatItems.map(i => ({ ...i, text: i.text.trim(), url: i.url.trim(), icon: i.icon.trim() }))
|
|
129
178
|
).filter(i => i.text || i.url);
|
|
130
179
|
|
|
131
|
-
|
|
180
|
+
const brandIcon = $container.find('#field-brand-icon').val().trim();
|
|
181
|
+
const navData = {
|
|
132
182
|
brand: {
|
|
133
183
|
text: $container.find('#field-brand-text').val().trim(),
|
|
134
|
-
|
|
184
|
+
url: $container.find('#field-brand-url').val().trim() || '/',
|
|
185
|
+
...(brandIcon && {icon: brandIcon})
|
|
135
186
|
},
|
|
136
187
|
items: nested,
|
|
137
188
|
variant: $container.find('#field-nav-variant').val(),
|
|
138
189
|
position: nav.position || 'sticky'
|
|
139
190
|
};
|
|
140
191
|
|
|
192
|
+
const validFooterLinks = footerLinks.filter(l => l.text || l.url);
|
|
193
|
+
|
|
141
194
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
} catch {
|
|
148
|
-
|
|
195
|
+
await api.navigation.save(navData);
|
|
196
|
+
nav = navData;
|
|
197
|
+
flatItems = flattenItems(nav.items);
|
|
198
|
+
renderItems();
|
|
199
|
+
E.toast('Navigation saved.', {type: 'success'});
|
|
200
|
+
} catch (e) {
|
|
201
|
+
console.error('[navigation] save failed:', e);
|
|
202
|
+
E.toast('Failed to save navigation.', {type: 'error'});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Save footer links into site config — best-effort, independent of nav save
|
|
207
|
+
try {
|
|
208
|
+
const currentSite = await api.settings.get().catch(() => ({}));
|
|
209
|
+
await api.settings.save({...currentSite, footer: {...(currentSite.footer || {}), links: validFooterLinks}});
|
|
210
|
+
footerLinks = validFooterLinks;
|
|
211
|
+
renderFooterLinks();
|
|
212
|
+
} catch (e) {
|
|
213
|
+
console.error('[navigation] footer links save failed:', e);
|
|
214
|
+
E.toast('Footer links could not be saved.', {type: 'warning'});
|
|
149
215
|
}
|
|
150
216
|
});
|
|
151
217
|
}
|
|
@@ -170,6 +170,95 @@ function openEditorHelp() {
|
|
|
170
170
|
|
|
171
171
|
wrap.appendChild(section3);
|
|
172
172
|
|
|
173
|
+
// --- Section 3c: Hero shortcode ---
|
|
174
|
+
const section3c = document.createElement('div');
|
|
175
|
+
section3c.appendChild(makeH3('Hero'));
|
|
176
|
+
section3c.appendChild(makeNote('Full-width hero sections — no plugin required. Uses Domma\'s built-in Hero CSS component.'));
|
|
177
|
+
|
|
178
|
+
const heroAttrTable = document.createElement('table');
|
|
179
|
+
heroAttrTable.style.cssText = 'width:100%;border-collapse:collapse;font-size:.85em;margin-bottom:.5rem;';
|
|
180
|
+
[
|
|
181
|
+
['title="..."', 'Heading text'],
|
|
182
|
+
['tagline="..."', 'Subtitle text'],
|
|
183
|
+
['size="sm|lg|full"', 'Height variant (default: normal)'],
|
|
184
|
+
['variant="dark|primary|gradient-blue|gradient-purple|gradient-sunset|gradient-ocean"', 'Colour / gradient preset'],
|
|
185
|
+
['image="url"', 'Background image URL (adds cover mode)'],
|
|
186
|
+
['overlay="light|dark|darker|gradient|gradient-reverse"', 'Image overlay style'],
|
|
187
|
+
['align="center|left"', 'Content alignment (default: center)'],
|
|
188
|
+
['fullwidth="true"', 'Break out of the page container to span full viewport width'],
|
|
189
|
+
['class="..."', 'Extra CSS classes'],
|
|
190
|
+
['id="..."', 'Element id'],
|
|
191
|
+
].forEach(([attr, desc]) => {
|
|
192
|
+
const tr = document.createElement('tr');
|
|
193
|
+
const tdCode = document.createElement('td');
|
|
194
|
+
tdCode.style.cssText = 'padding:.3rem .4rem;border-bottom:1px solid var(--dm-border,#333);vertical-align:top;';
|
|
195
|
+
tdCode.appendChild(makeCode(attr));
|
|
196
|
+
const tdDesc = document.createElement('td');
|
|
197
|
+
tdDesc.style.cssText = 'padding:.3rem .4rem;border-bottom:1px solid var(--dm-border,#333);vertical-align:top;color:var(--dm-text-muted,#aaa);';
|
|
198
|
+
tdDesc.textContent = desc;
|
|
199
|
+
tr.appendChild(tdCode);
|
|
200
|
+
tr.appendChild(tdDesc);
|
|
201
|
+
heroAttrTable.appendChild(tr);
|
|
202
|
+
});
|
|
203
|
+
section3c.appendChild(heroAttrTable);
|
|
204
|
+
|
|
205
|
+
section3c.appendChild(makeSnippet(
|
|
206
|
+
'Basic hero',
|
|
207
|
+
'[hero title="Welcome" tagline="Build something great"][/hero]',
|
|
208
|
+
null
|
|
209
|
+
));
|
|
210
|
+
|
|
211
|
+
section3c.appendChild(makeSnippet(
|
|
212
|
+
'Gradient with body content',
|
|
213
|
+
'[hero title="Get Started" tagline="Everything you need." size="lg" variant="gradient-blue" align="center"]\nSome introductory **Markdown** content here.\n[/hero]',
|
|
214
|
+
null
|
|
215
|
+
));
|
|
216
|
+
|
|
217
|
+
section3c.appendChild(makeSnippet(
|
|
218
|
+
'Background image with overlay',
|
|
219
|
+
'[hero title="Our Story" image="/media/hero.jpg" overlay="dark" size="full"][/hero]',
|
|
220
|
+
'Combine image + overlay for text legibility over photos.'
|
|
221
|
+
));
|
|
222
|
+
|
|
223
|
+
wrap.appendChild(section3c);
|
|
224
|
+
|
|
225
|
+
// --- Section 3b: Interactive Components ---
|
|
226
|
+
const section3b = document.createElement('div');
|
|
227
|
+
section3b.appendChild(makeH3('Interactive Components'));
|
|
228
|
+
section3b.appendChild(makeNote('Tabs, accordion, carousel, and countdown — no plugin required.'));
|
|
229
|
+
|
|
230
|
+
section3b.appendChild(makeSnippet(
|
|
231
|
+
'Tabs',
|
|
232
|
+
'[tabs]\n[tab title="First"]Content **A**[/tab]\n[tab title="Second"]Content **B**[/tab]\n[/tabs]',
|
|
233
|
+
'Add style="pills" for pill-style navigation.'
|
|
234
|
+
));
|
|
235
|
+
|
|
236
|
+
section3b.appendChild(makeSnippet(
|
|
237
|
+
'Accordion',
|
|
238
|
+
'[accordion]\n[item title="Question 1"]Answer here.[/item]\n[item title="Question 2"]Answer here.[/item]\n[/accordion]',
|
|
239
|
+
'Add multiple="true" to allow several panels open at once.'
|
|
240
|
+
));
|
|
241
|
+
|
|
242
|
+
section3b.appendChild(makeSnippet(
|
|
243
|
+
'Carousel',
|
|
244
|
+
'[carousel autoplay="true" interval="5000"]\n[slide title="Slide 1"]Description[/slide]\n[slide image="/media/photo.jpg" title="Slide 2"]Caption[/slide]\n[/carousel]',
|
|
245
|
+
'Omit autoplay for a manual carousel. loop="false" disables wrapping.'
|
|
246
|
+
));
|
|
247
|
+
|
|
248
|
+
section3b.appendChild(makeSnippet(
|
|
249
|
+
'Countdown (to date)',
|
|
250
|
+
'[countdown to="2026-12-31" format="DD:HH:mm:ss" /]',
|
|
251
|
+
null
|
|
252
|
+
));
|
|
253
|
+
|
|
254
|
+
section3b.appendChild(makeSnippet(
|
|
255
|
+
'Countdown (duration)',
|
|
256
|
+
'[countdown duration="300" format="mm:ss" /]',
|
|
257
|
+
'duration is in seconds. format options: mm:ss · HH:mm:ss · DD:HH:mm:ss'
|
|
258
|
+
));
|
|
259
|
+
|
|
260
|
+
wrap.appendChild(section3b);
|
|
261
|
+
|
|
173
262
|
// --- Section 4: Embedding a Form ---
|
|
174
263
|
const section4 = document.createElement('div');
|
|
175
264
|
section4.appendChild(makeH3('Embedding a Form'));
|
|
@@ -192,7 +281,8 @@ function openEditorHelp() {
|
|
|
192
281
|
['[breathe]...[/breathe]', 'Continuous gentle scale loop'],
|
|
193
282
|
['[pulse]...[/pulse]', 'Repeating scale pulse'],
|
|
194
283
|
['[shake]...[/shake]', 'One-shot shake'],
|
|
195
|
-
['[scribe speed="50"]...[/scribe]', 'Typewriter
|
|
284
|
+
['[scribe speed="50"]...[/scribe]', 'Typewriter — simple mode (text typed letter by letter)'],
|
|
285
|
+
['[scribe loop="true"][render]Text[/render][wait]1500[/wait][undo /][/scribe]', 'Typewriter — script mode (sequenced render/wait/undo actions)'],
|
|
196
286
|
['[scramble]...[/scramble]', 'Character-scramble reveal'],
|
|
197
287
|
['[counter to="100" /]', 'Animated number counter (self-closing)'],
|
|
198
288
|
['[ripple]...[/ripple]', 'Click-triggered ripple'],
|
|
@@ -220,17 +310,105 @@ function openEditorHelp() {
|
|
|
220
310
|
});
|
|
221
311
|
section5.appendChild(effectsTable);
|
|
222
312
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
section5.appendChild(effectsLink);
|
|
313
|
+
section5.appendChild(makeSnippet(
|
|
314
|
+
'Scribe script — looping multi-phrase typewriter',
|
|
315
|
+
'[scribe loop="true" loop-delay="2000" delete-speed="20"]\n[render]We build things that matter.[/render]\n[wait]1200[/wait]\n[undo all="true" /]\n[render]We design for humans.[/render]\n[wait]1200[/wait]\n[undo all="true" /]\n[/scribe]',
|
|
316
|
+
'Use [render]...[/render] to type text, [wait]Ms[/wait] to pause, and [undo /] to delete. loop="true" replays the sequence. Only [render], [wait], and [undo] shortcodes are parsed — plain text between actions is ignored.'
|
|
317
|
+
));
|
|
318
|
+
|
|
319
|
+
section5.appendChild(makeNote('Enable the Domma Effects plugin to activate these shortcodes on your site.'));
|
|
231
320
|
|
|
232
321
|
wrap.appendChild(section5);
|
|
233
322
|
|
|
323
|
+
// --- Section 6: Tables shortcode ---
|
|
324
|
+
const section6t = document.createElement('div');
|
|
325
|
+
section6t.appendChild(makeH3('Tables'));
|
|
326
|
+
section6t.appendChild(makeNote('Wrap a standard Markdown (GFM) table with Domma CSS classes and responsive horizontal scrolling.'));
|
|
327
|
+
|
|
328
|
+
const tableAttrTable = document.createElement('table');
|
|
329
|
+
tableAttrTable.style.cssText = 'width:100%;border-collapse:collapse;font-size:.85em;margin-bottom:.5rem;';
|
|
330
|
+
[
|
|
331
|
+
['striped="true"', 'Alternate row background (.table-striped)'],
|
|
332
|
+
['bordered="true"', 'Borders on all cells (.table-bordered)'],
|
|
333
|
+
['compact="true"', 'Reduced cell padding (.table-compact)'],
|
|
334
|
+
['caption="..."', 'Caption text above the table'],
|
|
335
|
+
['class="..."', 'Extra CSS classes appended to .table'],
|
|
336
|
+
['id="..."', 'id attribute on the <table> element'],
|
|
337
|
+
].forEach(([attr, desc]) => {
|
|
338
|
+
const tr = document.createElement('tr');
|
|
339
|
+
const tdCode = document.createElement('td');
|
|
340
|
+
tdCode.style.cssText = 'padding:.3rem .4rem;border-bottom:1px solid var(--dm-border,#333);vertical-align:top;';
|
|
341
|
+
tdCode.appendChild(makeCode(attr));
|
|
342
|
+
const tdDesc = document.createElement('td');
|
|
343
|
+
tdDesc.style.cssText = 'padding:.3rem .4rem;border-bottom:1px solid var(--dm-border,#333);vertical-align:top;color:var(--dm-text-muted,#aaa);';
|
|
344
|
+
tdDesc.textContent = desc;
|
|
345
|
+
tr.appendChild(tdCode);
|
|
346
|
+
tr.appendChild(tdDesc);
|
|
347
|
+
tableAttrTable.appendChild(tr);
|
|
348
|
+
});
|
|
349
|
+
section6t.appendChild(tableAttrTable);
|
|
350
|
+
|
|
351
|
+
section6t.appendChild(makeSnippet(
|
|
352
|
+
'Striped + bordered table',
|
|
353
|
+
'[table striped="true" bordered="true" caption="Product Pricing"]\n| Product | Price |\n| ------- | ----: |\n| Widget | $9.99 |\n| Gadget | $14.99 |\n[/table]',
|
|
354
|
+
'The inner content must be a valid GFM Markdown table. The shortcode adds Domma CSS classes and wraps in a responsive scroll container.'
|
|
355
|
+
));
|
|
356
|
+
|
|
357
|
+
wrap.appendChild(section6t);
|
|
358
|
+
|
|
359
|
+
// --- Section 7: Slideover shortcode ---
|
|
360
|
+
const section6 = document.createElement('div');
|
|
361
|
+
section6.appendChild(makeH3('Slideover'));
|
|
362
|
+
section6.appendChild(makeNote('A trigger button that opens a slide-in panel with Markdown content. No plugin required.'));
|
|
363
|
+
|
|
364
|
+
const soAttrTable = document.createElement('table');
|
|
365
|
+
soAttrTable.style.cssText = 'width:100%;border-collapse:collapse;font-size:.85em;margin-bottom:.5rem;';
|
|
366
|
+
[
|
|
367
|
+
['title="..."', 'Panel header text'],
|
|
368
|
+
['trigger="..."', 'Button label (default: "Open")'],
|
|
369
|
+
['size="sm|md|lg"', 'Panel width (default: md)'],
|
|
370
|
+
['position="right|left"', 'Slide direction (default: right)'],
|
|
371
|
+
].forEach(([attr, desc]) => {
|
|
372
|
+
const tr = document.createElement('tr');
|
|
373
|
+
const tdCode = document.createElement('td');
|
|
374
|
+
tdCode.style.cssText = 'padding:.3rem .4rem;border-bottom:1px solid var(--dm-border,#333);vertical-align:top;';
|
|
375
|
+
tdCode.appendChild(makeCode(attr));
|
|
376
|
+
const tdDesc = document.createElement('td');
|
|
377
|
+
tdDesc.style.cssText = 'padding:.3rem .4rem;border-bottom:1px solid var(--dm-border,#333);vertical-align:top;color:var(--dm-text-muted,#aaa);';
|
|
378
|
+
tdDesc.textContent = desc;
|
|
379
|
+
tr.appendChild(tdCode);
|
|
380
|
+
tr.appendChild(tdDesc);
|
|
381
|
+
soAttrTable.appendChild(tr);
|
|
382
|
+
});
|
|
383
|
+
section6.appendChild(soAttrTable);
|
|
384
|
+
|
|
385
|
+
section6.appendChild(makeSnippet(
|
|
386
|
+
'Example',
|
|
387
|
+
'[slideover title="More Info" trigger="Read more" size="md"]\n## Details\nMarkdown content here.\n[/slideover]',
|
|
388
|
+
'Nested [card] and [grid] shortcodes work inside the slideover body.'
|
|
389
|
+
));
|
|
390
|
+
|
|
391
|
+
wrap.appendChild(section6);
|
|
392
|
+
|
|
393
|
+
// --- Section 7: DConfig ---
|
|
394
|
+
const section7 = document.createElement('div');
|
|
395
|
+
section7.appendChild(makeH3('DConfig — Declarative Behaviour'));
|
|
396
|
+
section7.appendChild(makeNote('Define click handlers and class toggles without writing JavaScript. Use the DConfig section above or embed inline with [dconfig]...[/dconfig] in the content body. Inline shortcodes win on selector conflict.'));
|
|
397
|
+
|
|
398
|
+
section7.appendChild(makeSnippet(
|
|
399
|
+
'Toggle a class on click',
|
|
400
|
+
'{ "#my-btn": { "events": { "click": { "target": "#panel", "toggleClass": "hidden" } } } }',
|
|
401
|
+
'Selector keys use standard CSS selectors. "target" is optional — defaults to the element itself.'
|
|
402
|
+
));
|
|
403
|
+
|
|
404
|
+
section7.appendChild(makeSnippet(
|
|
405
|
+
'Inline shortcode syntax',
|
|
406
|
+
'[dconfig]\n{ "#my-btn": { "events": { "click": { "target": "#panel", "toggleClass": "hidden" } } } }\n[/dconfig]',
|
|
407
|
+
null
|
|
408
|
+
));
|
|
409
|
+
|
|
410
|
+
wrap.appendChild(section7);
|
|
411
|
+
|
|
234
412
|
so.setContent(wrap);
|
|
235
413
|
so.open();
|
|
236
414
|
}
|
|
@@ -238,7 +416,6 @@ function openEditorHelp() {
|
|
|
238
416
|
// Module-level state for unsaved-changes guard
|
|
239
417
|
let _dirty = false;
|
|
240
418
|
let _beforeUnload = null;
|
|
241
|
-
let _saveShortcut = null;
|
|
242
419
|
let _guardRegistered = false;
|
|
243
420
|
|
|
244
421
|
export const pageEditorView = {
|
|
@@ -281,6 +458,9 @@ export const pageEditorView = {
|
|
|
281
458
|
$container.find('#field-visibility').val(page.visibility || 'public');
|
|
282
459
|
$container.find('#field-seo-title').val(page.seo?.title || '');
|
|
283
460
|
$container.find('#field-seo-desc').val(page.seo?.description || '');
|
|
461
|
+
if (page.dconfig) {
|
|
462
|
+
$container.find('#field-dconfig').val(JSON.stringify(page.dconfig, null, 2));
|
|
463
|
+
}
|
|
284
464
|
}
|
|
285
465
|
|
|
286
466
|
// Layouts dropdown
|
|
@@ -290,6 +470,9 @@ export const pageEditorView = {
|
|
|
290
470
|
$layoutSelect.append(`<option value="${key}" ${selected}>${preset.label || key}</option>`);
|
|
291
471
|
});
|
|
292
472
|
|
|
473
|
+
// Initialise meta tabs
|
|
474
|
+
E.tabs($container.find('#editor-meta-tabs').get(0));
|
|
475
|
+
|
|
293
476
|
// Markdown editor + live preview
|
|
294
477
|
const $editor = $container.find('#markdown-editor');
|
|
295
478
|
const $preview = $container.find('#markdown-preview');
|
|
@@ -402,10 +585,6 @@ export const pageEditorView = {
|
|
|
402
585
|
E.confirm('You have unsaved changes. Leave this page?').then(ok => {
|
|
403
586
|
if (ok) {
|
|
404
587
|
_dirty = false;
|
|
405
|
-
if (_saveShortcut) {
|
|
406
|
-
document.removeEventListener('keydown', _saveShortcut);
|
|
407
|
-
_saveShortcut = null;
|
|
408
|
-
}
|
|
409
588
|
next();
|
|
410
589
|
}
|
|
411
590
|
});
|
|
@@ -423,6 +602,18 @@ export const pageEditorView = {
|
|
|
423
602
|
const enteredPath = $container.find('#page-url-path').val().trim();
|
|
424
603
|
if (!enteredPath) { E.toast('URL path is required.', { type: 'warning' }); return; }
|
|
425
604
|
|
|
605
|
+
// Validate DConfig JSON before save attempt
|
|
606
|
+
const dconfigRaw = $container.find('#field-dconfig').val().trim();
|
|
607
|
+
let dconfig = null;
|
|
608
|
+
if (dconfigRaw) {
|
|
609
|
+
try {
|
|
610
|
+
dconfig = JSON.parse(dconfigRaw);
|
|
611
|
+
} catch {
|
|
612
|
+
E.toast('DConfig JSON is invalid. Please check the format before saving.', {type: 'warning'});
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
426
617
|
const frontmatter = {
|
|
427
618
|
title: $container.find('#field-title').val().trim() || 'Untitled',
|
|
428
619
|
description: $container.find('#field-description').val().trim(),
|
|
@@ -436,7 +627,8 @@ export const pageEditorView = {
|
|
|
436
627
|
seo: {
|
|
437
628
|
title: $container.find('#field-seo-title').val().trim(),
|
|
438
629
|
description: $container.find('#field-seo-desc').val().trim()
|
|
439
|
-
}
|
|
630
|
+
},
|
|
631
|
+
dconfig
|
|
440
632
|
};
|
|
441
633
|
|
|
442
634
|
try {
|
|
@@ -461,16 +653,6 @@ export const pageEditorView = {
|
|
|
461
653
|
}
|
|
462
654
|
});
|
|
463
655
|
|
|
464
|
-
// Ctrl+S / Cmd+S → save
|
|
465
|
-
if (_saveShortcut) document.removeEventListener('keydown', _saveShortcut);
|
|
466
|
-
_saveShortcut = (e) => {
|
|
467
|
-
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
468
|
-
e.preventDefault();
|
|
469
|
-
$container.find('#save-btn').get(0).click();
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
document.addEventListener('keydown', _saveShortcut);
|
|
473
|
-
|
|
474
656
|
// Cancel
|
|
475
657
|
$container.find('#cancel-btn').on('click', () => R.navigate('/pages'));
|
|
476
658
|
|
package/admin/js/views/pages.js
CHANGED
|
@@ -14,13 +14,21 @@ export const pagesView = {
|
|
|
14
14
|
T.create('#pages-table', {
|
|
15
15
|
data,
|
|
16
16
|
columns: [
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
{
|
|
18
|
+
key: 'title',
|
|
19
|
+
title: 'Title',
|
|
20
|
+
render: (val, row) => `<a href="#/pages/edit${row.urlPath}">${val}</a>`
|
|
21
|
+
},
|
|
22
|
+
{key: 'urlPath', title: 'URL', render: (val) => `<code>${val}</code>`},
|
|
23
|
+
{key: 'layout', title: 'Layout'},
|
|
24
|
+
{
|
|
25
|
+
key: 'status',
|
|
26
|
+
title: 'Status',
|
|
27
|
+
render: (val) => `<span class="badge badge-${val === 'published' ? 'success' : 'warning'}">${val}</span>`
|
|
28
|
+
},
|
|
29
|
+
{key: 'updatedAt', title: 'Updated', render: (val) => val ? D(val).format('DD MMM YYYY') : '—'},
|
|
22
30
|
{
|
|
23
|
-
|
|
31
|
+
key: 'actions', title: 'Actions',
|
|
24
32
|
render: (_, row) => `
|
|
25
33
|
<a href="#/pages/edit${row.urlPath}" class="btn btn-sm btn-outline">Edit</a>
|
|
26
34
|
<a href="${row.urlPath}" target="_blank" class="btn btn-sm btn-ghost" data-tooltip="View"><span data-icon="external-link"></span></a>
|