domma-cms 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +469 -0
  3. package/admin/css/admin.css +1123 -0
  4. package/admin/index.html +72 -0
  5. package/admin/js/api.js +210 -0
  6. package/admin/js/app.js +270 -0
  7. package/admin/js/config/sidebar-config.js +107 -0
  8. package/admin/js/lib/card.js +63 -0
  9. package/admin/js/lib/image-editor.js +869 -0
  10. package/admin/js/lib/markdown-toolbar.js +421 -0
  11. package/admin/js/templates/dashboard.html +50 -0
  12. package/admin/js/templates/documentation.html +237 -0
  13. package/admin/js/templates/layouts.html +11 -0
  14. package/admin/js/templates/login.html +58 -0
  15. package/admin/js/templates/media.html +16 -0
  16. package/admin/js/templates/navigation.html +50 -0
  17. package/admin/js/templates/page-editor.html +126 -0
  18. package/admin/js/templates/pages.html +18 -0
  19. package/admin/js/templates/plugins.html +12 -0
  20. package/admin/js/templates/settings.html +190 -0
  21. package/admin/js/templates/tutorials.html +233 -0
  22. package/admin/js/templates/user-editor.html +12 -0
  23. package/admin/js/templates/users.html +10 -0
  24. package/admin/js/views/dashboard.js +48 -0
  25. package/admin/js/views/documentation.js +12 -0
  26. package/admin/js/views/index.js +33 -0
  27. package/admin/js/views/layouts.js +49 -0
  28. package/admin/js/views/login.js +254 -0
  29. package/admin/js/views/media.js +240 -0
  30. package/admin/js/views/navigation.js +152 -0
  31. package/admin/js/views/page-editor.js +479 -0
  32. package/admin/js/views/pages.js +64 -0
  33. package/admin/js/views/plugins.js +100 -0
  34. package/admin/js/views/settings.js +64 -0
  35. package/admin/js/views/tutorials.js +12 -0
  36. package/admin/js/views/user-editor.js +88 -0
  37. package/admin/js/views/users.js +73 -0
  38. package/bin/cli.js +334 -0
  39. package/config/auth.json +20 -0
  40. package/config/content.json +10 -0
  41. package/config/navigation.json +63 -0
  42. package/config/plugins.json +47 -0
  43. package/config/presets.json +34 -0
  44. package/config/server.json +6 -0
  45. package/config/site.json +33 -0
  46. package/package.json +67 -0
  47. package/plugins/back-to-top/admin/templates/back-to-top-settings.html +55 -0
  48. package/plugins/back-to-top/admin/views/back-to-top-settings.js +44 -0
  49. package/plugins/back-to-top/config.js +10 -0
  50. package/plugins/back-to-top/plugin.js +24 -0
  51. package/plugins/back-to-top/plugin.json +36 -0
  52. package/plugins/back-to-top/public/inject-body.html +105 -0
  53. package/plugins/cookie-consent/admin/templates/cookie-consent-settings.html +113 -0
  54. package/plugins/cookie-consent/admin/views/cookie-consent-settings.js +73 -0
  55. package/plugins/cookie-consent/config.js +30 -0
  56. package/plugins/cookie-consent/plugin.js +24 -0
  57. package/plugins/cookie-consent/plugin.json +36 -0
  58. package/plugins/cookie-consent/public/inject-body.html +69 -0
  59. package/plugins/custom-css/admin/templates/custom-css.html +17 -0
  60. package/plugins/custom-css/admin/views/custom-css.js +35 -0
  61. package/plugins/custom-css/config.js +1 -0
  62. package/plugins/custom-css/data/custom.css +0 -0
  63. package/plugins/custom-css/plugin.js +63 -0
  64. package/plugins/custom-css/plugin.json +32 -0
  65. package/plugins/custom-css/public/inject-head.html +1 -0
  66. package/plugins/domma-effects/admin/templates/domma-effects.html +488 -0
  67. package/plugins/domma-effects/admin/views/domma-effects.js +56 -0
  68. package/plugins/domma-effects/config.js +9 -0
  69. package/plugins/domma-effects/plugin.js +22 -0
  70. package/plugins/domma-effects/plugin.json +36 -0
  71. package/plugins/domma-effects/public/celebrations/core/canvas.js +111 -0
  72. package/plugins/domma-effects/public/celebrations/core/particles.js +144 -0
  73. package/plugins/domma-effects/public/celebrations/core/physics.js +166 -0
  74. package/plugins/domma-effects/public/celebrations/index.js +535 -0
  75. package/plugins/domma-effects/public/celebrations/themes/christmas.js +1805 -0
  76. package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1477 -0
  77. package/plugins/domma-effects/public/celebrations/themes/halloween.js +1837 -0
  78. package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1175 -0
  79. package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1258 -0
  80. package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1754 -0
  81. package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1290 -0
  82. package/plugins/domma-effects/public/celebrations/themes/valentines.js +1361 -0
  83. package/plugins/domma-effects/public/inject-body.html +268 -0
  84. package/plugins/example-analytics/admin/templates/analytics.html +10 -0
  85. package/plugins/example-analytics/admin/views/analytics.js +51 -0
  86. package/plugins/example-analytics/config.js +6 -0
  87. package/plugins/example-analytics/plugin.js +58 -0
  88. package/plugins/example-analytics/plugin.json +27 -0
  89. package/plugins/example-analytics/public/inject-body.html +13 -0
  90. package/plugins/example-analytics/public/inject-head.html +1 -0
  91. package/plugins/example-analytics/stats.json +1 -0
  92. package/plugins/form-builder/admin/templates/form-editor.html +158 -0
  93. package/plugins/form-builder/admin/templates/form-settings.html +29 -0
  94. package/plugins/form-builder/admin/templates/form-submissions.html +30 -0
  95. package/plugins/form-builder/admin/templates/forms-list.html +17 -0
  96. package/plugins/form-builder/admin/views/form-editor.js +817 -0
  97. package/plugins/form-builder/admin/views/form-settings.js +38 -0
  98. package/plugins/form-builder/admin/views/form-submissions.js +295 -0
  99. package/plugins/form-builder/admin/views/forms-list.js +164 -0
  100. package/plugins/form-builder/config.js +9 -0
  101. package/plugins/form-builder/data/forms/contact-details.json +63 -0
  102. package/plugins/form-builder/data/forms/contact.json +52 -0
  103. package/plugins/form-builder/data/submissions/contact-details.json +1 -0
  104. package/plugins/form-builder/data/submissions/contact.json +14 -0
  105. package/plugins/form-builder/email.js +103 -0
  106. package/plugins/form-builder/plugin.js +454 -0
  107. package/plugins/form-builder/plugin.json +56 -0
  108. package/plugins/form-builder/public/inject-body.html +270 -0
  109. package/plugins/form-builder/public/inject-head.html +42 -0
  110. package/public/css/site.css +189 -0
  111. package/public/js/site.js +109 -0
  112. package/scripts/copy-domma.js +48 -0
  113. package/scripts/fresh.js +41 -0
  114. package/scripts/reset.js +124 -0
  115. package/scripts/seed.js +666 -0
  116. package/scripts/setup.js +263 -0
  117. package/server/config.js +56 -0
  118. package/server/middleware/auth.js +97 -0
  119. package/server/routes/api/auth.js +116 -0
  120. package/server/routes/api/layouts.js +25 -0
  121. package/server/routes/api/media.js +93 -0
  122. package/server/routes/api/navigation.js +37 -0
  123. package/server/routes/api/pages.js +118 -0
  124. package/server/routes/api/plugins.js +46 -0
  125. package/server/routes/api/settings.js +25 -0
  126. package/server/routes/api/users.js +110 -0
  127. package/server/routes/public.js +108 -0
  128. package/server/server.js +169 -0
  129. package/server/services/content.js +298 -0
  130. package/server/services/images.js +334 -0
  131. package/server/services/markdown.js +297 -0
  132. package/server/services/plugins.js +246 -0
  133. package/server/services/renderer.js +80 -0
  134. package/server/services/users.js +212 -0
  135. package/server/templates/page.html +78 -0
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Markdown Toolbar Module
3
+ * Provides text manipulation helpers and toolbar rendering for the page editor.
4
+ */
5
+
6
+ // Register editor icons not in Domma's built-in set
7
+ function registerIcons() {
8
+ I.register('bold', {
9
+ viewBox: '0 0 24 24',
10
+ path: 'M7 5H14a3 3 0 0 1 0 6H7V5zM7 11H15a3 3 0 0 1 0 6H7V11z',
11
+ stroke: 'currentColor', fill: 'none', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round'
12
+ });
13
+ I.register('italic', {
14
+ viewBox: '0 0 24 24',
15
+ path: 'M11 5h4M9 19h4M13 5l-2 14',
16
+ stroke: 'currentColor', fill: 'none', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round'
17
+ });
18
+ I.register('strikethrough', {
19
+ viewBox: '0 0 24 24',
20
+ path: 'M16 4H9a3 3 0 0 0 0 6h6a3 3 0 0 1 0 6H6M3 12h18',
21
+ stroke: 'currentColor', fill: 'none', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round'
22
+ });
23
+ I.register('quote', {
24
+ viewBox: '0 0 24 24',
25
+ path: 'M3 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1zM15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z',
26
+ stroke: 'currentColor', fill: 'none', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round'
27
+ });
28
+ I.register('eye', {
29
+ viewBox: '0 0 24 24',
30
+ path: 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8zM12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z',
31
+ stroke: 'currentColor', fill: 'none', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round'
32
+ });
33
+ I.register('expand', {
34
+ viewBox: '0 0 24 24',
35
+ path: 'M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3',
36
+ stroke: 'currentColor', fill: 'none', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round'
37
+ });
38
+ I.register('columns', {
39
+ viewBox: '0 0 24 24',
40
+ path: 'M3 4h18a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1zM12 4v16',
41
+ stroke: 'currentColor', fill: 'none', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round'
42
+ });
43
+ I.register('card', {
44
+ viewBox: '0 0 24 24',
45
+ paths: [
46
+ 'M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z',
47
+ 'M3 9h18'
48
+ ],
49
+ stroke: 'currentColor', fill: 'none', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round'
50
+ });
51
+ I.register('help-circle', {
52
+ viewBox: '0 0 24 24',
53
+ path: 'M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zM12 17h.01M12 13a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2',
54
+ stroke: 'currentColor', fill: 'none', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round'
55
+ });
56
+ I.register('sparkles', {
57
+ viewBox: '0 0 24 24',
58
+ paths: [
59
+ 'M12 3L13.5 8.5H19L14.5 11.5L16 17L12 14L8 17L9.5 11.5L5 8.5H10.5L12 3Z',
60
+ 'M19 3L19.7 5.3H22L20.2 6.6L20.9 9L19 7.7L17.1 9L17.8 6.6L16 5.3H18.3L19 3Z',
61
+ 'M5 13L5.5 14.7H7L5.8 15.5L6.3 17.2L5 16.3L3.7 17.2L4.2 15.5L3 14.7H4.5L5 13Z'
62
+ ],
63
+ stroke: 'currentColor', fill: 'none', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round'
64
+ });
65
+ }
66
+
67
+ // --- Text manipulation helpers ---
68
+
69
+ export function wrapSelection(textarea, before, after) {
70
+ const start = textarea.selectionStart;
71
+ const end = textarea.selectionEnd;
72
+ const value = textarea.value;
73
+ const selected = value.substring(start, end);
74
+
75
+ if (selected) {
76
+ textarea.value = value.substring(0, start) + before + selected + after + value.substring(end);
77
+ textarea.selectionStart = start + before.length;
78
+ textarea.selectionEnd = end + before.length;
79
+ } else {
80
+ textarea.value = value.substring(0, start) + before + after + value.substring(start);
81
+ textarea.selectionStart = textarea.selectionEnd = start + before.length;
82
+ }
83
+
84
+ textarea.dispatchEvent(new Event('input', {bubbles: true}));
85
+ textarea.focus();
86
+ }
87
+
88
+ export function insertAtCursor(textarea, text) {
89
+ const start = textarea.selectionStart;
90
+ const value = textarea.value;
91
+ textarea.value = value.substring(0, start) + text + value.substring(start);
92
+ textarea.selectionStart = textarea.selectionEnd = start + text.length;
93
+ textarea.dispatchEvent(new Event('input', {bubbles: true}));
94
+ textarea.focus();
95
+ }
96
+
97
+ export function insertLine(textarea, prefix) {
98
+ const start = textarea.selectionStart;
99
+ const value = textarea.value;
100
+ const lineStart = value.lastIndexOf('\n', start - 1) + 1;
101
+ const lineEnd = value.indexOf('\n', lineStart);
102
+ const lineText = value.substring(lineStart, lineEnd === -1 ? value.length : lineEnd);
103
+
104
+ if (lineText.startsWith(prefix)) {
105
+ // Toggle off: remove the prefix
106
+ const after = lineEnd === -1 ? '' : value.substring(lineEnd);
107
+ textarea.value = value.substring(0, lineStart) + lineText.substring(prefix.length) + after;
108
+ textarea.selectionStart = textarea.selectionEnd = Math.max(lineStart, start - prefix.length);
109
+ } else {
110
+ // Toggle on: insert prefix at line start
111
+ textarea.value = value.substring(0, lineStart) + prefix + value.substring(lineStart);
112
+ textarea.selectionStart = textarea.selectionEnd = start + prefix.length;
113
+ }
114
+
115
+ textarea.dispatchEvent(new Event('input', {bubbles: true}));
116
+ textarea.focus();
117
+ }
118
+
119
+ // --- Effects dropdown ---
120
+
121
+ const effectsMenuItems = [
122
+ {category: 'Entrance'},
123
+ {label: 'Reveal (fade)', snippet: (sel) => `[reveal animation="fade"]\n${sel || 'Content'}\n[/reveal]`},
124
+ {label: 'Reveal (slide up)', snippet: (sel) => `[reveal animation="slide-up"]\n${sel || 'Content'}\n[/reveal]`},
125
+ {label: 'Reveal (zoom)', snippet: (sel) => `[reveal animation="zoom"]\n${sel || 'Content'}\n[/reveal]`},
126
+ {category: 'Animation'},
127
+ {label: 'Breathe', snippet: (sel) => `[breathe]\n${sel || 'Content'}\n[/breathe]`},
128
+ {label: 'Pulse', snippet: (sel) => `[pulse]\n${sel || 'Content'}\n[/pulse]`},
129
+ {label: 'Shake', snippet: (sel) => `[shake]\n${sel || 'Content'}\n[/shake]`},
130
+ {label: 'CSS Animate', snippet: (sel) => `[animate type="fade-in-up"]\n${sel || 'Content'}\n[/animate]`},
131
+ {category: 'Text'},
132
+ {label: 'Typewriter', snippet: (sel) => `[scribe speed="50" cursor="true"]\n${sel || 'Content'}\n[/scribe]`},
133
+ {label: 'Scramble', snippet: (sel) => `[scramble]\n${sel || 'Content'}\n[/scramble]`},
134
+ {label: 'Counter', snippet: () => `[counter to="100" /]`, insert: true},
135
+ {category: 'Visual'},
136
+ {label: 'Ripple', snippet: (sel) => `[ripple]\n${sel || 'Content'}\n[/ripple]`},
137
+ {label: 'Twinkle', snippet: (sel) => `[twinkle count="80" shape="star"]\n${sel || 'Content'}\n[/twinkle]`},
138
+ {label: 'Ambient bg', snippet: (sel) => `[ambient type="float-blobs" speed="slow"]\n${sel || 'Content'}\n[/ambient]`},
139
+ {category: 'Celebrations'},
140
+ {label: 'Firework (burst)', snippet: () => `[firework type="burst" colour="rainbow" /]`, insert: true},
141
+ {label: 'Firework (sparkle)', snippet: () => `[firework type="sparkle" colour="primary" /]`, insert: true},
142
+ {label: 'Firework (wrap)', snippet: (sel) => `[firework type="burst" colour="rainbow"]\n${sel || 'Content'}\n[/firework]`},
143
+ {label: 'Fireworks show', snippet: () => `[fireworks]\n[firework type="burst" colour="rainbow" /]\n[firework type="sparkle" colour="primary" /]\n[/fireworks]`, insert: true},
144
+ {label: 'Celebrate', snippet: () => `[celebrate theme="auto" intensity="medium" /]`, insert: true},
145
+ ];
146
+
147
+ function openEffectsDropdown(ta, triggerEl) {
148
+ // Remove any existing dropdown
149
+ const existing = document.querySelector('.editor-effects-dropdown-menu');
150
+ if (existing) {
151
+ existing.remove();
152
+ return;
153
+ }
154
+
155
+ const menu = document.createElement('div');
156
+ menu.className = 'editor-effects-dropdown-menu';
157
+
158
+ effectsMenuItems.forEach(function (item) {
159
+ if (item.category) {
160
+ const cat = document.createElement('div');
161
+ cat.className = 'editor-effects-category';
162
+ cat.textContent = item.category;
163
+ menu.appendChild(cat);
164
+ } else {
165
+ const row = document.createElement('button');
166
+ row.type = 'button';
167
+ row.className = 'editor-effects-item';
168
+ row.textContent = item.label;
169
+ row.addEventListener('click', function () {
170
+ menu.remove();
171
+ if (item.insert) {
172
+ insertAtCursor(ta, item.snippet(''));
173
+ } else {
174
+ const start = ta.selectionStart;
175
+ const end = ta.selectionEnd;
176
+ const selected = ta.value.substring(start, end);
177
+ const snippet = item.snippet(selected);
178
+ ta.value = ta.value.substring(0, start) + snippet + ta.value.substring(end);
179
+ ta.selectionStart = start;
180
+ ta.selectionEnd = start + snippet.length;
181
+ ta.dispatchEvent(new Event('input', {bubbles: true}));
182
+ ta.focus();
183
+ }
184
+ });
185
+ menu.appendChild(row);
186
+ }
187
+ });
188
+
189
+ // Position relative to trigger button
190
+ const rect = triggerEl.getBoundingClientRect();
191
+ const toolbarRect = triggerEl.closest('.editor-toolbar').getBoundingClientRect();
192
+ menu.style.top = (rect.bottom - toolbarRect.top + 4) + 'px';
193
+ menu.style.left = (rect.left - toolbarRect.left) + 'px';
194
+
195
+ triggerEl.closest('.editor-toolbar').appendChild(menu);
196
+
197
+ // Close on outside click
198
+ function onOutside(e) {
199
+ if (!menu.contains(e.target) && e.target !== triggerEl) {
200
+ menu.remove();
201
+ document.removeEventListener('click', onOutside, true);
202
+ }
203
+ }
204
+
205
+ setTimeout(function () {
206
+ document.addEventListener('click', onOutside, true);
207
+ }, 0);
208
+ }
209
+
210
+ // --- Toolbar button definitions ---
211
+
212
+ const buttons = [
213
+ {icon: 'bold', title: 'Bold (Ctrl+B)', action: (ta) => wrapSelection(ta, '**', '**')},
214
+ {icon: 'italic', title: 'Italic (Ctrl+I)', action: (ta) => wrapSelection(ta, '_', '_')},
215
+ {icon: 'strikethrough', title: 'Strikethrough', action: (ta) => wrapSelection(ta, '~~', '~~')},
216
+ '|',
217
+ {icon: 'heading-1', title: 'Heading 1', action: (ta) => insertLine(ta, '# ')},
218
+ {icon: 'heading-2', title: 'Heading 2', action: (ta) => insertLine(ta, '## ')},
219
+ {icon: 'heading-3', title: 'Heading 3', action: (ta) => insertLine(ta, '### ')},
220
+ '|',
221
+ {icon: 'list-bullet', title: 'Bullet list', action: (ta) => insertLine(ta, '- ')},
222
+ {icon: 'list-numbered', title: 'Numbered list', action: (ta) => insertLine(ta, '1. ')},
223
+ {icon: 'quote', title: 'Blockquote', action: (ta) => insertLine(ta, '> ')},
224
+ '|',
225
+ {icon: 'link-2', title: 'Link (Ctrl+K)', action: 'link'},
226
+ {icon: 'image-add', title: 'Image', action: 'image'},
227
+ {icon: 'code-inline', title: 'Inline code', action: (ta) => wrapSelection(ta, '`', '`')},
228
+ {icon: 'code-block', title: 'Code block', action: (ta) => wrapSelection(ta, '\n```\n', '\n```\n')},
229
+ {icon: 'minus-circle', title: 'Horizontal rule', action: (ta) => insertAtCursor(ta, '\n---\n')},
230
+ '|',
231
+ {icon: 'card', title: 'Wrap in card', action: 'card'},
232
+ {icon: 'columns', title: 'Insert grid', action: 'grid'},
233
+ {icon: 'sparkles', title: 'Effects', action: 'effects'},
234
+ {icon: 'help-circle', title: 'Editor help', action: 'help'},
235
+ ];
236
+
237
+ /**
238
+ * Creates a formatting toolbar for a Markdown textarea.
239
+ * @param {object} $editor - Domma-wrapped textarea element
240
+ * @param {object} $toolbar - Domma-wrapped container for toolbar HTML
241
+ * @returns {{ $toolbar, onLink(cb), onImage(cb) }}
242
+ */
243
+ export function createToolbar($editor, $toolbar) {
244
+ registerIcons();
245
+
246
+ const handlers = {link: null, image: null, card: null, grid: null, help: null, effects: null};
247
+ const ta = $editor.get(0); // native textarea element
248
+
249
+ // Build toolbar using DOM APIs (avoids Domma's DOMPurify stripping <button>)
250
+ const container = $toolbar.get(0);
251
+ container.textContent = '';
252
+
253
+ buttons.forEach((btn, idx) => {
254
+ if (btn === '|') {
255
+ const sep = document.createElement('span');
256
+ sep.className = 'editor-toolbar-sep';
257
+ container.appendChild(sep);
258
+ } else {
259
+ const el = document.createElement('button');
260
+ el.className = 'editor-toolbar-btn';
261
+ el.setAttribute('data-tooltip', btn.title);
262
+ el.setAttribute('data-idx', String(idx));
263
+ el.type = 'button';
264
+ const icon = document.createElement('span');
265
+ icon.setAttribute('data-icon', btn.icon);
266
+ el.appendChild(icon);
267
+ container.appendChild(el);
268
+ }
269
+ });
270
+
271
+ // Right-side: view mode toggles + fullscreen
272
+ const rightDiv = document.createElement('div');
273
+ rightDiv.className = 'editor-toolbar-right';
274
+
275
+ [
276
+ {mode: 'split', icon: 'columns', label: 'Split view', active: true},
277
+ {mode: 'write', icon: 'file-text', label: 'Write only', active: false},
278
+ {mode: 'preview', icon: 'eye', label: 'Preview only', active: false},
279
+ ].forEach(vm => {
280
+ const el = document.createElement('button');
281
+ el.className = 'editor-view-btn' + (vm.active ? ' active' : '');
282
+ el.setAttribute('data-mode', vm.mode);
283
+ el.setAttribute('data-tooltip', vm.label);
284
+ el.type = 'button';
285
+ const icon = document.createElement('span');
286
+ icon.setAttribute('data-icon', vm.icon);
287
+ el.appendChild(icon);
288
+ rightDiv.appendChild(el);
289
+ });
290
+
291
+ const rightSep = document.createElement('span');
292
+ rightSep.className = 'editor-toolbar-sep';
293
+ rightDiv.appendChild(rightSep);
294
+
295
+ const fsBtn = document.createElement('button');
296
+ fsBtn.id = 'fullscreen-btn';
297
+ fsBtn.className = 'editor-toolbar-btn';
298
+ fsBtn.setAttribute('data-tooltip', 'Toggle fullscreen');
299
+ fsBtn.type = 'button';
300
+ const fsIcon = document.createElement('span');
301
+ fsIcon.setAttribute('data-icon', 'expand');
302
+ fsBtn.appendChild(fsIcon);
303
+ rightDiv.appendChild(fsBtn);
304
+
305
+ container.appendChild(rightDiv);
306
+
307
+ Domma.icons.scan();
308
+ container.querySelectorAll('[data-tooltip]').forEach(el => {
309
+ E.tooltip(el, {content: el.getAttribute('data-tooltip'), position: 'top'});
310
+ });
311
+
312
+ // Click handlers for action buttons
313
+ $toolbar.on('click', '.editor-toolbar-btn[data-idx]', function () {
314
+ const idx = parseInt($(this).data('idx'), 10);
315
+ const btn = buttons[idx];
316
+ if (!btn || btn === '|') return;
317
+
318
+ if (typeof btn.action === 'function') {
319
+ btn.action(ta);
320
+ } else if (btn.action === 'link' && handlers.link) {
321
+ handlers.link(ta);
322
+ } else if (btn.action === 'image' && handlers.image) {
323
+ handlers.image(ta);
324
+ } else if (btn.action === 'card') {
325
+ if (handlers.card) {
326
+ handlers.card(ta);
327
+ } else {
328
+ // Default: wrap selection (or placeholder) in [card] shortcode
329
+ const start = ta.selectionStart;
330
+ const end = ta.selectionEnd;
331
+ const selected = ta.value.substring(start, end) || 'Content here';
332
+ const snippet = `[card title="Card Title"]\n${selected}\n[/card]`;
333
+ ta.value = ta.value.substring(0, start) + snippet + ta.value.substring(end);
334
+ // Position cursor on "Card Title" so user can type it immediately
335
+ ta.selectionStart = start + '[card title="'.length;
336
+ ta.selectionEnd = start + '[card title="Card Title'.length;
337
+ ta.dispatchEvent(new Event('input', {bubbles: true}));
338
+ ta.focus();
339
+ }
340
+ } else if (btn.action === 'grid') {
341
+ if (handlers.grid) {
342
+ handlers.grid(ta);
343
+ } else {
344
+ // Default: insert a 2-column Domma Grid template
345
+ const start = ta.selectionStart;
346
+ const snippet = `[grid cols="2" gap="4"]\n[col]\nColumn 1\n[/col]\n[col]\nColumn 2\n[/col]\n[/grid]\n`;
347
+ ta.value = ta.value.substring(0, start) + snippet + ta.value.substring(start);
348
+ ta.selectionStart = ta.selectionEnd = start + snippet.length;
349
+ ta.dispatchEvent(new Event('input', {bubbles: true}));
350
+ ta.focus();
351
+ }
352
+ } else if (btn.action === 'effects') {
353
+ if (handlers.effects) {
354
+ handlers.effects(ta);
355
+ } else {
356
+ openEffectsDropdown(ta, $toolbar.get(0).querySelector(`[data-idx="${idx}"]`));
357
+ }
358
+ } else if (btn.action === 'help') {
359
+ if (handlers.help) {
360
+ handlers.help(ta);
361
+ }
362
+ }
363
+ });
364
+
365
+ // Keyboard shortcuts on the textarea
366
+ $editor.on('keydown', function (e) {
367
+ const key = e.key.toLowerCase();
368
+
369
+ if (e.ctrlKey || e.metaKey) {
370
+ if (key === 'b') {
371
+ e.preventDefault();
372
+ wrapSelection(ta, '**', '**');
373
+ } else if (key === 'i') {
374
+ e.preventDefault();
375
+ wrapSelection(ta, '_', '_');
376
+ } else if (key === 'k') {
377
+ e.preventDefault();
378
+ if (handlers.link) handlers.link(ta);
379
+ }
380
+ } else if (e.key === 'Tab') {
381
+ e.preventDefault();
382
+ if (e.shiftKey) {
383
+ // Remove up to 4 leading spaces from current line
384
+ const lineStart = ta.value.lastIndexOf('\n', ta.selectionStart - 1) + 1;
385
+ const line = ta.value.substring(lineStart);
386
+ const spaces = line.match(/^ {1,4}/);
387
+ if (spaces) {
388
+ const before = ta.value.substring(0, lineStart);
389
+ const after = ta.value.substring(lineStart + spaces[0].length);
390
+ ta.value = before + line.substring(spaces[0].length);
391
+ ta.selectionStart = ta.selectionEnd = Math.max(lineStart, ta.selectionStart - spaces[0].length);
392
+ ta.dispatchEvent(new Event('input', {bubbles: true}));
393
+ }
394
+ } else {
395
+ insertAtCursor(ta, ' ');
396
+ }
397
+ }
398
+ });
399
+
400
+ return {
401
+ $toolbar,
402
+ onLink(cb) {
403
+ handlers.link = cb;
404
+ },
405
+ onImage(cb) {
406
+ handlers.image = cb;
407
+ },
408
+ onCard(cb) {
409
+ handlers.card = cb;
410
+ },
411
+ onGrid(cb) {
412
+ handlers.grid = cb;
413
+ },
414
+ onHelp(cb) {
415
+ handlers.help = cb;
416
+ },
417
+ onEffects(cb) {
418
+ handlers.effects = cb;
419
+ }
420
+ };
421
+ }
@@ -0,0 +1,50 @@
1
+ <div class="view-header">
2
+ <h1><span data-icon="home"></span> Dashboard</h1>
3
+ <a href="#/pages/new" class="btn btn-primary"><span data-icon="plus"></span> New Page</a>
4
+ </div>
5
+
6
+ <div class="row mb-4">
7
+ <div class="col-4">
8
+ <div class="card stat-card">
9
+ <div class="card-body">
10
+ <div class="stat-icon"><span data-icon="file-text"></span></div>
11
+ <div class="stat-body">
12
+ <div class="stat-value" id="stat-total">—</div>
13
+ <div class="stat-label">Total Pages</div>
14
+ </div>
15
+ </div>
16
+ </div>
17
+ </div>
18
+ <div class="col-4">
19
+ <div class="card stat-card">
20
+ <div class="card-body">
21
+ <div class="stat-icon stat-icon--success"><span data-icon="check-circle"></span></div>
22
+ <div class="stat-body">
23
+ <div class="stat-value" id="stat-published">—</div>
24
+ <div class="stat-label">Published</div>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ <div class="col-4">
30
+ <div class="card stat-card">
31
+ <div class="card-body">
32
+ <div class="stat-icon stat-icon--warning"><span data-icon="clock"></span></div>
33
+ <div class="stat-body">
34
+ <div class="stat-value" id="stat-drafts">—</div>
35
+ <div class="stat-label">Drafts</div>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </div>
41
+
42
+ <div class="card card-collapsible mt-4">
43
+ <div class="card-header" role="button" tabindex="0">
44
+ <div class="card-header-content"><h2>Recent Pages</h2></div>
45
+ <span class="card-collapse-icon" data-icon="chevron-down"></span>
46
+ </div>
47
+ <div class="card-body">
48
+ <div id="recent-pages-table"></div>
49
+ </div>
50
+ </div>