domma-cms 0.17.0 → 0.21.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 (136) hide show
  1. package/CLAUDE.md +39 -3
  2. package/admin/css/admin.css +1 -1
  3. package/admin/css/dashboard.css +1 -1
  4. package/admin/index.html +2 -2
  5. package/admin/js/api.js +1 -1
  6. package/admin/js/app.js +4 -4
  7. package/admin/js/config/sidebar-config.js +1 -1
  8. package/admin/js/lib/card-builder.js +3 -3
  9. package/admin/js/lib/crud-tutorial.js +1 -0
  10. package/admin/js/lib/effects-builder.js +1 -1
  11. package/admin/js/lib/markdown-toolbar.js +6 -6
  12. package/admin/js/lib/project-context.js +1 -0
  13. package/admin/js/lib/sidebar-renderer.js +4 -0
  14. package/admin/js/templates/action-editor.html +7 -0
  15. package/admin/js/templates/block-editor.html +7 -0
  16. package/admin/js/templates/collection-editor.html +9 -0
  17. package/admin/js/templates/dashboard/cache.html +32 -0
  18. package/admin/js/templates/dashboard.html +4 -0
  19. package/admin/js/templates/form-editor.html +9 -0
  20. package/admin/js/templates/menu-editor.html +98 -0
  21. package/admin/js/templates/menu-locations.html +14 -0
  22. package/admin/js/templates/menus.html +14 -0
  23. package/admin/js/templates/page-editor.html +9 -2
  24. package/admin/js/templates/project-detail.html +50 -0
  25. package/admin/js/templates/project-editor.html +45 -0
  26. package/admin/js/templates/project-settings.html +60 -0
  27. package/admin/js/templates/projects.html +13 -0
  28. package/admin/js/templates/role-editor.html +11 -0
  29. package/admin/js/templates/settings.html +26 -0
  30. package/admin/js/templates/tutorials.html +335 -2
  31. package/admin/js/templates/view-editor.html +7 -0
  32. package/admin/js/views/action-editor.js +1 -1
  33. package/admin/js/views/actions-list.js +1 -1
  34. package/admin/js/views/block-editor-enhance.js +1 -1
  35. package/admin/js/views/block-editor.js +8 -8
  36. package/admin/js/views/blocks.js +2 -2
  37. package/admin/js/views/collection-editor.js +4 -4
  38. package/admin/js/views/collections.js +1 -1
  39. package/admin/js/views/dashboard/widgets/activity-feed.js +1 -1
  40. package/admin/js/views/dashboard/widgets/cache.js +1 -0
  41. package/admin/js/views/dashboard/widgets/journeys.js +1 -1
  42. package/admin/js/views/dashboard/widgets/spike-feed.js +1 -1
  43. package/admin/js/views/dashboard/widgets/top-pages.js +1 -1
  44. package/admin/js/views/dashboard.js +1 -1
  45. package/admin/js/views/form-editor.js +6 -6
  46. package/admin/js/views/forms.js +1 -1
  47. package/admin/js/views/index.js +1 -1
  48. package/admin/js/views/menu-editor.js +19 -0
  49. package/admin/js/views/menu-locations.js +1 -0
  50. package/admin/js/views/menus.js +5 -0
  51. package/admin/js/views/page-editor.js +41 -36
  52. package/admin/js/views/pages.js +3 -3
  53. package/admin/js/views/project-detail.js +4 -0
  54. package/admin/js/views/project-editor.js +1 -0
  55. package/admin/js/views/project-settings.js +1 -0
  56. package/admin/js/views/projects.js +7 -0
  57. package/admin/js/views/role-editor.js +1 -1
  58. package/admin/js/views/roles.js +3 -3
  59. package/admin/js/views/settings.js +3 -3
  60. package/admin/js/views/tutorials.js +1 -1
  61. package/admin/js/views/user-editor.js +1 -1
  62. package/admin/js/views/users.js +3 -3
  63. package/admin/js/views/view-editor.js +1 -1
  64. package/admin/js/views/views-list.js +1 -1
  65. package/config/cache.json +4 -0
  66. package/config/cache.json.example +12 -0
  67. package/config/menu-locations.json +5 -0
  68. package/config/menus/admin-sidebar.json +185 -0
  69. package/config/menus/footer.json +33 -0
  70. package/config/menus/main.json +35 -0
  71. package/config/menus/sproj-1779696558011-menu.json +17 -0
  72. package/config/menus/sproj-1779696960337-menu.json +18 -0
  73. package/config/menus/sproj-1779696985353-menu.json +18 -0
  74. package/config/site.json +6 -22
  75. package/package.json +4 -3
  76. package/plugins/analytics/daily.json +3 -0
  77. package/plugins/analytics/journeys.json +8 -0
  78. package/plugins/analytics/lifetime.json +1 -1
  79. package/public/css/site.css +1 -1
  80. package/public/js/collection-browser.js +4 -0
  81. package/public/js/forms.js +1 -1
  82. package/public/js/site.js +1 -1
  83. package/server/config.js +12 -1
  84. package/server/middleware/auth.js +88 -22
  85. package/server/routes/api/actions.js +58 -5
  86. package/server/routes/api/auth.js +2 -2
  87. package/server/routes/api/blocks.js +18 -3
  88. package/server/routes/api/cache.js +57 -0
  89. package/server/routes/api/collections.js +201 -8
  90. package/server/routes/api/forms.js +266 -21
  91. package/server/routes/api/menu-locations.js +46 -0
  92. package/server/routes/api/menus.js +115 -0
  93. package/server/routes/api/navigation.js +2 -0
  94. package/server/routes/api/pages.js +1 -1
  95. package/server/routes/api/projects.js +107 -0
  96. package/server/routes/api/scaffold.js +86 -0
  97. package/server/routes/api/settings.js +3 -0
  98. package/server/routes/api/sidebar.js +23 -0
  99. package/server/routes/api/users.js +32 -7
  100. package/server/routes/api/views.js +10 -2
  101. package/server/routes/public.js +88 -7
  102. package/server/server.js +54 -3
  103. package/server/services/actions.js +137 -8
  104. package/server/services/adapters/FileAdapter.js +23 -8
  105. package/server/services/adapters/MongoAdapter.js +36 -18
  106. package/server/services/blocks.js +23 -8
  107. package/server/services/cache/drivers/MemoryDriver.js +118 -0
  108. package/server/services/cache/drivers/NoneDriver.js +12 -0
  109. package/server/services/cache/index.js +229 -0
  110. package/server/services/cache/lru.js +61 -0
  111. package/server/services/collections.js +102 -12
  112. package/server/services/content.js +25 -6
  113. package/server/services/filterEngine.js +281 -0
  114. package/server/services/forms.js +3 -0
  115. package/server/services/hooks.js +48 -0
  116. package/server/services/markdown.js +711 -124
  117. package/server/services/menus-migration.js +107 -0
  118. package/server/services/menus.js +422 -0
  119. package/server/services/permissionRegistry.js +26 -0
  120. package/server/services/plugins.js +9 -2
  121. package/server/services/presetCollections.js +22 -0
  122. package/server/services/projects.js +429 -0
  123. package/server/services/recipes/contact-list.json +78 -0
  124. package/server/services/recipes/onboarding.json +426 -0
  125. package/server/services/references.js +174 -0
  126. package/server/services/renderer.js +237 -40
  127. package/server/services/roles.js +6 -1
  128. package/server/services/rowAccess.js +86 -13
  129. package/server/services/scaffolder.js +465 -0
  130. package/server/services/sidebar-migration.js +117 -0
  131. package/server/services/sitemap.js +112 -0
  132. package/server/services/userRoles.js +86 -0
  133. package/server/services/users.js +23 -2
  134. package/server/services/views.js +19 -4
  135. package/server/templates/page.html +135 -130
  136. /package/config/{navigation.json → navigation.json.bak} +0 -0
