domma-cms 0.2.1 → 0.5.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 (166) hide show
  1. package/README.md +3 -3
  2. package/admin/css/admin.css +1 -1200
  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 -242
  7. package/admin/js/app.js +9 -279
  8. package/admin/js/config/sidebar-config.js +1 -115
  9. package/admin/js/lib/card.js +1 -63
  10. package/admin/js/lib/image-editor.js +1 -869
  11. package/admin/js/lib/markdown-toolbar.js +54 -421
  12. package/admin/js/templates/action-editor.html +171 -0
  13. package/admin/js/templates/actions-list.html +19 -0
  14. package/admin/js/templates/api-reference.html +1411 -0
  15. package/admin/js/templates/block-editor.html +158 -0
  16. package/admin/js/templates/blocks.html +8 -0
  17. package/admin/js/templates/collection-editor.html +47 -0
  18. package/admin/js/templates/collection-entries.html +3 -0
  19. package/admin/js/templates/collections.html +51 -4
  20. package/admin/js/templates/documentation.html +258 -0
  21. package/admin/js/templates/form-editor.html +238 -0
  22. package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
  23. package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
  24. package/admin/js/templates/layouts.html +44 -7
  25. package/admin/js/templates/login.html +29 -4
  26. package/admin/js/templates/my-profile.html +17 -0
  27. package/admin/js/templates/page-editor.html +48 -0
  28. package/admin/js/templates/pages.html +6 -1
  29. package/admin/js/templates/pro-docs.html +259 -0
  30. package/admin/js/templates/role-editor.html +59 -0
  31. package/admin/js/templates/roles.html +10 -0
  32. package/admin/js/templates/settings.html +137 -18
  33. package/admin/js/templates/tutorials.html +81 -0
  34. package/admin/js/templates/user-editor.html +7 -0
  35. package/admin/js/templates/users.html +3 -1
  36. package/admin/js/templates/view-editor.html +201 -0
  37. package/admin/js/templates/view-preview.html +51 -0
  38. package/admin/js/templates/views-list.html +19 -0
  39. package/admin/js/views/action-editor.js +1 -0
  40. package/admin/js/views/actions-list.js +1 -0
  41. package/admin/js/views/api-reference.js +1 -0
  42. package/admin/js/views/block-editor.js +8 -0
  43. package/admin/js/views/blocks.js +4 -0
  44. package/admin/js/views/collection-editor.js +3 -487
  45. package/admin/js/views/collection-entries.js +1 -484
  46. package/admin/js/views/collections.js +1 -153
  47. package/admin/js/views/dashboard.js +1 -56
  48. package/admin/js/views/documentation.js +1 -12
  49. package/admin/js/views/form-editor.js +8 -0
  50. package/admin/js/views/form-submissions.js +1 -0
  51. package/admin/js/views/forms.js +1 -0
  52. package/admin/js/views/index.js +1 -39
  53. package/admin/js/views/layouts.js +9 -42
  54. package/admin/js/views/login.js +7 -251
  55. package/admin/js/views/media.js +1 -240
  56. package/admin/js/views/my-profile.js +1 -0
  57. package/admin/js/views/navigation.js +14 -212
  58. package/admin/js/views/page-editor.js +72 -661
  59. package/admin/js/views/pages.js +5 -72
  60. package/admin/js/views/plugins.js +13 -90
  61. package/admin/js/views/pro-docs.js +1 -0
  62. package/admin/js/views/role-editor.js +1 -0
  63. package/admin/js/views/roles.js +4 -0
  64. package/admin/js/views/settings.js +3 -199
  65. package/admin/js/views/tutorials.js +1 -12
  66. package/admin/js/views/user-editor.js +1 -88
  67. package/admin/js/views/users.js +4 -76
  68. package/admin/js/views/view-editor.js +1 -0
  69. package/admin/js/views/view-preview.js +1 -0
  70. package/admin/js/views/views-list.js +1 -0
  71. package/bin/cli.js +1 -1
  72. package/config/auth.json +2 -17
  73. package/config/connections.json.bak +9 -0
  74. package/config/connections.json.example +9 -0
  75. package/config/navigation.json +15 -0
  76. package/config/plugins.json +19 -29
  77. package/config/server.json +6 -6
  78. package/config/site.json +17 -6
  79. package/package.json +24 -10
  80. package/plugins/domma-effects/public/celebrations/core/canvas.js +2 -104
  81. package/plugins/domma-effects/public/celebrations/core/particles.js +1 -144
  82. package/plugins/domma-effects/public/celebrations/core/physics.js +1 -166
  83. package/plugins/domma-effects/public/celebrations/index.js +1 -535
  84. package/plugins/domma-effects/public/celebrations/themes/christmas.js +1 -1805
  85. package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1 -1477
  86. package/plugins/domma-effects/public/celebrations/themes/halloween.js +1 -1837
  87. package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1 -1175
  88. package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1 -1258
  89. package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1 -1754
  90. package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1 -1290
  91. package/plugins/domma-effects/public/celebrations/themes/valentines.js +1 -1361
  92. package/plugins/example-analytics/stats.json +21 -12
  93. package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
  94. package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
  95. package/plugins/theme-roller/config.js +1 -0
  96. package/plugins/theme-roller/plugin.js +233 -0
  97. package/plugins/theme-roller/plugin.json +31 -0
  98. package/plugins/theme-roller/public/active-theme.css +0 -0
  99. package/plugins/theme-roller/public/inject-head-late.html +1 -0
  100. package/public/css/forms.css +1 -0
  101. package/public/css/site.css +1 -302
  102. package/public/js/btt.js +1 -90
  103. package/public/js/cookie-consent.js +1 -61
  104. package/public/js/form-logic-engine.js +1 -0
  105. package/public/js/forms.js +1 -0
  106. package/public/js/site.js +1 -204
  107. package/scripts/build.js +194 -129
  108. package/scripts/pro.js +254 -0
  109. package/scripts/reset.js +33 -8
  110. package/scripts/seed.js +343 -78
  111. package/scripts/setup.js +5 -4
  112. package/server/middleware/auth.js +136 -97
  113. package/server/routes/api/actions.js +200 -0
  114. package/server/routes/api/auth.js +292 -116
  115. package/server/routes/api/blocks.js +84 -0
  116. package/server/routes/api/collections.js +88 -23
  117. package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +483 -505
  118. package/server/routes/api/layouts.js +49 -25
  119. package/server/routes/api/media.js +118 -93
  120. package/server/routes/api/navigation.js +40 -37
  121. package/server/routes/api/pages.js +132 -118
  122. package/server/routes/api/plugins.js +6 -3
  123. package/server/routes/api/settings.js +104 -89
  124. package/server/routes/api/users.js +27 -21
  125. package/server/routes/api/views.js +148 -0
  126. package/server/routes/public.js +124 -108
  127. package/server/server.js +269 -173
  128. package/server/services/actions.js +387 -0
  129. package/server/services/adapterRegistry.js +98 -0
  130. package/server/services/adapters/FileAdapter.js +192 -0
  131. package/server/services/adapters/MongoAdapter.js +220 -0
  132. package/server/services/blocks.js +162 -0
  133. package/server/services/collections.js +74 -86
  134. package/server/services/connectionManager.js +102 -0
  135. package/server/services/content.js +312 -307
  136. package/{plugins/form-builder → server/services}/email.js +126 -103
  137. package/server/services/forms.js +173 -0
  138. package/server/services/markdown.js +1378 -648
  139. package/server/services/permissionRegistry.js +173 -0
  140. package/server/services/presetCollections.js +251 -0
  141. package/server/services/renderer.js +75 -1
  142. package/server/services/roles.js +227 -0
  143. package/server/services/rowAccess.js +104 -0
  144. package/server/services/userProfiles.js +199 -0
  145. package/server/services/users.js +281 -212
  146. package/server/services/views.js +280 -0
  147. package/server/templates/page.html +119 -113
  148. package/plugins/form-builder/admin/templates/form-editor.html +0 -171
  149. package/plugins/form-builder/admin/templates/form-settings.html +0 -29
  150. package/plugins/form-builder/admin/views/form-editor.js +0 -1442
  151. package/plugins/form-builder/admin/views/form-settings.js +0 -38
  152. package/plugins/form-builder/admin/views/form-submissions.js +0 -295
  153. package/plugins/form-builder/admin/views/forms-list.js +0 -164
  154. package/plugins/form-builder/config.js +0 -9
  155. package/plugins/form-builder/data/forms/consent.json +0 -104
  156. package/plugins/form-builder/data/forms/contact-details.json +0 -63
  157. package/plugins/form-builder/data/forms/contacts.json +0 -66
  158. package/plugins/form-builder/data/submissions/consent.json +0 -13
  159. package/plugins/form-builder/data/submissions/contact-details.json +0 -1
  160. package/plugins/form-builder/data/submissions/contacts.json +0 -26
  161. package/plugins/form-builder/plugin.json +0 -52
  162. package/plugins/form-builder/public/form-logic-engine.js +0 -568
  163. package/plugins/form-builder/public/inject-body.html +0 -352
  164. package/plugins/form-builder/public/inject-head.html +0 -58
  165. package/plugins/form-builder/public/package.json +0 -1
  166. package/scripts/copy-domma.js +0 -48
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Permission Registry
3
+ * Single source of truth for all resource/action definitions in the CMS.
4
+ * Adding a new resource or custom action is a one-file change here.
5
+ */
6
+
7
+ /** @type {ReadonlyArray<{key:string,label:string,description:string,icon:string,group:string,actions:{key:string,label:string,description:string}[]}>} */
8
+ export const REGISTRY = Object.freeze([
9
+ {
10
+ key: 'pages',
11
+ label: 'Pages',
12
+ description: 'Manage site pages and their content.',
13
+ icon: 'file-text',
14
+ group: 'Content',
15
+ actions: [
16
+ {key: 'read', label: 'View', description: 'View pages and their content'},
17
+ {key: 'create', label: 'Create', description: 'Create new pages'},
18
+ {key: 'update', label: 'Edit', description: 'Edit existing pages'},
19
+ {key: 'delete', label: 'Delete', description: 'Delete pages'}
20
+ ]
21
+ },
22
+ {
23
+ key: 'media',
24
+ label: 'Media',
25
+ description: 'Upload and manage media files.',
26
+ icon: 'image',
27
+ group: 'Content',
28
+ actions: [
29
+ {key: 'read', label: 'View', description: 'View media library'},
30
+ {key: 'create', label: 'Upload', description: 'Upload new files'},
31
+ {key: 'update', label: 'Edit', description: 'Rename or update file metadata'},
32
+ {key: 'delete', label: 'Delete', description: 'Delete media files'}
33
+ ]
34
+ },
35
+ {
36
+ key: 'blocks',
37
+ label: 'Blocks',
38
+ description: 'Manage reusable content blocks.',
39
+ icon: 'box',
40
+ group: 'Content',
41
+ actions: [
42
+ {key: 'read', label: 'View', description: 'View content blocks'},
43
+ {key: 'create', label: 'Create', description: 'Create new blocks'},
44
+ {key: 'update', label: 'Edit', description: 'Edit existing blocks'},
45
+ {key: 'delete', label: 'Delete', description: 'Delete blocks'}
46
+ ]
47
+ },
48
+ {
49
+ key: 'navigation',
50
+ label: 'Navigation',
51
+ description: 'Configure site navigation menus.',
52
+ icon: 'menu',
53
+ group: 'Structure',
54
+ actions: [
55
+ {key: 'read', label: 'View', description: 'View navigation menus'},
56
+ {key: 'create', label: 'Create', description: 'Create new menu items'},
57
+ {key: 'update', label: 'Edit', description: 'Edit navigation structure'},
58
+ {key: 'delete', label: 'Delete', description: 'Remove menu items'}
59
+ ]
60
+ },
61
+ {
62
+ key: 'layouts',
63
+ label: 'Layouts',
64
+ description: 'Manage page layouts and templates.',
65
+ icon: 'layout',
66
+ group: 'Structure',
67
+ actions: [
68
+ {key: 'read', label: 'View', description: 'View layouts'},
69
+ {key: 'create', label: 'Create', description: 'Create new layouts'},
70
+ {key: 'update', label: 'Edit', description: 'Edit layouts'},
71
+ {key: 'delete', label: 'Delete', description: 'Delete layouts'}
72
+ ]
73
+ },
74
+ {
75
+ key: 'collections',
76
+ label: 'Collections',
77
+ description: 'Manage data collections and entries.',
78
+ icon: 'database',
79
+ group: 'Data',
80
+ actions: [
81
+ {key: 'read', label: 'View', description: 'View collection entries'},
82
+ {key: 'create', label: 'Create', description: 'Create new entries'},
83
+ {key: 'update', label: 'Edit', description: 'Edit existing entries'},
84
+ {key: 'delete', label: 'Delete', description: 'Delete entries'}
85
+ ]
86
+ },
87
+ {
88
+ key: 'views',
89
+ label: 'Views',
90
+ description: 'Create and manage data views.',
91
+ icon: 'eye',
92
+ group: 'Data',
93
+ actions: [
94
+ {key: 'read', label: 'View', description: 'View data views'},
95
+ {key: 'create', label: 'Create', description: 'Create new views'},
96
+ {key: 'update', label: 'Edit', description: 'Edit views'},
97
+ {key: 'delete', label: 'Delete', description: 'Delete views'}
98
+ ]
99
+ },
100
+ {
101
+ key: 'actions',
102
+ label: 'Actions',
103
+ description: 'Configure automated actions.',
104
+ icon: 'zap',
105
+ group: 'Data',
106
+ actions: [
107
+ {key: 'read', label: 'View', description: 'View automated actions'},
108
+ {key: 'create', label: 'Create', description: 'Create new actions'},
109
+ {key: 'update', label: 'Edit', description: 'Edit actions'},
110
+ {key: 'delete', label: 'Delete', description: 'Delete actions'}
111
+ ]
112
+ },
113
+ {
114
+ key: 'users',
115
+ label: 'Users',
116
+ description: 'Manage user accounts.',
117
+ icon: 'users',
118
+ group: 'Configuration',
119
+ actions: [
120
+ {key: 'read', label: 'View', description: 'View user accounts'},
121
+ {key: 'create', label: 'Create', description: 'Create new users'},
122
+ {key: 'update', label: 'Edit', description: 'Edit user accounts'},
123
+ {key: 'delete', label: 'Delete', description: 'Delete users'}
124
+ ]
125
+ },
126
+ {
127
+ key: 'settings',
128
+ label: 'Settings',
129
+ description: 'Configure site settings.',
130
+ icon: 'settings',
131
+ group: 'Configuration',
132
+ actions: [
133
+ {key: 'read', label: 'View', description: 'View site settings'},
134
+ {key: 'create', label: 'Create', description: 'Add new settings'},
135
+ {key: 'update', label: 'Edit', description: 'Modify settings'},
136
+ {key: 'delete', label: 'Delete', description: 'Remove settings'}
137
+ ]
138
+ },
139
+ {
140
+ key: 'plugins',
141
+ label: 'Plugins',
142
+ description: 'Manage CMS plugins.',
143
+ icon: 'package',
144
+ group: 'Configuration',
145
+ actions: [
146
+ {key: 'read', label: 'View', description: 'View installed plugins'},
147
+ {key: 'create', label: 'Install', description: 'Install new plugins'},
148
+ {key: 'update', label: 'Configure', description: 'Configure plugin settings'},
149
+ {key: 'delete', label: 'Remove', description: 'Remove plugins'}
150
+ ]
151
+ }
152
+ ]);
153
+
154
+ /** Derived array of resource key strings — mirrors the old RESOURCES constant. */
155
+ export const RESOURCES = REGISTRY.map(r => r.key);
156
+
157
+ /** Default CRUD action keys — mirrors the old ACTIONS constant. */
158
+ export const ACTIONS = ['read', 'create', 'update', 'delete'];
159
+
160
+ /** Display order for permission groups. */
161
+ export const GROUP_ORDER = ['Content', 'Structure', 'Data', 'Configuration'];
162
+
163
+ /**
164
+ * Return the action keys defined for a given resource.
165
+ * Falls back to the default CRUD set if the resource is not in the registry.
166
+ *
167
+ * @param {string} resourceKey
168
+ * @returns {string[]}
169
+ */
170
+ export function getActionsForResource(resourceKey) {
171
+ const resource = REGISTRY.find(r => r.key === resourceKey);
172
+ return resource ? resource.actions.map(a => a.key) : [...ACTIONS];
173
+ }
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Preset Collections
3
+ * Built-in collections whose schemas are defined in code and overwritten on boot.
4
+ * Data files are never touched — only schema.json is reset each startup.
5
+ */
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import {config} from '../config.js';
9
+ import {ensureFormForCollection} from './forms.js';
10
+
11
+ const COLLECTIONS_DIR = path.resolve(config.content.collectionsDir);
12
+
13
+ const PRESETS = [
14
+ {
15
+ slug: 'contacts',
16
+ title: 'Contacts',
17
+ description: 'People and their contact information.',
18
+ preset: true,
19
+ fields: [
20
+ {name: 'full_name', label: 'Full Name', type: 'string', required: true},
21
+ {name: 'email_address', label: 'Email', type: 'string', required: true},
22
+ {name: 'phone_number', label: 'Phone', type: 'string', required: false}
23
+ ],
24
+ api: {
25
+ create: {enabled: true, access: 'public'},
26
+ read: {enabled: true, access: 'admin'},
27
+ update: {enabled: false, access: 'admin'},
28
+ delete: {enabled: false, access: 'admin'}
29
+ }
30
+ },
31
+ {
32
+ slug: 'enquiries',
33
+ title: 'Enquiries',
34
+ description: 'Messages and enquiries from website visitors.',
35
+ preset: true,
36
+ fields: [
37
+ {name: 'full_name', label: 'Full Name', type: 'text', required: true},
38
+ {name: 'email', label: 'Email', type: 'text', required: true},
39
+ {name: 'phone', label: 'Phone', type: 'text'},
40
+ {
41
+ name: 'subject', label: 'Subject', type: 'select', required: true,
42
+ options: [
43
+ {value: 'general-enquiry', label: 'General Enquiry'},
44
+ {value: 'support', label: 'Support'},
45
+ {value: 'sales', label: 'Sales'},
46
+ {value: 'partnership', label: 'Partnership'},
47
+ {value: 'other', label: 'Other'}
48
+ ]
49
+ },
50
+ {name: 'message', label: 'Message', type: 'text', required: true}
51
+ ],
52
+ api: {
53
+ create: {enabled: true, access: 'public'},
54
+ read: {enabled: true, access: 'admin'},
55
+ update: {enabled: false, access: 'admin'},
56
+ delete: {enabled: false, access: 'admin'}
57
+ }
58
+ },
59
+ {
60
+ slug: 'feedback',
61
+ title: 'Feedback',
62
+ description: 'User feedback and ratings.',
63
+ preset: true,
64
+ fields: [
65
+ {name: 'name', label: 'Name', type: 'string', required: true},
66
+ {name: 'email', label: 'Email', type: 'string', required: true},
67
+ {
68
+ name: 'rating', label: 'Rating', type: 'select', required: true,
69
+ options: [
70
+ {value: 'excellent', label: 'Excellent'},
71
+ {value: 'good', label: 'Good'},
72
+ {value: 'average', label: 'Average'},
73
+ {value: 'poor', label: 'Poor'}
74
+ ]
75
+ },
76
+ {
77
+ name: 'category', label: 'Category', type: 'select',
78
+ options: [
79
+ {value: 'feature-request', label: 'Feature Request'},
80
+ {value: 'praise', label: 'Praise'},
81
+ {value: 'bug-report', label: 'Bug Report'},
82
+ {value: 'support', label: 'Support'}
83
+ ]
84
+ },
85
+ {name: 'subject', label: 'Subject', type: 'string'},
86
+ {name: 'message', label: 'Message', type: 'string', required: true}
87
+ ],
88
+ api: {
89
+ create: {enabled: true, access: 'public'},
90
+ read: {enabled: true, access: 'admin'},
91
+ update: {enabled: false, access: 'admin'},
92
+ delete: {enabled: false, access: 'admin'}
93
+ }
94
+ },
95
+ {
96
+ slug: 'notes',
97
+ title: 'Notes',
98
+ description: 'Free-form notes with categories and tags.',
99
+ preset: true,
100
+ fields: [
101
+ {name: 'title', label: 'Title', type: 'text', required: true},
102
+ {name: 'content', label: 'Content', type: 'text', required: true},
103
+ {
104
+ name: 'category', label: 'Category', type: 'select',
105
+ options: [
106
+ {value: 'general', label: 'General'},
107
+ {value: 'idea', label: 'Idea'},
108
+ {value: 'reminder', label: 'Reminder'},
109
+ {value: 'reference', label: 'Reference'}
110
+ ]
111
+ },
112
+ {name: 'tags', label: 'Tags', type: 'text'}
113
+ ],
114
+ api: {
115
+ create: {enabled: false, access: 'admin'},
116
+ read: {enabled: false, access: 'admin'},
117
+ update: {enabled: false, access: 'admin'},
118
+ delete: {enabled: false, access: 'admin'}
119
+ }
120
+ },
121
+ {
122
+ slug: 'categories',
123
+ title: 'Categories',
124
+ description: 'Content categories for organising posts.',
125
+ preset: true,
126
+ fields: [
127
+ {name: 'name', label: 'Name', type: 'text', required: true},
128
+ {name: 'slug', label: 'Slug', type: 'text', required: true},
129
+ {name: 'description', label: 'Description', type: 'textarea'},
130
+ {name: 'parent_category', label: 'Parent Category', type: 'text'},
131
+ {name: 'sort_order', label: 'Sort Order', type: 'number'}
132
+ ],
133
+ api: {
134
+ create: {enabled: false, access: 'admin'},
135
+ read: {enabled: true, access: 'public'},
136
+ update: {enabled: false, access: 'admin'},
137
+ delete: {enabled: false, access: 'admin'}
138
+ }
139
+ },
140
+ {
141
+ slug: 'posts',
142
+ title: 'Posts',
143
+ description: 'Blog posts and articles.',
144
+ preset: true,
145
+ fields: [
146
+ {name: 'title', label: 'Title', type: 'text', required: true},
147
+ {name: 'slug', label: 'Slug', type: 'text', required: true},
148
+ {name: 'content', label: 'Content', type: 'textarea', required: true},
149
+ {name: 'excerpt', label: 'Excerpt', type: 'textarea'},
150
+ {name: 'featured_image', label: 'Featured Image', type: 'url'},
151
+ {name: 'category', label: 'Category', type: 'text'},
152
+ {name: 'tags', label: 'Tags', type: 'text'},
153
+ {
154
+ name: 'status', label: 'Status', type: 'select', required: true,
155
+ options: [
156
+ {value: 'draft', label: 'Draft'},
157
+ {value: 'published', label: 'Published'},
158
+ {value: 'archived', label: 'Archived'}
159
+ ]
160
+ },
161
+ {name: 'publish_date', label: 'Publish Date', type: 'date'},
162
+ {name: 'author', label: 'Author', type: 'text'}
163
+ ],
164
+ api: {
165
+ create: {enabled: false, access: 'admin'},
166
+ read: {enabled: true, access: 'public'},
167
+ update: {enabled: false, access: 'admin'},
168
+ delete: {enabled: false, access: 'admin'}
169
+ }
170
+ },
171
+ {
172
+ slug: 'comments',
173
+ title: 'Comments',
174
+ description: 'User comments on posts and content.',
175
+ preset: true,
176
+ fields: [
177
+ {name: 'post_slug', label: 'Post', type: 'text', required: true},
178
+ {name: 'author_name', label: 'Name', type: 'text', required: true},
179
+ {name: 'author_email', label: 'Email', type: 'email', required: true},
180
+ {name: 'body', label: 'Comment', type: 'textarea', required: true},
181
+ {
182
+ name: 'status', label: 'Status', type: 'select', required: true,
183
+ options: [
184
+ {value: 'pending', label: 'Pending'},
185
+ {value: 'approved', label: 'Approved'},
186
+ {value: 'rejected', label: 'Rejected'}
187
+ ]
188
+ }
189
+ ],
190
+ api: {
191
+ create: {enabled: true, access: 'public'},
192
+ read: {enabled: true, access: 'public'},
193
+ update: {enabled: false, access: 'admin'},
194
+ delete: {enabled: false, access: 'admin'}
195
+ }
196
+ },
197
+ {
198
+ slug: 'to-do',
199
+ title: 'To-Do',
200
+ description: 'Task tracking with priorities and due dates.',
201
+ preset: true,
202
+ fields: [
203
+ {name: 'title', label: 'Title', type: 'text', required: true},
204
+ {name: 'description', label: 'Description', type: 'text'},
205
+ {
206
+ name: 'status', label: 'Status', type: 'select', required: true,
207
+ options: ['Pending', 'In Progress', 'Done']
208
+ },
209
+ {
210
+ name: 'priority', label: 'Priority', type: 'select',
211
+ options: ['Low', 'Medium', 'High']
212
+ },
213
+ {name: 'due_date', label: 'Due Date', type: 'text'},
214
+ {name: 'assigned_to', label: 'Assigned To', type: 'text'}
215
+ ],
216
+ api: {
217
+ create: {enabled: false, access: 'admin'},
218
+ read: {enabled: false, access: 'admin'},
219
+ update: {enabled: false, access: 'admin'},
220
+ delete: {enabled: false, access: 'admin'}
221
+ }
222
+ }
223
+ ];
224
+
225
+ /** Slugs exported for use in adapterRegistry and the delete guard. */
226
+ export const PRESET_COLLECTION_SLUGS = PRESETS.map(p => p.slug);
227
+
228
+ /**
229
+ * Seed all preset collections at boot.
230
+ * Always overwrites schema.json; only creates data.json if absent.
231
+ *
232
+ * @returns {Promise<void>}
233
+ */
234
+ export async function seedAll() {
235
+ for (const schema of PRESETS) {
236
+ const dir = path.join(COLLECTIONS_DIR, schema.slug);
237
+ const schemaPath = path.join(dir, 'schema.json');
238
+ const dataPath = path.join(dir, 'data.json');
239
+
240
+ await fs.mkdir(dir, {recursive: true});
241
+ await fs.writeFile(schemaPath, JSON.stringify(schema, null, 2) + '\n', 'utf8');
242
+
243
+ try {
244
+ await fs.access(dataPath);
245
+ } catch {
246
+ await fs.writeFile(dataPath, '[]\n', 'utf8');
247
+ }
248
+
249
+ await ensureFormForCollection(schema);
250
+ }
251
+ }
@@ -61,6 +61,8 @@ export async function renderPage(page) {
61
61
  ? `window.__CMS_DCONFIG__ = ${dconfigJson};`
62
62
  : '';
63
63
 
64
+ const breadcrumbsHtml = buildBreadcrumbsHtml(page, site);
65
+
64
66
  const {fontLink, fontOverride} = buildFontVars(site.fontFamily, site.fontSize);
65
67
  const fontStyleTag = fontOverride
66
68
  ? `<style>${fontOverride}</style>`
@@ -70,13 +72,23 @@ export async function renderPage(page) {
70
72
  ? `<style>${customCss.replace(/<\/style>/gi, '<\\/style>')}</style>`
71
73
  : '';
72
74
 
75
+ const activeTheme = page.theme || site.theme || 'charcoal-dark';
76
+ // When a custom theme is active, Domma's theme engine must receive the base
77
+ // built-in theme (which it recognises). The custom override class is added
78
+ // separately after Domma's init so _applyTheme() cannot strip it.
79
+ const dommaTheme = site.baseTheme || activeTheme;
80
+ const customThemeClass = site.baseTheme ? `dm-theme-${activeTheme}` : '';
81
+
73
82
  const vars = {
74
83
  seoTitle,
75
84
  seoDescription,
76
85
  ogImage,
77
86
  title: page.title,
78
87
  html: page.html,
79
- theme: site.theme || 'charcoal-dark',
88
+ breadcrumbsHtml,
89
+ theme: activeTheme,
90
+ dommaTheme,
91
+ customThemeClass,
80
92
  fontLink,
81
93
  fontStyleTag,
82
94
  layout: page.layout || 'default',
@@ -104,6 +116,68 @@ export async function renderPage(page) {
104
116
  return applyTransforms('render:afterRender', html, {page});
105
117
  }
106
118
 
119
+ /**
120
+ * Build breadcrumbs HTML for a page.
121
+ * Returns an empty string if breadcrumbs are disabled globally or per-page.
122
+ *
123
+ * @param {object} page - Parsed page object with urlPath, title, breadcrumbs fields
124
+ * @param {object} site - Site config
125
+ * @returns {string}
126
+ */
127
+ function buildBreadcrumbsHtml(page, site) {
128
+ const cfg = site?.breadcrumbs || {};
129
+ if (!cfg.enabled) return '';
130
+ if (page.breadcrumbs === false) return '';
131
+
132
+ const urlPath = page.urlPath || '/';
133
+ // Don't render breadcrumbs on the home page
134
+ if (urlPath === '/') return '';
135
+
136
+ const homeLabel = cfg.homeLabel || 'Home';
137
+ const segments = urlPath.split('/').filter(Boolean);
138
+ if (!segments.length) return '';
139
+
140
+ // Build fixed-position inline style from corner + offsets
141
+ const pos = (cfg.position || 'TL').toUpperCase();
142
+ const offsetX = parseInt(cfg.offsetX, 10) || 8;
143
+ const offsetY = parseInt(cfg.offsetY, 10) || 60;
144
+ const posStyle = [
145
+ pos.includes('T') ? `top:${offsetY}px` : `bottom:${offsetY}px`,
146
+ pos.includes('L') ? `left:${offsetX}px` : `right:${offsetX}px`
147
+ ].join(';');
148
+
149
+ const crumbs = [];
150
+
151
+ const homeIcon = '<svg class="dm-breadcrumbs-home-icon" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>';
152
+ // Home crumb
153
+ crumbs.push(`<a href="/" class="dm-breadcrumbs-item dm-breadcrumbs-link">${homeIcon}${escapeHtml(homeLabel)}</a>`);
154
+
155
+ // Middle crumbs (each intermediate path segment)
156
+ for (let i = 0; i < segments.length - 1; i++) {
157
+ const href = '/' + segments.slice(0, i + 1).join('/');
158
+ const label = toTitleCase(segments[i]);
159
+ crumbs.push(`<a href="${href}" class="dm-breadcrumbs-item dm-breadcrumbs-link">${escapeHtml(label)}</a>`);
160
+ }
161
+
162
+ // Last crumb — current page title, no link
163
+ crumbs.push(`<span class="dm-breadcrumbs-item dm-breadcrumbs-current" aria-current="page">${escapeHtml(page.title || toTitleCase(segments[segments.length - 1]))}</span>`);
164
+
165
+ const sep = '<span class="dm-breadcrumbs-separator" aria-hidden="true">›</span>';
166
+ return `<nav class="dm-breadcrumbs" aria-label="Breadcrumb" style="${posStyle}">${crumbs.join(sep)}</nav>`;
167
+ }
168
+
169
+ function toTitleCase(str) {
170
+ return str.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
171
+ }
172
+
173
+ function escapeHtml(str) {
174
+ return String(str)
175
+ .replace(/&/g, '&amp;')
176
+ .replace(/</g, '&lt;')
177
+ .replace(/>/g, '&gt;')
178
+ .replace(/"/g, '&quot;');
179
+ }
180
+
107
181
  function buildFontVars(fontFamily, fontSize) {
108
182
  const PRECONNECT = '<link rel="preconnect" href="https://fonts.googleapis.com">\n <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>';
109
183
  const family = fontFamily || 'Roboto';