domma-cms 1.8.23 → 1.8.25

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 +1 @@
1
- export const RECENT_MAX=5,RESULTS_MAX=40;export function hopTarget(t){const n=String(t||"").replace(/^#/,"").split("?")[0];if(n==="/pages/new")return{kind:"page",id:""};if(n==="/blocks/new")return{kind:"block",id:""};let r=/^\/pages\/edit(\/.*)$/.exec(n);if(r)return{kind:"page",id:r[1]};if(r=/^\/blocks\/edit\/([^/]+)\/?$/.exec(n),r)try{return{kind:"block",id:decodeURIComponent(r[1])}}catch{return{kind:"block",id:r[1]}}return null}export const isHop=(t,n)=>!!(hopTarget(t)&&hopTarget(n))&&String(t).split("?")[0]!==String(n).split("?")[0],pathFor=t=>t.kind==="page"?`/pages/edit${t.id}`:`/blocks/edit/${encodeURIComponent(t.id)}`,keyOf=t=>`${t.kind}:${t.id}`;export function pushRecent(t,n,r=5){const l=(Array.isArray(t)?t:[]).filter(i=>i&&(i.kind==="page"||i.kind==="block")&&typeof i.id=="string"&&i.id);if(!n?.id)return l.slice(0,r);const e={kind:n.kind,id:n.id,label:String(n.label||n.id).slice(0,120)};return[e,...l.filter(i=>keyOf(i)!==keyOf(e))].slice(0,r)}export function hopItems(t,n){const r=(Array.isArray(t)?t:[]).filter(e=>e?.urlPath).map(e=>({kind:"page",id:e.urlPath,label:e.title||"Untitled",meta:e.urlPath})).sort((e,i)=>e.label.localeCompare(i.label)),l=(Array.isArray(n)?n:[]).filter(e=>e?.name).map(e=>({kind:"block",id:e.name,label:e.name,meta:e.description||""})).sort((e,i)=>e.label.localeCompare(i.label));return[...r,...l]}export function hopMatches(t,n){const r=String(n||"").trim().toLowerCase();return r?[t.label,t.id,t.meta].some(l=>String(l||"").toLowerCase().includes(r)):!0}export function orderHop(t,n,{recent:r=[],current:l=null,max:e=40}={}){const i=l?.id?keyOf(l):null,a=t.filter(o=>keyOf(o)!==i),d=String(n||"").trim().toLowerCase();if(!d){const o=new Map(r.map((s,c)=>[keyOf(s),c]));return[...a.filter(s=>o.has(keyOf(s))).sort((s,c)=>o.get(keyOf(s))-o.get(keyOf(c))),...a.filter(s=>!o.has(keyOf(s)))].slice(0,e)}const p=o=>[o.label,o.kind==="page"?o.id.slice(1):o.id].some(u=>String(u).toLowerCase().startsWith(d));return[...a.filter(p),...a.filter(o=>!p(o))].slice(0,e)}export function recentChips(t,n,r=()=>!1){const l=n?.id?keyOf(n):null;return t.filter(e=>keyOf(e)!==l).map(e=>({...e,key:keyOf(e),path:pathFor(e),unsaved:!!r(e)}))}
1
+ export const KINDS=["page","block","component"];export function hopTarget(o){const n=String(o||"").replace(/^#/,"").split("?")[0];if(n==="/pages/new")return{kind:"page",id:""};if(n==="/blocks/new")return{kind:"block",id:""};if(n==="/components/new")return{kind:"component",id:""};let e=/^\/pages\/edit(\/.*)$/.exec(n);if(e)return{kind:"page",id:e[1]};if(e=/^\/(blocks|components)\/edit\/([^/]+)\/?$/.exec(n),e){const t=e[1]==="blocks"?"block":"component";try{return{kind:t,id:decodeURIComponent(e[2])}}catch{return{kind:t,id:e[2]}}}return null}export const keyOf=o=>`${o.kind}:${o.id}`;
@@ -1,19 +1 @@
1
- import{draftKey as F}from"./page-editor-arrange.js";import{draftKey as $,draftOlderThan as K}from"./block-editor-arrange.js";import{hopItems as A,hopMatches as D,hopTarget as C,keyOf as l,orderHop as q,pathFor as m,pushRecent as _,recentChips as z}from"./editor-hop-arrange.js";const T="editor-hop.recent",f=new Set;let L=!1;function U(){L||!window.M?.subscribe||(L=!0,M.subscribe("router:afterChange",({to:e}={})=>{C(e?.path)||f.clear()}))}export const hold=e=>{f.add(l(e))},takeHeld=e=>f.delete(l(e));export function hasDraft(e){try{return!!S.get(e.kind==="page"?F(e.id):$(e.id))}catch{return!1}}function k(){try{const e=S.get(T);return Array.isArray(e)?e:[]}catch{return[]}}export function noteRecent(e){const t=_(k(),e);try{S.set(T,t)}catch{}return t}const W=`
2
- <div class="dm-hop">
3
- <div class="dm-hop-find">
4
- <span class="dm-hop-find-icon" data-icon="search" data-icon-size="14"></span>
5
- <input type="text" class="form-input form-input--sm dm-hop-input" placeholder="Find a page or block\u2026"
6
- aria-label="Find a page or block to edit" autocomplete="off" spellcheck="false">
7
- </div>
8
- <div class="dm-hop-picks">
9
- <select class="form-select form-select-sm dm-hop-pick" data-kind="page" aria-label="Open a page"></select>
10
- <select class="form-select form-select-sm dm-hop-pick" data-kind="block" aria-label="Open a block"></select>
11
- </div>
12
- <div class="dm-hop-group" hidden data-bind-hidden="!recent.value.length">
13
- <span class="dm-hop-label">Recent</span>
14
- <div class="dm-hop-chips dm-hop-recent" data-each="recent.value key=key"></div>
15
- </div>
16
- </div>`,X=`
17
- <button type="button" class="dm-hop-chip is-{{kind}}" data-path="{{path}}" title="{{title}}">
18
- <span class="dm-hop-kind">{{kindText}}</span><span class="dm-hop-name">{{label}}</span><span class="dm-hop-dot" aria-label="{{dotLabel}}">{{dot}}</span>
19
- </button>`,g=e=>e==="page"?"Page":"Block";function Y(e){const t=e.unsaved??hasDraft(e);return{...e,key:l(e),path:m(e),kindText:g(e.kind),title:`${g(e.kind)}: ${e.kind==="page"?`${e.label} (${e.id})`:e.id}${t?" - unsaved changes kept":""}`,dot:t?"\u2022":"",dotLabel:t?"unsaved changes":""}}function j(e){const t=document.createElement("div");t.className="dm-hop-result";const r=document.createElement("span");r.className=`dm-hop-kind is-${e.kind}`,r.textContent=g(e.kind);const d=document.createElement("span");d.className="dm-hop-name",d.textContent=e.label;const o=document.createElement("small");if(o.className="dm-hop-meta",o.textContent=e.meta,t.append(r,d,o),hasDraft(e)){const s=document.createElement("span");s.className="dm-hop-dot",s.textContent="\u2022",s.title="Unsaved changes kept",t.appendChild(s)}return t}export function mountEditorHop(e,{current:t,pages:r=[],blocks:d=[],open:o=null}){const s=n=>{const a=C(n);a?.id&&o?.(a)||R.navigate(n)};U(),e.innerHTML=W,e.querySelectorAll("[data-each]").forEach(n=>{n.innerHTML=X});const c=t?.id?noteRecent(t):k(),p=A(r,d),N=new Set(p.map(l)),O={recent:M.observable(z(c.filter(n=>N.has(l(n))),t,hasDraft).map(Y))},B=M.applyBindings(O,e),b=e.querySelector(".dm-hop-input"),H=E.autocomplete?E.autocomplete(b,{data:p,minChars:0,maxResults:0,debounce:60,highlightMatches:!1,clearOnSelect:!0,emptyMessage:"No page or block matches",filterFn:D,onFilter:(n,a)=>q(n,a,{recent:c,current:t}),renderItem:n=>j(n),onSelect:n=>{b.blur(),s(m(n))}}):null,P=[...e.querySelectorAll(".dm-hop-pick")];for(const n of P){const a=n.dataset.kind,h=p.filter(i=>i.kind===a);n.append(new Option(`${a==="page"?"Pages":"Blocks"} (${h.length})\u2026`,""));for(const i of h){const u=t?.id&&l(i)===l(t),x=new Option(`${!u&&hasDraft(i)?"\u2022 ":""}${a==="page"?`${i.label} (${i.id})`:i.label}${u?" - editing":""}`,m(i));x.disabled=!!u,n.append(x)}n.hidden=h.length===0}const v=n=>{const a=n.target.closest(".dm-hop-pick");if(!a?.value)return;const h=a.value;a.value="",s(h)};e.addEventListener("change",v);const y=n=>{const a=n.target.closest(".dm-hop-chip");a?.dataset.path&&s(a.dataset.path)};return e.addEventListener("click",y),I.scan(e),{dispose(){e.removeEventListener("click",y),e.removeEventListener("change",v);try{H?.destroy?.()}catch{}B.dispose()}}}export function lastPage(){return k().find(e=>e.kind==="page")?.id||""}const w=2e5;export function heldBlockEdits(e,t={}){const r=o=>t instanceof Map?t.get(o):t[o],d={};for(const o of e){let s=null;try{s=S.get($(o))}catch{}const c=s?.form;if(!c||typeof c.content!="string"||K(s,r(o)))continue;const p=typeof c.css=="string"?c.css:"";c.content.length>w||p.length>w||(d[o]={content:c.content,css:p})}return d}
1
+ import{draftKey as u}from"./page-editor-arrange.js";import{draftKey as s,draftOlderThan as h}from"./block-editor-arrange.js";import{draftKey as y}from"./component-editor-arrange.js";import{hopTarget as g,keyOf as i}from"./editor-hop-arrange.js";const c=new Set;let d=!1;export function wireHeldRouter(){d||!window.M?.subscribe||(d=!0,M.subscribe("router:afterChange",({to:t}={})=>{g(t?.path)||c.clear()}))}export const hold=t=>{c.add(i(t))},takeHeld=t=>c.delete(i(t)),draftKeyOf=t=>t.kind==="page"?u(t.id):t.kind==="component"?y(t.id):s(t.id);export function hasDraft(t){try{return!!S.get(draftKeyOf(t))}catch{return!1}}const l=2e5;export function heldBlockEdits(t,r={}){const p=e=>r instanceof Map?r.get(e):r[e],f={};for(const e of t){let n=null;try{n=S.get(s(e))}catch{}const o=n?.form;if(!o||typeof o.content!="string"||h(n,p(e)))continue;const a=typeof o.css=="string"?o.css:"";o.content.length>l||a.length>l||(f[e]={content:o.content,css:a})}return f}
@@ -0,0 +1 @@
1
+ import{KINDS as S,keyOf as k}from"./editor-hop-arrange.js";import{SAMPLE_IMAGE as w,sampleValue as b}from"./block-editor-arrange.js";export const LAYOUTS=["split","write","preview"],nextLayout=t=>LAYOUTS[(LAYOUTS.indexOf(t)+1)%LAYOUTS.length],cleanLayout=t=>LAYOUTS.includes(t)?t:"split";export function workspaceKey(t){if(!t||!(t.ctrlKey||t.metaKey)||t.altKey)return null;const n=String(t.key||"").toLowerCase();return!t.shiftKey&&n==="s"?"save":!t.shiftKey&&(n==="\\"||t.code==="Backslash")?"layout":t.shiftKey&&n==="e"?"explorer":null}export const tabId=t=>k(t);export function tabItem(t){const n=String(t||""),r=n.indexOf(":");if(r<0)return null;const e=n.slice(0,r);return S.includes(e)?{kind:e,id:n.slice(r+1)}:null}export function openTab(t,n){return{ids:t.includes(n)?[...t]:[...t,n],active:n}}export function closeTab(t,n,r){const e=t.indexOf(n);if(e<0)return{ids:[...t],active:r};const o=t.filter(i=>i!==n);return r!==n?{ids:o,active:r}:{ids:o,active:o[Math.min(e,o.length-1)]??""}}export function moveTab(t,n,r){const e=t.indexOf(n),o=e+(r<0?-1:1);if(e<0||o<0||o>=t.length)return[...t];const i=[...t];return[i[e],i[o]]=[i[o],i[e]],i}export function tabsToClose(t,n,r){const e=t.indexOf(n);return e<0?[]:r==="others"?t.filter(o=>o!==n):r==="right"?t.slice(e+1):r==="all"?[...t]:[]}export function renameTab(t,n,r,e){return{ids:t.map(i=>i===n?r:i).filter((i,u,c)=>c.indexOf(i)===u),active:e===n?r:e}}export function rememberedTabs(t){const n=(Array.isArray(t?.ids)?t.ids:[]).filter(e=>typeof e=="string"&&tabItem(e)).filter((e,o,i)=>i.indexOf(e)===o).slice(0,40),r=typeof t?.active=="string"&&n.includes(t.active)?t.active:n[0]||"";return{ids:n,active:r}}export function startingTabs(t,n,r){if(!t||!t.id&&t.kind!=="page")return n;const e=tabId(t);return r&&n.active===e?n:openTab(n.ids,e)}export function addressFor(t){return t.kind==="page"?t.id?`/pages/edit${t.id}`:"/pages/new":`/${t.kind==="component"?"components":"blocks"}/edit/${encodeURIComponent(t.id)}`}const p=t=>String(t||"").split("/").filter(Boolean);export function comparePaths(t,n){const r=p(t),e=p(n);for(let o=0;o<Math.min(r.length,e.length);o++)if(r[o]!==e[o])return r[o]<e[o]?-1:1;return r.length-e.length}export function pageRows(t,{filter:n="",folded:r=new Set}={}){const e=(Array.isArray(t)?t:[]).filter(s=>typeof s?.urlPath=="string"&&s.urlPath.startsWith("/")),o=String(n||"").trim().toLowerCase(),i=(s,a,l)=>({key:`page:${s.urlPath}`,kind:"page",id:s.urlPath,label:String(s.title||"").trim()||s.urlPath,meta:s.urlPath,depth:a,folder:l,status:typeof s.status=="string"?s.status:""});if(o)return e.filter(s=>[s.title,s.urlPath].some(a=>String(a||"").toLowerCase().includes(o))).sort((s,a)=>comparePaths(s.urlPath,a.urlPath)).map(s=>i(s,0,!1));const u=new Map(e.map(s=>[s.urlPath,s])),c=new Set;for(const s of e){const a=p(s.urlPath);for(let l=1;l<a.length;l++)c.add("/"+a.slice(0,l).join("/"))}const d=[...new Set([...u.keys(),...c])].sort(comparePaths),y=r instanceof Set?r:new Set(r||[]),f=[];for(const s of d){const a=p(s),l=Math.max(0,a.length-1);let m=!1;for(let g=1;g<a.length;g++)if(y.has("/"+a.slice(0,g).join("/"))){m=!0;break}if(m)continue;const x=c.has(s),h=u.get(s);h?f.push(i(h,l,x)):f.push({key:`folder:${s}`,kind:"folder",id:s,label:a.at(-1)||s,meta:s,depth:l,folder:!0,status:""})}return f}export function blockRows(t,n=""){const r=String(n||"").trim().toLowerCase();return(Array.isArray(t)?t:[]).filter(e=>typeof e?.name=="string"&&e.name).filter(e=>!r||[e.name,e.description].some(o=>String(o||"").toLowerCase().includes(r))).sort((e,o)=>e.name.localeCompare(o.name)).map(e=>({key:`block:${e.name}`,kind:"block",id:e.name,label:e.name,meta:String(e.description||"")}))}export const componentKind=t=>t?.origin==="plugin"?"plugin":t?.bundled?"builtin":"mine";const $={plugin:"Plugin - read only",builtin:"Built in",mine:""};export function componentRows(t,n=""){const r=String(n||"").trim().toLowerCase();return(Array.isArray(t)?t:[]).filter(e=>typeof e?.name=="string"&&e.name).filter(e=>!r||[e.name,...Object.keys(e.props&&typeof e.props=="object"?e.props:{})].some(o=>String(o||"").toLowerCase().includes(r))).sort((e,o)=>e.name.localeCompare(o.name)).map(e=>({key:`component:${e.name}`,kind:"component",id:e.name,label:e.name,meta:$[componentKind(e)],readOnly:componentKind(e)==="plugin"}))}export function componentsInBody(t,n){const r=n instanceof Set?n:new Set(n||[]),e=[];for(const o of String(t||"").matchAll(/<dm-([a-z][a-z0-9-]*)|\[component\s+[^\]]*?name=["']?([a-z][a-z0-9-]*)/g)){const i=o[1]||o[2];r.has(i)&&!e.includes(i)&&e.push(i)}return e}export function componentStatusText({name:t="",readOnly:n=!1,state:r=""}={}){const e=`<dm-${t}>`;if(n)return`${e} \xB7 From a plugin - read only`;const o={compiling:"Compiling\u2026",ok:"Compiles",error:"Does not compile - see the editor"};return o[r]?`${e} \xB7 ${o[r]}`:e}export function blockAloneBody(t,n={}){if(!/^[a-z0-9][a-z0-9-]*$/.test(String(t||"")))return"";const r=[];for(const[e,o]of Object.entries(n||{})){if(!/^[\w_]+$/.test(e)||e==="template")continue;const i=String(o??"").replace(/\s+/g," ");!i||i.includes("]")||(i.includes('"')?i.includes("'")||r.push(`${e}='${i}'`):r.push(`${e}="${i}"`))}return`[block template="${t}"${r.length?" "+r.join(" "):""} /]`}export const SAMPLE_IMAGE_URL="/admin/preview/sample-image.svg";export function sampleRows(t,n,r={},e=new Date,o=SAMPLE_IMAGE_URL){return t.map(i=>({name:i,raw:n.has(i),value:Object.prototype.hasOwnProperty.call(r,i)?String(r[i]):b(i,e,{raw:n.has(i)}).split(w).join(o)}))}export function pageStatusText({urlPath:t="",status:n="draft",isEdit:r=!1}={}){const e=String(t||"").trim()||(r?"":"New page"),o=n==="published"?"Published":n==="review"?"In review":"Draft";return e?`${e} \xB7 ${o}`:o}export function saveAllLabel(t,n){return t.filter(e=>!n.includes(e)).length?`Save all (${t.length})`:""}
@@ -1 +1 @@
1
- export function blockTabs(e,t=[],i=null){const n=i?i instanceof Set?i:new Set(i):null,s=[],o=new Set;for(const r of Array.isArray(e)?e:[])typeof r!="string"||o.has(r)||(o.add(r),s.push({name:r,footer:!1}));for(const r of Array.isArray(t)?t:[])typeof r!="string"||o.has(r)||n&&!n.has(r)||(o.add(r),s.push({name:r,footer:!0}));return s}export function otherUses(e,t=""){return!e||typeof e!="object"?null:{pages:(Array.isArray(e.pages)?e.pages:[]).filter(i=>i!==t),views:Array.isArray(e.views)?e.views.filter(Boolean):[],footer:e.footer===!0}}const a=(e,t,i=`${t}s`)=>`${e} ${e===1?t:i}`;export function describeUses(e){if(!e)return"";const t=[];return e.pages.length&&t.push(a(e.pages.length,"other page")),e.views.length&&t.push(a(e.views.length,"view")),e.footer&&t.push("the footer"),t.length<2?t.join(""):`${t.slice(0,-1).join(", ")} and ${t.at(-1)}`}export function usesLine(e){if(!e)return"Where else it is used could not be read - saving changes it everywhere it is.";if(e.footer&&!e.pages.length&&!e.views.length)return"In the footer - on every page. Saving changes it everywhere.";const t=describeUses(e);return t?`Also used by ${t} - saving changes it there too.`:"Only used on this page."}export function notHereLine(e){const t="Not on this page, so the preview cannot show it (right-click \u203A Add block puts it in).";if(!e||typeof e!="object")return`${t} Where else it is used could not be read.`;const i=Array.isArray(e.pages)?e.pages.length:0,n=Array.isArray(e.views)?e.views.filter(Boolean).length:0,s=[];if(i&&s.push(a(i,"page")),n&&s.push(a(n,"view")),e.footer===!0&&s.push("the footer"),!s.length)return`${t} Not used anywhere yet.`;const o=s.length<2?s[0]:`${s.slice(0,-1).join(", ")} and ${s.at(-1)}`;return`${t} Used by ${o} - saving changes it there.`}export function saveQuestion(e,t={},i=new Set){const n=[];for(const s of e){if(i.has(s))continue;if(t[s]==null){n.push(`Where else "${s}" is used could not be read.`);continue}const o=describeUses(t[s]);o&&n.push(`"${s}" is also used by ${o}.`)}return n.length?`${n.join(" ")} Saving changes ${n.length===1?"it":"them"} everywhere ${n.length===1?"it is":"they are"} used. Save anyway?`:""}export function previewBlocks(e={},t=[],i=2e5){const n={...e};for(const s of t){if(!s.dirty){delete n[s.name];continue}s.content.length>i||s.css.length>i||(n[s.name]={content:s.content,css:s.css})}return n}export function savedMessage({page:e=!1,css:t=!1,blocks:i=[]}={}){const n=[];if(e&&n.push("Page"),t&&n.push("CSS"),i.length===1?n.push(`block "${i[0]}"`):i.length&&n.push(`${i.length} blocks`),!n.length)return"Nothing to save.";const s=n.length===1?n[0]:`${n.slice(0,-1).join(", ")} and ${n.at(-1)}`;return`${s[0].toUpperCase()}${s.slice(1)} saved.`}export function outlineBlock(e,t){const i=String(e??"");if(!t||!/^[a-z0-9][a-z0-9-]*$/.test(t))return i;const n=`<style>[data-block="${t}"]{outline:2px dashed var(--dm-primary,#4f6bed);outline-offset:6px}</style>`;return/<\/head>/i.test(i)?i.replace(/<\/head>/i,`${n}</head>`):n+i}
1
+ export function blockTabs(e,t=[],i=null){const s=i?i instanceof Set?i:new Set(i):null,n=[],r=new Set;for(const o of Array.isArray(e)?e:[])typeof o!="string"||r.has(o)||(r.add(o),n.push({name:o,footer:!1}));for(const o of Array.isArray(t)?t:[])typeof o!="string"||r.has(o)||s&&!s.has(o)||(r.add(o),n.push({name:o,footer:!0}));return n}export function otherUses(e,t=""){return!e||typeof e!="object"?null:{pages:(Array.isArray(e.pages)?e.pages:[]).filter(i=>i!==t),views:Array.isArray(e.views)?e.views.filter(Boolean):[],footer:e.footer===!0}}const a=(e,t,i=`${t}s`)=>`${e} ${e===1?t:i}`;export function describeUses(e){if(!e)return"";const t=[];return e.pages.length&&t.push(a(e.pages.length,"other page")),e.views.length&&t.push(a(e.views.length,"view")),e.footer&&t.push("the footer"),t.length<2?t.join(""):`${t.slice(0,-1).join(", ")} and ${t.at(-1)}`}export function usesLine(e){if(!e)return"Where else it is used could not be read - saving changes it everywhere it is.";if(e.footer&&!e.pages.length&&!e.views.length)return"In the footer - on every page. Saving changes it everywhere.";const t=describeUses(e);return t?`Also used by ${t} - saving changes it there too.`:"Only used on this page."}export function notHereLine(e){const t="Not on this page, so the preview cannot show it (right-click \u203A Add block puts it in).";if(!e||typeof e!="object")return`${t} Where else it is used could not be read.`;const i=Array.isArray(e.pages)?e.pages.length:0,s=Array.isArray(e.views)?e.views.filter(Boolean).length:0,n=[];if(i&&n.push(a(i,"page")),s&&n.push(a(s,"view")),e.footer===!0&&n.push("the footer"),!n.length)return`${t} Not used anywhere yet.`;const r=n.length<2?n[0]:`${n.slice(0,-1).join(", ")} and ${n.at(-1)}`;return`${t} Used by ${r} - saving changes it there.`}export function saveQuestion(e,t={},i=new Set){const s=[];for(const n of e){if(i.has(n))continue;if(t[n]==null){s.push(`Where else "${n}" is used could not be read.`);continue}const r=describeUses(t[n]);r&&s.push(`"${n}" is also used by ${r}.`)}return s.length?`${s.join(" ")} Saving changes ${s.length===1?"it":"them"} everywhere ${s.length===1?"it is":"they are"} used. Save anyway?`:""}export function previewBlocks(e={},t=[],i=2e5){const s={...e};for(const n of t){if(!n.dirty){delete s[n.name];continue}n.content.length>i||n.css.length>i||(s[n.name]={content:n.content,css:n.css})}return s}export function savedMessage({page:e=!1,pages:t=0,css:i=!1,blocks:s=[],components:n=[]}={}){const r=[];if(t>1?r.push(`${t} pages`):(e||t===1)&&r.push("Page"),i&&r.push("CSS"),s.length===1?r.push(`block "${s[0]}"`):s.length&&r.push(`${s.length} blocks`),n.length===1?r.push(`component "${n[0]}"`):n.length&&r.push(`${n.length} components`),!r.length)return"Nothing to save.";const o=r.length===1?r[0]:`${r.slice(0,-1).join(", ")} and ${r.at(-1)}`;return`${o[0].toUpperCase()}${o.slice(1)} saved.`}export function outlineBlock(e,t){const i=String(e??"");if(!t||!/^[a-z0-9][a-z0-9-]*$/.test(t))return i;const s=`<style>[data-block="${t}"]{outline:2px dashed var(--dm-primary,#4f6bed);outline-offset:6px}</style>`;return/<\/head>/i.test(i)?i.replace(/<\/head>/i,`${s}</head>`):s+i}
@@ -1 +1 @@
1
- export const SCREEN=/^\/pages\/(new|edit\/.*?)(\?.*)?$/;export function pageOfPath(t){const e=String(t||"").match(/^\/pages\/edit(\/[^?#]*)/);return e?e[1]:""}export const draftKey=t=>`page-editor.draft.${t||"new"}`,FIELDS=["urlPath","title","description","layout","status","sortOrder","showInNav","sidebar","breadcrumbs","bundled","tags","category","visibility","project","theme","seoTitle","seoDesc","dconfig","body","css"];export function snapshot(t={}){const e=t||{},o={};for(const n of FIELDS){const r=e[n];n==="tags"?o[n]=Array.isArray(r)?r.map(String):[]:typeof r=="boolean"?o[n]=r:n==="css"?o[n]=r==null?null:String(r):o[n]=r==null?"":String(r)}return JSON.stringify(o)}export function makeDraft(t,e,o=new Date().toISOString()){return{state:JSON.parse(snapshot(t)),base:e,at:o}}export function readDraft(t){return!t||typeof t!="object"||!t.state||typeof t.state!="object"?null:{state:JSON.parse(snapshot(t.state)),base:typeof t.base=="string"?t.base:"",at:typeof t.at=="string"?t.at:""}}export const draftIsStale=(t,e)=>!!(t?.base&&e&&t.base!==e);export function problems(t={}){const e=[],o=String(t.urlPath||"").trim();o?o.startsWith("/")?/\s/.test(o)&&e.push({field:"urlPath",text:"The address cannot contain spaces."}):e.push({field:"urlPath",text:"The address starts with / - for example /about."}):e.push({field:"urlPath",text:"The page needs an address - its URL path."});const n=String(t.dconfig||"").trim();if(n)try{const r=JSON.parse(n);(!r||typeof r!="object"||Array.isArray(r))&&e.push({field:"dconfig",text:"DConfig must be a JSON object ({ \u2026 })."})}catch{e.push({field:"dconfig",text:"DConfig is not valid JSON."})}return e}export function bannerSection(t,e,o){return t?String(e||"").trim()||o||"Untitled":"New page"}export function initialProject({isEdit:t,page:e=null,projects:o=[],fromHash:n="",urlPath:r=""}){if(!t&&n)return n;if(!e||e.project===null)return"";if(typeof e.project=="string"&&e.project)return e.project;if(!r)return"";let i="",a=-1;for(const s of o||[])s?.rootUrl&&(r===s.rootUrl||r.startsWith(s.rootUrl+"/"))&&s.rootUrl.length>a&&(i=s.slug,a=s.rootUrl.length);return i}
1
+ export const SCREEN=/^\/(pages\/(new|edit\/.*?)|(blocks|components)\/(new|edit\/[^/?#]+\/?))(\?.*)?$/;export function pageOfPath(t){const e=String(t||"").match(/^\/pages\/edit(\/[^?#]*)/);return e?e[1]:""}export const draftKey=t=>`page-editor.draft.${t||"new"}`,FIELDS=["urlPath","title","description","layout","status","sortOrder","showInNav","sidebar","breadcrumbs","bundled","tags","category","visibility","project","theme","seoTitle","seoDesc","dconfig","body","css"];export function snapshot(t={}){const e=t||{},o={};for(const n of FIELDS){const r=e[n];n==="tags"?o[n]=Array.isArray(r)?r.map(String):[]:typeof r=="boolean"?o[n]=r:n==="css"?o[n]=r==null?null:String(r):o[n]=r==null?"":String(r)}return JSON.stringify(o)}export function makeDraft(t,e,o=new Date().toISOString()){return{state:JSON.parse(snapshot(t)),base:e,at:o}}export function readDraft(t){return!t||typeof t!="object"||!t.state||typeof t.state!="object"?null:{state:JSON.parse(snapshot(t.state)),base:typeof t.base=="string"?t.base:"",at:typeof t.at=="string"?t.at:""}}export const draftIsStale=(t,e)=>!!(t?.base&&e&&t.base!==e);export function problems(t={}){const e=[],o=String(t.urlPath||"").trim();o?o.startsWith("/")?/\s/.test(o)&&e.push({field:"urlPath",text:"The address cannot contain spaces."}):e.push({field:"urlPath",text:"The address starts with / - for example /about."}):e.push({field:"urlPath",text:"The page needs an address - its URL path."});const n=String(t.dconfig||"").trim();if(n)try{const r=JSON.parse(n);(!r||typeof r!="object"||Array.isArray(r))&&e.push({field:"dconfig",text:"DConfig must be a JSON object ({ \u2026 })."})}catch{e.push({field:"dconfig",text:"DConfig is not valid JSON."})}return e}export function bannerSection(t,e,o){return t?String(e||"").trim()||o||"Untitled":"New page"}export function initialProject({isEdit:t,page:e=null,projects:o=[],fromHash:n="",urlPath:r=""}){if(!t&&n)return n;if(!e||e.project===null)return"";if(typeof e.project=="string"&&e.project)return e.project;if(!r)return"";let i="",a=-1;for(const s of o||[])s?.rootUrl&&(r===s.rootUrl||r.startsWith(s.rootUrl+"/"))&&s.rootUrl.length>a&&(i=s.slug,a=s.rootUrl.length);return i}
@@ -0,0 +1,6 @@
1
+ import{attachSmoothCaret as g}from"./smooth-caret.js";import{highlight as C}from"./syntax-highlight.js";export function attachColourLayer(t,E){const s=document.createElement("div");s.className="pe-plain-host";const l=document.createElement("pre");l.className="editor-textarea pe-plain-colours",l.setAttribute("aria-hidden","true"),t.classList.add("pe-plain-coloured"),t.parentNode.insertBefore(s,t),s.append(l,t);let p=null;const d=()=>{const e=getComputedStyle(t);l.style.whiteSpace!==e.whiteSpace&&(l.style.whiteSpace=e.whiteSpace,l.style.overflowWrap=e.overflowWrap),p!==t.value&&(p=t.value,l.innerHTML=C(t.value,E))},o=()=>{l.scrollTop=t.scrollTop,l.scrollLeft=t.scrollLeft},i=()=>{d(),o()};return t.addEventListener("input",i),t.addEventListener("scroll",o),d(),{host:s,colours:l,paint:()=>{d(),o()},destroy(){t.removeEventListener("input",i),t.removeEventListener("scroll",o),t.classList.remove("pe-plain-coloured"),s.parentNode&&(s.parentNode.insertBefore(t,s),s.remove())}}}export function createPlainEditor(t,{initialValue:E="",onChange:s=null,ariaLabel:l="",placeholder:p="",language:d=null}={}){if(!t)throw new Error("createPlainEditor: a mount element is required.");for(;t.firstChild;)t.removeChild(t.firstChild);const o=document.createElement("div");o.className="pe-plain";const i=document.createElement("div");i.className="editor-line-numbers",i.setAttribute("aria-hidden","true");const e=document.createElement("textarea");e.className="editor-textarea",e.spellcheck=!1,e.wrap="off",e.setAttribute("autocomplete","off"),e.setAttribute("autocorrect","off"),e.setAttribute("autocapitalize","off"),l&&e.setAttribute("aria-label",l),p&&(e.placeholder=p);let c=null;if(d){const n=document.createElement("div");n.className="pe-plain-host",c=document.createElement("pre"),c.className="editor-textarea pe-plain-colours",c.setAttribute("aria-hidden","true"),e.classList.add("pe-plain-coloured"),n.append(c,e),o.append(i,n)}else o.append(i,e);t.appendChild(o);let y=0;const L=()=>{const n=e.value.split(`
2
+ `).length||1;n!==y&&(y=n,i.textContent=Array.from({length:n},(r,m)=>m+1).join(`
3
+ `))};let w=null;const b=()=>{c&&w!==e.value&&(w=e.value,c.innerHTML=C(e.value,d))},v=()=>{i.scrollTop=e.scrollTop,c&&(c.scrollTop=e.scrollTop,c.scrollLeft=e.scrollLeft)},T=()=>{L(),b(),v(),s&&s(e.value)},N=n=>{if(n.isComposing)return;const{selectionStart:r,selectionEnd:m,value:u}=e;if(n.key==="Tab"){if(n.preventDefault(),!n.shiftKey)e.setRangeText(" ",r,m,"end");else{const a=u.lastIndexOf(`
4
+ `,r-1)+1,h=u.slice(a,a+2),f=h===" "?2:h[0]===" "?1:0;f&&(e.setRangeText("",a,a+f,"start"),e.selectionStart=e.selectionEnd=Math.max(a,r-f))}e.dispatchEvent(new Event("input",{bubbles:!0}))}else if(n.key==="Enter"){n.preventDefault();const a=u.lastIndexOf(`
5
+ `,r-1)+1,h=(u.slice(a,r).match(/^[ \t]*/)||[""])[0],f=u.slice(0,r).trimEnd().slice(-1);e.setRangeText(`
6
+ `+h+("{[(".includes(f)?" ":""),r,m,"end"),e.dispatchEvent(new Event("input",{bubbles:!0}))}};e.addEventListener("input",T),e.addEventListener("scroll",v),e.addEventListener("keydown",N),e.value=E??"",L(),b();const S=g(e);return{textarea:e,gutter:i,colours:c,getValue(){return e.value},setValue(n){const r=n??"";e.value!==r&&(e.value=r,L(),b(),v(),S.refresh?.())},focus(){e.focus()},destroy(){S.destroy?.(),e.removeEventListener("input",T),e.removeEventListener("scroll",v),e.removeEventListener("keydown",N),o.parentNode===t&&t.removeChild(o)}}}
@@ -1 +1 @@
1
- import{PHONE_QUERY as D}from"./sidebar-drawer.js";export const MIN_WIDTH=180,MAX_WIDTH=480,COLLAPSED_WIDTH=56,COLLAPSE_SNAP=120,STEP=16,BIG_STEP=64,WIDTH_KEY="sidebar.width",COLLAPSED_KEY="sidebar.collapsed";export function railShown(e,r){return e===!0&&!r}function I(){return typeof matchMedia=="function"&&matchMedia(D).matches}function M(){try{return S.get(COLLAPSED_KEY)===!0}catch{return!1}}export function clampWidth(e){return Math.max(MIN_WIDTH,Math.min(MAX_WIDTH,Math.round(e)))}export function parseWidth(e){const r=Number.parseInt(e,10);return Number.isFinite(r)&&r>=MIN_WIDTH&&r<=MAX_WIDTH?r:null}export function dragOutcome(e){return e<COLLAPSE_SNAP?{collapsed:!0,width:null}:{collapsed:!1,width:clampWidth(e)}}export function keyOutcome({key:e,shiftKey:r=!1},{width:t,collapsed:i}){const s=r?BIG_STEP:STEP;switch(e){case"ArrowLeft":return i?{collapsed:!0,width:null}:t<=MIN_WIDTH?{collapsed:!0,width:null}:{collapsed:!1,width:clampWidth(t-s)};case"ArrowRight":return i?{collapsed:!1,width:null}:{collapsed:!1,width:clampWidth(t+s)};case"Home":return{collapsed:!1,width:MIN_WIDTH};case"End":return{collapsed:!1,width:MAX_WIDTH};case"Enter":case" ":return{collapsed:!i,width:null};default:return null}}export function collapseButtonMarkup(){return'<button type="button" class="btn btn-sm btn-ghost dm-admin-sidebar-collapse" aria-expanded="true" aria-label="Collapse sidebar" title="Collapse sidebar"><span data-icon="chevrons-left" data-icon-size="16"></span></button>'}function o(e){return e.classList.contains("is-collapsed")}function z(){try{return parseWidth(S.get(WIDTH_KEY))}catch{return null}}function f(e,r){try{r==null?S.remove(e):S.set(e,r)}catch{}}function y(e,r){e.classList.toggle("is-collapsed",r);const t=e.querySelector(".dm-admin-sidebar-collapse");if(t){const i=r?"Expand sidebar":"Collapse sidebar";t.setAttribute("aria-expanded",String(!r)),t.setAttribute("aria-label",i),t.title=i}r?e.querySelector(".dm-admin-sidebar")?.scrollTo?.(0,0):d();for(const i of e.querySelectorAll("details > summary"))r?i.setAttribute("aria-expanded","false"):i.removeAttribute("aria-expanded");q(e)}function g(e){return!!e.querySelector(".dm-admin-sidebar.is-arranging")}function v(e){return o(e)?COLLAPSED_WIDTH:parseInt(e.style.width,10)||e.getBoundingClientRect().width}function q(e){const r=e.querySelector(".dm-admin-sidebar-handle");if(r)if(o(e))r.setAttribute("aria-valuenow",String(MIN_WIDTH)),r.setAttribute("aria-valuetext","Collapsed");else{const t=Math.round(v(e));if(!t)return;r.setAttribute("aria-valuenow",String(clampWidth(t))),r.setAttribute("aria-valuetext",`${t} pixels`)}}function u(e){q(e),e.__dmGutter?.()}function x(e,r){y(e,r),f(COLLAPSED_KEY,r?!0:null),u(e);const t=i=>{i.target!==e||i.propertyName!=="width"||(e.removeEventListener("transitionend",t),u(e))};e.addEventListener("transitionend",t)}function H(e,r){e.style.width=r+"px",f(WIDTH_KEY,r)}let n=null,p=null;function d({restoreFocus:e=!1}={}){if(!n)return;n.remove();const r=p?.querySelector(":scope > summary");r?.setAttribute("aria-expanded","false"),e&&r?.focus(),n=null,p=null}function F(e,r,{focus:t=!1}={}){const i=p===r;if(d(),i)return;const s=r.querySelector(":scope > summary"),c=r.querySelector(":scope > .sidebar-children");if(!s||!c)return;const w=e.querySelector(".dm-admin-sidebar");n=document.createElement("div"),n.className="dm-admin-sidebar-flyout";const l=document.createElement("nav");l.className=w?w.className:"dm-admin-sidebar",l.setAttribute("aria-label",s.querySelector(".sidebar-text")?.textContent.trim()||"Folder");const b=document.createElement("div");b.className="dm-admin-sidebar-flyout-title",b.textContent=l.getAttribute("aria-label"),l.appendChild(b);for(const a of c.children)l.appendChild(a.cloneNode(!0));for(const a of l.querySelectorAll(".sidebar-children"))a.style.maxHeight="";n.appendChild(l);for(const a of["data-menu-hover","data-menu-active"])e.hasAttribute(a)&&n.setAttribute(a,e.getAttribute(a));const A=e.style.getPropertyValue("--dm-menu-highlight");A&&n.style.setProperty("--dm-menu-highlight",A),document.body.appendChild(n);const m=e.getBoundingClientRect(),P=s.getBoundingClientRect(),L=window.innerHeight-8;n.style.maxHeight=`${L-8}px`;const{width:C,height:k}=n.getBoundingClientRect(),N=m.right+4+C<=window.innerWidth-8?m.right+4:m.left-4-C;n.style.left=`${Math.round(Math.max(8,N))}px`,n.style.top=`${Math.round(Math.max(8,Math.min(P.top,L-k)))}px`,p=r,s.setAttribute("aria-expanded","true"),t&&n.querySelector("a, summary")?.focus(),n.addEventListener("click",a=>{a.target.closest("a[href]")&&d()}),n.addEventListener("keydown",a=>{if(a.key==="Escape"){a.preventDefault(),d({restoreFocus:!0});return}if(a.key!=="Tab")return;const _=[...n.querySelectorAll("a[href], summary")].filter(h=>h.offsetParent);if(a.shiftKey&&document.activeElement===_[0])a.preventDefault(),d({restoreFocus:!0});else if(!a.shiftKey&&document.activeElement===_.at(-1)){a.preventDefault();const h=[...e.querySelectorAll('a[href], summary, button, [tabindex="0"]')].filter(W=>W.offsetParent),T=h[h.indexOf(s)+1];d(),(T||s).focus()}}),n.addEventListener("focusout",a=>{!n?.contains(a.relatedTarget)&&!s.contains(a.relatedTarget)&&a.relatedTarget&&d()})}function O(e){if(e.__dmSizeWired)return;e.__dmSizeWired=!0,e.addEventListener("click",t=>{if(!o(e))return;const i=t.target.closest("summary");!i||!e.contains(i)||(t.preventDefault(),F(e,i.parentElement,{focus:t.detail===0}))}),e.addEventListener("keydown",t=>{t.key==="Escape"&&n&&d({restoreFocus:!0})}),document.addEventListener("pointerdown",t=>{!n||n.contains(t.target)||p?.querySelector(":scope > summary")?.contains(t.target)||d()}),window.addEventListener("hashchange",()=>d()),window.addEventListener("resize",()=>d()),document.addEventListener("pointermove",t=>{const i=e.__dmDrag;if(!i||t.pointerId!==i.pointerId)return;if(t.pointerType==="mouse"&&t.buttons===0)return r(t);const s=t.clientX-i.x;if(!i.moved&&Math.abs(s)<3)return;i.moved=!0;const c=dragOutcome(i.width+s);c.collapsed!==o(e)&&y(e,c.collapsed),c.collapsed||(e.style.width=c.width+"px"),u(e)});const r=t=>{const i=e.__dmDrag;!i||t.pointerId!==i.pointerId||(e.__dmDrag=null,e.classList.remove("is-resizing"),document.body.classList.remove("dm-sidebar-resizing"),i.moved&&(o(e)?(e.style.width=i.inline,f(COLLAPSED_KEY,!0)):(f(COLLAPSED_KEY,null),f(WIDTH_KEY,parseInt(e.style.width,10))),u(e)))};document.addEventListener("pointerup",r),document.addEventListener("pointercancel",r),typeof matchMedia=="function"&&matchMedia(D).addEventListener?.("change",t=>{const i=railShown(M(),t.matches);i!==o(e)&&y(e,i),u(e)})}export function wireSidebarSize(e){O(e),d();const r=z();r&&(e.style.width=r+"px");const t=document.createElement("div");t.className="dm-admin-sidebar-handle",t.tabIndex=0,t.setAttribute("role","separator"),t.setAttribute("aria-orientation","vertical"),t.setAttribute("aria-label","Resize sidebar"),e.id&&t.setAttribute("aria-controls",e.id),t.setAttribute("aria-valuemin",String(MIN_WIDTH)),t.setAttribute("aria-valuemax",String(MAX_WIDTH)),t.title="Drag to resize - double-click to reset",e.appendChild(t),y(e,railShown(M(),I())),e.querySelector(".dm-admin-sidebar-collapse")?.addEventListener("click",()=>{I()||x(e,!o(e))}),t.addEventListener("pointerdown",i=>{if(!(i.button!==0||g(e))){e.__dmDrag={pointerId:i.pointerId,x:i.clientX,width:v(e),inline:e.style.width,moved:!1};try{t.setPointerCapture(i.pointerId)}catch{}e.classList.add("is-resizing"),document.body.classList.add("dm-sidebar-resizing"),i.preventDefault()}}),t.addEventListener("dblclick",()=>{g(e)||(e.style.width="",f(WIDTH_KEY,null),x(e,!1))}),t.addEventListener("keydown",i=>{if(g(e))return;const s=keyOutcome(i,{width:v(e),collapsed:o(e)});s&&(i.preventDefault(),s.collapsed!==o(e)&&x(e,s.collapsed),s.width!=null&&H(e,s.width),u(e))}),u(e)}
1
+ import{PHONE_QUERY as C}from"./sidebar-drawer.js";export const MIN_WIDTH=180,MAX_WIDTH=480,COLLAPSED_WIDTH=56,COLLAPSE_SNAP=120,STEP=16,BIG_STEP=64,WIDTH_KEY="sidebar.width",COLLAPSED_KEY="sidebar.collapsed";export function railShown(e,r){return e===!0&&!r}function _(){return typeof matchMedia=="function"&&matchMedia(C).matches}function D(){try{return S.get(COLLAPSED_KEY)===!0}catch{return!1}}export function clampWidth(e){return Math.max(MIN_WIDTH,Math.min(MAX_WIDTH,Math.round(e)))}export function parseWidth(e){const r=Number.parseInt(e,10);return Number.isFinite(r)&&r>=MIN_WIDTH&&r<=MAX_WIDTH?r:null}export function dragOutcome(e){return e<COLLAPSE_SNAP?{collapsed:!0,width:null}:{collapsed:!1,width:clampWidth(e)}}export function keyOutcome({key:e,shiftKey:r=!1},{width:t,collapsed:i}){const n=r?BIG_STEP:STEP;switch(e){case"ArrowLeft":return i?{collapsed:!0,width:null}:t<=MIN_WIDTH?{collapsed:!0,width:null}:{collapsed:!1,width:clampWidth(t-n)};case"ArrowRight":return i?{collapsed:!1,width:null}:{collapsed:!1,width:clampWidth(t+n)};case"Home":return{collapsed:!1,width:MIN_WIDTH};case"End":return{collapsed:!1,width:MAX_WIDTH};case"Enter":case" ":return{collapsed:!i,width:null};default:return null}}export function collapseButtonMarkup(){return'<button type="button" class="btn btn-sm btn-ghost dm-admin-sidebar-collapse" aria-expanded="true" aria-label="Collapse sidebar" title="Collapse sidebar"><span data-icon="chevrons-left" data-icon-size="16"></span></button>'}export function isCollapsed(e){return e.classList.contains("is-collapsed")}function T(){try{return parseWidth(S.get(WIDTH_KEY))}catch{return null}}function u(e,r){try{r==null?S.remove(e):S.set(e,r)}catch{}}function h(e,r){e.classList.toggle("is-collapsed",r);const t=e.querySelector(".dm-admin-sidebar-collapse");if(t){const i=r?"Expand sidebar":"Collapse sidebar";t.setAttribute("aria-expanded",String(!r)),t.setAttribute("aria-label",i),t.title=i}r?e.querySelector(".dm-admin-sidebar")?.scrollTo?.(0,0):d();for(const i of e.querySelectorAll("details > summary"))r?i.setAttribute("aria-expanded","false"):i.removeAttribute("aria-expanded");I(e)}function m(e){return!!e.querySelector(".dm-admin-sidebar.is-arranging")}function g(e){return isCollapsed(e)?COLLAPSED_WIDTH:parseInt(e.style.width,10)||e.getBoundingClientRect().width}function I(e){const r=e.querySelector(".dm-admin-sidebar-handle");if(r)if(isCollapsed(e))r.setAttribute("aria-valuenow",String(MIN_WIDTH)),r.setAttribute("aria-valuetext","Collapsed");else{const t=Math.round(g(e));if(!t)return;r.setAttribute("aria-valuenow",String(clampWidth(t))),r.setAttribute("aria-valuetext",`${t} pixels`)}}function l(e){I(e),e.__dmGutter?.()}export function setCollapsed(e,r,t=!0){h(e,r),t&&u(COLLAPSED_KEY,r?!0:null),l(e);const i=n=>{n.target!==e||n.propertyName!=="width"||(e.removeEventListener("transitionend",i),l(e))};e.addEventListener("transitionend",i)}function W(e,r){e.style.width=r+"px",u(WIDTH_KEY,r)}let a=null,f=null;function d({restoreFocus:e=!1}={}){if(!a)return;a.remove();const r=f?.querySelector(":scope > summary");r?.setAttribute("aria-expanded","false"),e&&r?.focus(),a=null,f=null}function z(e,r,{focus:t=!1}={}){const i=f===r;if(d(),i)return;const n=r.querySelector(":scope > summary"),o=r.querySelector(":scope > .sidebar-children");if(!n||!o)return;const v=e.querySelector(".dm-admin-sidebar");a=document.createElement("div"),a.className="dm-admin-sidebar-flyout";const c=document.createElement("nav");c.className=v?v.className:"dm-admin-sidebar",c.setAttribute("aria-label",n.querySelector(".sidebar-text")?.textContent.trim()||"Folder");const y=document.createElement("div");y.className="dm-admin-sidebar-flyout-title",y.textContent=c.getAttribute("aria-label"),c.appendChild(y);for(const s of o.children)c.appendChild(s.cloneNode(!0));for(const s of c.querySelectorAll(".sidebar-children"))s.style.maxHeight="";a.appendChild(c);for(const s of["data-menu-hover","data-menu-active"])e.hasAttribute(s)&&a.setAttribute(s,e.getAttribute(s));const x=e.style.getPropertyValue("--dm-menu-highlight");x&&a.style.setProperty("--dm-menu-highlight",x),document.body.appendChild(a);const b=e.getBoundingClientRect(),M=n.getBoundingClientRect(),w=window.innerHeight-8;a.style.maxHeight=`${w-8}px`;const{width:A,height:q}=a.getBoundingClientRect(),P=b.right+4+A<=window.innerWidth-8?b.right+4:b.left-4-A;a.style.left=`${Math.round(Math.max(8,P))}px`,a.style.top=`${Math.round(Math.max(8,Math.min(M.top,w-q)))}px`,f=r,n.setAttribute("aria-expanded","true"),t&&a.querySelector("a, summary")?.focus(),a.addEventListener("click",s=>{s.target.closest("a[href]")&&d()}),a.addEventListener("keydown",s=>{if(s.key==="Escape"){s.preventDefault(),d({restoreFocus:!0});return}if(s.key!=="Tab")return;const L=[...a.querySelectorAll("a[href], summary")].filter(p=>p.offsetParent);if(s.shiftKey&&document.activeElement===L[0])s.preventDefault(),d({restoreFocus:!0});else if(!s.shiftKey&&document.activeElement===L.at(-1)){s.preventDefault();const p=[...e.querySelectorAll('a[href], summary, button, [tabindex="0"]')].filter(N=>N.offsetParent),k=p[p.indexOf(n)+1];d(),(k||n).focus()}}),a.addEventListener("focusout",s=>{!a?.contains(s.relatedTarget)&&!n.contains(s.relatedTarget)&&s.relatedTarget&&d()})}function H(e){if(e.__dmSizeWired)return;e.__dmSizeWired=!0,e.addEventListener("click",t=>{if(!isCollapsed(e))return;const i=t.target.closest("summary");!i||!e.contains(i)||(t.preventDefault(),z(e,i.parentElement,{focus:t.detail===0}))}),e.addEventListener("keydown",t=>{t.key==="Escape"&&a&&d({restoreFocus:!0})}),document.addEventListener("pointerdown",t=>{!a||a.contains(t.target)||f?.querySelector(":scope > summary")?.contains(t.target)||d()}),window.addEventListener("hashchange",()=>d()),window.addEventListener("resize",()=>d()),document.addEventListener("pointermove",t=>{const i=e.__dmDrag;if(!i||t.pointerId!==i.pointerId)return;if(t.pointerType==="mouse"&&t.buttons===0)return r(t);const n=t.clientX-i.x;if(!i.moved&&Math.abs(n)<3)return;i.moved=!0;const o=dragOutcome(i.width+n);o.collapsed!==isCollapsed(e)&&h(e,o.collapsed),o.collapsed||(e.style.width=o.width+"px"),l(e)});const r=t=>{const i=e.__dmDrag;!i||t.pointerId!==i.pointerId||(e.__dmDrag=null,e.classList.remove("is-resizing"),document.body.classList.remove("dm-sidebar-resizing"),i.moved&&(isCollapsed(e)?(e.style.width=i.inline,u(COLLAPSED_KEY,!0)):(u(COLLAPSED_KEY,null),u(WIDTH_KEY,parseInt(e.style.width,10))),l(e)))};document.addEventListener("pointerup",r),document.addEventListener("pointercancel",r),typeof matchMedia=="function"&&matchMedia(C).addEventListener?.("change",t=>{const i=railShown(D(),t.matches);i!==isCollapsed(e)&&h(e,i),l(e)})}export function wireSidebarSize(e){H(e),d();const r=T();r&&(e.style.width=r+"px");const t=document.createElement("div");t.className="dm-admin-sidebar-handle",t.tabIndex=0,t.setAttribute("role","separator"),t.setAttribute("aria-orientation","vertical"),t.setAttribute("aria-label","Resize sidebar"),e.id&&t.setAttribute("aria-controls",e.id),t.setAttribute("aria-valuemin",String(MIN_WIDTH)),t.setAttribute("aria-valuemax",String(MAX_WIDTH)),t.title="Drag to resize - double-click to reset",e.appendChild(t),h(e,railShown(D(),_())),e.querySelector(".dm-admin-sidebar-collapse")?.addEventListener("click",()=>{_()||setCollapsed(e,!isCollapsed(e))}),t.addEventListener("pointerdown",i=>{if(!(i.button!==0||m(e))){e.__dmDrag={pointerId:i.pointerId,x:i.clientX,width:g(e),inline:e.style.width,moved:!1};try{t.setPointerCapture(i.pointerId)}catch{}e.classList.add("is-resizing"),document.body.classList.add("dm-sidebar-resizing"),i.preventDefault()}}),t.addEventListener("dblclick",()=>{m(e)||(e.style.width="",u(WIDTH_KEY,null),setCollapsed(e,!1))}),t.addEventListener("keydown",i=>{if(m(e))return;const n=keyOutcome(i,{width:g(e),collapsed:isCollapsed(e)});n&&(i.preventDefault(),n.collapsed!==isCollapsed(e)&&setCollapsed(e,n.collapsed),n.width!=null&&W(e,n.width),l(e))}),l(e)}
@@ -1,2 +1,2 @@
1
- export function escapeHtml(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const i=(e,t)=>`<span class="dm-tok-${e}">${escapeHtml(t)}</span>`;function m(e,t){let n="",s=0;const o=t.map(r=>({cls:r.cls,re:new RegExp(r.re.source,r.re.flags.replace("y","")+"y")}));for(;s<e.length;){let r=!1;for(const{cls:l,re:a}of o){a.lastIndex=s;const c=a.exec(e);if(c&&c[0].length>0){n+=i(l,c[0]),s+=c[0].length,r=!0;break}}if(!r){let l=s+1;if(/[A-Za-z0-9_]/.test(e[s]))for(;l<e.length&&/[A-Za-z0-9_]/.test(e[l]);)l++;n+=escapeHtml(e.slice(s,l)),s=l}}return n}const d=/\b(?:async|await|break|case|catch|class|const|continue|default|delete|do|else|export|extends|finally|for|from|function|if|import|in|instanceof|let|new|of|return|static|super|switch|this|throw|try|typeof|var|void|while|yield)\b/,g={js:[{cls:"comment",re:/\/\/[^\n]*/},{cls:"comment",re:/\/\*[\s\S]*?(?:\*\/|$)/},{cls:"string",re:/`(?:[^`\\]|\\[\s\S])*(?:`|$)/},{cls:"string",re:/'(?:[^'\\\n]|\\[\s\S])*(?:'|$)/},{cls:"string",re:/"(?:[^"\\\n]|\\[\s\S])*(?:"|$)/},{cls:"keyword",re:d},{cls:"literal",re:/\b(?:true|false|null|undefined|NaN|Infinity)\b/},{cls:"number",re:/\b0[xXbBoO]?[\da-fA-F_]+n?\b|\b\d[\d_]*(?:\.\d+)?(?:[eE][+-]?\d+)?\b/},{cls:"func",re:/[A-Za-z_$][\w$]*(?=\s*\()/}],json:[{cls:"key",re:/"(?:[^"\\]|\\[\s\S])*"(?=\s*:)/},{cls:"string",re:/"(?:[^"\\]|\\[\s\S])*(?:"|$)/},{cls:"literal",re:/\b(?:true|false|null)\b/},{cls:"number",re:/-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/}],css:[{cls:"comment",re:/\/\*[\s\S]*?(?:\*\/|$)/},{cls:"string",re:/'(?:[^'\\\n]|\\[\s\S])*(?:'|$)|"(?:[^"\\\n]|\\[\s\S])*(?:"|$)/},{cls:"number",re:/-?\b\d+(?:\.\d+)?(?:px|rem|em|vh|vw|s|ms|deg|fr|%)?\b/},{cls:"key",re:/--?[a-zA-Z][\w-]*(?=\s*:)/},{cls:"keyword",re:/@[a-zA-Z-]+/},{cls:"func",re:/[a-zA-Z-]+(?=\()/},{cls:"literal",re:/#[0-9a-fA-F]{3,8}\b/},{cls:"tag",re:/\.[a-zA-Z_-][\w-]*|#[a-zA-Z_-][\w-]*/}],html:[{cls:"comment",re:/<!--[\s\S]*?(?:-->|$)/},{cls:"string",re:/"(?:[^"]*)(?:"|$)|'(?:[^']*)(?:'|$)/},{cls:"tag",re:/<\/?[a-zA-Z][\w-]*|\/?>/},{cls:"key",re:/\b[a-zA-Z-]+(?==)/}],md:[{cls:"keyword",re:/^#{1,6}[^\n]*/m},{cls:"comment",re:/```[\s\S]*?(?:```|$)/},{cls:"string",re:/`[^`\n]*(?:`|$)/},{cls:"comment",re:/<!--[\s\S]*?(?:-->|$)/},{cls:"comment",re:/^ {0,3}\/\/[^\n]*/m},{cls:"literal",re:/\*\*[^*\n]+\*\*|\*[^*\n]+\*/},{cls:"func",re:/\[[^\]\n]*\]\([^)\n]*\)/},{cls:"tag",re:/^\s*[-*+]\s|^\s*\d+\.\s/m}]};export function languageForFile(e){const t=String(e||"").toLowerCase().split(".").pop();return{js:"js",json:"json",css:"css",html:"html",htm:"html",svg:"html",md:"md",markdown:"md"}[t]||null}export function highlight(e,t){const n=g[t];return(n?m(String(e??""),n):escapeHtml(String(e??"")))+`
1
+ export function escapeHtml(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const i=(e,t)=>`<span class="dm-tok-${e}">${escapeHtml(t)}</span>`;function m(e,t){let n="",s=0;const o=t.map(r=>({cls:r.cls,re:new RegExp(r.re.source,r.re.flags.replace("y","")+"y")}));for(;s<e.length;){let r=!1;for(const{cls:l,re:a}of o){a.lastIndex=s;const c=a.exec(e);if(c&&c[0].length>0){n+=i(l,c[0]),s+=c[0].length,r=!0;break}}if(!r){let l=s+1;if(/[A-Za-z0-9_]/.test(e[s]))for(;l<e.length&&/[A-Za-z0-9_]/.test(e[l]);)l++;n+=escapeHtml(e.slice(s,l)),s=l}}return n}const d=/\b(?:async|await|break|case|catch|class|const|continue|default|delete|do|else|export|extends|finally|for|from|function|if|import|in|instanceof|let|new|of|return|static|super|switch|this|throw|try|typeof|var|void|while|yield)\b/,g={js:[{cls:"comment",re:/\/\/[^\n]*/},{cls:"comment",re:/\/\*[\s\S]*?(?:\*\/|$)/},{cls:"string",re:/`(?:[^`\\]|\\[\s\S])*(?:`|$)/},{cls:"string",re:/'(?:[^'\\\n]|\\[\s\S])*(?:'|$)/},{cls:"string",re:/"(?:[^"\\\n]|\\[\s\S])*(?:"|$)/},{cls:"keyword",re:d},{cls:"literal",re:/\b(?:true|false|null|undefined|NaN|Infinity)\b/},{cls:"number",re:/\b0[xXbBoO]?[\da-fA-F_]+n?\b|\b\d[\d_]*(?:\.\d+)?(?:[eE][+-]?\d+)?\b/},{cls:"func",re:/[A-Za-z_$][\w$]*(?=\s*\()/}],json:[{cls:"key",re:/"(?:[^"\\]|\\[\s\S])*"(?=\s*:)/},{cls:"string",re:/"(?:[^"\\]|\\[\s\S])*(?:"|$)/},{cls:"literal",re:/\b(?:true|false|null)\b/},{cls:"number",re:/-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/}],css:[{cls:"comment",re:/\/\*[\s\S]*?(?:\*\/|$)/},{cls:"string",re:/'(?:[^'\\\n]|\\[\s\S])*(?:'|$)|"(?:[^"\\\n]|\\[\s\S])*(?:"|$)/},{cls:"number",re:/-?\b\d+(?:\.\d+)?(?:px|rem|em|vh|vw|s|ms|deg|fr|%)?\b/},{cls:"key",re:/--?[a-zA-Z][\w-]*(?=\s*:)/},{cls:"keyword",re:/@[a-zA-Z-]+/},{cls:"func",re:/[a-zA-Z-]+(?=\()/},{cls:"literal",re:/#[0-9a-fA-F]{3,8}\b/},{cls:"tag",re:/\.[a-zA-Z_-][\w-]*|#[a-zA-Z_-][\w-]*/}],html:[{cls:"comment",re:/<!--[\s\S]*?(?:-->|$)/},{cls:"string",re:/"(?:[^"]*)(?:"|$)|'(?:[^']*)(?:'|$)/},{cls:"tag",re:/<\/?[a-zA-Z][\w-]*|\/?>/},{cls:"key",re:/\b[a-zA-Z-]+(?==)/}],md:[{cls:"keyword",re:/^#{1,6}[^\n]*/m},{cls:"comment",re:/```[\s\S]*?(?:```|$)/},{cls:"string",re:/`[^`\n]*(?:`|$)/},{cls:"comment",re:/<!--[\s\S]*?(?:-->|$)/},{cls:"comment",re:/^ {0,3}\/\/[^\n]*/m},{cls:"literal",re:/\*\*[^*\n]+\*\*|\*[^*\n]+\*/},{cls:"func",re:/\[[^\]\n]*\]\([^)\n]*\)/},{cls:"key",re:/\[\/?[a-z][\w-]*(?:\s[^\]\n]*)?\/?\]/},{cls:"tag",re:/^\s*[-*+]\s|^\s*\d+\.\s/m}]};export function languageForFile(e){const t=String(e||"").toLowerCase().split(".").pop();return{js:"js",json:"json",css:"css",html:"html",htm:"html",svg:"html",md:"md",markdown:"md"}[t]||null}export function highlight(e,t){const n=g[t];return(n?m(String(e??""),n):escapeHtml(String(e??"")))+`
2
2
  `}
@@ -155,7 +155,7 @@ onMount() { this.set({ count: this.props.initial }); }</code></pre>
155
155
  <ol>
156
156
  <li>On <a href="#/components">Components</a>, right-click the row (or use its menu) and choose
157
157
  <span data-icon="download"></span> <strong>Export .dmcomponent.json</strong> - you'll download
158
- <code>name.dmcomponent.json</code>. The editor has the same Export in its banner.</li>
158
+ <code>name.dmcomponent.json</code>.</li>
159
159
  <li>On the other site, click <strong>Import</strong> in the Components banner and pick that file. On a
160
160
  name clash you'll be asked to confirm an overwrite.</li>
161
161
  </ol>
@@ -163,7 +163,7 @@ onMount() { this.set({ count: this.props.initial }); }</code></pre>
163
163
  <hr>
164
164
 
165
165
  <h2>File it under a project</h2>
166
- <p>In the editor, set the <strong>Project</strong> dropdown. That writes <code>project</code> to the
166
+ <p>In the editor's dock (the <strong>Component</strong> panel), set the <strong>Project</strong> dropdown. That writes <code>project</code> to the
167
167
  component's <code>.meta.json</code> sidecar, so it shows in that project's Components list
168
168
  (<code>#/projects/&lt;slug&gt;/components</code>).</p>
169
169
 
@@ -19,7 +19,7 @@
19
19
  We'll build one component end-to-end: a <strong>star rating</strong> widget you can drop onto any
20
20
  page with <code>[component name="star-rating" max="5" value="3" /]</code>. By the end you'll have
21
21
  used every section of a <code>.dmc</code> file, reactive state, an event listener, a custom event,
22
- and the live preview. Follow along in a real <strong>New component</strong> editor.
22
+ and the live preview. Follow along in a real component - <strong>New component</strong> in the Components banner opens one as a tab in the editor.
23
23
  </p>
24
24
 
25
25
  <hr>
@@ -0,0 +1,156 @@
1
+ <div class="view-header">
2
+ <h1><span data-icon="edit"></span> The Editor</h1>
3
+ <a href="#/tutorials" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> All tutorials</a>
4
+ </div>
5
+
6
+ <div class="row">
7
+ <div class="col-12">
8
+ <div class="docs-body" id="tutorial-editor">
9
+
10
+ <p class="lead">
11
+ Pages, blocks and components are all written in one place: the editor. It works the way a code
12
+ editor does - a list on the left, a tab for everything you have open, the text and a live preview
13
+ in the middle, and a settings dock on the right. This tutorial walks through a working session:
14
+ opening things, writing with colour, previewing, saving, and setting the editor up the way you
15
+ like it, smooth typing included.
16
+ </p>
17
+
18
+ <hr>
19
+
20
+ <h2>The screen</h2>
21
+
22
+ <p>Open any page from <strong>Pages</strong> and you are in the editor. Five parts:</p>
23
+
24
+ <ul>
25
+ <li><strong>The list</strong> (left): <em>This page</em> - the blocks and components the open page uses -
26
+ then every page as a tree, every block and every component. A filter box at the top narrows all three.
27
+ <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>E</kbd> hides and shows it.</li>
28
+ <li><strong>The tabs</strong> (top): one for each page, block or component you have open, in the order you
29
+ opened them. A dot on a tab means unsaved work.</li>
30
+ <li><strong>The editor and the preview</strong> (middle): the text on the left, the page as your site will
31
+ show it on the right. Drag the divider between them.</li>
32
+ <li><strong>The status bar</strong> (bottom): where you are, <strong>Unsaved</strong>, <strong>Discard</strong>,
33
+ <strong>Save</strong> and <strong>Save all</strong>.</li>
34
+ <li><strong>The dock</strong> (right, the sliders button at the top right): the page's details, SEO, DConfig
35
+ and history. On a block's tab it holds the block's details and sample values; on a component's, its details
36
+ and preview props.</li>
37
+ </ul>
38
+
39
+ <p>The banner above the editor names what you are on - <em>Pages › About</em>, <em>Blocks › contact-card</em>,
40
+ <em>Components › counter</em> - and the address bar follows the tab, so a bookmark or a link always opens the
41
+ right thing.</p>
42
+
43
+ <h2>Opening things</h2>
44
+
45
+ <p>Everything in the list opens with a click, as a tab. So does <strong>Edit</strong> in the Pages, Blocks and
46
+ Components lists, and so does an address someone sends you. If the editor is already up, the new tab joins the
47
+ ones you have; nothing you were doing is disturbed.</p>
48
+
49
+ <p>The <strong>+</strong> beside each section makes a new one. A new page opens straight away as a tab. A new
50
+ block or component asks for its name first (lowercase letters, digits and hyphens - it becomes the tag, so it
51
+ cannot change later), is created, and opens.</p>
52
+
53
+ <p>Close a tab with its <strong>×</strong>, a middle-click, or <kbd>Delete</kbd> with the tab focused. Right-click
54
+ a tab for more: close the others, close those to its right, close all, move it left or right, clone the page
55
+ or duplicate the block or component, or copy its link. The open tabs are remembered: reload, or come back
56
+ tomorrow, and they are where you left them.</p>
57
+
58
+ <div class="alert alert-info">
59
+ <strong>Try it:</strong> open <em>About</em>, then click a block under <em>This page</em>, then a component from
60
+ the <em>Components</em> section. Three tabs. Click between them - each keeps its own text, scroll position
61
+ and unsaved changes.
62
+ </div>
63
+
64
+ <h2>Writing</h2>
65
+
66
+ <h3>A page</h3>
67
+ <p>The page is Markdown with shortcodes, and the text is coloured as you type: headings, code, emphasis, links
68
+ and lists in Markdown's colours, and every <code>[shortcode]</code> - its opening tag, its closing tag, a
69
+ self-closing one - in its own, so a block or a card stands out from the prose around it. The toolbar inserts
70
+ shortcodes with a dialog each; right-click inside one to edit its attributes or jump to the block it names.
71
+ Long shortcodes fold (the chevrons in the gutter, or the fold-all button).</p>
72
+
73
+ <h3>A block</h3>
74
+ <p>A block's tab has two parts, <strong>HTML</strong> and <strong>CSS</strong>, in the same editor the page uses,
75
+ coloured for each language. The <strong>Format</strong> button tidies the HTML's indentation. The dock's
76
+ <strong>Block</strong> panel shows its short tag and shortcode (copy either), its project, <strong>Bundled</strong>
77
+ and where it is used; <strong>Samples</strong> is one value per <code>&#123;&#123;placeholder&#125;&#125;</code> - right-click one
78
+ to insert the placeholder into the template.</p>
79
+
80
+ <h3>A component</h3>
81
+ <p>A component's tab has four parts - <code>&lt;template&gt;</code>, <code>&lt;props&gt;</code>,
82
+ <code>&lt;script&gt;</code> and <code>&lt;style&gt;</code> - coloured as HTML, JSON, JS and CSS. It is
83
+ compiled as you type; the dot beside the parts says <em>Compiles</em> or <em>Does not compile</em>, and the
84
+ errors show above the text. The dock's <strong>Preview props</strong> are the values the preview tries - change
85
+ one and the preview updates; right-click a row to copy it as an attribute or a whole tag. A component from a
86
+ plugin opens read only, with <strong>Copy to a new component</strong> to make your own.</p>
87
+
88
+ <h2>Previewing</h2>
89
+
90
+ <p>Three layouts, from the toolbar's three buttons or <kbd>Ctrl</kbd>+<kbd>\</kbd> to cycle: editor and preview
91
+ side by side, editor only, preview only. Preview only adds the width presets (full, tablet, phone),
92
+ <strong>Refresh</strong> and <strong>Share link</strong> - a link that lets someone see the page before it is
93
+ published, without signing in, that expires and can be revoked.</p>
94
+
95
+ <p>The preview is the real page, rendered by your site with your theme, fonts and custom CSS, and it shows your
96
+ unsaved changes - to the page and to any open block it uses, outlined so you can find it. A block no open
97
+ page uses is drawn on its own, its placeholders filled from the sample values. A component is drawn on its
98
+ own too, with its preview props.</p>
99
+
100
+ <h2>Saving</h2>
101
+
102
+ <p><strong>Save</strong> (<kbd>Ctrl</kbd>+<kbd>S</kbd>) saves the tab you are on. On a page, that includes the
103
+ blocks of its you changed - and if one of them is also used elsewhere, you are told where and asked first,
104
+ once. <strong>Save all</strong> saves every open page, block and component with changes. A component that does
105
+ not compile is not saved, and the status bar says so.</p>
106
+
107
+ <p>Nothing is lost by closing a tab or leaving the editor: unsaved work is kept as a draft and comes back when
108
+ you open that page, block or component again. Leaving the editor itself with unsaved work asks first, and
109
+ <strong>Cancel</strong> keeps you where you were. <strong>Discard</strong> throws away the current tab's
110
+ changes.</p>
111
+
112
+ <h2>Setting the editor up</h2>
113
+
114
+ <p>The cog in the banner opens the editor settings, kept per browser:</p>
115
+
116
+ <ul>
117
+ <li><strong>Font, size and line spacing</strong>: the lettering of the text, the gutter and the colours - all
118
+ three follow the same setting, on pages, blocks and components alike. Monospace fonts keep shortcodes and
119
+ Markdown lined up.</li>
120
+ <li><strong>Wrap long lines</strong>: off, a long line scrolls sideways and each line number is exactly one
121
+ line.</li>
122
+ <li><strong>Caret movement</strong> - smooth typing. <em>Smooth</em> draws the caret as a small light that
123
+ glides from letter to letter as you type and move, instead of jumping, so your eye never loses it on a busy
124
+ line. Typing, selecting, undo and dictation are exactly as before: only the caret you see is animated.
125
+ <em>Automatic</em> is smooth unless your system asks for reduced motion; <em>Native</em> is the browser's
126
+ own caret. The choice reaches every text box in the admin and your site's forms in this browser.</li>
127
+ <li><strong>Close the admin menu when the editor opens</strong>: the left-hand menu folds to its icon rail for
128
+ the room while you edit, and comes back when you leave the editor.</li>
129
+ </ul>
130
+
131
+ <p>The fullscreen button hides the admin around the editor. On a narrow screen the list and the dock slide over
132
+ the text instead of sitting beside it.</p>
133
+
134
+ <h2>Keys</h2>
135
+
136
+ <table class="table">
137
+ <thead><tr><th>Key</th><th>Does</th></tr></thead>
138
+ <tbody>
139
+ <tr><td><kbd>Ctrl</kbd>+<kbd>S</kbd></td><td>Save the tab you are on</td></tr>
140
+ <tr><td><kbd>Ctrl</kbd>+<kbd>\</kbd></td><td>Next layout: split, editor only, preview only</td></tr>
141
+ <tr><td><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>E</kbd></td><td>Show or hide the list</td></tr>
142
+ <tr><td><kbd>Ctrl</kbd>+<kbd>B</kbd>, <kbd>Ctrl</kbd>+<kbd>I</kbd>, <kbd>Ctrl</kbd>+<kbd>K</kbd></td><td>Bold, italic, link in the page text</td></tr>
143
+ <tr><td><kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd></td><td>Indent and dedent in a block's or component's source</td></tr>
144
+ <tr><td><kbd>Delete</kbd> on a tab, <kbd>Shift</kbd>+<kbd>F10</kbd></td><td>Close it; its menu</td></tr>
145
+ </tbody>
146
+ </table>
147
+
148
+ <div class="alert alert-success">
149
+ <strong>Where next:</strong> <a href="#/docs/usage/pages">Pages</a> for frontmatter, visibility and history,
150
+ <a href="#/docs/usage/blocks">Blocks</a> for placeholders and collection displays,
151
+ <a href="#/docs/components-walkthrough">the Components walkthrough</a> to build one from scratch.
152
+ </div>
153
+
154
+ </div>
155
+ </div>
156
+ </div>
@@ -187,23 +187,27 @@
187
187
  <p><em>In use</em> is worked out from page sources and view settings. A Tool that draws a block from its own code
188
188
  is not counted, so check before deleting a block marked unused.</p>
189
189
 
190
- <h3>The block editor</h3>
190
+ <h3>Editing a block</h3>
191
+ <p>A block opens as a tab in the page editor's workspace, beside pages and components - from the Blocks list,
192
+ from the list on the editor's left, or from a page's <em>This page</em> section (the blocks that page uses).</p>
191
193
  <ul>
192
- <li><strong>Details</strong>: the name (lowercase letters, digits and hyphens, fixed once the block is saved),
193
- an optional project, and <strong>Bundled</strong>, which ships the block with fresh installs.</li>
194
- <li><strong>Template HTML</strong>: the editor, with undo, cut, copy, paste and <strong>Format</strong> (tidies
195
- the indentation). <strong>Edit</strong>, <strong>Split</strong> and <strong>Preview</strong> choose what you
196
- see.</li>
197
- <li><strong>Sample values</strong>: one box per placeholder the template uses, filled in for the preview only.
198
- Nothing in them is saved. Right-click one to insert its placeholder into the template.</li>
199
- <li><strong>Preview</strong>: the block drawn inside your own site page, with its theme, fonts and Custom CSS.
200
- Try it at full, tablet or mobile width, as <strong>Three in a grid</strong> (as a collection with
201
- <code>cols="3"</code> shows it), or in another theme. The site itself is not changed.</li>
202
- <li><strong>Custom CSS</strong>: the block's scoped stylesheet (see Styles above).</li>
203
- <li><strong>Placeholder reference</strong>: the table above, at the foot of the screen.</li>
194
+ <li><strong>HTML</strong> and <strong>CSS</strong>: the two parts of the block, in the editor pane, with
195
+ <strong>Format</strong> (tidies the indentation). The toolbar's three buttons - or <kbd>Ctrl</kbd>+<kbd>\</kbd> -
196
+ choose editor and preview, editor only or preview only.</li>
197
+ <li><strong>Preview</strong>: the block drawn on the open page that uses it, outlined, with your changes as you
198
+ type - or, when no open page uses it, on its own inside your site's page shell, with its theme, fonts and
199
+ Custom CSS. Preview only has full, tablet and mobile widths.</li>
200
+ <li><strong>The dock</strong> (the sliders button): <strong>Block</strong> - its short tag and shortcode, an
201
+ optional project, <strong>Bundled</strong> (ships the block with fresh installs) and where it is used;
202
+ <strong>Samples</strong> - one box per placeholder the template uses, filled in for the on-its-own preview
203
+ only. Nothing in them is saved. Right-click one to insert its placeholder into the template.</li>
204
+ <li><strong>New block</strong>: the <strong>+</strong> by Blocks in the editor's list, or <strong>New
205
+ block</strong> in the Blocks banner - a name (lowercase letters, digits and hyphens, fixed once created),
206
+ then its tab.</li>
204
207
  </ul>
205
- <p><kbd>Ctrl</kbd>+<kbd>S</kbd> saves from anywhere on the screen. If you leave with unsaved changes you are
206
- asked first, and your work is kept as a draft for that block until you come back to it.</p>
208
+ <p><kbd>Ctrl</kbd>+<kbd>S</kbd> saves the tab you are on; <strong>Save all</strong> saves every open page, block
209
+ and component. A block used elsewhere changes there too - you are asked the first time. Closing a tab, or
210
+ leaving with unsaved changes, keeps your work as a draft for that block until you open it again.</p>
207
211
 
208
212
  <h3>Copying blocks between sites</h3>
209
213
  <p><strong>Export</strong> on a block's right-click menu downloads it as a <code>&lt;name&gt;.dmblock.json</code>
@@ -96,6 +96,10 @@
96
96
  <h2 style="margin-top:2rem;">Tutorials</h2>
97
97
  <p class="text-muted">End-to-end walkthroughs.</p>
98
98
  <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:1rem;margin-top:1rem;">
99
+ <a href="#/tutorials/editor" class="card" style="text-decoration:none;display:block;">
100
+ <div class="card-body"><h3 style="margin-top:0;"><span data-icon="edit"></span> The Editor</h3>
101
+ <p class="text-muted" style="margin-bottom:0;">One editor for pages, blocks and components: tabs, colour, preview, drafts, smooth typing.</p></div>
102
+ </a>
99
103
  <a href="#/tutorials/crud" class="card" style="text-decoration:none;display:block;">
100
104
  <div class="card-body"><h3 style="margin-top:0;"><span data-icon="database"></span> Building a CRUD App</h3>
101
105
  <p class="text-muted" style="margin-bottom:0;">Collections, forms, actions and pages composed into a working app.</p></div>