@@ -0,0 +1,4 @@
1
+ import{api as c}from"../api.js";function g(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const q=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu"}];function P(e,i){return i?!e||!e.length?!1:e.includes(i)?!0:["read","create","update","delete"].some(t=>e.includes(`${i}.${t}`)):!0}function L(e,i){const t=[];for(const n of e){if(n.hidden||n.permission&&!P(i,n.permission))continue;const s=Array.isArray(n.items)&&n.items.length?L(n.items,i):[];t.push({...n,items:s})}return t}function N(e,i){if(!i||!i.length)return e;const t=(n,s)=>{for(const l of n){if((l.text||"").toLowerCase()===s.toLowerCase())return l;if(Array.isArray(l.items)){const r=t(l.items,s);if(r)return r}}return null};for(const n of i)if(n.parent){const s=t(e,n.parent);s?(s.items=Array.isArray(s.items)?s.items:[],s.items.push(n.item)):e.push(n.item)}else e.push(n.item);return e}async function W(e){if(!P(e,"projects"))return[];let i=[];try{i=await c.projects.list()}catch{return[]}const t={};await Promise.all(i.map(async s=>{try{const l=await c.projects.artefacts(s.slug);t[s.slug]=l}catch{t[s.slug]={}}}));const n=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"}];return i.map(s=>{const r="#/projects/"+encodeURIComponent(s.slug),f=t[s.slug]||{},u=[{text:"Overview",url:r,icon:s.icon||"folder"}];for(const m of n){const y=f[m.key],b=Array.isArray(y)?y.length:0;b<=0||u.push({text:`${m.text} (${b})`,url:r+m.path,icon:m.icon})}return u.push({text:"Settings",url:r+"/settings",icon:"settings"}),{text:s.name||s.slug,icon:s.icon||"folder",items:u}})}function D(e,i){for(const t of e)if((t.text||"").toLowerCase()==="projects"){t.items=Array.isArray(t.items)?t.items.slice():[],t.items.push(...i);return}}function T(e,i){if(!e.url||!i)return"";const t=i[e.url];return t==null||t<=0?"":`<span class="sidebar-badge">${g(String(t))}</span>`}function U(e){const i=(e.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),t=(e.url||"").match(/^#\/projects\/([^/]+)/);return`sidebar.expanded.${t?`project-${decodeURIComponent(t[1])}.`:""}${i}`}function F(e,i){const t=e.icon?`<span data-icon="${g(e.icon)}"></span>`:"",n=g(e.text||""),s=e.url?g(e.url):"";if(Array.isArray(e.items)&&e.items.length>0){const r=U(e),f=S.get(r)!==!1,u=e.items.map(m=>F(m,i)).join("");return`<details data-state-key="${g(r)}"${f?" open":""}>
2
+ <summary>${t} <span class="sidebar-text">${n}</span></summary>
3
+ <div class="sidebar-children">${u}</div>
4
+ </details>`}return`<a href="${s}" class="sidebar-link" data-url="${s}">${t} <span class="sidebar-text">${n}</span>${T(e,i)}</a>`}async function K(){const e=h=>h.then(p=>Array.isArray(p)?p.length:Array.isArray(p?.entries)?p.entries.length:0).catch(()=>0),[i,t,n,s,l,r,f,u,m,y,b]=await Promise.all([e(c.pages.list()),e(c.media.list()),e(c.collections.list()),e(c.forms.list()),e(c.views.list()),e(c.actions.list()),e(c.blocks.list()),e(c.components.list()),e(c.users.list()),e(c.plugins.list()),c.system?.notifications?.unreadCount?.().then(h=>h?.count??0).catch(()=>0)??Promise.resolve(0)]);return{"#/pages":i,"#/media":t,"#/collections":n,"#/forms":s,"#/views":l,"#/actions":r,"#/blocks":f,"#/components":u,"#/users":m,"#/plugins":y,"#/system/notifications":b}}async function X(){try{const e=await c.settings.get();return e?.adminBrand?.title||e?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:e,permissions:i}){const t=$(e).get(0);if(!t)return;let n,s=null,l=null,r=null;try{const a=await c.menus.get("admin-sidebar");n=Array.isArray(a?.items)?a.items:null,s=a?.variant||null,l=a?.position||null,r=a?.style||null}catch{n=null}(!n||!n.length)&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),n=q.slice());let f=[];try{f=await H.get("/api/sidebar/registered-items")||[]}catch{}n=N(n,f);const u=await W(i);u.length&&D(n,u),n=L(n,i);const[m,y]=await Promise.all([K().catch(()=>({})),X()]),b=s?` dm-admin-sidebar--${g(s)}`:"";function h(a,d="px"){if(a==null)return a;const o=String(a).trim();return/^\d+(\.\d+)?$/.test(o)?`${o}${d}`:o}let p="";if(r){const a=[];r.fontFamily&&a.push(`font-family: ${r.fontFamily}, sans-serif`),r.fontSize&&a.push(`font-size: ${h(r.fontSize)}`),r.fontWeight&&a.push(`font-weight: ${r.fontWeight}`),r.letterSpacing&&a.push(`letter-spacing: ${h(r.letterSpacing,"em")}`);const d=[];if(a.length&&d.push(`#admin-sidebar .dm-admin-sidebar, #admin-sidebar .dm-admin-sidebar .sidebar-link, #admin-sidebar .dm-admin-sidebar summary, #admin-sidebar .dm-admin-sidebar .sidebar-text { ${a.join("; ")} }`),r.iconSize){const o=h(r.iconSize);d.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${o} !important; height: ${o} !important; }`)}d.length&&(p=`<style data-admin-sidebar-style>${d.join(" ")}</style>`)}const z=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span> ${g(y)}</div>`,B=`${p}<nav class="dm-admin-sidebar${b}">${z}${n.map(a=>F(a,m)).join("")}</nav>`,k=document.createRange();k.selectNodeContents(t);const R=k.createContextualFragment(B);t.replaceChildren(R),Domma.icons.scan(t);const x=Number.parseInt(S.get("sidebar.width"),10);Number.isFinite(x)&&x>=180&&x<=480&&(t.style.width=x+"px");const w=document.createElement("div");w.className="dm-admin-sidebar-handle",w.setAttribute("aria-label","Resize sidebar"),t.appendChild(w),t.querySelectorAll("details").forEach(a=>{a.addEventListener("toggle",function(){const d=this.getAttribute("data-state-key");d&&S.set(d,this.open);const o=this.querySelector(":scope > .sidebar-children");if(o)if(this.open){const v=o.scrollHeight;o.style.maxHeight="0",requestAnimationFrame(()=>{o.style.maxHeight=v+"px",o.addEventListener("transitionend",function I(){o.style.maxHeight="none",o.removeEventListener("transitionend",I)})})}else{const v=o.scrollHeight;o.style.maxHeight=v+"px",requestAnimationFrame(()=>{o.style.maxHeight="0"})}})});let A=!1,C=0,j=0;w.addEventListener("mousedown",a=>{A=!0,C=a.clientX,j=t.getBoundingClientRect().width,document.body.style.cursor="col-resize",document.body.style.userSelect="none",a.preventDefault()}),document.addEventListener("mousemove",a=>{if(!A)return;const d=Math.max(180,Math.min(480,j+(a.clientX-C)));t.style.width=d+"px"}),document.addEventListener("mouseup",()=>{A&&(A=!1,document.body.style.cursor="",document.body.style.userSelect="",S.set("sidebar.width",parseInt(t.style.width,10)))});function E(){const a=location.hash||"#/";$(t).find(".sidebar-link").removeClass("active"),$(t).find(`.sidebar-link[data-url="${a}"]`).addClass("active")}E(),M.subscribe("router:afterChange",E)}
@@ -43,6 +43,13 @@
43
43
  </select>
44
44
  <small class="text-muted">The collection this action operates on.</small>
45
45
  </div>
46
+ <div>
47
+ <label class="form-label">Project</label>
48
+ <select id="action-project" class="form-input">
49
+ <option value="">— none —</option>
50
+ </select>
51
+ <small class="text-muted">Tag this action to a project for grouping.</small>
52
+ </div>
46
53
  <div>
47
54
  <label class="form-check-label" title="Included in fresh installs via the seed script">
48
55
  <input id="action-bundled" type="checkbox" class="form-check"> Bundled
@@ -25,6 +25,13 @@
25
25
  <label class="form-label">Name <span style="color:var(--dm-danger,#f87171);">*</span></label>
26
26
  <input id="block-name" type="text" class="form-input" placeholder="e.g. feedback-card">
27
27
  <small class="text-muted">Lowercase letters, digits, and hyphens only. Cannot be changed after creation.</small>
28
+ <div class="mt-3">
29
+ <label class="form-label">Project</label>
30
+ <select id="block-project" class="form-input">
31
+ <option value="">— none —</option>
32
+ </select>
33
+ <small class="text-muted">Tag this block to a project for grouping.</small>
34
+ </div>
28
35
  <div class="mt-3">
29
36
  <label class="form-check-label" title="Included in fresh installs via the seed script">
30
37
  <input id="block-bundled" type="checkbox" class="form-check"> Bundled
@@ -53,6 +53,15 @@
53
53
  <input id="collection-columns" type="number" class="form-input" min="1" max="6" value="2">
54
54
  </div>
55
55
  </div>
56
+ <div class="row mt-3">
57
+ <div class="col-6">
58
+ <label class="form-label">Project</label>
59
+ <select id="collection-project" class="form-input">
60
+ <option value="">— none —</option>
61
+ </select>
62
+ <p class="form-hint text-muted" style="margin-top:.3rem;font-size:.8rem;">Tag this collection to a project for grouping.</p>
63
+ </div>
64
+ </div>
56
65
  <div class="row mt-3">
57
66
  <div class="col-auto">
58
67
  <label class="form-check-label" title="Included in fresh installs via the seed script">
@@ -0,0 +1,32 @@
1
+ <div class="dash-card-header">
2
+ <h3>Response Cache</h3>
3
+ <label class="dash-cache-toggle" title="Enable / disable cache">
4
+ <input type="checkbox" data-field="toggle">
5
+ <span data-field="toggle-label">…</span>
6
+ </label>
7
+ </div>
8
+ <div class="dash-cache-grid">
9
+ <div class="dash-cache-stat">
10
+ <div class="dash-cache-stat-label">Hit rate</div>
11
+ <div class="dash-cache-stat-value" data-field="hit-rate">—</div>
12
+ <div class="dash-cache-stat-sub" data-field="hits-misses">no traffic yet</div>
13
+ </div>
14
+ <div class="dash-cache-stat">
15
+ <div class="dash-cache-stat-label">Entries</div>
16
+ <div class="dash-cache-stat-value" data-field="size">—</div>
17
+ <div class="dash-cache-stat-sub" data-field="size-bar"><span data-field="size-fill"></span></div>
18
+ </div>
19
+ <div class="dash-cache-stat">
20
+ <div class="dash-cache-stat-label">Writes</div>
21
+ <div class="dash-cache-stat-value" data-field="writes">0</div>
22
+ <div class="dash-cache-stat-sub" data-field="invalidations">0 invalidations</div>
23
+ </div>
24
+ <div class="dash-cache-stat">
25
+ <div class="dash-cache-stat-label">Last cleared</div>
26
+ <div class="dash-cache-stat-value" data-field="last-cleared">never</div>
27
+ <div class="dash-cache-stat-sub" data-field="driver">driver: —</div>
28
+ </div>
29
+ </div>
30
+ <div class="dash-cache-actions">
31
+ <button class="btn btn-sm btn-ghost" data-field="clear-btn"><span data-icon="trash"></span> Clear cache</button>
32
+ </div>
@@ -25,4 +25,8 @@
25
25
  <div id="dash-health-detail" class="dash-card"></div>
26
26
  <div id="dash-activity-feed" class="dash-card"></div>
27
27
  </section>
28
+
29
+ <section class="dash-row">
30
+ <div id="dash-cache" class="dash-card dash-card-wide"></div>
31
+ </section>
28
32
  </div>
@@ -95,6 +95,15 @@
95
95
  placeholder="Optional form description..."></textarea>
96
96
  </div>
97
97
  </div>
98
+ <div class="row mt-3">
99
+ <div class="col-6">
100
+ <label class="form-label">Project</label>
101
+ <select id="field-project" class="form-input">
102
+ <option value="">— none —</option>
103
+ </select>
104
+ <p class="form-hint text-muted" style="margin-top:.3rem;font-size:.8rem;">Tag this form to a project for grouping.</p>
105
+ </div>
106
+ </div>
98
107
  <div class="row mt-3">
99
108
  <div class="col-auto">
100
109
  <label class="form-check-label" title="Included in fresh installs via the seed script">
@@ -0,0 +1,98 @@
1
+ <div class="page-header">
2
+ <h2><span data-icon="menu"></span> <span id="me-title">Edit menu</span></h2>
3
+ <div class="header-actions">
4
+ <a class="btn btn-secondary" href="#/menus"><span data-icon="arrow-left"></span> Back</a>
5
+ <button class="btn btn-primary" id="me-save"><span data-icon="check"></span> Save</button>
6
+ </div>
7
+ </div>
8
+
9
+ <div class="card">
10
+ <div class="card-body" style="padding-top:0;">
11
+ <div class="tabs" id="me-tabs">
12
+ <div class="tab-list">
13
+ <button class="tab-item active" data-tab="me-items"><span data-icon="list"></span> Items</button>
14
+ <button class="tab-item" data-tab="me-behaviour"><span data-icon="sliders"></span> Behaviour</button>
15
+ <button class="tab-item" data-tab="me-meta"><span data-icon="info"></span> Meta</button>
16
+ </div>
17
+ <div class="tab-content">
18
+ <div class="tab-panel active" id="me-items" data-panel="me-items">
19
+ <div class="toolbar">
20
+ <button class="btn btn-sm btn-secondary" id="me-add-item"><span data-icon="plus"></span> Add item</button>
21
+ </div>
22
+ <div id="me-items-list" class="menu-tree"></div>
23
+ </div>
24
+
25
+ <div class="tab-panel" id="me-behaviour" data-panel="me-behaviour">
26
+ <p class="text-muted mb-3" id="me-behaviour-note">
27
+ Variant + style (font / icon size) apply both to the public <strong>navbar</strong> and the <strong>admin sidebar</strong>. The <strong>position</strong> field only affects the public navbar — the admin sidebar's position is fixed by the admin shell. Footer columns ignore everything here.
28
+ </p>
29
+ <div class="grid grid-cols-2 dm-pe-grid">
30
+ <div class="form-group">
31
+ <label for="me-variant">Variant</label>
32
+ <select id="me-variant" name="variant" class="form-input">
33
+ <option value="">— default —</option>
34
+ <option value="transparent">Transparent</option>
35
+ <option value="dark">Dark</option>
36
+ <option value="light">Light</option>
37
+ <option value="default">Default</option>
38
+ </select>
39
+ </div>
40
+ <div class="form-group">
41
+ <label for="me-position">Position</label>
42
+ <select id="me-position" name="position" class="form-input">
43
+ <option value="">— default —</option>
44
+ <option value="sticky">Sticky</option>
45
+ <option value="fixed">Fixed</option>
46
+ <option value="static">Static</option>
47
+ <option value="floating">Floating</option>
48
+ </select>
49
+ </div>
50
+ <div class="form-group">
51
+ <label for="me-fontFamily">Font family</label>
52
+ <input type="text" id="me-fontFamily" name="fontFamily" class="form-input" placeholder="Inter">
53
+ </div>
54
+ <div class="form-group">
55
+ <label for="me-fontSize">Font size</label>
56
+ <input type="text" id="me-fontSize" name="fontSize" class="form-input" placeholder="14 (px assumed) or 1rem">
57
+ </div>
58
+ <div class="form-group">
59
+ <label for="me-fontWeight">Font weight</label>
60
+ <input type="text" id="me-fontWeight" name="fontWeight" class="form-input" placeholder="500">
61
+ </div>
62
+ <div class="form-group">
63
+ <label for="me-letterSpacing">Letter spacing</label>
64
+ <input type="text" id="me-letterSpacing" name="letterSpacing" class="form-input" placeholder="0.01 (em assumed) or 1px">
65
+ </div>
66
+ <div class="form-group">
67
+ <label for="me-iconSize">Icon size</label>
68
+ <input type="text" id="me-iconSize" name="iconSize" class="form-input" placeholder="15 (px assumed) or 1.25em">
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <div class="tab-panel" id="me-meta" data-panel="me-meta">
74
+ <div class="grid grid-cols-2 dm-pe-grid">
75
+ <div class="form-group">
76
+ <label for="me-slug">Slug <span class="required">*</span></label>
77
+ <input type="text" id="me-slug" name="slug" class="form-input" required>
78
+ <small class="form-hint">Lowercase alphanumeric + hyphen — affects file name</small>
79
+ </div>
80
+ <div class="form-group">
81
+ <label for="me-name">Name <span class="required">*</span></label>
82
+ <input type="text" id="me-name" name="name" class="form-input" required>
83
+ </div>
84
+ <div class="form-group col-span-2">
85
+ <label for="me-description">Description</label>
86
+ <textarea id="me-description" name="description" class="form-input" rows="2"></textarea>
87
+ </div>
88
+ <div class="form-group col-span-2">
89
+ <label for="me-project">Project</label>
90
+ <select id="me-project" name="project" class="form-input"></select>
91
+ <small class="form-hint">Tag this menu to a project so it appears under that project's sidebar entry</small>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </div>
@@ -0,0 +1,14 @@
1
+ <div class="page-header">
2
+ <h2><span data-icon="map"></span> Menu locations</h2>
3
+ <div class="header-actions">
4
+ <a class="btn btn-secondary" href="#/menus"><span data-icon="arrow-left"></span> Back to menus</a>
5
+ <button class="btn btn-primary" id="ml-save"><span data-icon="check"></span> Save</button>
6
+ </div>
7
+ </div>
8
+ <p class="text-muted mb-4">Map each registered slot to a menu. Plugins can register additional slots via <code>hooks.registerMenuLocation()</code>.</p>
9
+
10
+ <div class="card">
11
+ <div class="card-body">
12
+ <div id="ml-table"></div>
13
+ </div>
14
+ </div>
@@ -0,0 +1,14 @@
1
+ <div class="page-header">
2
+ <h2><span data-icon="menu"></span> Menus</h2>
3
+ <div class="header-actions">
4
+ <a class="btn btn-secondary" href="#/menu-locations"><span data-icon="map"></span> Manage locations</a>
5
+ <a class="btn btn-primary" href="#/menus/new"><span data-icon="plus"></span> New menu</a>
6
+ </div>
7
+ </div>
8
+ <p class="text-muted mb-4">Multi-named menus for the public site, footers, and any plugin-registered slot.</p>
9
+
10
+ <div class="card">
11
+ <div class="card-body">
12
+ <div id="menus-table"></div>
13
+ </div>
14
+ </div>
@@ -82,11 +82,11 @@
82
82
  </div>
83
83
  </div>
84
84
  <div class="row mb-3">
85
- <div class="col-6">
85
+ <div class="col-4">
86
86
  <label class="form-label">Category</label>
87
87
  <input id="field-category" type="text" class="form-input" placeholder="e.g. news, docs, products">
88
88
  </div>
89
- <div class="col-6">
89
+ <div class="col-4">
90
90
  <label class="form-label">Visibility</label>
91
91
  <select id="field-visibility" class="form-select">
92
92
  <option value="public">Public — everyone</option>
@@ -96,6 +96,13 @@
96
96
  <option value="admin">Admins only</option>
97
97
  </select>
98
98
  </div>
99
+ <div class="col-4">
100
+ <label class="form-label">Project</label>
101
+ <select id="field-project" class="form-select">
102
+ <option value="">(none)</option>
103
+ </select>
104
+ <small class="form-hint">Pre-filled from URL inheritance; choose explicitly to override.</small>
105
+ </div>
99
106
  </div>
100
107
  <div class="row mb-3">
101
108
  <div class="col-6">
@@ -0,0 +1,50 @@
1
+ <style>
2
+ .dm-qa-grid {
3
+ display: grid;
4
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
5
+ gap: 0.75rem;
6
+ }
7
+ .dm-count-card {
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: space-between;
11
+ gap: 0.5rem;
12
+ }
13
+ .dm-card-link {
14
+ display: block;
15
+ text-decoration: none;
16
+ color: inherit;
17
+ }
18
+ .dm-card-link:hover .dm-count-card {
19
+ background: var(--dm-bg-subtle, rgba(0,0,0,0.04));
20
+ border-radius: 4px;
21
+ }
22
+ </style>
23
+
24
+ <div class="page-header">
25
+ <h2><span id="pd-icon" data-icon="folder"></span> <span id="pd-name">Project</span></h2>
26
+ <div class="header-actions">
27
+ <a class="btn btn-secondary" href="#/projects"><span data-icon="arrow-left"></span> Back to projects</a>
28
+ <a class="btn btn-secondary" id="pd-settings"><span data-icon="settings"></span> Settings</a>
29
+ </div>
30
+ </div>
31
+ <p id="pd-desc" class="text-muted mb-4"></p>
32
+
33
+ <div class="card mb-4">
34
+ <div class="card-header">
35
+ <span data-icon="layers"></span> Artefacts in this project
36
+ </div>
37
+ <div class="card-body">
38
+ <div id="pd-counts"></div>
39
+ </div>
40
+ </div>
41
+
42
+ <div class="card">
43
+ <div class="card-header">
44
+ <span data-icon="plus-circle"></span> Quick add
45
+ </div>
46
+ <div class="card-body">
47
+ <p class="text-muted mb-3" style="margin-top:0;">Create a new artefact and have it tagged to this project automatically.</p>
48
+ <div id="pd-quick-add"></div>
49
+ </div>
50
+ </div>
@@ -0,0 +1,45 @@
1
+ <div class="page-header">
2
+ <h2><span data-icon="folder"></span> <span id="pe-title">Edit project</span></h2>
3
+ <div class="header-actions">
4
+ <a class="btn btn-secondary" href="#/projects"><span data-icon="arrow-left"></span> Back</a>
5
+ <button class="btn btn-primary" id="pe-save"><span data-icon="check"></span> Save</button>
6
+ </div>
7
+ </div>
8
+
9
+ <div class="card">
10
+ <div class="card-header">
11
+ <span data-icon="info"></span> Project details
12
+ </div>
13
+ <div class="card-body">
14
+ <div class="grid grid-cols-2 dm-pe-grid">
15
+ <div class="form-group">
16
+ <label for="pe-slug">Slug <span class="required">*</span></label>
17
+ <input type="text" id="pe-slug" name="slug" class="form-input" required>
18
+ <small class="form-hint">Lowercase alphanumeric + hyphen; immutable after create</small>
19
+ </div>
20
+ <div class="form-group">
21
+ <label for="pe-name">Name <span class="required">*</span></label>
22
+ <input type="text" id="pe-name" name="name" class="form-input" required>
23
+ </div>
24
+ <div class="form-group col-span-2">
25
+ <label for="pe-description">Description</label>
26
+ <textarea id="pe-description" name="description" class="form-input" rows="2"></textarea>
27
+ </div>
28
+ <div class="form-group">
29
+ <label for="pe-icon">Icon</label>
30
+ <div id="pe-icon-mount"></div>
31
+ <small class="form-hint">Click <code>⊞</code> to browse Domma icons; defaults to <code>folder</code></small>
32
+ </div>
33
+ <div class="form-group">
34
+ <label for="pe-rootUrl">Root URL</label>
35
+ <input type="text" id="pe-rootUrl" name="rootUrl" class="form-input" placeholder="/members">
36
+ <small class="form-hint">URL prefix for auto-inheritance (must start with /)</small>
37
+ </div>
38
+ <div class="form-group">
39
+ <label for="pe-sortOrder">Sort order</label>
40
+ <input type="number" id="pe-sortOrder" name="sortOrder" class="form-input" value="0">
41
+ <small class="form-hint">Lower = higher in the sidebar</small>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </div>
@@ -0,0 +1,60 @@
1
+ <div class="page-header">
2
+ <h2><span id="ps-icon" data-icon="settings"></span> <span id="ps-title">Project settings</span></h2>
3
+ <div class="header-actions">
4
+ <a class="btn btn-secondary" id="ps-back"><span data-icon="arrow-left"></span> Back to project</a>
5
+ <button class="btn btn-primary" id="ps-save"><span data-icon="check"></span> Save</button>
6
+ </div>
7
+ </div>
8
+
9
+ <div class="card">
10
+ <div class="card-header">
11
+ <span data-icon="info"></span> Project details
12
+ </div>
13
+ <div class="card-body">
14
+ <div class="grid grid-cols-2 dm-pe-grid">
15
+ <div class="form-group">
16
+ <label for="ps-slug">Slug</label>
17
+ <input type="text" id="ps-slug" name="slug" class="form-input" disabled>
18
+ <small class="form-hint">Immutable after create</small>
19
+ </div>
20
+ <div class="form-group">
21
+ <label for="ps-name">Name <span class="required">*</span></label>
22
+ <input type="text" id="ps-name" name="name" class="form-input" required>
23
+ </div>
24
+ <div class="form-group col-span-2">
25
+ <label for="ps-description">Description</label>
26
+ <textarea id="ps-description" name="description" class="form-input" rows="2"></textarea>
27
+ </div>
28
+ <div class="form-group">
29
+ <label for="ps-icon">Icon</label>
30
+ <div id="ps-icon-mount"></div>
31
+ <small class="form-hint">Click <code>⊞</code> to browse Domma icons</small>
32
+ </div>
33
+ <div class="form-group">
34
+ <label for="ps-rootUrl">Root URL</label>
35
+ <input type="text" id="ps-rootUrl" name="rootUrl" class="form-input" placeholder="/members">
36
+ <small class="form-hint">URL prefix for auto-inheritance (must start with /)</small>
37
+ </div>
38
+ <div class="form-group">
39
+ <label for="ps-sortOrder">Sort order</label>
40
+ <input type="number" id="ps-sortOrder" name="sortOrder" class="form-input" value="0">
41
+ <small class="form-hint">Lower = higher in the sidebar</small>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </div>
46
+
47
+ <div class="card" style="border-color:#c33;">
48
+ <div class="card-header"><h3>Danger zone</h3></div>
49
+ <div class="card-body">
50
+ <p>Untagging removes <code>meta.project</code> from every artefact currently tagged with this project. The artefacts remain (untagged); the project record is unchanged.</p>
51
+ <p>
52
+ <button class="btn btn-warning" id="ps-untag-all"><span data-icon="tag"></span> Untag all artefacts</button>
53
+ </p>
54
+ <hr>
55
+ <p>Deleting a project is only possible when no artefacts are tagged. The server returns an explanatory error otherwise.</p>
56
+ <p>
57
+ <button class="btn btn-danger" id="ps-delete"><span data-icon="trash"></span> Delete project</button>
58
+ </p>
59
+ </div>
60
+ </div>
@@ -0,0 +1,13 @@
1
+ <div class="page-header">
2
+ <h2><span data-icon="folder"></span> Projects</h2>
3
+ <div class="header-actions">
4
+ <a class="btn btn-primary" href="#/projects/new"><span data-icon="plus"></span> New project</a>
5
+ </div>
6
+ </div>
7
+ <p class="text-muted mb-4">Group related artefacts under a project for navigation and per-user access scoping.</p>
8
+
9
+ <div class="card">
10
+ <div class="card-body">
11
+ <div id="projects-table"></div>
12
+ </div>
13
+ </div>
@@ -47,6 +47,17 @@
47
47
  </div>
48
48
  </div>
49
49
  </div>
50
+ <div class="row">
51
+ <div class="col-6">
52
+ <div class="mb-3">
53
+ <label class="form-label">Project</label>
54
+ <select id="role-project" class="form-input">
55
+ <option value="">— none —</option>
56
+ </select>
57
+ <p class="form-hint">Tag this role to a project for grouping.</p>
58
+ </div>
59
+ </div>
60
+ </div>
50
61
  </div>
51
62
  </div>
52
63
  </div>
@@ -13,6 +13,7 @@
13
13
  <button class="tab-item">Cookie Consent</button>
14
14
  <button class="tab-item">Breadcrumbs</button>
15
15
  <button class="tab-item">Custom CSS</button>
16
+ <button class="tab-item">Cache</button>
16
17
  </div>
17
18
  <div class="tab-content">
18
19
 
@@ -502,5 +503,30 @@
502
503
  </div>
503
504
  </div>
504
505
 
506
+ <!-- Cache -->
507
+ <div class="tab-panel">
508
+ <div class="row mb-3">
509
+ <div class="col">
510
+ <p class="text-muted" style="font-size:.875rem;margin-bottom:1rem;">
511
+ Status: <strong id="cache-status">checking…</strong>
512
+ </p>
513
+ <p class="text-muted" style="font-size:.875rem;margin-bottom:1rem;">
514
+ Public page renders are cached by URL and role. Writes (page, collection, form, block, view, navigation, site settings) auto-invalidate the relevant tags. Use the button below to flush every cached entry — the next visitor to each page will trigger a fresh render.
515
+ </p>
516
+ <button id="btn-clear-cache" class="btn btn-secondary" type="button"><span data-icon="trash"></span> Clear cache</button>
517
+ <button id="btn-refresh-cache-keys" class="btn btn-ghost" type="button" style="margin-left:8px;"><span data-icon="refresh"></span> Refresh list</button>
518
+ </div>
519
+ </div>
520
+ <div class="row">
521
+ <div class="col">
522
+ <h4 style="margin-top:1.5rem;">Cached entries <span id="cache-key-count" class="text-muted" style="font-weight:normal;font-size:.875rem;"></span></h4>
523
+ <p class="text-muted" style="font-size:.85rem;margin-bottom:.5rem;">
524
+ Snapshot of what's currently keyed. Newest entries first. Values are not shown — they can be large HTML blobs.
525
+ </p>
526
+ <div id="cache-keys-table"></div>
527
+ </div>
528
+ </div>
529
+ </div>
530
+
505
531
  </div>
506
532
  </div>