domma-cms 0.6.14 → 0.6.15

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.
@@ -1,6 +1,10 @@
1
1
  <div class="view-header">
2
2
  <h1><span data-icon="file-text"></span> Pages</h1>
3
- <a href="#/pages/new" class="btn btn-primary"><span data-icon="plus"></span> New Page</a>
3
+ <div style="display:flex;gap:.5rem;align-items:center;margin-left:auto;">
4
+ <input id="pages-search" type="text" class="form-input form-input-sm" placeholder="Search pages…"
5
+ style="width:220px;">
6
+ <a href="#/pages/new" class="btn btn-primary"><span data-icon="plus"></span> New Page</a>
7
+ </div>
4
8
  </div>
5
9
 
6
10
  <div class="toolbar mb-3" style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;">
@@ -1,5 +1,5 @@
1
- import{api as d}from"../api.js";export const pagesView={templateUrl:"/admin/js/templates/pages.html",async onMount(e){const c=E.loader(e.get(0),{type:"dots"});let n=await d.pages.list().catch(()=>[]);c.destroy();const l=s=>{T.create("#pages-table",{data:s,columns:[{key:"title",title:"Title",render:(t,a)=>`<a href="#/pages/edit${a.urlPath}">${t}</a>`},{key:"urlPath",title:"URL",render:t=>`<code>${t}</code>`},{key:"layout",title:"Layout"},{key:"status",title:"Status",render:t=>`<span class="badge badge-${t==="published"?"success":"warning"}">${t}</span>`},{key:"tags",title:"Tags",render:t=>Array.isArray(t)&&t.length?t.map(a=>`<span class="badge badge-info badge-pill badge-sm">${a}</span>`).join(" "):"\u2014"},{key:"updatedAt",title:"Updated",render:t=>t?D(t).format("DD MMM YYYY"):"\u2014"},{key:"actions",title:"Actions",render:(t,a)=>`
1
+ import{api as p}from"../api.js";export const pagesView={templateUrl:"/admin/js/templates/pages.html",async onMount(e){const c=E.loader(e.get(0),{type:"dots"});let i=await p.pages.list().catch(()=>[]);c.destroy();const l=s=>{T.create("#pages-table",{data:s,columns:[{key:"title",title:"Title",render:(t,a)=>`<a href="#/pages/edit${a.urlPath}">${t}</a>`},{key:"urlPath",title:"URL",render:t=>`<code>${t}</code>`},{key:"layout",title:"Layout"},{key:"status",title:"Status",render:t=>`<span class="badge badge-${t==="published"?"success":"warning"}">${t}</span>`},{key:"tags",title:"Tags",render:t=>Array.isArray(t)&&t.length?t.map(a=>`<span class="badge badge-info badge-pill badge-sm">${a}</span>`).join(" "):"\u2014"},{key:"updatedAt",title:"Updated",render:t=>t?D(t).format("DD MMM YYYY"):"\u2014"},{key:"actions",title:"Actions",render:(t,a)=>`
2
2
  <a href="#/pages/edit${a.urlPath}" class="btn btn-sm btn-primary">Edit</a>
3
3
  <a href="${a.urlPath}" target="_blank" class="btn btn-sm btn-ghost" data-tooltip="View"><span data-icon="external-link"></span></a>
4
4
  <button class="btn btn-sm btn-danger btn-delete" data-path="${a.urlPath}">Delete</button>
5
- `}],emptyMessage:'No pages found. <a href="#/pages/new">Create one</a>.'}),Domma.icons.scan(),document.querySelectorAll("#pages-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}),Domma.effects.reveal(".card",{animation:"fade",duration:350})};l(n);const p=s=>{const t=e.find("#pages-tree").empty().get(0);if(!s.length){t.textContent="No pages found.";return}const a=s.map(i=>{const r=i.urlPath.split("/").filter(Boolean),o=r.length>1?"/"+r.slice(0,-1).join("/"):null,g=o&&s.some(b=>b.urlPath===o);return{id:i.urlPath,parent_id:g?o:null,name:i.title||i.urlPath,icon:i.status==="published"?"check-circle":"file-text"}});E.treeView(t,{data:a,idKey:"id",parentKey:"parent_id",labelKey:"name",iconKey:"icon",expandedByDefault:!0,onSelect:i=>{R.navigate(`/pages/edit${i}`)}}),Domma.icons.scan(t)};e.find("#view-table-btn").on("click",function(){e.find("#pages-table").show(),e.find("#pages-tree").hide(),$(this).addClass("btn-primary").removeClass("btn-ghost"),e.find("#view-tree-btn").addClass("btn-ghost").removeClass("btn-primary")}),e.find("#view-tree-btn").on("click",function(){e.find("#pages-table").hide(),e.find("#pages-tree").show(),$(this).addClass("btn-primary").removeClass("btn-ghost"),e.find("#view-table-btn").addClass("btn-ghost").removeClass("btn-primary"),p(n)}),e.find("#view-table-btn, #view-tree-btn").each(function(){E.tooltip(this,{content:this.getAttribute("data-tooltip"),position:"top"})}),e.find("#status-filter").off("change").on("change",function(){const s=$(this).val(),t=s?n.filter(a=>a.status===s):n;l(t)}),e.off("click",".btn-delete").on("click",".btn-delete",async function(){const s=$(this).data("path");if(await E.confirm(`Delete page at <strong>${s}</strong>? This cannot be undone.`))try{await d.pages.delete(s),E.toast("Page deleted.",{type:"success"}),n=n.filter(a=>a.urlPath!==s),l(n)}catch{E.toast("Failed to delete page.",{type:"error"})}})}};
5
+ `}],emptyMessage:'No pages found. <a href="#/pages/new">Create one</a>.'}),Domma.icons.scan(),document.querySelectorAll("#pages-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}),Domma.effects.reveal(".card",{animation:"fade",duration:350})};l(i);const u=s=>{const t=e.find("#pages-tree").empty().get(0);if(!s.length){t.textContent="No pages found.";return}const a=s.map(n=>{const d=n.urlPath.split("/").filter(Boolean),r=d.length>1?"/"+d.slice(0,-1).join("/"):null,g=r&&s.some(f=>f.urlPath===r);return{id:n.urlPath,parent_id:g?r:null,name:n.title||n.urlPath,icon:n.status==="published"?"check-circle":"file-text"}});E.treeView(t,{data:a,idKey:"id",parentKey:"parent_id",labelKey:"name",iconKey:"icon",expandedByDefault:!0,onSelect:n=>{R.navigate(`/pages/edit${n}`)}}),Domma.icons.scan(t)};e.find("#view-table-btn").on("click",function(){e.find("#pages-table").show(),e.find("#pages-tree").hide(),$(this).addClass("btn-primary").removeClass("btn-ghost"),e.find("#view-tree-btn").addClass("btn-ghost").removeClass("btn-primary")}),e.find("#view-tree-btn").on("click",function(){e.find("#pages-table").hide(),e.find("#pages-tree").show(),$(this).addClass("btn-primary").removeClass("btn-ghost"),e.find("#view-table-btn").addClass("btn-ghost").removeClass("btn-primary"),u(i)}),e.find("#view-table-btn, #view-tree-btn").each(function(){E.tooltip(this,{content:this.getAttribute("data-tooltip"),position:"top"})});const o=()=>{const s=e.find("#status-filter").val(),t=e.find("#pages-search").val().toLowerCase().trim(),a=i.filter(n=>!(s&&n.status!==s||t&&!`${n.title} ${n.urlPath} ${(n.tags||[]).join(" ")}`.toLowerCase().includes(t)));l(a)};e.find("#status-filter").off("change").on("change",o),e.find("#pages-search").get(0).addEventListener("input",o),e.off("click",".btn-delete").on("click",".btn-delete",async function(){const s=$(this).data("path");if(await E.confirm(`Delete page at <strong>${s}</strong>? This cannot be undone.`))try{await p.pages.delete(s),E.toast("Page deleted.",{type:"success"}),i=i.filter(a=>a.urlPath!==s),l(i)}catch{E.toast("Failed to delete page.",{type:"error"})}})}};
@@ -9,7 +9,13 @@
9
9
  },
10
10
  "site-search": {
11
11
  "enabled": true,
12
- "settings": {}
12
+ "settings": {
13
+ "placeholder": "Search",
14
+ "keyboardShortcut": false,
15
+ "maxResults": 10,
16
+ "minQueryLength": 2,
17
+ "debounceMs": 300
18
+ }
13
19
  },
14
20
  "domma-effects": {
15
21
  "enabled": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.6.14",
3
+ "version": "0.6.15",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -1,6 +1,6 @@
1
1
  {
2
- "/": 140,
3
- "/about": 71,
2
+ "/": 156,
3
+ "/about": 72,
4
4
  "/blog": 36,
5
5
  "/contact": 30,
6
6
  "/resources/typography": 4,
@@ -16,7 +16,7 @@
16
16
  "/resources/dependencies": 2,
17
17
  "/resources/components": 6,
18
18
  "/gdpr": 3,
19
- "/scratch": 69,
19
+ "/scratch": 70,
20
20
  "/getting-started": 3,
21
21
  "/resources/pro": 1,
22
22
  "/todo": 23,
@@ -0,0 +1,10 @@
1
+ <div class="view-header">
2
+ <h1><span data-icon="chart-bar"></span> Analytics</h1>
3
+ <button id="reset-btn" class="btn btn-ghost btn-sm">Reset stats</button>
4
+ </div>
5
+
6
+ <div class="card">
7
+ <div class="card-body">
8
+ <div id="analytics-table"></div>
9
+ </div>
10
+ </div>
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Analytics Plugin — Admin View
3
+ * Shows a sortable table of page hit counts.
4
+ * Loaded dynamically from /plugins/ static path.
5
+ */
6
+ export const analyticsView = {
7
+ templateUrl: '/plugins/example-analytics/admin/templates/analytics.html',
8
+
9
+ async onMount($container) {
10
+ await loadStats($container);
11
+
12
+ $container.find('#reset-btn').on('click', async () => {
13
+ const confirmed = await E.confirm('Reset all analytics data? This cannot be undone.');
14
+ if (!confirmed) return;
15
+ try {
16
+ await fetch('/api/plugins/example-analytics/stats', {
17
+ method: 'DELETE',
18
+ headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}
19
+ });
20
+ E.toast('Analytics reset.', {type: 'success'});
21
+ await loadStats($container);
22
+ } catch {
23
+ E.toast('Reset failed.', {type: 'error'});
24
+ }
25
+ });
26
+
27
+ Domma.icons.scan();
28
+ }
29
+ };
30
+
31
+ async function loadStats($container) {
32
+ let stats = [];
33
+ try {
34
+ const res = await fetch('/api/plugins/example-analytics/stats', {
35
+ headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}
36
+ });
37
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
38
+ stats = await res.json();
39
+ } catch {
40
+ E.toast('Could not load analytics data.', {type: 'error'});
41
+ }
42
+
43
+ T.create('#analytics-table', {
44
+ data: stats,
45
+ columns: [
46
+ {key: 'url', title: 'Page URL', render: (val) => `<code>${val}</code>`},
47
+ {key: 'hits', title: 'Page views'}
48
+ ],
49
+ emptyMessage: 'No page views recorded yet.'
50
+ });
51
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Example Analytics Plugin — Default Configuration
3
+ */
4
+ export default {
5
+ excludeAdmin: true
6
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Example Analytics Plugin — Server
3
+ * Tracks page hits in a JSON file alongside the plugin.
4
+ * Endpoints:
5
+ * POST /api/plugins/example-analytics/hit - public: record a hit { url }
6
+ * GET /api/plugins/example-analytics/stats - admin: return all hit counts
7
+ * DELETE /api/plugins/example-analytics/stats - admin: reset all stats
8
+ */
9
+ import fs from 'fs/promises';
10
+ import path from 'path';
11
+ import {fileURLToPath} from 'url';
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ const STATS_FILE = path.join(__dirname, 'stats.json');
15
+
16
+ async function readStats() {
17
+ try {
18
+ return JSON.parse(await fs.readFile(STATS_FILE, 'utf8'));
19
+ } catch {
20
+ return {};
21
+ }
22
+ }
23
+
24
+ async function writeStats(stats) {
25
+ await fs.writeFile(STATS_FILE, JSON.stringify(stats, null, 2) + '\n', 'utf8');
26
+ }
27
+
28
+ export default async function analyticsPlugin(fastify, options) {
29
+ const {authenticate, requireAdmin} = options.auth;
30
+
31
+ // Record a page hit — called by the client-side injection script (public)
32
+ fastify.post('/hit', async (request, reply) => {
33
+ const {url} = request.body || {};
34
+ if (!url || typeof url !== 'string') {
35
+ return reply.status(400).send({error: 'url is required'});
36
+ }
37
+
38
+ const normalised = url.split('?')[0].replace(/\/$/, '') || '/';
39
+ const stats = await readStats();
40
+ stats[normalised] = (stats[normalised] || 0) + 1;
41
+ await writeStats(stats);
42
+ return {ok: true};
43
+ });
44
+
45
+ // Return all stats — admin only
46
+ fastify.get('/stats', {preHandler: [authenticate, requireAdmin]}, async () => {
47
+ const stats = await readStats();
48
+ return Object.entries(stats)
49
+ .map(([url, hits]) => ({url, hits}))
50
+ .sort((a, b) => b.hits - a.hits);
51
+ });
52
+
53
+ // Reset stats — admin only
54
+ fastify.delete('/stats', {preHandler: [authenticate, requireAdmin]}, async () => {
55
+ await writeStats({});
56
+ return {ok: true};
57
+ });
58
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "example-analytics",
3
+ "displayName": "Analytics",
4
+ "version": "1.0.0",
5
+ "description": "Basic page view analytics. Tracks hits per page using a simple JSON store.",
6
+ "author": "Darryl Waterhouse",
7
+ "date": "2026-03-01",
8
+ "icon": "chart-bar",
9
+ "admin": {
10
+ "sidebar": [
11
+ {
12
+ "id": "analytics",
13
+ "text": "Analytics",
14
+ "icon": "chart-bar",
15
+ "url": "#/plugins/analytics",
16
+ "section": "#/plugins/analytics"
17
+ }
18
+ ],
19
+ "routes": [
20
+ {
21
+ "path": "/plugins/analytics",
22
+ "view": "plugin-analytics",
23
+ "title": "Analytics - Domma CMS"
24
+ }
25
+ ],
26
+ "views": {
27
+ "plugin-analytics": {
28
+ "entry": "example-analytics/admin/views/analytics.js",
29
+ "exportName": "analyticsView"
30
+ }
31
+ }
32
+ },
33
+ "inject": {
34
+ "head": "public/inject-head.html",
35
+ "bodyEnd": "public/inject-body.html"
36
+ },
37
+ "scaffold": {
38
+ "reset": [
39
+ {
40
+ "path": "stats.json",
41
+ "content": "{}"
42
+ }
43
+ ]
44
+ }
45
+ }
@@ -0,0 +1,14 @@
1
+ <!-- example-analytics: page view tracker -->
2
+ <script>
3
+ (function () {
4
+ var url = window.location.pathname;
5
+ if (typeof fetch === 'function') {
6
+ fetch('/api/plugins/example-analytics/hit', {
7
+ method: 'POST',
8
+ headers: {'Content-Type': 'application/json'},
9
+ body: JSON.stringify({url: url})
10
+ }).catch(function () { /* silent fail */
11
+ });
12
+ }
13
+ })();
14
+ </script>
@@ -0,0 +1 @@
1
+ <!-- example-analytics: head injection (empty — tracking is done via body script) -->
@@ -0,0 +1,24 @@
1
+ {
2
+ "/": 138,
3
+ "/about": 71,
4
+ "/blog": 30,
5
+ "/contact": 30,
6
+ "/resources/typography": 4,
7
+ "/resources": 13,
8
+ "/resources/shortcodes": 14,
9
+ "/resources/cards": 15,
10
+ "/resources/interactive": 13,
11
+ "/resources/grid": 6,
12
+ "/forms": 14,
13
+ "/resources/effects": 6,
14
+ "/blog/hello-world": 20,
15
+ "/feedback": 38,
16
+ "/resources/dependencies": 2,
17
+ "/resources/components": 6,
18
+ "/gdpr": 3,
19
+ "/scratch": 51,
20
+ "/getting-started": 3,
21
+ "/resources/pro": 1,
22
+ "/todo": 23,
23
+ "/thank-you": 1
24
+ }
@@ -0,0 +1,66 @@
1
+ {
2
+ "slug": "contacts",
3
+ "title": "Contacts",
4
+ "description": "Contact Information",
5
+ "fields": [
6
+ {
7
+ "name": "full_name",
8
+ "type": "string",
9
+ "label": "Full Name",
10
+ "required": false,
11
+ "placeholder": "Full Name",
12
+ "helper": "Full Name",
13
+ "minLength": 8,
14
+ "maxLength": 255
15
+ },
16
+ {
17
+ "type": "spacer"
18
+ },
19
+ {
20
+ "name": "phone_number",
21
+ "type": "tel",
22
+ "label": "Phone Number",
23
+ "required": false,
24
+ "placeholder": "Phone Number",
25
+ "helper": "Primary Phone Number"
26
+ },
27
+ {
28
+ "type": "spacer"
29
+ },
30
+ {
31
+ "name": "email_address",
32
+ "type": "string",
33
+ "label": "Email Address",
34
+ "required": false,
35
+ "placeholder": "Email Address",
36
+ "helper": "Email Address",
37
+ "minLength": 8,
38
+ "maxLength": 255
39
+ }
40
+ ],
41
+ "settings": {
42
+ "submitText": "Submit",
43
+ "successMessage": "Thank you for your submission.",
44
+ "layout": "stacked",
45
+ "honeypot": true,
46
+ "rateLimitPerMinute": 3
47
+ },
48
+ "actions": {
49
+ "email": {
50
+ "enabled": false,
51
+ "recipients": "",
52
+ "subjectPrefix": "[Contacts]"
53
+ },
54
+ "webhook": {
55
+ "enabled": false,
56
+ "url": "",
57
+ "method": "POST"
58
+ },
59
+ "collection": {
60
+ "enabled": true,
61
+ "slug": "contacts"
62
+ }
63
+ },
64
+ "createdAt": "2026-03-17T12:35:44.569Z",
65
+ "updatedAt": "2026-03-17T12:35:44.569Z"
66
+ }
@@ -0,0 +1,103 @@
1
+ {
2
+ "slug": "enquiries",
3
+ "title": "Enquiries",
4
+ "description": "Get in touch with us",
5
+ "fields": [
6
+ {
7
+ "name": "full_name",
8
+ "type": "string",
9
+ "label": "Full Name",
10
+ "required": true,
11
+ "placeholder": "Your full name",
12
+ "helper": "",
13
+ "validation": {
14
+ "min": 2,
15
+ "max": 100
16
+ }
17
+ },
18
+ {
19
+ "name": "email",
20
+ "type": "email",
21
+ "label": "Email Address",
22
+ "required": true,
23
+ "placeholder": "your@email.com",
24
+ "helper": ""
25
+ },
26
+ {
27
+ "name": "phone",
28
+ "type": "tel",
29
+ "label": "Phone Number",
30
+ "required": false,
31
+ "placeholder": "+44 7700 000000",
32
+ "helper": "Optional"
33
+ },
34
+ {
35
+ "name": "subject",
36
+ "type": "select",
37
+ "label": "Subject",
38
+ "required": true,
39
+ "placeholder": "Please select a subject",
40
+ "helper": "",
41
+ "options": [
42
+ {
43
+ "value": "general",
44
+ "label": "General Enquiry"
45
+ },
46
+ {
47
+ "value": "support",
48
+ "label": "Support"
49
+ },
50
+ {
51
+ "value": "sales",
52
+ "label": "Sales"
53
+ },
54
+ {
55
+ "value": "partnership",
56
+ "label": "Partnership"
57
+ },
58
+ {
59
+ "value": "other",
60
+ "label": "Other"
61
+ }
62
+ ]
63
+ },
64
+ {
65
+ "name": "message",
66
+ "type": "textarea",
67
+ "label": "Message",
68
+ "required": true,
69
+ "placeholder": "How can we help you?",
70
+ "helper": "",
71
+ "rows": 4,
72
+ "validation": {
73
+ "min": 10,
74
+ "max": 2000
75
+ }
76
+ }
77
+ ],
78
+ "settings": {
79
+ "submitText": "Send Message",
80
+ "successMessage": "Thanks for reaching out! We'll get back to you shortly.",
81
+ "layout": "stacked",
82
+ "honeypot": true,
83
+ "rateLimitPerMinute": 3
84
+ },
85
+ "actions": {
86
+ "email": {
87
+ "enabled": false,
88
+ "recipients": "",
89
+ "subjectPrefix": "[enquiries]"
90
+ },
91
+ "webhook": {
92
+ "enabled": false,
93
+ "url": "",
94
+ "method": "POST"
95
+ },
96
+ "collection": {
97
+ "enabled": true,
98
+ "slug": "enquiries"
99
+ }
100
+ },
101
+ "createdAt": "2026-03-17T12:35:44.569Z",
102
+ "updatedAt": "2026-03-17T12:35:44.569Z"
103
+ }
@@ -0,0 +1,131 @@
1
+ {
2
+ "slug": "feedback",
3
+ "title": "Feedback",
4
+ "description": "Share your feedback with us",
5
+ "fields": [
6
+ {
7
+ "name": "name",
8
+ "type": "string",
9
+ "label": "Your Name",
10
+ "required": true,
11
+ "placeholder": "Please enter your full name",
12
+ "helper": "Please enter your full name",
13
+ "validation": {
14
+ "min": 2,
15
+ "max": 100
16
+ }
17
+ },
18
+ {
19
+ "name": "email",
20
+ "type": "email",
21
+ "label": "Email Address",
22
+ "required": true,
23
+ "placeholder": "your@email.com",
24
+ "helper": "Please enter your email address"
25
+ },
26
+ {
27
+ "name": "rating",
28
+ "type": "select",
29
+ "label": "Overall Rating",
30
+ "required": true,
31
+ "helper": "Tell us how we are doing!",
32
+ "options": [
33
+ {
34
+ "value": "none",
35
+ "label": "Please Choose"
36
+ },
37
+ {
38
+ "value": "excellent",
39
+ "label": "Excellent"
40
+ },
41
+ {
42
+ "value": "good",
43
+ "label": "Good"
44
+ },
45
+ {
46
+ "value": "average",
47
+ "label": "Average"
48
+ },
49
+ {
50
+ "value": "poor",
51
+ "label": "Poor"
52
+ }
53
+ ]
54
+ },
55
+ {
56
+ "name": "category",
57
+ "type": "select",
58
+ "label": "Category",
59
+ "required": true,
60
+ "placeholder": "Please select a category",
61
+ "helper": "",
62
+ "options": [
63
+ {
64
+ "value": "general",
65
+ "label": "General"
66
+ },
67
+ {
68
+ "value": "bug-report",
69
+ "label": "Bug Report"
70
+ },
71
+ {
72
+ "value": "feature-request",
73
+ "label": "Feature Request"
74
+ },
75
+ {
76
+ "value": "praise",
77
+ "label": "Praise"
78
+ }
79
+ ]
80
+ },
81
+ {
82
+ "name": "subject",
83
+ "type": "string",
84
+ "label": "Subject",
85
+ "required": true,
86
+ "placeholder": "Brief summary of your feedback",
87
+ "helper": "",
88
+ "validation": {
89
+ "max": 200
90
+ }
91
+ },
92
+ {
93
+ "name": "message",
94
+ "type": "textarea",
95
+ "label": "Your Feedback",
96
+ "required": true,
97
+ "placeholder": "Please share your thoughts in detail…",
98
+ "helper": "Please share your thoughts in detail…",
99
+ "rows": 4,
100
+ "validation": {
101
+ "min": 10,
102
+ "max": 2000
103
+ }
104
+ }
105
+ ],
106
+ "settings": {
107
+ "submitText": "Submit Feedback",
108
+ "successMessage": "Thank you for your feedback! We appreciate you taking the time.",
109
+ "layout": "stacked",
110
+ "honeypot": true,
111
+ "rateLimitPerMinute": 3
112
+ },
113
+ "actions": {
114
+ "email": {
115
+ "enabled": true,
116
+ "recipients": "",
117
+ "subjectPrefix": "[feedback]"
118
+ },
119
+ "webhook": {
120
+ "enabled": false,
121
+ "url": "",
122
+ "method": "POST"
123
+ },
124
+ "collection": {
125
+ "enabled": true,
126
+ "slug": "feedback"
127
+ }
128
+ },
129
+ "createdAt": "2026-03-17T12:35:44.569Z",
130
+ "updatedAt": "2026-03-17T12:35:44.569Z"
131
+ }
@@ -0,0 +1,79 @@
1
+ {
2
+ "slug": "notes",
3
+ "title": "Notes",
4
+ "description": "Free-form notes with categories and tags.",
5
+ "fields": [
6
+ {
7
+ "name": "title",
8
+ "type": "string",
9
+ "label": "Title",
10
+ "required": true,
11
+ "placeholder": "Note title"
12
+ },
13
+ {
14
+ "name": "content",
15
+ "type": "textarea",
16
+ "label": "Content",
17
+ "required": true,
18
+ "placeholder": "Write your note here…",
19
+ "rows": 5
20
+ },
21
+ {
22
+ "name": "category",
23
+ "type": "select",
24
+ "label": "Category",
25
+ "required": false,
26
+ "options": [
27
+ {
28
+ "value": "general",
29
+ "label": "General"
30
+ },
31
+ {
32
+ "value": "idea",
33
+ "label": "Idea"
34
+ },
35
+ {
36
+ "value": "reminder",
37
+ "label": "Reminder"
38
+ },
39
+ {
40
+ "value": "reference",
41
+ "label": "Reference"
42
+ }
43
+ ]
44
+ },
45
+ {
46
+ "name": "tags",
47
+ "type": "string",
48
+ "label": "Tags",
49
+ "required": false,
50
+ "placeholder": "Comma-separated tags",
51
+ "helper": "Separate tags with commas"
52
+ }
53
+ ],
54
+ "settings": {
55
+ "submitText": "Save Note",
56
+ "successMessage": "Note saved successfully.",
57
+ "layout": "stacked",
58
+ "honeypot": true,
59
+ "rateLimitPerMinute": 3
60
+ },
61
+ "actions": {
62
+ "email": {
63
+ "enabled": false,
64
+ "recipients": "",
65
+ "subjectPrefix": "[notes]"
66
+ },
67
+ "webhook": {
68
+ "enabled": false,
69
+ "url": "",
70
+ "method": "POST"
71
+ },
72
+ "collection": {
73
+ "enabled": true,
74
+ "slug": "notes"
75
+ }
76
+ },
77
+ "createdAt": "2026-03-17T12:35:44.569Z",
78
+ "updatedAt": "2026-03-17T12:35:44.569Z"
79
+ }
@@ -0,0 +1,100 @@
1
+ {
2
+ "slug": "to-do",
3
+ "title": "To-Do",
4
+ "description": "Task tracking with status, priority, and due dates.",
5
+ "fields": [
6
+ {
7
+ "name": "title",
8
+ "type": "string",
9
+ "label": "Title",
10
+ "required": true,
11
+ "placeholder": "Task title"
12
+ },
13
+ {
14
+ "name": "description",
15
+ "type": "textarea",
16
+ "label": "Description",
17
+ "required": false,
18
+ "placeholder": "Task details…",
19
+ "rows": 3
20
+ },
21
+ {
22
+ "name": "status",
23
+ "type": "select",
24
+ "label": "Status",
25
+ "required": true,
26
+ "options": [
27
+ {
28
+ "value": "pending",
29
+ "label": "Pending"
30
+ },
31
+ {
32
+ "value": "in-progress",
33
+ "label": "In Progress"
34
+ },
35
+ {
36
+ "value": "done",
37
+ "label": "Done"
38
+ }
39
+ ]
40
+ },
41
+ {
42
+ "name": "priority",
43
+ "type": "select",
44
+ "label": "Priority",
45
+ "required": false,
46
+ "options": [
47
+ {
48
+ "value": "low",
49
+ "label": "Low"
50
+ },
51
+ {
52
+ "value": "medium",
53
+ "label": "Medium"
54
+ },
55
+ {
56
+ "value": "high",
57
+ "label": "High"
58
+ }
59
+ ]
60
+ },
61
+ {
62
+ "name": "due_date",
63
+ "type": "date",
64
+ "label": "Due Date",
65
+ "required": false
66
+ },
67
+ {
68
+ "name": "assigned_to",
69
+ "type": "string",
70
+ "label": "Assigned To",
71
+ "required": false,
72
+ "placeholder": "Name or email"
73
+ }
74
+ ],
75
+ "settings": {
76
+ "submitText": "Add Task",
77
+ "successMessage": "Task added successfully.",
78
+ "layout": "stacked",
79
+ "honeypot": true,
80
+ "rateLimitPerMinute": 3
81
+ },
82
+ "actions": {
83
+ "email": {
84
+ "enabled": false,
85
+ "recipients": "",
86
+ "subjectPrefix": "[to-do]"
87
+ },
88
+ "webhook": {
89
+ "enabled": false,
90
+ "url": "",
91
+ "method": "POST"
92
+ },
93
+ "collection": {
94
+ "enabled": true,
95
+ "slug": "to-do"
96
+ }
97
+ },
98
+ "createdAt": "2026-03-17T12:35:44.569Z",
99
+ "updatedAt": "2026-03-17T12:35:44.569Z"
100
+ }
@@ -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:clamp(1.5rem,4vw,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:clamp(1.2rem,3vw,1.5rem)}.page-body h3{font-size:clamp(1.1rem,2.5vw,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}.hero-gradient-purple{background:linear-gradient(135deg,#ede9fe,#ddd6fe);color:#1e1b4b}.hero-gradient-blue{background:linear-gradient(135deg,#dbeafe,#bfdbfe);color:#1e3a5f}.hero-gradient-green{background:linear-gradient(135deg,#d1fae5,#a7f3d0);color:#064e3b}.hero-gradient-sunset{background:linear-gradient(135deg,#fef3c7,#fde68a);color:#78350f}.hero-gradient-ocean{background:linear-gradient(135deg,#e0f2fe,#bae6fd);color:#0c4a6e}.hero-gradient-rose{background:linear-gradient(135deg,#fce7f3,#fbcfe8);color:#831843}.hero-gradient-forest{background:linear-gradient(135deg,#dcfce7,#bbf7d0);color:#14532d}.hero-gradient-night{background:linear-gradient(135deg,#334155,#1e293b);color:#e2e8f0}.hero-gradient-ocean-light{background:linear-gradient(135deg,#e0f2fe,#caf0f8);color:#1e293b}.hero-gradient-ocean-dark{background:linear-gradient(135deg,#0c4a6e,#164e63);color:#e2e8f0}.hero-gradient-forest-light{background:linear-gradient(135deg,#d1fae5,#c6f6dc);color:#1e293b}.hero-gradient-forest-dark{background:linear-gradient(135deg,#1a4731,#166534);color:#e2e8f0}.hero-gradient-sunset-light{background:linear-gradient(135deg,#fde8d8,#fddcc9);color:#1e293b}.hero-gradient-sunset-dark{background:linear-gradient(135deg,#6b3727,#7c4036);color:#f5ede8}.hero-gradient-royal-light{background:linear-gradient(135deg,#e8f0fd,#dce8fc);color:#1e293b}.hero-gradient-royal-dark{background:linear-gradient(135deg,#1e3465,#263d7a);color:#e2e8f0}.hero-gradient-lemon-light{background:linear-gradient(135deg,#fefce8,#fef9c3);color:#1e293b}.hero-gradient-lemon-dark{background:linear-gradient(135deg,#5c4d1a,#6b5920);color:#fefce8}.hero-gradient-silver-light{background:linear-gradient(135deg,#f1f5f9,#e2e8f0);color:#1e293b}.hero-gradient-silver-dark{background:linear-gradient(135deg,#2d3748,#374151);color:#e2e8f0}.hero-gradient-charcoal-light{background:linear-gradient(135deg,#eceff1,#e1e7eb);color:#1e293b}.hero-gradient-charcoal-dark{background:linear-gradient(135deg,#2c3843,#374451);color:#e2e8f0}.hero-gradient-christmas-light{background:linear-gradient(135deg,#fde8ea,#fdd5d8);color:#1e293b}.hero-gradient-christmas-dark{background:linear-gradient(135deg,#5c0f1d,#7a1525);color:#fde8ea}.hero-gradient-unicorn-light{background:linear-gradient(135deg,#f5e8fd,#edd6fb);color:#1e293b}.hero-gradient-unicorn-dark{background:linear-gradient(135deg,#3d1a5a,#4a2068);color:#f5e8fd}.hero-gradient-dreamy-light{background:linear-gradient(135deg,#f5ede8,#eeddd4);color:#1e293b}.hero-gradient-dreamy-dark{background:linear-gradient(135deg,#3d2820,#503328);color:#f5ede8}.hero-gradient-grayve-light{background:linear-gradient(135deg,#e0f7f9,#cbf2f5);color:#1e293b}.hero-gradient-grayve-dark{background:linear-gradient(135deg,#00363d,#00444d);color:#e0f7f9}.hero-gradient-mint-light{background:linear-gradient(135deg,#d8f5ea,#c5efdd);color:#1e293b}.hero-gradient-mint-dark{background:linear-gradient(135deg,#134d33,#195f3f);color:#d8f5ea}.hero-gradient-wedding-light{background:linear-gradient(135deg,#faf3e0,#f5e9c7);color:#1e293b}.hero-gradient-wedding-dark{background:linear-gradient(135deg,#5c4418,#6f5320);color:#faf3e0}.tabs-centered{text-align:center}.tabs-centered .tab-list{display:inline-flex}.tabs-centered .tab-content{text-align:left}.site-main{overflow-x:hidden}@media(max-width:768px){.hero .hero-cta a,.dm-so-trigger,.dm-cta-trigger{min-height:44px;padding:.6rem 1.25rem}}
1
+ body,button,input,select,textarea{font-family:Roboto,sans-serif}.navbar-actions{order:4;margin-left:auto;display:flex;align-items:center;gap:.5rem}.navbar-dark .navbar-actions{color:var(--dm-text-inverse, rgba(255, 255, 255, .85))}.navbar-dark .site-search-shortcut-hint{color:#fff9;border-color:#ffffff40}.navbar-light .navbar-actions{color:var(--dm-text, #111)}.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:clamp(1.5rem,4vw,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:clamp(1.2rem,3vw,1.5rem)}.page-body h3{font-size:clamp(1.1rem,2.5vw,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}.hero-gradient-purple{background:linear-gradient(135deg,#ede9fe,#ddd6fe);color:#1e1b4b}.hero-gradient-blue{background:linear-gradient(135deg,#dbeafe,#bfdbfe);color:#1e3a5f}.hero-gradient-green{background:linear-gradient(135deg,#d1fae5,#a7f3d0);color:#064e3b}.hero-gradient-sunset{background:linear-gradient(135deg,#fef3c7,#fde68a);color:#78350f}.hero-gradient-ocean{background:linear-gradient(135deg,#e0f2fe,#bae6fd);color:#0c4a6e}.hero-gradient-rose{background:linear-gradient(135deg,#fce7f3,#fbcfe8);color:#831843}.hero-gradient-forest{background:linear-gradient(135deg,#dcfce7,#bbf7d0);color:#14532d}.hero-gradient-night{background:linear-gradient(135deg,#334155,#1e293b);color:#e2e8f0}.hero-gradient-ocean-light{background:linear-gradient(135deg,#e0f2fe,#caf0f8);color:#1e293b}.hero-gradient-ocean-dark{background:linear-gradient(135deg,#0c4a6e,#164e63);color:#e2e8f0}.hero-gradient-forest-light{background:linear-gradient(135deg,#d1fae5,#c6f6dc);color:#1e293b}.hero-gradient-forest-dark{background:linear-gradient(135deg,#1a4731,#166534);color:#e2e8f0}.hero-gradient-sunset-light{background:linear-gradient(135deg,#fde8d8,#fddcc9);color:#1e293b}.hero-gradient-sunset-dark{background:linear-gradient(135deg,#6b3727,#7c4036);color:#f5ede8}.hero-gradient-royal-light{background:linear-gradient(135deg,#e8f0fd,#dce8fc);color:#1e293b}.hero-gradient-royal-dark{background:linear-gradient(135deg,#1e3465,#263d7a);color:#e2e8f0}.hero-gradient-lemon-light{background:linear-gradient(135deg,#fefce8,#fef9c3);color:#1e293b}.hero-gradient-lemon-dark{background:linear-gradient(135deg,#5c4d1a,#6b5920);color:#fefce8}.hero-gradient-silver-light{background:linear-gradient(135deg,#f1f5f9,#e2e8f0);color:#1e293b}.hero-gradient-silver-dark{background:linear-gradient(135deg,#2d3748,#374151);color:#e2e8f0}.hero-gradient-charcoal-light{background:linear-gradient(135deg,#eceff1,#e1e7eb);color:#1e293b}.hero-gradient-charcoal-dark{background:linear-gradient(135deg,#2c3843,#374451);color:#e2e8f0}.hero-gradient-christmas-light{background:linear-gradient(135deg,#fde8ea,#fdd5d8);color:#1e293b}.hero-gradient-christmas-dark{background:linear-gradient(135deg,#5c0f1d,#7a1525);color:#fde8ea}.hero-gradient-unicorn-light{background:linear-gradient(135deg,#f5e8fd,#edd6fb);color:#1e293b}.hero-gradient-unicorn-dark{background:linear-gradient(135deg,#3d1a5a,#4a2068);color:#f5e8fd}.hero-gradient-dreamy-light{background:linear-gradient(135deg,#f5ede8,#eeddd4);color:#1e293b}.hero-gradient-dreamy-dark{background:linear-gradient(135deg,#3d2820,#503328);color:#f5ede8}.hero-gradient-grayve-light{background:linear-gradient(135deg,#e0f7f9,#cbf2f5);color:#1e293b}.hero-gradient-grayve-dark{background:linear-gradient(135deg,#00363d,#00444d);color:#e0f7f9}.hero-gradient-mint-light{background:linear-gradient(135deg,#d8f5ea,#c5efdd);color:#1e293b}.hero-gradient-mint-dark{background:linear-gradient(135deg,#134d33,#195f3f);color:#d8f5ea}.hero-gradient-wedding-light{background:linear-gradient(135deg,#faf3e0,#f5e9c7);color:#1e293b}.hero-gradient-wedding-dark{background:linear-gradient(135deg,#5c4418,#6f5320);color:#faf3e0}.tabs-centered{text-align:center}.tabs-centered .tab-list{display:inline-flex}.tabs-centered .tab-content{text-align:left}.site-main{overflow-x:hidden}@media(max-width:768px){.hero .hero-cta a,.dm-so-trigger,.dm-cta-trigger{min-height:44px;padding:.6rem 1.25rem}}
package/server/server.js CHANGED
@@ -75,6 +75,7 @@ await app.register(helmet, {
75
75
  }
76
76
  },
77
77
  crossOriginEmbedderPolicy: false, // allow embedding images/resources
78
+ hsts: false, // disable HSTS — server runs HTTP only; HSTS would force browser to https
78
79
  });
79
80
 
80
81
  await app.register(jwt, { secret: process.env.JWT_SECRET });
@@ -0,0 +1,227 @@
1
+ /**
2
+ * User Types Service
3
+ * Preset Collection — seeds roles on first startup, caches them in memory.
4
+ * Auth middleware calls getRoleMap() / getPermissionsFor() at request time.
5
+ */
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import {v4 as uuidv4} from 'uuid';
9
+ import {config} from '../config.js';
10
+
11
+ const COLLECTIONS_DIR = path.resolve(config.content.collectionsDir);
12
+ const SLUG = 'user-types';
13
+ const DIR = path.join(COLLECTIONS_DIR, SLUG);
14
+ const SCHEMA_PATH = path.join(DIR, 'schema.json');
15
+ const DATA_PATH = path.join(DIR, 'data.json');
16
+
17
+ export const RESOURCES = ['pages', 'settings', 'navigation', 'layouts', 'media', 'users', 'plugins', 'collections', 'views', 'actions', 'blocks'];
18
+ export const ACTIONS = ['read', 'create', 'update', 'delete'];
19
+
20
+ const PRESET_SCHEMA = {
21
+ slug: SLUG,
22
+ title: 'User Types',
23
+ description: 'CMS role definitions — managed by the system.',
24
+ preset: true,
25
+ fields: [
26
+ {name: 'name', label: 'Name (slug)', type: 'text', required: true},
27
+ {name: 'label', label: 'Label', type: 'text', required: true},
28
+ {name: 'level', label: 'Level', type: 'number', required: true},
29
+ {
30
+ name: 'permissions',
31
+ label: 'Permissions',
32
+ type: 'multi-select',
33
+ options: RESOURCES.flatMap(r => [r, ...ACTIONS.map(a => `${r}.${a}`)])
34
+ },
35
+ {
36
+ name: 'badgeClass', label: 'Badge Class', type: 'select',
37
+ options: ['badge-danger', 'badge-warning', 'badge-info', 'badge-secondary', 'badge-success', 'badge-primary']
38
+ }
39
+ ],
40
+ api: {
41
+ create: {enabled: false, access: 'admin'},
42
+ read: {enabled: false, access: 'admin'},
43
+ update: {enabled: false, access: 'admin'},
44
+ delete: {enabled: false, access: 'admin'}
45
+ }
46
+ };
47
+
48
+ const SEED_ENTRIES = [
49
+ {name: 'admin', label: 'Admin', level: 0, permissions: RESOURCES, badgeClass: 'badge-danger'},
50
+ {
51
+ name: 'manager',
52
+ label: 'Manager',
53
+ level: 1,
54
+ permissions: ['pages', 'settings', 'navigation', 'layouts', 'media', 'users', 'collections', 'views', 'actions'],
55
+ badgeClass: 'badge-warning'
56
+ },
57
+ {
58
+ name: 'editor',
59
+ label: 'Editor',
60
+ level: 2,
61
+ permissions: ['pages.read', 'pages.create', 'pages.update', 'media'],
62
+ badgeClass: 'badge-info'
63
+ },
64
+ {name: 'subscriber', label: 'Subscriber', level: 3, permissions: [], badgeClass: 'badge-secondary'}
65
+ ];
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // In-memory cache
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /** @type {Map<string,{label:string,level:number,badgeClass:string}>} */
72
+ let roleMap = new Map();
73
+
74
+ /** @type {Map<string,string[]>} resource (and resource.action) → role names */
75
+ let permissionsMap = new Map();
76
+
77
+ /** @type {Map<string,string[]>} role name → raw permissions array */
78
+ let rawPermissionsMap = new Map();
79
+
80
+ /**
81
+ * Build in-memory maps from an array of data entries.
82
+ * Supports both bare resource names ('pages') and dotted action strings ('pages.read').
83
+ * Bare names expand to all four actions for backward compatibility.
84
+ *
85
+ * @param {object[]} entries
86
+ */
87
+ function buildCache(entries) {
88
+ roleMap = new Map();
89
+ permissionsMap = new Map();
90
+ rawPermissionsMap = new Map();
91
+
92
+ const addTo = (key, role) => {
93
+ if (!permissionsMap.has(key)) permissionsMap.set(key, []);
94
+ if (!permissionsMap.get(key).includes(role)) permissionsMap.get(key).push(role);
95
+ };
96
+
97
+ for (const entry of entries) {
98
+ const d = entry.data;
99
+ roleMap.set(d.name, {label: d.label, level: d.level, badgeClass: d.badgeClass || ''});
100
+ rawPermissionsMap.set(d.name, d.permissions || []);
101
+ for (const perm of (d.permissions || [])) {
102
+ if (perm.includes('.')) {
103
+ const [res] = perm.split('.');
104
+ addTo(perm, d.name); // 'pages.read' → [role]
105
+ addTo(res, d.name); // 'pages' → [role] (any-action union)
106
+ } else {
107
+ addTo(perm, d.name); // 'pages' (bare) → [role]
108
+ for (const action of ACTIONS) {
109
+ addTo(`${perm}.${action}`, d.name); // expand to all actions
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Public API
118
+ // ---------------------------------------------------------------------------
119
+
120
+ /**
121
+ * Seed the preset collection on first startup (no-op if data.json already exists).
122
+ *
123
+ * @returns {Promise<void>}
124
+ */
125
+ export async function seed() {
126
+ await fs.mkdir(DIR, {recursive: true});
127
+
128
+ // Always write schema (overwrite to keep in sync with code)
129
+ await fs.writeFile(SCHEMA_PATH, JSON.stringify(PRESET_SCHEMA, null, 2) + '\n', 'utf8');
130
+
131
+ // Only write data if it doesn't exist yet
132
+ try {
133
+ await fs.access(DATA_PATH);
134
+ } catch {
135
+ const entries = SEED_ENTRIES.map(data => ({
136
+ id: uuidv4(),
137
+ data,
138
+ createdAt: new Date().toISOString(),
139
+ updatedAt: new Date().toISOString()
140
+ }));
141
+ await fs.writeFile(DATA_PATH, JSON.stringify(entries, null, 2) + '\n', 'utf8');
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Load the collection from disk into the in-memory cache.
147
+ *
148
+ * @returns {Promise<void>}
149
+ */
150
+ export async function load() {
151
+ try {
152
+ const raw = await fs.readFile(DATA_PATH, 'utf8');
153
+ const entries = JSON.parse(raw);
154
+ buildCache(entries);
155
+ } catch (err) {
156
+ console.warn('[userTypes] Failed to load user-types collection:', err.message);
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Reload from disk — call after any CRUD on the user-types collection.
162
+ *
163
+ * @returns {Promise<void>}
164
+ */
165
+ export async function invalidate() {
166
+ await load();
167
+ }
168
+
169
+ /**
170
+ * Return the full role map.
171
+ *
172
+ * @returns {Map<string,{label:string,level:number,badgeClass:string}>}
173
+ */
174
+ export function getRoleMap() {
175
+ return roleMap;
176
+ }
177
+
178
+ /**
179
+ * Return the level for a named role, or Infinity if not found.
180
+ *
181
+ * @param {string} roleName
182
+ * @returns {number}
183
+ */
184
+ export function getRoleLevel(roleName) {
185
+ return roleMap.get(roleName)?.level ?? Infinity;
186
+ }
187
+
188
+ /**
189
+ * Return the role names allowed to access a resource (and optional action).
190
+ * - getPermissionsFor('pages') → roles with ANY action on pages (backward compat)
191
+ * - getPermissionsFor('pages', 'delete')→ roles with delete on pages
192
+ * - getPermissionsFor('pages.delete') → same as above (dot-notation shorthand)
193
+ *
194
+ * @param {string} resource - Resource key, or 'resource.action' dot notation
195
+ * @param {string} [action] - Optional action (read | create | update | delete)
196
+ * @returns {string[]}
197
+ */
198
+ export function getPermissionsFor(resource, action) {
199
+ if (action) {
200
+ return permissionsMap.get(`${resource}.${action}`) ?? [];
201
+ }
202
+ if (resource.includes('.')) {
203
+ return permissionsMap.get(resource) ?? [];
204
+ }
205
+ return permissionsMap.get(resource) ?? [];
206
+ }
207
+
208
+ /**
209
+ * Return the raw permissions array for a role — used by the /api/auth/permissions endpoint.
210
+ *
211
+ * @param {string} roleName
212
+ * @returns {string[]}
213
+ */
214
+ export function getPermissionsForRole(roleName) {
215
+ return rawPermissionsMap.get(roleName) ?? [];
216
+ }
217
+
218
+ /**
219
+ * Return role names ordered from most to least privileged.
220
+ *
221
+ * @returns {string[]}
222
+ */
223
+ export function getRoleHierarchy() {
224
+ return [...roleMap.entries()]
225
+ .sort((a, b) => a[1].level - b[1].level)
226
+ .map(([key]) => key);
227
+ }