domma-cms 0.22.2 → 0.22.4

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.
@@ -0,0 +1,2 @@
1
+ import{api as g}from"../api.js";const C={pages:"Page",collections:"Collection",menus:"Menu",blocks:"Block"};export const INLINE_TYPES=Object.keys(C);function d(n){return String(n||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}function L(n){return String(n??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function l(n,t=""){const e=document.createElement("input");return e.type="text",e.className="form-input",e.placeholder=n,e.value=t,e}function T(n,t,e){const r=document.createElement("div");r.style.cssText="display:flex;flex-direction:column;gap:.35rem;";const a=document.createElement("label");if(a.className="form-label",a.textContent=n,r.appendChild(a),r.appendChild(t),e){const o=document.createElement("small");o.style.cssText="color:var(--text-muted,#888);font-size:.78rem;",o.textContent=e,r.appendChild(o)}return r}function x(n,t){const e=document.createElement("button");return e.type="button",e.className=t,e.textContent=n,e}function v(n,t,e=""){let r=!1;t.addEventListener("input",()=>{r=!0}),n.addEventListener("input",()=>{r||(t.value=e+d(n.value))})}const k={pages(n){const t=l("e.g. Squad List"),e=(n.rootUrl||"").replace(/\/+$/,""),r=l(e?`${e}/squad-list`:"/squad-list");return v(t,r,e+"/"),{rows:[["Title",t,"Shown as the page heading and in the pages list."],["URL path",r,e?`Keeping it under ${e}/ files the page in this project automatically.`:"Where the page lives, e.g. /squad-list."]],async submit(){const a=t.value.trim();if(!a)throw new Error("Title is required.");let o=r.value.trim()||"/"+d(a);return o.startsWith("/")||(o="/"+o),await g.pages.create({urlPath:o,frontmatter:{title:a,project:n.slug},body:""}),{editorHash:"#/pages/edit"+o,label:a}}}},collections(n){const t=l("e.g. Players"),e=l("players");return v(t,e),{rows:[["Title",t,"Display name for the collection."],["Slug",e,"Used in URLs and the API. Lowercase letters, numbers and hyphens."]],note:"Creates an empty collection tagged to this project \u2014 add fields in the editor.",async submit(){const r=t.value.trim();if(!r)throw new Error("Title is required.");const a=d(e.value||r);if(!a)throw new Error("A valid slug is required.");return await g.collections.create({title:r,slug:a,fields:[],meta:{project:n.slug}}),{editorHash:"#/collections/edit/"+encodeURIComponent(a),label:r}}}},menus(n){const t=l("e.g. Tournament Nav"),e=l("tournament-nav");return v(t,e),{rows:[["Name",t,"Display name for the menu."],["Slug",e,'Referenced by [menu slug="\u2026"] and menu locations.']],note:"Creates an empty menu tagged to this project \u2014 add items in the editor.",async submit(){const r=t.value.trim();if(!r)throw new Error("Name is required.");const a=d(e.value||r);if(!a)throw new Error("A valid slug is required.");return await g.menus.create({slug:a,name:r,items:[],meta:{project:n.slug}}),{editorHash:"#/menus/edit/"+encodeURIComponent(a),label:r}}}},blocks(n){const t=l("e.g. score-banner");return{rows:[["Name",t,'Lowercase letters, numbers and hyphens. Embed with [block name="\u2026"].']],note:"Creates a placeholder block tagged to this project \u2014 edit its HTML in the editor.",async submit(){const e=d(t.value);if(!e||!/^[a-z0-9][a-z0-9-]*$/.test(e))throw new Error("Name must be lowercase letters, numbers and hyphens.");return await g.blocks.put(e,{content:`<!-- ${e} -->
2
+ `,meta:{project:n.slug}}),{editorHash:"#/blocks/edit/"+encodeURIComponent(e),label:e}}}}};export function openQuickCreate({type:n,project:t,onCreated:e}){const r=k[n];if(!r)return;const a=r(t),o=C[n],f=E.slideover({title:`New ${o}`,size:"lg",position:"right"}),i=document.createElement("div");i.style.cssText="display:flex;flex-direction:column;gap:1rem;padding:1.25rem;";const h=document.createElement("p");if(h.style.cssText="margin:0;color:var(--text-muted,#888);font-size:.9rem;",h.innerHTML=`Filed under <strong>${L(t.name||t.slug)}</strong>.`,i.appendChild(h),a.rows.forEach(([s,c,w])=>i.appendChild(T(s,c,w))),a.note){const s=document.createElement("p");s.style.cssText="margin:0;color:var(--text-muted,#888);font-size:.82rem;",s.textContent=a.note,i.appendChild(s)}const u=document.createElement("div");u.style.cssText="display:flex;gap:.5rem;flex-wrap:wrap;padding-top:.75rem;border-top:1px solid var(--border-color,rgba(255,255,255,.1));";const m=x("Create","btn btn-primary"),p=x("Create & edit","btn btn-secondary");u.appendChild(m),u.appendChild(p),i.appendChild(u);let b=!1;async function y(s){if(!b){b=!0,m.disabled=p.disabled=!0;try{const{editorHash:c,label:w}=await a.submit();E.toast(`${o} \u201C${w}\u201D created.`,{type:"success"}),f.close(),typeof e=="function"&&await e(),s&&c&&(location.hash=c)}catch(c){E.toast(c?.message||`Failed to create ${o.toLowerCase()}.`,{type:"error"}),b=!1,m.disabled=p.disabled=!1}}}m.addEventListener("click",()=>y(!1)),p.addEventListener("click",()=>y(!0)),i.addEventListener("keydown",s=>{s.key==="Enter"&&s.target.tagName==="INPUT"&&(s.preventDefault(),y(!1))}),f.setContent(i),f.open(),setTimeout(()=>{const s=i.querySelector("input");s&&s.focus()},60)}
@@ -1,50 +1,152 @@
1
1
  <style>
2
- .dm-qa-grid {
2
+ .pd-wrap { display: flex; flex-direction: column; gap: 1.5rem; }
3
+
4
+ /* ── Hero ─────────────────────────────────────────────────────────── */
5
+ .pd-hero {
6
+ display: flex;
7
+ gap: 1.25rem;
8
+ align-items: flex-start;
9
+ padding: 1.5rem;
10
+ border: 1px solid var(--border-color, rgba(255,255,255,.08));
11
+ border-radius: 14px;
12
+ background:
13
+ radial-gradient(120% 160% at 0% 0%, rgba(91,140,255,.12), transparent 55%),
14
+ var(--card-bg, rgba(255,255,255,.04));
15
+ position: relative;
16
+ overflow: hidden;
17
+ }
18
+ .pd-hero::before {
19
+ content: '';
20
+ position: absolute;
21
+ top: 0; bottom: 0; left: 0;
22
+ width: 4px;
23
+ background: linear-gradient(180deg, #5b8cff, #7c6af7);
24
+ }
25
+ .pd-hero-icon {
26
+ width: 64px; height: 64px;
27
+ flex-shrink: 0;
28
+ border-radius: 16px;
29
+ display: flex; align-items: center; justify-content: center;
30
+ background: linear-gradient(135deg, rgba(91,140,255,.28), rgba(124,106,247,.28));
31
+ color: #cfe0ff;
32
+ box-shadow: 0 6px 18px rgba(91,140,255,.18);
33
+ }
34
+ .pd-hero-icon svg, .pd-hero-icon span[data-icon] { width: 32px; height: 32px; }
35
+ .pd-hero-main { flex: 1; min-width: 0; }
36
+ .pd-hero-top {
37
+ display: flex; align-items: flex-start; justify-content: space-between;
38
+ gap: 1rem; flex-wrap: wrap;
39
+ }
40
+ .pd-title { font-size: 1.6rem; font-weight: 700; margin: 0; line-height: 1.15; }
41
+ .pd-actions { display: flex; gap: .5rem; flex-shrink: 0; }
42
+ .pd-desc { color: var(--text-muted, #888); margin: .5rem 0 0; max-width: 64ch; }
43
+ .pd-desc:empty { display: none; }
44
+
45
+ /* ── Meta badges ──────────────────────────────────────────────────── */
46
+ .pd-meta { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1.1rem; }
47
+ .pd-meta-badge {
48
+ display: inline-flex; align-items: center; gap: .45rem;
49
+ padding: .4rem .7rem;
50
+ border-radius: 999px;
51
+ font-size: .8rem; line-height: 1;
52
+ border: 1px solid transparent;
53
+ }
54
+ .pd-meta-badge svg, .pd-meta-badge span[data-icon] { width: 14px; height: 14px; }
55
+ .pd-meta-k { color: var(--text-muted, #999); font-weight: 500; }
56
+ .pd-meta-v { font-weight: 600; }
57
+
58
+ /* ── Section header ───────────────────────────────────────────────── */
59
+ .pd-section-head {
60
+ display: flex; align-items: baseline; gap: .6rem;
61
+ margin: 0 0 .35rem;
62
+ }
63
+ .pd-section-title {
64
+ font-size: 1rem; font-weight: 600; margin: 0;
65
+ display: flex; align-items: center; gap: .5rem;
66
+ }
67
+ .pd-section-title svg, .pd-section-title span[data-icon] {
68
+ width: 17px; height: 17px; color: var(--text-muted, #888);
69
+ }
70
+ .pd-total { font-size: .8rem; color: var(--text-muted, #888); }
71
+ .pd-hint { color: var(--text-muted, #888); margin: 0 0 .9rem; font-size: .82rem; }
72
+
73
+ /* ── Artefact stat tiles (each with a + to create one) ────────────── */
74
+ .pd-tiles {
3
75
  display: grid;
4
- grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
5
- gap: 0.75rem;
76
+ grid-template-columns: repeat(auto-fill, minmax(176px, 1fr));
77
+ gap: .85rem;
6
78
  }
7
- .dm-count-card {
8
- display: flex;
9
- align-items: center;
10
- justify-content: space-between;
11
- gap: 0.5rem;
79
+ .pd-tile {
80
+ position: relative;
81
+ border: 1px solid var(--border-color, rgba(255,255,255,.08));
82
+ border-left: 3px solid var(--c, #5b8cff);
83
+ border-radius: 11px;
84
+ background: var(--card-bg, rgba(255,255,255,.04));
85
+ transition: transform .14s ease, box-shadow .14s ease, border-color .14s ease;
86
+ }
87
+ .pd-tile--link:hover {
88
+ transform: translateY(-2px);
89
+ box-shadow: 0 8px 22px rgba(0,0,0,.22);
90
+ border-color: var(--c, #5b8cff);
91
+ }
92
+ .pd-tile-link {
93
+ display: flex; align-items: center; gap: .85rem;
94
+ padding: .9rem 1rem; padding-right: 2.6rem; /* room for the + */
95
+ text-decoration: none; color: inherit;
12
96
  }
13
- .dm-card-link {
14
- display: block;
15
- text-decoration: none;
16
- color: inherit;
97
+ .pd-tile-link--empty { opacity: .55; }
98
+ .pd-tile-chip {
99
+ width: 40px; height: 40px; flex-shrink: 0;
100
+ border-radius: 10px;
101
+ display: flex; align-items: center; justify-content: center;
17
102
  }
18
- .dm-card-link:hover .dm-count-card {
19
- background: var(--dm-bg-subtle, rgba(0,0,0,0.04));
20
- border-radius: 4px;
103
+ .pd-tile-chip svg, .pd-tile-chip span[data-icon] { width: 20px; height: 20px; }
104
+ .pd-tile-body { display: flex; flex-direction: column; min-width: 0; }
105
+ .pd-tile-count { font-size: 1.5rem; font-weight: 700; line-height: 1; }
106
+ .pd-tile-label {
107
+ font-size: .78rem; color: var(--text-muted, #888);
108
+ margin-top: .2rem; text-transform: uppercase; letter-spacing: .03em;
21
109
  }
110
+ .pd-tile-add {
111
+ position: absolute; top: 8px; right: 8px;
112
+ width: 26px; height: 26px; border-radius: 7px;
113
+ display: flex; align-items: center; justify-content: center;
114
+ border: 1px solid var(--border-color, rgba(255,255,255,.12));
115
+ background: var(--card-bg, rgba(255,255,255,.05));
116
+ color: var(--c, #5b8cff);
117
+ cursor: pointer; opacity: .7;
118
+ transition: opacity .14s ease, background .14s ease, border-color .14s ease, transform .14s ease;
119
+ }
120
+ .pd-tile-add:hover {
121
+ opacity: 1; transform: scale(1.08);
122
+ border-color: var(--c, #5b8cff);
123
+ background: rgba(255,255,255,.09);
124
+ }
125
+ .pd-tile-add svg, .pd-tile-add span[data-icon] { width: 15px; height: 15px; }
22
126
  </style>
23
127
 
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>
128
+ <div class="pd-wrap">
129
+ <div class="pd-hero">
130
+ <div class="pd-hero-icon"><span id="pd-icon" data-icon="folder"></span></div>
131
+ <div class="pd-hero-main">
132
+ <div class="pd-hero-top">
133
+ <h2 class="pd-title"><span id="pd-name">Project</span></h2>
134
+ <div class="pd-actions">
135
+ <a class="btn btn-secondary btn-sm" href="#/projects"><span data-icon="arrow-left"></span> Projects</a>
136
+ <a class="btn btn-secondary btn-sm" id="pd-settings"><span data-icon="settings"></span> Settings</a>
137
+ </div>
138
+ </div>
139
+ <p id="pd-desc" class="pd-desc"></p>
140
+ <div id="pd-meta" class="pd-meta"></div>
141
+ </div>
29
142
  </div>
30
- </div>
31
- <p id="pd-desc" class="text-muted mb-4"></p>
32
143
 
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">
144
+ <section>
145
+ <div class="pd-section-head">
146
+ <h3 class="pd-section-title"><span data-icon="layers"></span> Artefacts in this project</h3>
147
+ <span id="pd-total" class="pd-total"></span>
148
+ </div>
149
+ <p class="pd-hint">Click the <strong>+</strong> on any card to create one, filed under this project automatically.</p>
38
150
  <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>
151
+ </section>
50
152
  </div>
@@ -1,20 +1,20 @@
1
- import{api as I}from"../api.js";import{colourToCss as M}from"/public/js/menu-decor.mjs";import{openIconPicker as U}from"../lib/card-builder.js";let V=1;const z=()=>"i_"+V++,G=760;function q(t,s=null,l=[]){for(const n of t||[]){const e=z();if(n&&n.type==="separator"){l.push({id:e,parentId:s,type:"separator"});continue}l.push({id:e,parentId:s,text:n.text||"",url:n.url||"",icon:n.icon||"",visibility:n.visibility||"",permission:n.permission||"",hidden:!!n.hidden,bundled:!!n.bundled,badge:n.badge?{...n.badge}:null,pill:n.pill?{...n.pill}:null,colour:n.colour||""}),Array.isArray(n.items)&&n.items.length&&q(n.items,e,l)}return l}function K(t){const s=new Map;for(const n of t)s.has(n.parentId)||s.set(n.parentId,[]),s.get(n.parentId).push(n);function l(n){return(s.get(n)||[]).map(e=>{if(e.type==="separator")return{type:"separator"};const c=l(e.id);return{text:(e.text||"").trim(),url:(e.url||"").trim(),...e.icon&&{icon:e.icon.trim()},...e.visibility&&{visibility:e.visibility},...e.permission&&{permission:e.permission},...e.hidden&&{hidden:!0},...e.bundled&&{bundled:!0},...e.badge&&(e.badge.text||e.badge.countFrom)?{badge:{...e.badge.text&&{text:e.badge.text},...e.badge.countFrom&&{countFrom:e.badge.countFrom},...e.badge.variant&&{variant:e.badge.variant}}}:{},...e.pill&&e.pill.variant?{pill:{style:e.pill.style||"filled",variant:e.pill.variant}}:{},...e.colour&&{colour:e.colour},...c.length&&{items:c}}})}return l(null)}function N(t){return String(t||"").replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;")}function X(t,s){let l=0,n=s.find(e=>e.id===t);for(;n&&n.parentId;)l++,n=s.find(e=>e.id===n.parentId);return l}function y(t,s,l=()=>""){t.html("");function n(e){for(const c of s.filter(p=>p.parentId===e)){const p=X(c.id,s);if(c.type==="separator"){t.append(`
2
- <div class="menu-tree-row menu-tree-row--separator" data-id="${c.id}" style="margin-left:${p*28}px">
1
+ import{api as w}from"../api.js";import{colourToCss as U}from"/public/js/menu-decor.mjs";import{openIconPicker as V}from"../lib/card-builder.js";let G=1;const T=()=>"i_"+G++,K=760;function q(e,l=null,o=[]){for(const n of e||[]){const t=T();if(n&&n.type==="separator"){o.push({id:t,parentId:l,type:"separator"});continue}o.push({id:t,parentId:l,text:n.text||"",url:n.url||"",icon:n.icon||"",visibility:n.visibility||"",permission:n.permission||"",hidden:!!n.hidden,bundled:!!n.bundled,badge:n.badge?{...n.badge}:null,pill:n.pill?{...n.pill}:null,colour:n.colour||""}),Array.isArray(n.items)&&n.items.length&&q(n.items,t,o)}return o}function X(e){const l=new Map;for(const n of e)l.has(n.parentId)||l.set(n.parentId,[]),l.get(n.parentId).push(n);function o(n){return(l.get(n)||[]).map(t=>{if(t.type==="separator")return{type:"separator"};const r=o(t.id);return{text:(t.text||"").trim(),url:(t.url||"").trim(),...t.icon&&{icon:t.icon.trim()},...t.visibility&&{visibility:t.visibility},...t.permission&&{permission:t.permission},...t.hidden&&{hidden:!0},...t.bundled&&{bundled:!0},...t.badge&&(t.badge.text||t.badge.countFrom)?{badge:{...t.badge.text&&{text:t.badge.text},...t.badge.countFrom&&{countFrom:t.badge.countFrom},...t.badge.variant&&{variant:t.badge.variant}}}:{},...t.pill&&t.pill.variant?{pill:{style:t.pill.style||"filled",variant:t.pill.variant}}:{},...t.colour&&{colour:t.colour},...r.length&&{items:r}}})}return o(null)}function N(e){return String(e||"").replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;")}function J(e,l){let o=0,n=l.find(t=>t.id===e);for(;n&&n.parentId;)o++,n=l.find(t=>t.id===n.parentId);return o}function k(e,l,o=()=>""){e.html("");function n(t){for(const r of l.filter(u=>u.parentId===t)){const u=J(r.id,l);if(r.type==="separator"){e.append(`
2
+ <div class="menu-tree-row menu-tree-row--separator" data-id="${r.id}" style="margin-left:${u*28}px">
3
3
  <span class="menu-tree-grip" data-icon="grip-vertical"></span>
4
4
  <hr class="me-sep-line">
5
5
  <button class="btn btn-sm btn-danger me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
6
6
  </div>
7
- `);continue}const S=c.badge&&(c.badge.text||c.badge.countFrom)?`<span class="dm-menu-badge">${N(String(c.badge.text||"#"))}</span>`:"",w=M(c.colour),g=w?` style="color:${N(w)}"`:"";t.append(`
8
- <div class="menu-tree-row menu-tree-row--slim${c.hidden?" menu-tree-row--hidden":""}" data-id="${c.id}" style="margin-left:${p*28}px">
7
+ `);continue}const S=r.badge&&(r.badge.text||r.badge.countFrom)?`<span class="dm-menu-badge">${N(String(r.badge.text||"#"))}</span>`:"",y=U(r.colour),_=y?` style="color:${N(y)}"`:"";e.append(`
8
+ <div class="menu-tree-row menu-tree-row--slim${r.hidden?" menu-tree-row--hidden":""}" data-id="${r.id}" style="margin-left:${u*28}px">
9
9
  <span class="menu-tree-grip" data-icon="grip-vertical"></span>
10
- <span class="me-row-label"${g}>${N(c.text||"(untitled)")}${S}</span>
11
- <span class="me-row-url">${N(c.url||"")}</span>
10
+ <span class="me-row-label"${_}>${N(r.text||"(untitled)")}${S}</span>
11
+ <span class="me-row-url">${N(r.url||"")}</span>
12
12
  <button class="btn btn-sm btn-ghost me-outdent" data-tooltip="Outdent"><span data-icon="chevron-left"></span></button>
13
13
  <button class="btn btn-sm btn-ghost me-indent" data-tooltip="Indent"><span data-icon="chevron-right"></span></button>
14
14
  <button class="btn btn-sm btn-ghost me-up" data-tooltip="Move up"><span data-icon="arrow-up"></span></button>
15
15
  <button class="btn btn-sm btn-ghost me-down" data-tooltip="Move down"><span data-icon="arrow-down"></span></button>
16
- <button class="btn btn-sm btn-ghost me-hidden ${c.hidden?"active":""}" data-tooltip="${c.hidden?"Show":"Hide"}"><span data-icon="eye-off"></span></button>
16
+ <button class="btn btn-sm btn-ghost me-hidden ${r.hidden?"active":""}" data-tooltip="${r.hidden?"Show":"Hide"}"><span data-icon="eye-off"></span></button>
17
17
  <button class="btn btn-sm btn-secondary me-edit" data-tooltip="Edit"><span data-icon="edit"></span></button>
18
18
  <button class="btn btn-sm btn-danger me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
19
19
  </div>
20
- `),n(c.id)}}n(null),Domma.icons.scan(t.get(0)),t.get(0).querySelectorAll("[data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})}function F(t,s){t.find(".menu-tree-row").each(function(){const l=$(this).data("id"),n=s.find(g=>g.id===l);if(!n||n.type==="separator")return;const e=$(this).find(".me-text");e.length&&(n.text=e.val());const c=$(this).find(".me-url");c.length&&(n.url=c.val());const p=$(this).find(".me-icon");p.length&&(n.icon=p.val());const S=$(this).find(".me-vis");S.length&&(n.visibility=S.val());const w=$(this).find(".me-perm");w.length&&(n.permission=w.val())})}function et(t,s){const l={};for(const n of s){const e=t.querySelector(`[name="${n}"]`);e&&(l[n]=e.type==="checkbox"?e.checked:e.value)}return l}function b(t,s={},...l){const n=document.createElement(t);Object.assign(n,s);for(const e of l)e!=null&&n.append(e);return n}function x(t,s){return b("label",{className:"form-field"},b("span",{className:"form-label",textContent:t}),s)}function O(t,s){const l=b("select",{className:"form-select"});return t.forEach(([n,e])=>l.add(new Option(e,n))),l.value=s??"",l}function R(t,s){const l=typeof s=="string"&&s.startsWith("#"),n=O([["","\u2014 none \u2014"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],l?"__custom__":s||""),e=b("input",{type:"color",className:"form-input me-colour-hex",value:l?s:"#000000"});e.style.display=l?"":"none",n.addEventListener("change",()=>{e.style.display=n.value==="__custom__"?"":"none"});const c=x(t,n);return c.append(e),c._read=()=>n.value==="__custom__"?e.value:n.value,c}function J(t,s){const l=b("input",{className:"form-input me-icon",name:"icon",value:s||""}),n=b("span",{className:"me-icon-preview"});s&&n.setAttribute("data-icon",s);const e=b("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),c=p=>{p?n.setAttribute("data-icon",p):n.removeAttribute("data-icon"),Domma.icons.scan(n.parentNode||n)};return e.addEventListener("click",()=>U(p=>{l.value=p,c(p)})),l.addEventListener("input",()=>c(l.value.trim())),x(t,b("div",{className:"me-icon-field"},l,e,n))}function T(...t){return b("div",{className:"me-field-grid"},...t)}async function Q(t,s,l,n){const e=v=>b("h4",{className:"me-form-section",textContent:v}),c=x("Label",b("input",{className:"form-input",name:"text",value:t.text||""})),p=x("URL",b("input",{className:"form-input",name:"url",value:t.url||""})),S=J("Icon",t.icon||""),w=x("Visibility",b("input",{className:"form-input",name:"visibility",value:t.visibility||""})),g=O([["","\u2014 no permission gate \u2014"],...(s||[]).map(v=>[v.key,v.label||v.key])],t.permission||"");g.name="permission";const i=x("Permission",g),m=x("Badge text",b("input",{className:"form-input",name:"badgeText",value:t.badge?.text||""})),P=O([["","\u2014 none \u2014"],...(l||[]).map(v=>[v.slug,v.title||v.slug])],t.badge?.countFrom||"");P.name="badgeCount";const A=x("Badge count from",P),j=R("Badge colour",t.badge?.variant||""),a=O([["link","Link"],["pill","Pill"]],t.pill?"pill":"link");a.name="renderAs";const o=x("Render as",a),d=x("Pill style",O([["filled","Filled"],["outline","Outline"]],t.pill?.style||"filled"));d.querySelector("select").name="pillStyle";const f=R("Pill colour",t.pill?.variant||""),r=b("div",{className:"me-pill-wrap"},T(d,f));r.style.display=t.pill?"":"none",a.addEventListener("change",()=>{r.style.display=a.value==="pill"?"":"none"});const u=R("Text colour",t.colour||""),h=b("button",{className:"btn btn-primary",textContent:"Apply"}),C=b("div",{className:"me-item-form"},e("Link"),c,p,S,e("Access"),T(w,i),e("Badge"),T(m,j),A,e("Appearance"),o,r,u,h),_=E.slideover({title:"Edit menu item",size:"lg",position:"right"});_.element.appendChild(C),_.open();const B=_.element.closest(".dm-slideover")||_.element.querySelector(".dm-slideover")||_.element;B.style.setProperty("width",G+"px","important"),B.style.setProperty("max-width","95vw","important"),Domma.icons.scan(C);const k=v=>C.querySelector(`[name="${v}"]`)?.value||"";h.addEventListener("click",()=>{t.text=k("text"),t.url=k("url"),t.icon=k("icon"),t.visibility=k("visibility"),t.permission=k("permission");const v=k("badgeText"),L=k("badgeCount"),W=j._read();if(t.badge=v||L?{...v&&{text:v},...L&&{countFrom:L},...W&&{variant:W}}:null,k("renderAs")==="pill"){const D=f._read();t.pill={style:k("pillStyle")||"filled",...D&&{variant:D}}}else t.pill=null;t.colour=u._read(),_.close(),n()})}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(t){const s=location.hash.match(/^#\/menus\/edit\/(.+)$/),l=s?decodeURIComponent(s[1]):null,n=!l;let e;if(n)e={slug:"",name:"",description:"",items:[]};else try{e=await I.menus.get(l)}catch(a){E.toast(`Failed to load menu: ${a.message||a}`,{type:"error"}),location.hash="#/menus";return}const c=await I.projects.list().catch(()=>[]),p=await H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),S=await I.collections.list().catch(()=>[]),w='<option value="">\u2014 no permission gate \u2014</option>',g=a=>w+(p.resources||[]).map(o=>{const d=(a||"")===o.key?" selected":"";return`<option value="${o.key}"${d}>${o.label||o.key}</option>`}).join("");t.find("#me-title").text(n?"New menu":`Edit "${e.slug}"`);let i=q(e.items);const m=t.find("#me-items-list");y(m,i,g),t.find("#me-add-item").on("click",()=>{F(m,i),i.push({id:z(),parentId:null,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}),y(m,i,g)}),t.find("#me-add-separator").on("click",()=>{F(m,i),i.push({id:z(),parentId:null,type:"separator"}),y(m,i,g)}),t.off("click",".me-remove").on("click",".me-remove",function(){const a=$(this).closest(".menu-tree-row").data("id");i=i.filter(o=>o.id!==a&&o.parentId!==a),y(m,i,g)}),t.off("click",".me-indent").on("click",".me-indent",function(){F(m,i);const a=$(this).closest(".menu-tree-row").data("id"),o=i.find(r=>r.id===a);if(!o)return;const d=i.filter(r=>r.parentId===o.parentId),f=d.findIndex(r=>r.id===a);f<=0||(o.parentId=d[f-1].id,y(m,i,g))}),t.off("click",".me-outdent").on("click",".me-outdent",function(){F(m,i);const a=$(this).closest(".menu-tree-row").data("id"),o=i.find(f=>f.id===a);if(!o||o.parentId==null)return;const d=i.find(f=>f.id===o.parentId);o.parentId=d?d.parentId:null,y(m,i,g)}),t.off("click",".me-up").on("click",".me-up",function(){F(m,i);const a=$(this).closest(".menu-tree-row").data("id"),o=i.filter(u=>u.parentId===i.find(h=>h.id===a)?.parentId),d=o.findIndex(u=>u.id===a);if(d<=0)return;const f=i.indexOf(o[d]),r=i.indexOf(o[d-1]);[i[f],i[r]]=[i[r],i[f]],y(m,i,g)}),t.off("click",".me-down").on("click",".me-down",function(){F(m,i);const a=$(this).closest(".menu-tree-row").data("id"),o=i.filter(u=>u.parentId===i.find(h=>h.id===a)?.parentId),d=o.findIndex(u=>u.id===a);if(d===-1||d>=o.length-1)return;const f=i.indexOf(o[d]),r=i.indexOf(o[d+1]);[i[f],i[r]]=[i[r],i[f]],y(m,i,g)}),t.off("click",".me-hidden").on("click",".me-hidden",function(){F(m,i);const a=$(this).closest(".menu-tree-row").data("id"),o=i.find(d=>d.id===a);o&&(o.hidden=!o.hidden),y(m,i,g)}),t.off("click",".me-edit").on("click",".me-edit",function(){const a=$(this).closest(".menu-tree-row").data("id"),o=i.find(d=>d.id===a);!o||o.type==="separator"||Q(o,p.resources||[],S,()=>y(m,i,g))}),E.tabs(t.find("#me-tabs").get(0)),t.find("#me-variant").val(e.variant||""),t.find("#me-position").val(e.position||""),t.find("#me-fontFamily").val(e.style?.fontFamily||""),t.find("#me-fontSize").val(e.style?.fontSize||""),t.find("#me-fontWeight").val(e.style?.fontWeight||""),t.find("#me-letterSpacing").val(e.style?.letterSpacing||""),t.find("#me-iconSize").val(e.style?.iconSize||"");const{getProjectFromHash:P}=await import("../lib/project-context.js"),A=n?P():null,j=t.find("#me-project");j.html('<option value="">\u2014 none \u2014</option>'+c.map(a=>`<option value="${N(a.slug)}">${N(a.name||a.slug)}</option>`).join("")),t.find("#me-slug").val(e.slug||""),t.find("#me-name").val(e.name||""),t.find("#me-description").val(e.description||""),j.val(e.meta?.project||A||""),Domma.icons.scan(t.get(0)),t.find("#me-save").on("click",async()=>{F(m,i);const a={variant:t.find("#me-variant").val(),position:t.find("#me-position").val(),fontFamily:t.find("#me-fontFamily").val(),fontSize:t.find("#me-fontSize").val(),fontWeight:t.find("#me-fontWeight").val(),letterSpacing:t.find("#me-letterSpacing").val(),iconSize:t.find("#me-iconSize").val()},o={slug:t.find("#me-slug").val().trim(),name:t.find("#me-name").val().trim(),description:t.find("#me-description").val().trim(),project:t.find("#me-project").val()},d=["fontFamily","fontSize","fontWeight","letterSpacing","iconSize"],f=Object.fromEntries(Object.entries(a).filter(([u,h])=>d.includes(u)&&h)),r={slug:o.slug,name:o.name,description:o.description,...a.variant&&{variant:a.variant},...a.position&&{position:a.position},...Object.keys(f).length&&{style:f},items:K(i),meta:{...e.meta||{},project:o.project||null}};try{if(n)await I.menus.create(r);else if(r.slug!==e.slug){await I.menus.create(r);const u=await I.menuLocations.get();let h=!1;for(const[C,_]of Object.entries(u))_===e.slug&&(u[C]=r.slug,h=!0);h&&await I.menuLocations.save(u),await I.menus.remove(e.slug)}else await I.menus.update(e.slug,r);E.toast("Saved.",{type:"success"}),location.hash="#/menus/edit/"+encodeURIComponent(r.slug)}catch(u){E.toast(`Save failed: ${u.message||u}`,{type:"error"})}})}};
20
+ `),n(r.id)}}n(null),Domma.icons.scan(e.get(0)),e.get(0).querySelectorAll("[data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})})}function F(e,l){e.find(".menu-tree-row").each(function(){const o=$(this).data("id"),n=l.find(_=>_.id===o);if(!n||n.type==="separator")return;const t=$(this).find(".me-text");t.length&&(n.text=t.val());const r=$(this).find(".me-url");r.length&&(n.url=r.val());const u=$(this).find(".me-icon");u.length&&(n.icon=u.val());const S=$(this).find(".me-vis");S.length&&(n.visibility=S.val());const y=$(this).find(".me-perm");y.length&&(n.permission=y.val())})}function ie(e,l){const o={};for(const n of l){const t=e.querySelector(`[name="${n}"]`);t&&(o[n]=t.type==="checkbox"?t.checked:t.value)}return o}function b(e,l={},...o){const n=document.createElement(e);Object.assign(n,l);for(const t of o)t!=null&&n.append(t);return n}function I(e,l,o){const n=b("label",{className:"form-field"},b("span",{className:"form-label",textContent:e}),l);return o&&n.append(b("small",{className:"form-hint",textContent:o,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),n}function Q(e,l){const o=[["","\u2014 Everyone \u2014"],["public","Public \u2014 anyone, incl. logged-out"],...(l||[]).map(t=>[t.name,`${t.label||t.name} and above`]),["private","Private \u2014 super-admin only"]];e&&!o.some(([t])=>t===e)&&o.push([e,`${e} (custom)`]);const n=j(o,e||"");return n.name="visibility",n}function j(e,l){const o=b("select",{className:"form-select"});return e.forEach(([n,t])=>o.add(new Option(t,n))),o.value=l??"",o}function B(e,l){const o=typeof l=="string"&&l.startsWith("#"),n=j([["","\u2014 none \u2014"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],o?"__custom__":l||""),t=b("input",{type:"color",className:"form-input me-colour-hex",value:o?l:"#000000"});t.style.display=o?"":"none",n.addEventListener("change",()=>{t.style.display=n.value==="__custom__"?"":"none"});const r=I(e,n);return r.append(t),r._read=()=>n.value==="__custom__"?t.value:n.value,r}function Y(e,l){const o=b("input",{className:"form-input me-icon",name:"icon",value:l||""}),n=b("span",{className:"me-icon-preview"});l&&n.setAttribute("data-icon",l);const t=b("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),r=u=>{u?n.setAttribute("data-icon",u):n.removeAttribute("data-icon"),Domma.icons.scan(n.parentNode||n)};return t.addEventListener("click",()=>V(u=>{o.value=u,r(u)})),o.addEventListener("input",()=>r(o.value.trim())),I(e,b("div",{className:"me-icon-field"},o,t,n))}function W(...e){return b("div",{className:"me-field-grid"},...e)}async function Z(e,l,o,n,t){const r=v=>b("h4",{className:"me-form-section",textContent:v}),u=I("Label",b("input",{className:"form-input",name:"text",value:e.text||""}),"The text shown for this item."),S=I("URL",b("input",{className:"form-input",name:"url",value:e.url||""}),"Where it links. Must start with /, #, https://, or mailto: . Leave blank for a parent that only groups sub-items."),y=Y("Icon",e.icon||""),_=I("Visibility",Q(e.visibility||"",n),"Who can see this item, by role seniority. Picking a role shows it to that role and anyone more senior. Blank = everyone."),C=j([["","\u2014 no permission gate \u2014"],...(l||[]).map(v=>[v.key,v.label||v.key])],e.permission||"");C.name="permission";const z=I("Permission",C,"Hide unless the viewer holds this specific permission. Independent of Visibility \u2014 both must pass."),h=I("Badge text",b("input",{className:"form-input",name:"badgeText",value:e.badge?.text||""})),i=j([["","\u2014 none \u2014"],...(o||[]).map(v=>[v.slug,v.title||v.slug])],e.badge?.countFrom||"");i.name="badgeCount";const m=I("Badge count from",i,"Show a live count of this collection\u2019s entries as the badge."),A=B("Badge colour",e.badge?.variant||""),P=j([["link","Link"],["pill","Pill"]],e.pill?"pill":"link");P.name="renderAs";const O=I("Render as",P,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),s=I("Pill style",j([["filled","Filled"],["outline","Outline"]],e.pill?.style||"filled"));s.querySelector("select").name="pillStyle";const a=B("Pill colour",e.pill?.variant||""),c=b("div",{className:"me-pill-wrap"},W(s,a));c.style.display=e.pill?"":"none",P.addEventListener("change",()=>{c.style.display=P.value==="pill"?"":"none"});const f=B("Text colour",e.colour||""),d=b("button",{className:"btn btn-primary",textContent:"Apply"}),p=b("div",{className:"me-item-form"},r("Link"),u,S,y,r("Access"),W(_,z),r("Badge"),W(h,A),m,r("Appearance"),O,c,f,d),g=E.slideover({title:"Edit menu item",size:"lg",position:"right"});g.element.appendChild(p),g.open();const L=g.element.closest(".dm-slideover")||g.element.querySelector(".dm-slideover")||g.element;L.style.setProperty("width",K+"px","important"),L.style.setProperty("max-width","95vw","important"),Domma.icons.scan(p);const x=v=>p.querySelector(`[name="${v}"]`)?.value||"";d.addEventListener("click",()=>{e.text=x("text"),e.url=x("url"),e.icon=x("icon"),e.visibility=x("visibility"),e.permission=x("permission");const v=x("badgeText"),R=x("badgeCount"),D=A._read();if(e.badge=v||R?{...v&&{text:v},...R&&{countFrom:R},...D&&{variant:D}}:null,x("renderAs")==="pill"){const M=a._read();e.pill={style:x("pillStyle")||"filled",...M&&{variant:M}}}else e.pill=null;e.colour=f._read(),g.close(),t()})}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(e){const l=location.hash.match(/^#\/menus\/edit\/(.+)$/),o=l?decodeURIComponent(l[1]):null,n=!o;let t;if(n)t={slug:"",name:"",description:"",items:[]};else try{t=await w.menus.get(o)}catch(s){E.toast(`Failed to load menu: ${s.message||s}`,{type:"error"}),location.hash="#/menus";return}const r=await w.projects.list().catch(()=>[]),u=await H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),S=await w.collections.list().catch(()=>[]),y=await w.get("/collections/roles/entries?limit=100").catch(()=>null),C=(Array.isArray(y)?y:y?.entries||[]).map(s=>({name:s.data?.name,label:s.data?.label,level:s.data?.level??99})).filter(s=>s.name).sort((s,a)=>a.level-s.level),z='<option value="">\u2014 no permission gate \u2014</option>',h=s=>z+(u.resources||[]).map(a=>{const c=(s||"")===a.key?" selected":"";return`<option value="${a.key}"${c}>${a.label||a.key}</option>`}).join("");e.find("#me-title").text(n?"New menu":`Edit "${t.slug}"`);let i=q(t.items);const m=e.find("#me-items-list");k(m,i,h),e.find("#me-add-item").on("click",()=>{F(m,i),i.push({id:T(),parentId:null,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}),k(m,i,h)}),e.find("#me-add-separator").on("click",()=>{F(m,i),i.push({id:T(),parentId:null,type:"separator"}),k(m,i,h)}),e.off("click",".me-remove").on("click",".me-remove",function(){const s=$(this).closest(".menu-tree-row").data("id");i=i.filter(a=>a.id!==s&&a.parentId!==s),k(m,i,h)}),e.off("click",".me-indent").on("click",".me-indent",function(){F(m,i);const s=$(this).closest(".menu-tree-row").data("id"),a=i.find(d=>d.id===s);if(!a)return;const c=i.filter(d=>d.parentId===a.parentId),f=c.findIndex(d=>d.id===s);f<=0||(a.parentId=c[f-1].id,k(m,i,h))}),e.off("click",".me-outdent").on("click",".me-outdent",function(){F(m,i);const s=$(this).closest(".menu-tree-row").data("id"),a=i.find(f=>f.id===s);if(!a||a.parentId==null)return;const c=i.find(f=>f.id===a.parentId);a.parentId=c?c.parentId:null,k(m,i,h)}),e.off("click",".me-up").on("click",".me-up",function(){F(m,i);const s=$(this).closest(".menu-tree-row").data("id"),a=i.filter(p=>p.parentId===i.find(g=>g.id===s)?.parentId),c=a.findIndex(p=>p.id===s);if(c<=0)return;const f=i.indexOf(a[c]),d=i.indexOf(a[c-1]);[i[f],i[d]]=[i[d],i[f]],k(m,i,h)}),e.off("click",".me-down").on("click",".me-down",function(){F(m,i);const s=$(this).closest(".menu-tree-row").data("id"),a=i.filter(p=>p.parentId===i.find(g=>g.id===s)?.parentId),c=a.findIndex(p=>p.id===s);if(c===-1||c>=a.length-1)return;const f=i.indexOf(a[c]),d=i.indexOf(a[c+1]);[i[f],i[d]]=[i[d],i[f]],k(m,i,h)}),e.off("click",".me-hidden").on("click",".me-hidden",function(){F(m,i);const s=$(this).closest(".menu-tree-row").data("id"),a=i.find(c=>c.id===s);a&&(a.hidden=!a.hidden),k(m,i,h)}),e.off("click",".me-edit").on("click",".me-edit",function(){const s=$(this).closest(".menu-tree-row").data("id"),a=i.find(c=>c.id===s);!a||a.type==="separator"||Z(a,u.resources||[],S,C,()=>k(m,i,h))}),E.tabs(e.find("#me-tabs").get(0)),e.find("#me-variant").val(t.variant||""),e.find("#me-position").val(t.position||""),e.find("#me-fontFamily").val(t.style?.fontFamily||""),e.find("#me-fontSize").val(t.style?.fontSize||""),e.find("#me-fontWeight").val(t.style?.fontWeight||""),e.find("#me-letterSpacing").val(t.style?.letterSpacing||""),e.find("#me-iconSize").val(t.style?.iconSize||"");const{getProjectFromHash:A}=await import("../lib/project-context.js"),P=n?A():null,O=e.find("#me-project");O.html('<option value="">\u2014 none \u2014</option>'+r.map(s=>`<option value="${N(s.slug)}">${N(s.name||s.slug)}</option>`).join("")),e.find("#me-slug").val(t.slug||""),e.find("#me-name").val(t.name||""),e.find("#me-description").val(t.description||""),O.val(t.meta?.project||P||""),Domma.icons.scan(e.get(0)),e.find("#me-save").on("click",async()=>{F(m,i);const s={variant:e.find("#me-variant").val(),position:e.find("#me-position").val(),fontFamily:e.find("#me-fontFamily").val(),fontSize:e.find("#me-fontSize").val(),fontWeight:e.find("#me-fontWeight").val(),letterSpacing:e.find("#me-letterSpacing").val(),iconSize:e.find("#me-iconSize").val()},a={slug:e.find("#me-slug").val().trim(),name:e.find("#me-name").val().trim(),description:e.find("#me-description").val().trim(),project:e.find("#me-project").val()},c=["fontFamily","fontSize","fontWeight","letterSpacing","iconSize"],f=Object.fromEntries(Object.entries(s).filter(([p,g])=>c.includes(p)&&g)),d={slug:a.slug,name:a.name,description:a.description,...s.variant&&{variant:s.variant},...s.position&&{position:s.position},...Object.keys(f).length&&{style:f},items:X(i),meta:{...t.meta||{},project:a.project||null}};try{if(n)await w.menus.create(d);else if(d.slug!==t.slug){await w.menus.create(d);const p=await w.menuLocations.get();let g=!1;for(const[L,x]of Object.entries(p))x===t.slug&&(p[L]=d.slug,g=!0);g&&await w.menuLocations.save(p),await w.menus.remove(t.slug)}else await w.menus.update(t.slug,d);E.toast("Saved.",{type:"success"}),location.hash="#/menus/edit/"+encodeURIComponent(d.slug)}catch(p){E.toast(`Save failed: ${p.message||p}`,{type:"error"})}})}};
@@ -1,4 +1 @@
1
- import{api as d}from"../api.js";function o(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const j=[{key:"pages",text:"Page",icon:"file-text",path:"/pages/new"},{key:"collections",text:"Collection",icon:"database",path:"/collections/new"},{key:"forms",text:"Form",icon:"layout",path:"/forms/new"},{key:"actions",text:"Action",icon:"zap",path:"/actions/new"},{key:"menus",text:"Menu",icon:"menu",path:"/menus/new"},{key:"blocks",text:"Block",icon:"box",path:"/blocks/new"},{key:"views",text:"View",icon:"eye",path:"/views/new"},{key:"roles",text:"Role",icon:"shield",path:"/roles"},{key:"users",text:"User",icon:"users",path:"/users/new"}],h={pages:"Pages",collections:"Collections",forms:"Forms",actions:"Actions",menus:"Menus",blocks:"Blocks",views:"Views",roles:"Roles",users:"Users"};export const projectDetailView={templateUrl:"/admin/js/templates/project-detail.html",async onMount(t){const i=location.hash.split("?")[0].match(/^#\/projects\/([^/]+)$/),s=i?decodeURIComponent(i[1]):null;if(!s){location.hash="#/projects";return}const a=await d.projects.get(s).catch(()=>null);if(!a){E.toast("Project not found.",{type:"error"}),location.hash="#/projects";return}const r=encodeURIComponent(s);t.find("#pd-name").text(a.name||s),t.find("#pd-icon").attr("data-icon",a.icon||"folder"),t.find("#pd-desc").text(a.description||""),t.find("#pd-settings").attr("href","#/projects/"+r+"/settings"),Domma.icons.scan(t.find(".page-header").get(0));const l=await d.projects.artefacts(s).catch(()=>({})),m=t.find("#pd-counts"),u=Object.keys(h).map(e=>{const n=Array.isArray(l[e])?l[e].length:0,c=n>0?`#/projects/${r}/${o(e)}`:null,g=c?`<a href="${c}" class="dm-card-link">`:"",f=c?"</a>":"";return`<div class="card">${g}<div class="card-body dm-count-card">
2
- <strong>${o(h[e])}</strong>
3
- <span class="badge badge-${n>0?"info":"secondary"}">${n}</span>
4
- </div>${f}</div>`});m.html(`<div class="grid grid-cols-3" style="gap:1rem;">${u.join("")}</div>`);const p=t.find("#pd-quick-add");p.html(`<div class="dm-qa-grid">${j.map(e=>`<button class="btn btn-primary pd-qa-btn" data-path="${o(e.path)}" data-project="${o(s)}"><span data-icon="${o(e.icon)}"></span> ${o(e.text)}</button>`).join("")}</div>`),Domma.icons.scan(t.get(0)),p.off("click",".pd-qa-btn").on("click",".pd-qa-btn",function(){const e=$(this).data("path"),n=$(this).data("project");try{sessionStorage.setItem("__projectContext",n)}catch{}location.hash="#"+e})}};
1
+ import{api as b}from"../api.js";import{openQuickCreate as N,INLINE_TYPES as U}from"../lib/project-quick-create.js";function o(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const Y=[{key:"pages",label:"Pages",singular:"Page",icon:"file-text",color:"#5b8cff",path:"/pages/new"},{key:"collections",label:"Collections",singular:"Collection",icon:"database",color:"#7c6af7",path:"/collections/new"},{key:"forms",label:"Forms",singular:"Form",icon:"layout",color:"#34d399",path:"/forms/new"},{key:"actions",label:"Actions",singular:"Action",icon:"zap",color:"#fbbf24",path:"/actions/new"},{key:"menus",label:"Menus",singular:"Menu",icon:"menu",color:"#22d3ee",path:"/menus/new"},{key:"blocks",label:"Blocks",singular:"Block",icon:"box",color:"#f472b6",path:"/blocks/new"},{key:"views",label:"Views",singular:"View",icon:"eye",color:"#2dd4bf",path:"/views/new"},{key:"roles",label:"Roles",singular:"Role",icon:"shield",color:"#f87171",path:"/roles"},{key:"users",label:"Users",singular:"User",icon:"users",color:"#818cf8",path:"/users/new"}];function x(t){if(!t)return null;const l=new Date(t).getTime();if(Number.isNaN(l))return null;const s=Math.max(0,Math.round((Date.now()-l)/1e3)),n=Math.round(s/60),e=Math.round(n/60),c=Math.round(e/24);if(s<45)return"just now";if(n<60)return`${n} min${n===1?"":"s"} ago`;if(e<24)return`${e} hour${e===1?"":"s"} ago`;if(c<30)return`${c} day${c===1?"":"s"} ago`;const r=Math.round(c/30);if(r<12)return`${r} month${r===1?"":"s"} ago`;const i=Math.round(c/365);return`${i} year${i===1?"":"s"} ago`}function k(t,l){if(!t)return null;const s=new Date(t);if(Number.isNaN(s.getTime()))return null;try{return D(t).format(l)}catch{return s.toLocaleDateString()}}function m({icon:t,color:l,key:s,value:n,title:e}){if(n==null||n==="")return"";const c=`color:${l};background:${l}1f;border-color:${l}40`,r=e?` title="${o(e)}"`:"";return`<span class="pd-meta-badge" style="${c}"${r}><span data-icon="${o(t)}"></span><span class="pd-meta-k">${o(s)}</span><span class="pd-meta-v">${o(n)}</span></span>`}export const projectDetailView={templateUrl:"/admin/js/templates/project-detail.html",async onMount(t){const s=location.hash.split("?")[0].match(/^#\/projects\/([^/]+)$/),n=s?decodeURIComponent(s[1]):null;if(!n){location.hash="#/projects";return}const e=await b.projects.get(n).catch(()=>null);if(!e){E.toast("Project not found.",{type:"error"}),location.hash="#/projects";return}const c=encodeURIComponent(n);t.find("#pd-name").text(e.name||n),t.find("#pd-icon").attr("data-icon",e.icon||"folder"),t.find("#pd-desc").text(e.description||""),t.find("#pd-settings").attr("href","#/projects/"+c+"/settings");const r=[m({icon:"calendar",color:"#5b8cff",key:"Created",value:k(e.createdAt,"D MMM YYYY")}),m({icon:"user",color:"#7c6af7",key:"Creator",value:e.createdBy||"Unknown"}),m({icon:"clock",color:"#34d399",key:"Updated",value:x(e.updatedAt),title:k(e.updatedAt,"D MMM YYYY, HH:mm")})].join("");t.find("#pd-meta").html(r,{safe:!1}),Domma.icons.scan(t.find(".pd-hero").get(0));const i=t.find("#pd-counts"),w=t.find("#pd-total"),j={slug:n,name:e.name||n,rootUrl:e.rootUrl||""};async function h(){const p=await b.projects.artefacts(n).catch(()=>({}));let d=0;const f=Y.map(a=>{const u=Array.isArray(p[a.key])?p[a.key].length:0;d+=u;const M=U.includes(a.key),g=`<span class="pd-tile-chip" style="color:${a.color};background:${a.color}1f"><span data-icon="${o(a.icon)}"></span></span>`,y=`<span class="pd-tile-body"><span class="pd-tile-count">${u}</span><span class="pd-tile-label">${o(a.label)}</span></span>`,C=u>0?`<a class="pd-tile-link" href="#/projects/${c}/${o(a.key)}">${g}${y}</a>`:`<div class="pd-tile-link pd-tile-link--empty">${g}${y}</div>`,v=`<button class="pd-tile-add" data-type="${o(a.key)}" data-inline="${M?"1":""}" data-path="${o(a.path)}" title="New ${o(a.singular)}" aria-label="New ${o(a.singular)}"><span data-icon="plus"></span></button>`;return`<div class="pd-tile${u>0?" pd-tile--link":""}" style="--c:${a.color}">${C}${v}</div>`});i.html(`<div class="pd-tiles">${f.join("")}</div>`,{safe:!1}),w.text(d===1?"1 artefact":`${d} artefacts`),Domma.icons.scan(i.get(0))}await h(),i.off("click",".pd-tile-add").on("click",".pd-tile-add",function(){const p=$(this).data("type");if($(this).data("inline")){N({type:p,project:j,onCreated:h});return}const d=$(this).data("path"),f=String(d).includes("?")?"&":"?";location.hash="#"+d+f+"project="+encodeURIComponent(n)}),Domma.icons.scan(t.get(0))}};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.22.2",
3
+ "version": "0.22.4",
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",
@@ -115,12 +115,12 @@ export async function blocksRoutes(fastify) {
115
115
  // Create or update block
116
116
  fastify.put('/blocks/:name', canUpdate, async (request, reply) => {
117
117
  const {name} = request.params;
118
- const {content, bundled, css} = request.body || {};
118
+ const {content, bundled, css, meta} = request.body || {};
119
119
  if (typeof content !== 'string') return reply.status(400).send({error: 'content (string) is required'});
120
120
  if (css !== undefined && typeof css !== 'string') return reply.status(400).send({error: 'css must be a string'});
121
121
 
122
122
  try {
123
- return await saveBlock(name, content, {bundled: !!bundled, css});
123
+ return await saveBlock(name, content, {bundled: !!bundled, css, meta});
124
124
  } catch (err) {
125
125
  if (err.code === 'INVALID_NAME') return reply.status(400).send({error: err.message});
126
126
  if (err.code === 'CSS_TOO_LARGE') return reply.status(400).send({error: err.message});
@@ -174,10 +174,10 @@ export async function collectionsRoutes(fastify) {
174
174
  });
175
175
 
176
176
  fastify.post('/collections', canCreate, async (request, reply) => {
177
- const {title, slug, description, fields, api, storage} = request.body || {};
177
+ const {title, slug, description, fields, api, storage, meta} = request.body || {};
178
178
  if (!title) return reply.status(400).send({ error: 'title is required' });
179
179
  try {
180
- const schema = await createCollection({title, slug, description, fields, api, storage});
180
+ const schema = await createCollection({title, slug, description, fields, api, storage, meta});
181
181
  await ensureFormForCollection(schema);
182
182
  return reply.status(201).send(schema);
183
183
  } catch (err) {
@@ -51,6 +51,8 @@ export async function pagesRoutes(fastify) {
51
51
  category: p.category || null,
52
52
  visibility: p.visibility || 'public',
53
53
  tags: p.tags || [],
54
+ project: p.project ?? null,
55
+ resolvedProject: p.resolvedProject ?? null,
54
56
  updatedAt: p.updatedAt,
55
57
  createdAt: p.createdAt,
56
58
  versionCount: counts[i]
@@ -54,7 +54,8 @@ export async function projectsRoutes(fastify, opts = {}) {
54
54
 
55
55
  fastify.post('/projects', canCreate, async (request, reply) => {
56
56
  try {
57
- const project = await createProject(request.body || {});
57
+ const creator = request.user?.name || request.user?.email || null;
58
+ const project = await createProject({...(request.body || {}), createdBy: creator});
58
59
  await cache.invalidateTags(['projects']);
59
60
  return reply.status(201).send(project);
60
61
  } catch (err) {
@@ -66,6 +66,7 @@ export async function getProject(slug) {
66
66
  * Create a new project. Refuses on slug collision or validation failure.
67
67
  *
68
68
  * @param {object} input
69
+ * @param {string} [input.createdBy] Display name of the creator (optional).
69
70
  * @returns {Promise<object>} The persisted project record.
70
71
  * @throws {Error} If validation fails or the slug already exists.
71
72
  */
@@ -83,6 +84,7 @@ export async function createProject(input) {
83
84
  icon: input.icon || 'folder',
84
85
  ...(input.rootUrl != null && {rootUrl: input.rootUrl}),
85
86
  sortOrder: Number.isFinite(input.sortOrder) ? input.sortOrder : 0,
87
+ ...(typeof input.createdBy === 'string' && input.createdBy.trim() && {createdBy: input.createdBy.trim()}),
86
88
  createdAt: now,
87
89
  updatedAt: now
88
90
  };
@@ -286,10 +288,11 @@ export async function getArtefactsForProject(projectSlug) {
286
288
  } catch { /* skip */ }
287
289
 
288
290
  try {
289
- const {listBlocks, readBlock} = await import('./blocks.js');
291
+ // listBlocks() already surfaces each block's meta sidecar, so no
292
+ // per-block read is needed. (Blocks are keyed by `name`, not `slug`.)
293
+ const {listBlocks} = await import('./blocks.js');
290
294
  for (const b of await listBlocks()) {
291
- const full = (typeof readBlock === 'function') ? await readBlock(b.slug) : b;
292
- if (full?.meta?.project === projectSlug) out.blocks.push(b);
295
+ if (b?.meta?.project === projectSlug) out.blocks.push(b);
293
296
  }
294
297
  } catch { /* skip */ }
295
298
 
@@ -393,16 +396,14 @@ export async function untagAllForProject(projectSlug) {
393
396
  } catch { /* skip */ }
394
397
 
395
398
  try {
396
- const {readBlock, writeBlock} = await import('./blocks.js');
399
+ const {getBlock, saveBlock} = await import('./blocks.js');
397
400
  for (const b of grouped.blocks) {
398
- const full = (typeof readBlock === 'function') ? await readBlock(b.slug) : b;
401
+ const full = await getBlock(b.name).catch(() => null);
399
402
  if (!full) continue;
400
403
  const meta = {...(full.meta || {})};
401
404
  delete meta.project;
402
- if (typeof writeBlock === 'function') {
403
- await writeBlock(b.slug, {...full, meta});
404
- counts.blocks++;
405
- }
405
+ await saveBlock(b.name, full.content, {bundled: full.bundled, css: full.css, meta});
406
+ counts.blocks++;
406
407
  }
407
408
  } catch { /* skip */ }
408
409