domma-cms 0.3.0 → 0.5.2

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 (150) hide show
  1. package/README.md +3 -3
  2. package/admin/css/admin.css +1 -1
  3. package/admin/dist/domma/domma-tools.css +2313 -0
  4. package/admin/dist/domma/domma-tools.min.js +10 -0
  5. package/admin/index.html +4 -0
  6. package/admin/js/api.js +1 -1
  7. package/admin/js/app.js +8 -4
  8. package/admin/js/config/sidebar-config.js +1 -1
  9. package/admin/js/lib/markdown-toolbar.js +18 -10
  10. package/admin/js/templates/action-editor.html +171 -0
  11. package/admin/js/templates/actions-list.html +19 -0
  12. package/admin/js/templates/api-reference.html +1411 -0
  13. package/admin/js/templates/block-editor.html +158 -0
  14. package/admin/js/templates/blocks.html +8 -0
  15. package/admin/js/templates/collection-editor.html +47 -0
  16. package/admin/js/templates/collection-entries.html +3 -0
  17. package/admin/js/templates/collections.html +51 -4
  18. package/admin/js/templates/documentation.html +258 -0
  19. package/{plugins/form-builder/admin → admin/js}/templates/form-editor.html +238 -199
  20. package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
  21. package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
  22. package/admin/js/templates/login.html +29 -4
  23. package/admin/js/templates/my-profile.html +17 -0
  24. package/admin/js/templates/page-editor.html +39 -0
  25. package/admin/js/templates/pages.html +6 -1
  26. package/admin/js/templates/pro-docs.html +259 -0
  27. package/admin/js/templates/role-editor.html +59 -0
  28. package/admin/js/templates/roles.html +10 -0
  29. package/admin/js/templates/settings.html +167 -23
  30. package/admin/js/templates/tutorials.html +81 -0
  31. package/admin/js/templates/user-editor.html +7 -0
  32. package/admin/js/templates/users.html +3 -26
  33. package/admin/js/templates/view-editor.html +201 -0
  34. package/admin/js/templates/view-preview.html +51 -0
  35. package/admin/js/templates/views-list.html +19 -0
  36. package/admin/js/views/action-editor.js +1 -0
  37. package/admin/js/views/actions-list.js +1 -0
  38. package/admin/js/views/api-reference.js +1 -0
  39. package/admin/js/views/block-editor.js +8 -0
  40. package/admin/js/views/blocks.js +4 -0
  41. package/admin/js/views/collection-editor.js +3 -3
  42. package/admin/js/views/collection-entries.js +1 -1
  43. package/admin/js/views/collections.js +1 -1
  44. package/admin/js/views/dashboard.js +1 -1
  45. package/admin/js/views/form-editor.js +8 -0
  46. package/admin/js/views/form-submissions.js +1 -0
  47. package/admin/js/views/forms.js +1 -0
  48. package/admin/js/views/index.js +1 -1
  49. package/admin/js/views/login.js +2 -2
  50. package/admin/js/views/media.js +1 -1
  51. package/admin/js/views/my-profile.js +1 -0
  52. package/admin/js/views/page-editor.js +34 -15
  53. package/admin/js/views/pages.js +5 -5
  54. package/admin/js/views/plugins.js +10 -10
  55. package/admin/js/views/pro-docs.js +1 -0
  56. package/admin/js/views/role-editor.js +1 -0
  57. package/admin/js/views/roles.js +4 -0
  58. package/admin/js/views/settings.js +3 -1
  59. package/admin/js/views/user-editor.js +1 -1
  60. package/admin/js/views/users.js +4 -7
  61. package/admin/js/views/view-editor.js +1 -0
  62. package/admin/js/views/view-preview.js +1 -0
  63. package/admin/js/views/views-list.js +1 -0
  64. package/bin/cli.js +1 -1
  65. package/config/auth.json +1 -0
  66. package/config/connections.json.bak +9 -0
  67. package/config/connections.json.example +9 -0
  68. package/config/navigation.json +5 -15
  69. package/config/plugins.json +19 -29
  70. package/config/server.json +6 -6
  71. package/config/site.json +16 -6
  72. package/package.json +25 -10
  73. package/plugins/example-analytics/stats.json +17 -12
  74. package/plugins/form-builder/data/forms/contacts.json +62 -62
  75. package/plugins/form-builder/data/forms/enquiries.json +103 -0
  76. package/plugins/form-builder/data/forms/feedback.json +17 -16
  77. package/plugins/form-builder/data/forms/notes.json +79 -0
  78. package/plugins/form-builder/data/forms/to-do.json +100 -0
  79. package/plugins/form-builder/data/submissions/contacts.json +1 -26
  80. package/plugins/form-builder/data/submissions/notes.json +1 -0
  81. package/plugins/form-builder/data/submissions/to-do.json +1 -0
  82. package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
  83. package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
  84. package/plugins/theme-roller/config.js +1 -0
  85. package/plugins/theme-roller/plugin.js +233 -0
  86. package/plugins/theme-roller/plugin.json +31 -0
  87. package/plugins/theme-roller/public/active-theme.css +0 -0
  88. package/plugins/theme-roller/public/inject-head-late.html +1 -0
  89. package/public/css/forms.css +1 -0
  90. package/public/css/site.css +1 -1
  91. package/public/js/forms.js +1 -0
  92. package/public/js/site.js +1 -1
  93. package/scripts/build.js +194 -129
  94. package/scripts/pro.js +254 -0
  95. package/scripts/reset.js +33 -8
  96. package/scripts/seed.js +677 -128
  97. package/scripts/setup.js +1 -0
  98. package/server/middleware/auth.js +136 -120
  99. package/server/routes/api/actions.js +200 -0
  100. package/server/routes/api/auth.js +292 -146
  101. package/server/routes/api/blocks.js +84 -0
  102. package/server/routes/api/collections.js +79 -27
  103. package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +491 -505
  104. package/server/routes/api/layouts.js +49 -39
  105. package/server/routes/api/media.js +118 -92
  106. package/server/routes/api/navigation.js +40 -36
  107. package/server/routes/api/pages.js +132 -118
  108. package/server/routes/api/plugins.js +6 -3
  109. package/server/routes/api/settings.js +104 -88
  110. package/server/routes/api/users.js +27 -19
  111. package/server/routes/api/views.js +148 -0
  112. package/server/routes/public.js +124 -108
  113. package/server/server.js +269 -181
  114. package/server/services/actions.js +387 -0
  115. package/server/services/adapterRegistry.js +98 -0
  116. package/server/services/adapters/FileAdapter.js +192 -0
  117. package/server/services/adapters/MongoAdapter.js +220 -0
  118. package/server/services/blocks.js +162 -0
  119. package/server/services/collections.js +74 -86
  120. package/server/services/connectionManager.js +102 -0
  121. package/server/services/content.js +312 -307
  122. package/server/services/email.js +126 -0
  123. package/server/services/forms.js +173 -0
  124. package/server/services/markdown.js +1378 -747
  125. package/server/services/permissionRegistry.js +173 -0
  126. package/server/services/presetCollections.js +251 -0
  127. package/server/services/renderer.js +98 -2
  128. package/server/services/roles.js +227 -0
  129. package/server/services/rowAccess.js +104 -0
  130. package/server/services/userProfiles.js +199 -0
  131. package/server/services/users.js +281 -212
  132. package/server/services/views.js +280 -0
  133. package/server/templates/page.html +124 -113
  134. package/plugins/form-builder/admin/templates/form-settings.html +0 -29
  135. package/plugins/form-builder/admin/views/form-editor.js +0 -1444
  136. package/plugins/form-builder/admin/views/form-settings.js +0 -38
  137. package/plugins/form-builder/admin/views/form-submissions.js +0 -295
  138. package/plugins/form-builder/admin/views/forms-list.js +0 -164
  139. package/plugins/form-builder/config.js +0 -9
  140. package/plugins/form-builder/data/forms/consent.json +0 -104
  141. package/plugins/form-builder/data/forms/contact-details.json +0 -99
  142. package/plugins/form-builder/data/submissions/consent.json +0 -13
  143. package/plugins/form-builder/plugin.json +0 -52
  144. package/plugins/form-builder/public/inject-body.html +0 -352
  145. package/plugins/form-builder/public/inject-head.html +0 -58
  146. package/plugins/form-builder/public/package.json +0 -1
  147. package/scripts/copy-domma.js +0 -48
  148. package/server/services/userTypes.js +0 -167
  149. /package/plugins/form-builder/data/submissions/{contact-details.json → enquiries.json} +0 -0
  150. /package/{plugins/form-builder/public → public/js}/form-logic-engine.js +0 -0
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Theme Roller Plugin — Server
3
+ * Manages custom themes: CRUD on JSON files, CSS generation, and site config updates.
4
+ *
5
+ * Endpoints (prefix: /api/plugins/theme-roller):
6
+ * GET /themes — admin: list saved themes
7
+ * GET /themes/:name — admin: get single theme
8
+ * POST /themes — admin: save new theme
9
+ * PUT /themes/:name — admin: update existing theme
10
+ * DELETE /themes/:name — admin: delete theme
11
+ * POST /themes/:name/activate — admin: activate theme (generate CSS + update site config)
12
+ * POST /deactivate — admin: revert to a built-in theme
13
+ */
14
+ import fs from 'node:fs/promises';
15
+ import path from 'node:path';
16
+ import {fileURLToPath} from 'url';
17
+ import {getConfig, saveConfig} from '../../server/config.js';
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const THEMES_DIR = path.join(__dirname, 'data', 'themes');
21
+ const CSS_FILE = path.join(__dirname, 'public', 'active-theme.css');
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function themePath(name) {
28
+ return path.join(THEMES_DIR, `${name}.json`);
29
+ }
30
+
31
+ async function readTheme(name) {
32
+ return JSON.parse(await fs.readFile(themePath(name), 'utf8'));
33
+ }
34
+
35
+ async function writeTheme(name, data) {
36
+ await fs.writeFile(themePath(name), JSON.stringify(data, null, 4) + '\n', 'utf8');
37
+ }
38
+
39
+ async function listThemes() {
40
+ let entries;
41
+ try {
42
+ entries = await fs.readdir(THEMES_DIR);
43
+ } catch {
44
+ return [];
45
+ }
46
+ const themes = [];
47
+ for (const f of entries) {
48
+ if (!f.endsWith('.json')) continue;
49
+ try {
50
+ themes.push(JSON.parse(await fs.readFile(path.join(THEMES_DIR, f), 'utf8')));
51
+ } catch { /* skip corrupt files */ }
52
+ }
53
+ return themes.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
54
+ }
55
+
56
+ /**
57
+ * Generate CSS for a custom theme.
58
+ * Produces `.dm-theme-{name} { --var: value; }` matching exportCSS() format.
59
+ *
60
+ * @param {Object} themeData - The theme JSON object
61
+ * @returns {string} CSS string
62
+ */
63
+ function generateCSS(themeData) {
64
+ const {name, changes} = themeData;
65
+ if (!changes || Object.keys(changes).length === 0) return '';
66
+
67
+ const date = new Date().toISOString().split('T')[0];
68
+ let css = `/**\n * Custom Domma Theme: ${name}\n * Generated: ${date}\n */\n\n`;
69
+ // Use :root so overrides apply regardless of which built-in theme class is on <body>.
70
+ // active-theme.css loads last (headInjectLate), so same-specificity :root wins by source order.
71
+ css += `:root {\n`;
72
+ for (const [variable, value] of Object.entries(changes)) {
73
+ css += ` ${variable}: ${value};\n`;
74
+ }
75
+ css += '}\n';
76
+ return css;
77
+ }
78
+
79
+ /**
80
+ * Write the active-theme.css file.
81
+ *
82
+ * @param {string} css - CSS content (empty string = no custom theme)
83
+ */
84
+ async function writeActiveCSS(css) {
85
+ const content = css || '/* Domma CMS — active custom theme (empty until a custom theme is activated) */\n';
86
+ await fs.writeFile(CSS_FILE, content, 'utf8');
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Plugin registration
91
+ // ---------------------------------------------------------------------------
92
+
93
+ export default async function themeRollerPlugin(fastify, opts) {
94
+ const {authenticate, requireAdmin} = opts.auth;
95
+
96
+ // -----------------------------------------------------------------------
97
+ // GET /themes — list all saved themes
98
+ // -----------------------------------------------------------------------
99
+ fastify.get('/themes', {preHandler: [authenticate, requireAdmin]}, async (req, reply) => {
100
+ return listThemes();
101
+ });
102
+
103
+ // -----------------------------------------------------------------------
104
+ // GET /themes/:name — get single theme
105
+ // -----------------------------------------------------------------------
106
+ fastify.get('/themes/:name', {preHandler: [authenticate, requireAdmin]}, async (req, reply) => {
107
+ try {
108
+ return await readTheme(req.params.name);
109
+ } catch {
110
+ return reply.code(404).send({error: 'Theme not found'});
111
+ }
112
+ });
113
+
114
+ // -----------------------------------------------------------------------
115
+ // POST /themes — save a new theme
116
+ // -----------------------------------------------------------------------
117
+ fastify.post('/themes', {preHandler: [authenticate, requireAdmin]}, async (req, reply) => {
118
+ const {name, baseTheme, changes} = req.body || {};
119
+
120
+ if (!name || typeof name !== 'string') {
121
+ return reply.code(400).send({error: 'name is required'});
122
+ }
123
+
124
+ // Sanitise name: lowercase alphanumeric + hyphens only
125
+ const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
126
+ if (!safeName) return reply.code(400).send({error: 'Invalid theme name'});
127
+
128
+ const now = new Date().toISOString();
129
+ const theme = {
130
+ name: safeName,
131
+ baseTheme: baseTheme || 'charcoal-dark',
132
+ changes: changes || {},
133
+ createdAt: now,
134
+ updatedAt: now
135
+ };
136
+
137
+ await writeTheme(safeName, theme);
138
+ return reply.code(201).send(theme);
139
+ });
140
+
141
+ // -----------------------------------------------------------------------
142
+ // PUT /themes/:name — update an existing theme
143
+ // -----------------------------------------------------------------------
144
+ fastify.put('/themes/:name', {preHandler: [authenticate, requireAdmin]}, async (req, reply) => {
145
+ const {name} = req.params;
146
+ let existing;
147
+ try {
148
+ existing = await readTheme(name);
149
+ } catch {
150
+ return reply.code(404).send({error: 'Theme not found'});
151
+ }
152
+
153
+ const {baseTheme, changes} = req.body || {};
154
+ const updated = {
155
+ ...existing,
156
+ baseTheme: baseTheme ?? existing.baseTheme,
157
+ changes: changes ?? existing.changes,
158
+ updatedAt: new Date().toISOString()
159
+ };
160
+
161
+ await writeTheme(name, updated);
162
+
163
+ // If this is the currently active theme, regenerate CSS
164
+ const siteConfig = getConfig('site');
165
+ if (siteConfig.theme === name) {
166
+ await writeActiveCSS(generateCSS(updated));
167
+ }
168
+
169
+ return updated;
170
+ });
171
+
172
+ // -----------------------------------------------------------------------
173
+ // DELETE /themes/:name — delete a theme
174
+ // -----------------------------------------------------------------------
175
+ fastify.delete('/themes/:name', {preHandler: [authenticate, requireAdmin]}, async (req, reply) => {
176
+ const {name} = req.params;
177
+ try {
178
+ await fs.unlink(themePath(name));
179
+ } catch {
180
+ return reply.code(404).send({error: 'Theme not found'});
181
+ }
182
+
183
+ // If deleting the active theme, revert to charcoal-dark
184
+ const siteConfig = getConfig('site');
185
+ if (siteConfig.theme === name) {
186
+ await writeActiveCSS('');
187
+ await saveConfig('site', {...siteConfig, theme: 'charcoal-dark'});
188
+ }
189
+
190
+ return {success: true};
191
+ });
192
+
193
+ // -----------------------------------------------------------------------
194
+ // POST /themes/:name/activate — activate a custom theme
195
+ // -----------------------------------------------------------------------
196
+ fastify.post('/themes/:name/activate', {preHandler: [authenticate, requireAdmin]}, async (req, reply) => {
197
+ const {name} = req.params;
198
+ let theme;
199
+ try {
200
+ theme = await readTheme(name);
201
+ } catch {
202
+ return reply.code(404).send({error: 'Theme not found'});
203
+ }
204
+
205
+ // Generate and write CSS
206
+ const css = generateCSS(theme);
207
+ await writeActiveCSS(css);
208
+
209
+ // Update site config so renderer applies the theme class to <body>
210
+ // baseTheme is saved so the renderer can apply both the base and custom class
211
+ const siteConfig = getConfig('site');
212
+ await saveConfig('site', {...siteConfig, theme: name, baseTheme: theme.baseTheme});
213
+
214
+ return {success: true, theme: name, css};
215
+ });
216
+
217
+ // -----------------------------------------------------------------------
218
+ // POST /deactivate — revert to a built-in theme
219
+ // -----------------------------------------------------------------------
220
+ fastify.post('/deactivate', {preHandler: [authenticate, requireAdmin]}, async (req, reply) => {
221
+ const {theme: builtInTheme = 'charcoal-dark'} = req.body || {};
222
+
223
+ // Clear custom CSS
224
+ await writeActiveCSS('');
225
+
226
+ // Update site config to the chosen built-in theme, clearing custom baseTheme
227
+ const siteConfig = getConfig('site');
228
+ const {baseTheme: _, ...restConfig} = siteConfig;
229
+ await saveConfig('site', {...restConfig, theme: builtInTheme});
230
+
231
+ return {success: true, theme: builtInTheme};
232
+ });
233
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "theme-roller",
3
+ "displayName": "Theme Roller",
4
+ "version": "1.0.0",
5
+ "description": "Visual theme customisation with live preview. Create, save, and apply custom themes.",
6
+ "author": "Darryl Waterhouse",
7
+ "date": "2026-03-06",
8
+ "icon": "palette",
9
+ "admin": {
10
+ "sidebar": [{
11
+ "id": "theme-roller",
12
+ "text": "Theme Roller",
13
+ "icon": "palette",
14
+ "url": "#/plugins/theme-roller",
15
+ "section": "#/plugins/theme-roller",
16
+ "countKey": "themes"
17
+ }],
18
+ "routes": [
19
+ { "path": "/plugins/theme-roller", "view": "plugin-theme-roller", "title": "Theme Roller - Domma CMS" }
20
+ ],
21
+ "views": {
22
+ "plugin-theme-roller": {
23
+ "entry": "theme-roller/admin/views/theme-roller-view.js",
24
+ "exportName": "themeRollerView"
25
+ }
26
+ }
27
+ },
28
+ "inject": {
29
+ "headLate": "public/inject-head-late.html"
30
+ }
31
+ }
File without changes
@@ -0,0 +1 @@
1
+ <link rel="stylesheet" href="/plugins/theme-roller/public/active-theme.css">
@@ -0,0 +1 @@
1
+ .fb-form-wrapper{max-width:600px;margin:2rem auto}.fb-form-honeypot{position:absolute;left:-9999px;top:-9999px;width:0;height:0;overflow:hidden;opacity:0;z-index:-1}.fb-form-success{padding:1rem 1.25rem;border-radius:6px;background:#34d3991f;border:1px solid rgba(52,211,153,.3);color:#34d399;font-size:.95rem;margin-top:.5rem}.fb-form-error{padding:.75rem 1rem;border-radius:6px;background:#ef44441a;border:1px solid rgba(239,68,68,.25);color:#f87171;font-size:.875rem;margin-top:.75rem}.fb-form-loading{opacity:.6;pointer-events:none}.wizard-progress{height:4px;background:var(--border-color, #333);border-radius:2px;margin-bottom:1rem}.wizard-progress-bar{height:100%;background:var(--primary, #5b8cff);border-radius:2px;transition:width .3s ease}.wizard-step-title{font-size:1.1rem;font-weight:600;margin-bottom:.25rem}.wizard-step-description{font-size:.875rem;color:var(--text-muted, #888);margin-bottom:1rem}.wizard-nav{display:flex;justify-content:space-between;gap:.5rem;margin-top:1rem}.fb-field-hidden{display:none!important}.fb-validation-error{color:var(--danger, #ef4444);font-size:.8rem;margin-top:.25rem}.fb-form-wrapper .domma-form-field{margin-bottom:2rem}.fb-form-wrapper .form-group:not(.form-buttons){margin-bottom:2rem}.fb-spacer{height:2rem}
@@ -1 +1 @@
1
- body,button,input,select,textarea{font-family:Roboto,sans-serif}.site-main{min-height:calc(100vh - 60px);padding-top:2rem;padding-bottom:4rem}.site-main.with-sidebar{display:grid;grid-template-columns:260px 1fr;gap:0}.site-sidebar{min-height:100%;border-right:1px solid var(--border-color, rgba(255,255,255,.08))}.site-content{overflow:hidden}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.container{max-width:860px;margin:0 auto;padding:0 1.5rem}.page-title{font-size:2rem;font-weight:700;margin-bottom:1.5rem;line-height:1.2}.page-body{line-height:1.7;font-size:1rem}.page-body h1,.page-body h2,.page-body h3,.page-body h4{margin-top:2rem;margin-bottom:.75rem;font-weight:600}.page-body h2{font-size:1.5rem}.page-body h3{font-size:1.25rem}.page-body p{margin-bottom:1rem}.page-body ul,.page-body ol{margin-bottom:1rem;padding-left:1.5rem}.page-body a{color:var(--primary, #5b8cff)}.page-body a:hover{text-decoration:underline}.page-body code{font-family:Fira Code,Courier New,monospace;font-size:.9em;background:#ffffff0f;padding:.15em .35em;border-radius:3px}.page-body pre{background:#0000004d;border:1px solid rgba(255,255,255,.08);border-radius:6px;padding:1rem;overflow-x:auto;margin-bottom:1rem}.page-body pre code{background:none;padding:0}.page-body img{max-width:100%;border-radius:6px}.page-body blockquote{border-left:3px solid var(--primary, #5b8cff);margin:1.5rem 0;padding:.75rem 1rem;background:#5b8cff0f;border-radius:0 6px 6px 0}.page-body .card-header h2{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.card[data-collapsible] .card-header{cursor:pointer;user-select:none;display:flex;align-items:center;justify-content:space-between}.card[data-collapsible] .card-header:after{content:"\25be";font-size:1.1em;line-height:1;display:inline-block;transition:transform .25s ease;flex-shrink:0}.card[data-collapsible].is-collapsed .card-header:after{transform:rotate(-90deg)}.card[data-collapsible] .card-body{overflow:hidden;max-height:4000px;opacity:1;transition:max-height .3s ease,opacity .25s ease}.card[data-collapsible].is-collapsed .card-body{max-height:0;opacity:0}.navbar-link span[data-icon],.navbar-link svg,.navbar-dropdown-toggle span[data-icon],.navbar-dropdown-toggle svg,.navbar-dropdown-item span[data-icon],.navbar-dropdown-item svg{width:13px!important;height:13px!important;margin-right:10px!important}.navbar-dropdown-toggle{font-size:var(--dm-font-size-base)}@media(min-width:993px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-sm)}}@media(min-width:1201px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-xs)}}.dm-reduced-motion *,.dm-reduced-motion *:before,.dm-reduced-motion *:after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}.page-footer{border-top:1px solid var(--border-color, rgba(255,255,255,.08));padding:1.5rem 0}.footer-inner{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem}.footer-inner p{margin:0;color:var(--text-muted, #888);font-size:.875rem}.footer-links{display:flex;gap:1.25rem}.footer-links a{color:var(--text-muted, #888);font-size:.875rem;text-decoration:none}.footer-links a:hover{color:var(--text, #eee)}.footer-social{display:flex;gap:.5rem;align-items:center}.footer-social-link{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;color:var(--text-muted, #888);transition:color .15s}.footer-social-link:hover{color:var(--text, #eee)}.footer-social-link svg{width:1rem;height:1rem}.footer-motion-switch{font-size:.8rem;color:var(--text-muted, #888);white-space:nowrap}.footer-motion-switch .form-switch-label{color:var(--text-muted, #888)}.footer-motion-switch .form-switch-input{width:2rem;height:1.125rem}.footer-motion-switch .form-switch-input:after{width:.875rem;height:.875rem}.footer-motion-switch .form-switch-input:checked:after{transform:translate(.875rem)}.dm-slideover-header{display:flex;align-items:center;justify-content:space-between;padding:.875rem 1.25rem;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08));flex-shrink:0}.dm-slideover-title{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.dm-slideover-body{padding:1.25rem;overflow-y:auto;flex:1}@media(max-width:768px){.site-main.with-sidebar{grid-template-columns:1fr}.site-sidebar{display:none}}.dm-spacer{display:block;width:100%}.hero-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}body[data-layout=landing]>.site-main .container{max-width:none;padding:0}body[data-layout=landing] .page-body{padding-left:1.5rem;padding-right:1.5rem}body[data-layout=landing] .page-body>p,body[data-layout=landing] .page-body>h1,body[data-layout=landing] .page-body>h2,body[data-layout=landing] .page-body>h3,body[data-layout=landing] .page-body>ul,body[data-layout=landing] .page-body>ol,body[data-layout=landing] .page-body>blockquote{max-width:860px;margin-left:auto;margin-right:auto}body[data-layout=landing] .page-body .hero-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem}body[data-layout=landing] .page-body .grid-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem;padding-left:1.5rem;padding-right:1.5rem}.page-body .card{transition:transform .2s ease,box-shadow .2s ease}.page-body .card:hover{transform:translateY(-3px);box-shadow:0 8px 24px #00000059}.grid-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}
1
+ body,button,input,select,textarea{font-family:Roboto,sans-serif}.site-main{min-height:calc(100vh - 60px);padding-top:2rem;padding-bottom:4rem}.site-main.with-sidebar{display:grid;grid-template-columns:260px 1fr;gap:0}.site-sidebar{min-height:100%;border-right:1px solid var(--border-color, rgba(255,255,255,.08))}.site-content{overflow:hidden}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.container{max-width:860px;margin:0 auto;padding:0 1.5rem}.page-title{font-size:2rem;font-weight:700;margin-bottom:1.5rem;line-height:1.2}.page-body{line-height:1.7;font-size:1rem}.page-body h1,.page-body h2,.page-body h3,.page-body h4{margin-top:2rem;margin-bottom:.75rem;font-weight:600}.page-body h2{font-size:1.5rem}.page-body h3{font-size:1.25rem}.page-body p{margin-bottom:1rem}.page-body ul,.page-body ol{margin-bottom:1rem;padding-left:1.5rem}.page-body a{color:var(--primary, #5b8cff)}.page-body a:hover{text-decoration:underline}.page-body code{font-family:Fira Code,Courier New,monospace;font-size:.9em;background:#ffffff0f;padding:.15em .35em;border-radius:3px}.page-body pre{background:#0000004d;border:1px solid rgba(255,255,255,.08);border-radius:6px;padding:1rem;overflow-x:auto;margin-bottom:1rem}.page-body pre code{background:none;padding:0}.page-body img{max-width:100%;border-radius:6px}.page-body blockquote{border-left:3px solid var(--primary, #5b8cff);margin:1.5rem 0;padding:.75rem 1rem;background:#5b8cff0f;border-radius:0 6px 6px 0}h3.accordion-header{margin:0}.accordion-button{all:unset;display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer;font:inherit}.page-body .card-header h2{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.card[data-collapsible] .card-header{cursor:pointer;user-select:none;display:flex;align-items:center;justify-content:space-between}.card[data-collapsible] .card-header:after{content:"\25be";font-size:1.1em;line-height:1;display:inline-block;transition:transform .25s ease;flex-shrink:0}.card[data-collapsible].is-collapsed .card-header:after{transform:rotate(-90deg)}.card[data-collapsible] .card-body{overflow:hidden;max-height:4000px;opacity:1;transition:max-height .3s ease,opacity .25s ease}.card[data-collapsible].is-collapsed .card-body{max-height:0;opacity:0}.navbar-link span[data-icon],.navbar-link svg,.navbar-dropdown-toggle span[data-icon],.navbar-dropdown-toggle svg,.navbar-dropdown-item span[data-icon],.navbar-dropdown-item svg{width:13px!important;height:13px!important;margin-right:10px!important}.navbar-dropdown-toggle{font-size:var(--dm-font-size-base)}@media(min-width:993px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-sm)}}@media(min-width:1201px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-xs)}}.dm-reduced-motion *,.dm-reduced-motion *:before,.dm-reduced-motion *:after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}.page-footer{border-top:1px solid var(--border-color, rgba(255,255,255,.08));padding:1.5rem 0}.footer-inner{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem}.footer-inner p{margin:0;color:var(--text-muted, #888);font-size:.875rem}.footer-links{display:flex;gap:1.25rem}.footer-links a{color:var(--text-muted, #888);font-size:.875rem;text-decoration:none}.footer-links a:hover{color:var(--text, #eee)}.footer-social{display:flex;gap:.5rem;align-items:center}.footer-social-link{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;color:var(--text-muted, #888);transition:color .15s}.footer-social-link:hover{color:var(--text, #eee)}.footer-social-link svg{width:1rem;height:1rem}.footer-motion-switch{font-size:.8rem;color:var(--text-muted, #888);white-space:nowrap}.footer-motion-switch .form-switch-label{color:var(--text-muted, #888)}.footer-motion-switch .form-switch-input{width:2rem;height:1.125rem}.footer-motion-switch .form-switch-input:after{width:.875rem;height:.875rem}.footer-motion-switch .form-switch-input:checked:after{transform:translate(.875rem)}.dm-slideover-header{display:flex;align-items:center;justify-content:space-between;padding:.875rem 1.25rem;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08));flex-shrink:0}.dm-slideover-title{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.dm-slideover-body{padding:1.25rem;overflow-y:auto;flex:1}@media(max-width:768px){.site-main.with-sidebar{grid-template-columns:1fr}.site-sidebar{display:none}}.dm-spacer{display:block;width:100%}.hero-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.site-main:has(.page-body>.hero-breakout:first-child){padding-top:0}body[data-layout=landing]>.site-main{padding-top:0}body[data-layout=landing]>.site-main .container{max-width:none;padding:0}body[data-layout=landing] .page-body{padding-left:1.5rem;padding-right:1.5rem}body[data-layout=landing] .page-body>p,body[data-layout=landing] .page-body>h1,body[data-layout=landing] .page-body>h2,body[data-layout=landing] .page-body>h3,body[data-layout=landing] .page-body>ul,body[data-layout=landing] .page-body>ol,body[data-layout=landing] .page-body>blockquote{max-width:860px;margin-left:auto;margin-right:auto}body[data-layout=landing] .page-body .hero-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem}body[data-layout=landing] .page-body .grid-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem;padding-left:1.5rem;padding-right:1.5rem}.page-body .card{transition:transform .2s ease,box-shadow .2s ease}.page-body .card:hover{transform:translateY(-3px);box-shadow:0 8px 24px #00000059}.page-body .card-header-icon-inline{display:flex;align-items:center;gap:.6rem}.page-body .card-header-icon-inline [data-icon]{flex-shrink:0;line-height:0}.page-body .card-header-icon-inline [data-icon] svg,.page-body .card-header-icon-inline>svg{display:block;width:1.25rem;height:1.25rem}.page-body .card-header-icon-stacked{display:flex;flex-direction:column;align-items:center;text-align:center;gap:.35rem;padding-top:.25rem}.page-body .card-header-icon-stacked [data-icon],.page-body .card-header-icon-stacked svg{width:2rem;height:2rem}.hero.hero-dark{background:linear-gradient(135deg,#1f2937,#111827);color:#e2e8f0}.hero .hero-content{position:relative;z-index:2}.hero.hero-left .hero-content{text-align:left;align-items:flex-start;max-width:62%}@media(max-width:768px){.hero.hero-left .hero-content{max-width:100%}}.hero .hero-cta{display:flex;gap:.85rem;flex-wrap:wrap;margin-top:1.75rem}.hero .hero-cta a{display:inline-flex;align-items:center;gap:.4rem;padding:.55rem 1.35rem;border-radius:6px;font-size:.95rem;font-weight:500;text-decoration:none;transition:background .2s ease,border-color .2s ease,transform .15s ease,box-shadow .2s ease}.hero .hero-cta a:first-child{background:#ffffffeb;color:#111;border:1px solid transparent}.hero .hero-cta a:first-child:hover{background:#fff;box-shadow:0 4px 16px #00000040;transform:translateY(-2px)}.hero .hero-cta a:last-child{background:transparent;color:#fff;border:1px solid rgba(255,255,255,.4)}.hero .hero-cta a:last-child:hover{border-color:#ffffffbf;background:#ffffff14;transform:translateY(-2px)}.hero .hero-label{display:inline-block;margin-bottom:.9rem;padding:.2rem .8rem;border-radius:999px;font-size:.72rem;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:#ffffffb3;border:1px solid rgba(255,255,255,.22)}.grid-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.dm-breadcrumbs{position:fixed;z-index:200;display:inline-flex;align-items:center;gap:.2rem;padding:.3rem .8rem;border-radius:999px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);background:#00000047;border:1px solid rgba(255,255,255,.11);box-shadow:0 2px 10px #00000038;font-size:.72rem;font-weight:500;letter-spacing:.01em;line-height:1.4;max-width:calc(100vw - 2rem);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dm-breadcrumbs .dm-breadcrumbs-item{color:#ffffffa6}.dm-breadcrumbs .dm-breadcrumbs-link{display:inline-flex;align-items:center;gap:.25rem;color:#ffffff8c;text-decoration:none;transition:color .15s}.dm-breadcrumbs .dm-breadcrumbs-home-icon{flex-shrink:0;vertical-align:middle}.dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#fffffff2}.dm-breadcrumbs .dm-breadcrumbs-current{color:#ffffffeb;font-weight:600}.dm-breadcrumbs .dm-breadcrumbs-separator{color:#ffffff47;font-size:.8em;line-height:1;margin:0 .05rem}[data-mode=light] .dm-breadcrumbs{background:#ffffff8c;border-color:#00000012;box-shadow:0 2px 10px #00000014}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-item,[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link{color:#0000008c}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#000000e6}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-current{color:#000000d9}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-separator{color:#00000040}.dm-collection-display{margin:1.5rem 0}.dm-collection-list{display:flex;flex-direction:column;gap:0}.dm-collection-list-item{padding:1rem 0;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08))}.dm-collection-list-item:last-child{border-bottom:none}.dm-collection-list-item strong{display:block;font-size:1rem;margin-bottom:.25rem}.dm-collection-list-item p{margin:0;color:var(--text-muted, #888);font-size:.9rem}.dm-collection-empty p{color:var(--text-muted, #888);font-style:italic}
@@ -0,0 +1 @@
1
+ const targets=document.querySelectorAll("[data-form]");targets.length&&targets.forEach(initFormTarget);function showMessage(i,r,t){const e=i.querySelector(".fb-form-success, .fb-form-error");e&&e.remove();const s=document.createElement("div");s.className=t==="success"?"fb-form-success":"fb-form-error",s.textContent=r,i.appendChild(s)}function buildBlueprintFromFields(i,r){const t={};return i.forEach(function(e){if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const s=e.type==="checkbox"?"boolean":e.type==="date"?"string":e.type,o={...e.formConfig||{}};o.span==="full"&&r&&(o.span=r),t[e.name]={type:s,label:e.label||e.name,required:e.required||!1,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...o}},e.minLength!==void 0&&(t[e.name].minLength=e.minLength),e.maxLength!==void 0&&(t[e.name].maxLength=e.maxLength),e.min!==void 0&&(t[e.name].min=e.min),e.max!==void 0&&(t[e.name].max=e.max)}),t}function buildInitialData(i){const r={};return i.forEach(function(t){if(!(!t.name||t.type==="page-break"||t.type==="spacer")&&(t.type==="select"||t.type==="multiselect")&&t.required){const e=(t.options||[])[0];e&&(r[t.name]=typeof e=="object"?e.value:e)}}),r}function patchDateInputs(i,r){(r||[]).forEach(function(t){if(t.type!=="date"||!t.name)return;const e=i.querySelector('[name="'+t.name+'"]');e&&e.type!=="date"&&(e.type="date")})}function buildWizardSteps(i,r){const t=[];let e=[],s=r||"Step 1",o="";return i.forEach(function(m){m.type==="page-break"?(t.push({title:s,description:o,fieldGroup:e}),e=[],s=m.label||"Step "+(t.length+1),o=m.description||""):m.type!=="spacer"&&e.push(m)}),t.push({title:s,description:o,fieldGroup:e}),t}function injectHoneypot(i){const r=document.createElement("div");r.className="fb-form-honeypot",r.setAttribute("aria-hidden","true");const t=document.createElement("input");t.name="website",t.type="text",t.tabIndex=-1,t.autocomplete="url",t.placeholder="https://",r.appendChild(t);const e=document.createElement("input");e.name="_t",e.type="hidden",e.value=Date.now(),r.appendChild(e),i.appendChild(r)}function injectSpacers(i,r){const t=i.querySelector("form");if(!t)return;const e=Array.from(t.querySelectorAll(".form-group"));let s=0;r.forEach(function(o){if(o.type==="spacer"){const m=document.createElement("div");m.className="fb-spacer";const a=e[s];if(a)t.insertBefore(m,a);else{const u=t.querySelector('[type="submit"]');u?t.insertBefore(m,u):t.appendChild(m)}}else o.type!=="page-break"&&s++})}function submitForm(i,r,t,e,s){const o=s||e,m=o.querySelector('[name="website"]')?.value||"",a=o.querySelector('[name="_t"]')?.value||"",u=Object.assign({},r,{_hp:m,_t:a});return fetch("/api/forms/submit/"+i,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(u)}).then(c=>c.json().then(n=>({ok:c.ok,body:n}))).then(c=>{c.ok&&c.body.ok?(e.textContent="",showMessage(e,c.body.message||t.successMessage||"Thank you.","success")):showMessage(e,c.body.error||"Something went wrong.","error")}).catch(()=>{showMessage(e,"Unable to submit. Please check your connection.","error")})}function renderManualForm(i,r,t,e,s){const o=document.createElement("form");o.noValidate=!0,r.forEach(function(a){const u=document.createElement("div");u.className="form-group",u.style.marginBottom="1.25rem";const c=document.createElement("label");if(c.className="form-label",c.textContent=a.label||a.name,a.required){const p=document.createElement("span");p.textContent=" *",p.style.color="#f87171",c.appendChild(p)}let n;a.type==="textarea"?(n=document.createElement("textarea"),n.rows=a.formConfig?.rows||4,n.className="form-input"):a.type==="select"?(n=document.createElement("select"),n.className="form-input",(a.options||[]).forEach(function(p){const l=document.createElement("option");l.value=typeof p=="string"?p:p.value??"",l.textContent=typeof p=="string"?p:p.label||l.value,n.appendChild(l)})):(n=document.createElement("input"),n.type=a.type||"text",n.className="form-input",a.placeholder&&(n.placeholder=a.placeholder)),n.name=a.name,n.required=a.required||!1,u.appendChild(c),u.appendChild(n),o.appendChild(u)}),t.honeypot&&injectHoneypot(o);const m=document.createElement("button");m.type="submit",m.className="btn btn-primary",m.textContent=t.submitText||"Submit",o.appendChild(m),o.addEventListener("submit",function(a){a.preventDefault();const u={};if(r.forEach(function(c){const n=o.querySelector('[name="'+c.name+'"]');n&&(u[c.name]=n.value)}),window.FormLogicEngine&&s){const c=window.FormLogicEngine,n=[],p=[];if(r.forEach(function(l){if(c.evaluateFieldVisibility(l,u)==="hidden"){delete u[l.name];return}const h=c.evaluateFieldRequirement(l,u),f=u[l.name];h&&(!f||!String(f).trim())&&n.push(l.label||l.name);const d=c.validateField(l,f||"",u);d.length&&p.push(d[0].message)}),n.length||p.length){const l=[];n.length&&l.push("Required: "+n.join(", ")),p.length&&l.push(p.join("; ")),showMessage(i,l.join(". "),"error");return}}i.classList.add("fb-form-loading"),m.disabled=!0,submitForm(e,u,t,i,o).finally(function(){i.classList.remove("fb-form-loading"),m.disabled=!1})}),i.appendChild(o),window.FormLogicEngine&&s&&r.some(a=>a.logic)&&new window.FormLogicEngine.FormLogicRuntime(s,i).init()}function initFormTarget(i){const r=i.getAttribute("data-form");r&&fetch("/api/forms/"+r+"/public").then(t=>{if(!t.ok)throw new Error("Form not found: "+r);return t.json()}).then(t=>{const e=t.fields||[],s=t.settings||{},o=document.createElement("div");o.className="fb-form-wrapper",i.appendChild(o);const m=e.some(a=>a.type==="page-break");if(typeof Domma<"u"&&Domma.forms){const a=s.columns||1;if(m&&Domma.forms.wizard){const c=buildWizardSteps(e,t.title).map(function(n){return{title:n.title,description:n.description,fields:buildBlueprintFromFields(n.fieldGroup,a)}});if(Domma.forms.wizard(o,{schema:{steps:c},onSubmit:function(n){return submitForm(r,n,s,o,null)}}),patchDateInputs(o,e),window.FormLogicEngine&&e.some(n=>n.logic)&&new window.FormLogicEngine.FormLogicRuntime(t,o).init(),s.honeypot){const n=o.querySelector("form");n&&injectHoneypot(n)}}else if(Domma.forms.render){const u=buildBlueprintFromFields(e,a),c=buildInitialData(e);if(Domma.forms.render(o,u,c,{submitText:s.submitText||"Submit",layout:s.layout||"stacked",columns:a,onSubmit:function(n){return submitForm(r,n,s,o,null)}}),patchDateInputs(o,e),window.FormLogicEngine&&e.some(n=>n.logic)&&new window.FormLogicEngine.FormLogicRuntime(t,o).init(),e.some(n=>n.type==="spacer")&&injectSpacers(o,e),s.honeypot){const n=o.querySelector("form");n&&injectHoneypot(n)}}}else renderManualForm(o,e.filter(a=>a.type!=="page-break"&&a.type!=="spacer"),s,r,t)}).catch(t=>{const e=document.createElement("p");e.textContent="Form unavailable.",e.style.cssText="color:#f87171;font-style:italic;",i.appendChild(e),console.warn("[forms]",t.message)})}
package/public/js/site.js CHANGED
@@ -1 +1 @@
1
- $(()=>{const r=window.__CMS_NAV__||{},h=window.__CMS_SITE__||{};if($("#site-navbar").length&&r.brand){const t={...r.brand};if(t.icon){let a=`<span data-icon="${t.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`;t.text&&(a+=`<span class="navbar-brand-text">${t.text}</span>`),t.html=a}Domma.elements.navbar("#site-navbar",{brand:t,items:r.items||[],variant:r.variant||"dark",position:r.position||"sticky",collapsible:!0}),Domma.icons.scan("#site-navbar")}const m=$("#site-footer");if(m.length){const t=h.social||{},a={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let e='<div class="footer-inner container">';if(h.footer){const c=h.footer;e+=`<p>${c.copyright||""}</p>`,c.links?.length&&(e+='<nav class="footer-links">',c.links.forEach(o=>{e+=`<a href="${o.url}">${o.text}</a>`}),e+="</nav>");const l=Object.keys(a).filter(o=>t[o]);l.length&&(e+='<div class="footer-social">',l.forEach(o=>{const{label:s,svg:g}=a[o];e+=`<a href="${t[o]}" target="_blank" rel="noopener noreferrer" aria-label="${s}" class="footer-social-link">${g}</a>`}),e+="</div>")}const i=S.get("reduced_motion"),f=i!==null?!!i:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches);e+=`<label class="form-switch footer-motion-switch" title="Reduce motion"><input type="checkbox" class="form-switch-input" id="dm-motion-switch"${f?" checked":""}><span class="form-switch-label">Reduce motion</span></label>`,e+="</div>",m.html(e,{safe:!1}),document.getElementById("dm-motion-switch").addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()})}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan();const n=$(".page-body");if(n.length&&(n.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),n.find(".tabs").each(function(){Domma.elements.tabs(this)}),n.find(".carousel").each(function(){Domma.elements.carousel(this,{autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"})}),n.find(".dm-countdown").each(function(){const t={autoStart:!0};this.dataset.to&&(t.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(t.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(t.format=this.dataset.format),Domma.elements.timer(this,t)}),n.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),n.find(".card[data-collapsible]").each(function(){const t=this.querySelector(".card-header");t&&t.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),n.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const t=this.dataset.soTarget,a=document.getElementById(t);if(!a)return;const e=E.slideover({title:a.dataset.soTitle||"",size:a.dataset.soSize||"md",position:a.dataset.soPosition||"right"});a.style.display="",e.setContent(a),e.open()})})),typeof $.setup=="function"){const t=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(a=>{try{const e=atob(a.dataset.config),i=JSON.parse(e);Object.assign(t,i)}catch{}}),Object.keys(t).length){const a={};for(const[e,i]of Object.entries(t)){const f=i?.events?.click,{confirm:c,toast:l,alert:o,...s}=f||{};c||l||o?($(e).on("click",async function(v){if(v.preventDefault(),!(c&&!await E.confirm(c))){if(s.target){const d=$(s.target);s.toggleClass&&d.toggleClass(s.toggleClass),s.addClass&&d.addClass(s.addClass),s.removeClass&&d.removeClass(s.removeClass)}s.href&&(window.location.href=s.href),l&&E.toast(l,{type:s.toastType||"success"}),o&&E.alert(o)}}),Object.keys(s).length&&(a[e]={...i,events:{...i.events,click:s}})):a[e]=i}$.setup(a)}}});
1
+ $(()=>{const y=window.__CMS_NAV__||{},r=window.__CMS_SITE__||{};if(r.autoTheme?.enabled){let e=function(n){const u=(n||"07:00").split(":");return+u[0]*60+(+u[1]||0)},o=function(){const n=new Date,u=n.getHours()*60+n.getMinutes();return u>=e(t.dayStart)&&u<e(t.nightStart)?t.dayTheme:t.nightTheme};var s=e,l=o;const t=r.autoTheme;Domma.theme.set(o()),setInterval(()=>Domma.theme.set(o()),6e4)}if($("#site-navbar").length&&y.brand){const t={...y.brand};if(t.icon){let e=`<span data-icon="${t.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`;t.text&&(e+=`<span class="navbar-brand-text">${t.text}</span>`),t.html=e}Domma.elements.navbar("#site-navbar",{brand:t,items:y.items||[],variant:y.variant||"dark",position:y.position||"sticky",collapsible:!0}),Domma.icons.scan("#site-navbar")}const v=$("#site-footer");if(v.length){const t=r.social||{},e={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let o='<div class="footer-inner container">';if(r.footer){const h=r.footer;o+=`<p>${h.copyright||""}</p>`,h.links?.length&&(o+='<nav class="footer-links">',h.links.forEach(i=>{o+=`<a href="${i.url}">${i.text}</a>`}),o+="</nav>");const f=Object.keys(e).filter(i=>t[i]);f.length&&(o+='<div class="footer-social">',f.forEach(i=>{const{label:d,svg:a}=e[i];o+=`<a href="${t[i]}" target="_blank" rel="noopener noreferrer" aria-label="${d}" class="footer-social-link">${a}</a>`}),o+="</div>")}const n=S.get("reduced_motion"),u=n!==null?!!n:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches);o+=`<label class="form-switch footer-motion-switch" title="Reduce motion"><input type="checkbox" class="form-switch-input" id="dm-motion-switch"${u?" checked":""}><span class="form-switch-label">Reduce motion</span></label>`,o+="</div>",v.html(o,{safe:!1}),document.getElementById("dm-motion-switch").addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()})}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan();const c=$(".page-body");if(c.length&&(c.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),c.find(".tabs").each(function(){Domma.elements.tabs(this)}),c.find(".carousel").each(function(){Domma.elements.carousel(this,{autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"})}),c.find(".dm-countdown").each(function(){const t={autoStart:!0};this.dataset.to&&(t.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(t.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(t.format=this.dataset.format),Domma.elements.timer(this,t)}),c.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),c.find(".dm-progression").each(function(){Domma.elements.progression(this,{layout:this.dataset.layout||"vertical",theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0})}),c.find(".card[data-collapsible]").each(function(){const t=this.querySelector(".card-header");t&&t.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),c.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const t=this.dataset.soTarget,e=document.getElementById(t);if(!e)return;const o=E.slideover({title:e.dataset.soTitle||"",size:e.dataset.soSize||"md",position:e.dataset.soPosition||"right"});e.style.display="",o.setContent(e),o.open()})})),typeof $.setup=="function"){const t=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(e=>{try{const o=atob(e.dataset.config),n=JSON.parse(o);Object.assign(t,n)}catch{}}),Object.keys(t).length>0){const e={};for(const[o,n]of Object.entries(t)){const u=n?.events?.click,{confirm:h,toast:f,alert:i,prompt:d,...a}=u||{};h||f||i||d?($(o).on("click",async function(p){if(p.preventDefault(),h&&!await E.confirm(h))return;let C=null;if(!(d&&(C=await E.prompt(d,{inputPlaceholder:a.promptPlaceholder||"",inputValue:a.promptDefault||""}),C===null))){if(a.target){const w=$(a.target);a.toggleClass&&w.toggleClass(a.toggleClass),a.addClass&&w.addClass(a.addClass),a.removeClass&&w.removeClass(a.removeClass),C!==null&&(a.setText&&w.text(C),a.setVal&&w.val(C),a.setAttr&&w.attr(a.setAttr,C))}a.href&&(window.location.href=a.href),f&&E.toast(f,{type:a.toastType||"success"}),i&&E.alert(i)}}),Object.keys(a).length&&(e[o]={...n,events:{...n.events,click:a}})):e[o]=n}$.setup(e)}}c.length&&wireCTAButtons(c.get(0))});function wireCTAButtons(y){y.querySelectorAll(".dm-cta-trigger").forEach(r=>{r.addEventListener("click",async()=>{const b=r.dataset.action,v=r.dataset.entry,g=r.dataset.confirm;let c=S.get("auth_token");if(!c){E.toast("Please log in to perform this action.",{type:"warning"});return}if(g&&!await E.confirm(g))return;const s=Array.from(r.childNodes).map(t=>t.cloneNode(!0));r.disabled=!0,r.textContent="Running\u2026";const l=t=>fetch(`/api/actions/${b}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify({entryId:v})});try{let t=await l(c);if(t.status===401){const o=S.get("auth_refresh_token");if(o){const n=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:o})});if(n.ok){const{token:u}=await n.json();S.set("auth_token",u),c=u,t=await l(c)}}}const e=await t.json().catch(()=>({}));if(!t.ok)throw new Error(e.error||e.message||`Error ${t.status}`);E.toast(e.message||"Action completed.",{type:"success"})}catch(t){E.toast(t.message||"Action failed.",{type:"error"})}finally{r.disabled=!1,r.textContent="",s.forEach(t=>r.appendChild(t)),Domma.icons.scan(r)}})})}(function(){const r=document.querySelectorAll("[data-collection-table]");!r.length||typeof T>"u"||!T.create||r.forEach(b=>{let v;try{v=JSON.parse(atob(b.dataset.payload))}catch{return}const{columns:g,rows:c,search:s,sortable:l,exportable:t,pageSize:e,empty:o,ctaConfig:n}=v;if(!g?.length)return;if(n){const f=g.findIndex(i=>i.key==="_cta");f!==-1&&(g[f]={key:"_cta",title:"",render:(i,d)=>{const a=document.createElement("button");if(a.className=`btn btn-${n.style||"primary"} dm-cta-trigger`,a.dataset.action=n.action||"",a.dataset.entry=d._entryId||"",n.confirm&&(a.dataset.confirm=n.confirm),n.icon){const m=document.createElement("span");m.dataset.icon=n.icon,a.appendChild(m),a.appendChild(document.createTextNode(" "))}return a.appendChild(document.createTextNode(n.label||"Run")),a}})}const u="col-table-"+Math.random().toString(36).slice(2,7),h=document.createElement("div");h.id=u,b.replaceChildren(h),T.create("#"+u,{data:c,columns:g,search:s,sortable:l,exportable:t,pageSize:e,emptyMessage:o}),n&&wireCTAButtons(h)})})(),(function(){const r=document.querySelectorAll("[data-form-inline]");if(!r.length||typeof F>"u")return;function b(s,l){const t={};return(s||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const o=e.type==="checkbox"?"boolean":e.type==="date"?"string":e.type,n={...e.formConfig||{}};n.span==="full"&&l&&(n.span=l),t[e.name]={type:o,label:e.label,required:e.required,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...n}}}),t}function v(s){const l={};return(s||[]).forEach(t=>{if(!(!t.name||t.type==="page-break"||t.type==="spacer")&&(t.type==="select"||t.type==="multiselect")&&t.required){const e=(t.options||[])[0];e&&(l[t.name]=typeof e=="object"?e.value:e)}}),l}function g(s,l){(l||[]).forEach(t=>{if(t.type!=="date"||!t.name)return;const e=s.querySelector(`[name="${t.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function c(s,l,t){let e=s.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",s.appendChild(e)),e.textContent=l,e.style.cssText=t?"color:var(--danger,#f87171);margin-top:.75rem;":"color:var(--success,#4ade80);margin-top:.75rem;"}r.forEach(s=>{let l;try{l=JSON.parse(atob(s.dataset.formInline))}catch{return}const t=l.fields||[],e=l.settings||{},o=e.columns||1,n=e.layout||"stacked",u=t.some(i=>i.type==="page-break"),h=async i=>{try{const d=s.querySelector('[name="website"]'),a=s.querySelector('[name="_t"]'),m=Object.assign({},i);d!==null&&(m._hp=d.value),a!==null&&(m._t=a.value);const p=await H.post(`/api/forms/submit/${l.slug}`,m);if(p?.redirect){window.location.href=p.redirect;return}for(;s.firstChild;)s.removeChild(s.firstChild);c(s,p?.message||e.successMessage||"Thank you for your submission.",!1)}catch(d){throw c(s,d.message||"Submission failed. Please try again.",!0),d}};function f(i){const d=i.querySelector("form");if(!d)return;const a=document.createElement("div");a.className="fb-form-honeypot",a.setAttribute("aria-hidden","true");const m=document.createElement("input");m.name="website",m.type="text",m.tabIndex=-1,m.autocomplete="url",m.placeholder="https://",a.appendChild(m);const p=document.createElement("input");p.name="_t",p.type="hidden",p.value=Date.now(),a.appendChild(p),d.appendChild(a)}if(u&&F.wizard){const i=[];let d=[],a=l.title||"Step 1",m="";t.forEach(p=>{p.type==="page-break"?(i.push({title:a,description:m,fields:b(d,o)}),d=[],a=p.label||`Step ${i.length+1}`,m=p.description||""):p.type!=="spacer"&&d.push(p)}),i.push({title:a,description:m,fields:b(d,o)}),F.wizard(s,{schema:{steps:i},onSubmit:h}),g(s,t),e.honeypot!==!1&&f(s)}else if(F.render){if(F.render(s,b(t,o),v(t),{submitText:e.submitText||"Submit",layout:n,columns:o,onSubmit:h}),n==="grid"&&e.submitSpan==="full"){const i=s.querySelector(".form-buttons");i&&i.classList.add("col-span-full")}g(s,t),e.honeypot!==!1&&f(s)}})})();