kempo 4.2.33 → 4.2.35

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
+ export const registerComponentOverride=(e,o)=>{window.kempo=window.kempo||{},window.kempo.componentOverrides=window.kempo.componentOverrides||{},window.kempo.componentOverrides[e]=o};export const getComponentOverride=(e,o)=>window.kempo?.componentOverrides?.[e]||o;
2
+ //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\component-overrides.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src\\admin\\component-overrides.js","names":["registerComponentOverride","slot","tagName","window","kempo","componentOverrides","getComponentOverride","fallbackTagName"],"sources":["0"],"sourcesContent":["/*\n Lets an extension swap in its own custom element for a named admin panel slot — e.g. replacing\n the page/fragment content editor (`k-html-editor`) with a custom WYSIWYG.\n\n Backed by window.kempo rather than module-local state so registration works regardless of how\n many times this module is imported (once per admin page load, from wherever an extension's script\n runs — see docs/extensions/creating-extensions.md).\n\n A replacement for the 'page-content-editor' slot must, at minimum: accept a `.value` property\n with the initial HTML, expose a `getValue()` method returning the current HTML, and support a\n boolean `disabled` attribute. It may ignore `controls`/`mode`, which are k-html-editor-specific.\n*/\n\nexport const registerComponentOverride = (slot, tagName) => {\n window.kempo = window.kempo || {};\n window.kempo.componentOverrides = window.kempo.componentOverrides || {};\n window.kempo.componentOverrides[slot] = tagName;\n};\n\nexport const getComponentOverride = (slot, fallbackTagName) =>\n window.kempo?.componentOverrides?.[slot] || fallbackTagName;\n"],"mappings":"OAaO,MAAMA,0BAA4B,CAACC,EAAMC,KAC9CC,OAAOC,MAAQD,OAAOC,OAAS,CAAC,EAChCD,OAAOC,MAAMC,mBAAqBF,OAAOC,MAAMC,oBAAsB,CAAC,EACtEF,OAAOC,MAAMC,mBAAmBJ,GAAQC,UAGnC,MAAMI,qBAAuB,CAACL,EAAMM,IACzCJ,OAAOC,OAAOC,qBAAqBJ,IAASM","ignoreList":[]}
@@ -1,13 +1,13 @@
1
- import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,repeat as a}from"/kempo-ui/lit-all.min.js";import{getPage as n,updatePage as s,listTemplates as i,deletePages as l,listDirectories as o,movePage as r}from"/kempo/sdk.js";import c from"/kempo-ui/components/Dialog.js";import d from"/kempo-ui/components/Toast.js";import"/kempo-ui/components/Icon.js";import"/kempo-ui/components/Accordion.js";import"/kempo-ui/components/HtmlEditor.js";const m=new Set(["head","scripts"]),p=e=>e.toLowerCase().trim().replace(/[^\w\s-]/g,"").replace(/[\s_]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"");export default class g extends e{static properties={loading:{state:!0},error:{state:!0},saving:{state:!0},page:{state:!0},templates:{state:!0},contents:{state:!0}};constructor(){super(),this.loading=!0,this.error=!1,this.saving=!1,this.page=null,this.templates=[],this.contents=[],this.file=""}async connectedCallback(){if(super.connectedCallback(),document.addEventListener("keydown",this.handleKeyDown),this.file=new URLSearchParams(window.location.search).get("path")+".page.html",!this.file||".page.html"===this.file)return this.loading=!1,void(this.error=!0);const[[e,t],[,a]]=await Promise.all([n(this.file),i()]);this.loading=!1,e?this.error=!0:(this.templates=a?.templates||[],this.page=t,this.contents=this.buildContents(t.template,t.contents),document.title=`Edit: ${t.name||t.title||this.file} - Admin`)}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener("keydown",this.handleKeyDown)}getPageUrl(){return"/"+this.file.replace(/\.page\.html$/,"").split("/").filter(Boolean).join("/")}buildContents(e,t){const a=this.templates.find(t=>t.name===e),n=(a?.locations||[{name:"default",label:null}]).map(e=>"string"==typeof e?{name:e,label:null}:e),s=n.map(({name:e,label:a})=>{const n=t.find(t=>t.location===e);return{location:e,label:a||null,content:n?.content||"",orphaned:!1}});for(const e of t)n.some(t=>t.name===e.location)||s.push({location:e.location,label:null,content:e.content,orphaned:!0});return s}getFormState(){const e=this.shadowRoot,t=new Map([...e.querySelectorAll("k-html-editor[data-location]")].map(e=>[e.dataset.location,e.getValue()]));return{name:e.querySelector("#metaName").value,title:e.querySelector("#metaTitle").value,description:e.querySelector("#metaDescription").value,template:e.querySelector("#metaTemplate").value,contents:this.contents.map(({location:e,content:a,orphaned:n})=>({location:e,content:n?t.get(e)?.trim()||a:t.get(e)??a}))}}handleKeyDown=e=>{(e.ctrlKey||e.metaKey)&&"s"===e.key&&(e.preventDefault(),this.saving||this.page?.locked||this.handleSave())};handleTemplateChange=e=>{if(this.page?.locked)return;const t=e.target.value,a=new Map([...this.shadowRoot.querySelectorAll("k-html-editor[data-location]")].map(e=>[e.dataset.location,e.getValue()])),n=this.contents.map(({location:e,content:t})=>({location:e,content:a.get(e)?.trim()||t}));this.contents=this.buildContents(t,n),this.page={...this.page,template:t}};handleDelete=()=>{c.confirm("Delete this page? This action cannot be undone.",async e=>{if(!e)return;const[t]=await l([this.file]);t?d.error(t.msg||"Failed to delete page"):(d.success("Page deleted"),setTimeout(()=>{window.location.href="/admin/pages"},1e3))})};handleMove=async()=>{const[,e]=await o(),a=e?.directories||["."],n=this.file.replace(/\.page\.html$/,"").split("/"),i=n.pop(),l=n.join("/")||".",m=e=>/^\/[a-zA-Z0-9_\-\[\]\/]*$/.test(e)&&!e.includes("..")&&!e.includes("//"),g=e=>(e||"/").replace(/^\//,"").replace(/\/$/,"")||".",h="."===l?"/":"/"+l,u=c.create(t`
1
+ import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,repeat as a,staticHtml as n,unsafeStatic as s}from"/kempo-ui/lit-all.min.js";import{getPage as i,updatePage as l,listTemplates as o,deletePages as r,listDirectories as c,movePage as d}from"/kempo/sdk.js";import m from"/kempo-ui/components/Dialog.js";import p from"/kempo-ui/components/Toast.js";import{getComponentOverride as g}from"../component-overrides.js";import"/kempo-ui/components/Icon.js";import"/kempo-ui/components/Accordion.js";import"/kempo-ui/components/HtmlEditor.js";const h=new Set(["head","scripts"]),u=e=>e.toLowerCase().trim().replace(/[^\w\s-]/g,"").replace(/[\s_]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"");export default class v extends e{static properties={loading:{state:!0},error:{state:!0},saving:{state:!0},page:{state:!0},templates:{state:!0},contents:{state:!0}};constructor(){super(),this.loading=!0,this.error=!1,this.saving=!1,this.page=null,this.templates=[],this.contents=[],this.file=""}async connectedCallback(){if(super.connectedCallback(),document.addEventListener("keydown",this.handleKeyDown),this.file=new URLSearchParams(window.location.search).get("path")+".page.html",!this.file||".page.html"===this.file)return this.loading=!1,void(this.error=!0);const[[e,t],[,a]]=await Promise.all([i(this.file),o()]);this.loading=!1,e?this.error=!0:(this.templates=a?.templates||[],this.page=t,this.contents=this.buildContents(t.template,t.contents),document.title=`Edit: ${t.name||t.title||this.file} - Admin`)}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener("keydown",this.handleKeyDown)}getPageUrl(){return"/"+this.file.replace(/\.page\.html$/,"").split("/").filter(Boolean).join("/")}buildContents(e,t){const a=this.templates.find(t=>t.name===e),n=(a?.locations||[{name:"default",label:null}]).map(e=>"string"==typeof e?{name:e,label:null}:e),s=n.map(({name:e,label:a})=>{const n=t.find(t=>t.location===e);return{location:e,label:a||null,content:n?.content||"",orphaned:!1}});for(const e of t)n.some(t=>t.name===e.location)||s.push({location:e.location,label:null,content:e.content,orphaned:!0});return s}getFormState(){const e=this.shadowRoot,t=new Map([...e.querySelectorAll("[data-location]")].map(e=>[e.dataset.location,e.getValue()]));return{name:e.querySelector("#metaName").value,title:e.querySelector("#metaTitle").value,description:e.querySelector("#metaDescription").value,template:e.querySelector("#metaTemplate").value,contents:this.contents.map(({location:e,content:a,orphaned:n})=>({location:e,content:n?t.get(e)?.trim()||a:t.get(e)??a}))}}handleKeyDown=e=>{(e.ctrlKey||e.metaKey)&&"s"===e.key&&(e.preventDefault(),this.saving||this.page?.locked||this.handleSave())};handleTemplateChange=e=>{if(this.page?.locked)return;const t=e.target.value,a=new Map([...this.shadowRoot.querySelectorAll("[data-location]")].map(e=>[e.dataset.location,e.getValue()])),n=this.contents.map(({location:e,content:t})=>({location:e,content:a.get(e)?.trim()||t}));this.contents=this.buildContents(t,n),this.page={...this.page,template:t}};handleDelete=()=>{m.confirm("Delete this page? This action cannot be undone.",async e=>{if(!e)return;const[t]=await r([this.file]);t?p.error(t.msg||"Failed to delete page"):(p.success("Page deleted"),setTimeout(()=>{window.location.href="/admin/pages"},1e3))})};handleMove=async()=>{const[,e]=await c(),a=e?.directories||["."],n=this.file.replace(/\.page\.html$/,"").split("/"),s=n.pop(),i=n.join("/")||".",o=e=>/^\/[a-zA-Z0-9_\-\[\]\/]*$/.test(e)&&!e.includes("..")&&!e.includes("//"),r=e=>(e||"/").replace(/^\//,"").replace(/\/$/,"")||".",g="."===i?"/":"/"+i,h=m.create(t`
2
2
  <div class="p">
3
3
  <div class="mb">
4
4
  <label class="d-b mb-sm"><strong>Location</strong></label>
5
- <input type="text" id="dlg-move-dir" class="full" list="dlg-move-dir-list" .value="${h}" placeholder="/">
5
+ <input type="text" id="dlg-move-dir" class="full" list="dlg-move-dir-list" .value="${g}" placeholder="/">
6
6
  <datalist id="dlg-move-dir-list">
7
7
  ${a.map(e=>t`<option value="${"."===e?"/":"/"+e}">`)}
8
8
  </datalist>
9
9
  <p id="dlg-move-dir-error" class="mt-sm tc-danger d-n"><small>Invalid path — use format: /path/to/directory</small></p>
10
- <p class="mt-sm muted"><small>Supports dynamic segments e.g. <code>/blog/[slug]</code>. <button class="link" @click="${()=>c.alert(t`
10
+ <p class="mt-sm muted"><small>Supports dynamic segments e.g. <code>/blog/[slug]</code>. <button class="link" @click="${()=>m.alert(t`
11
11
  <div class="p">
12
12
  <p>Directory paths define where a page lives in the URL structure.</p>
13
13
  <p><strong>Examples:</strong></p>
@@ -23,17 +23,17 @@ import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,repeat a
23
23
  </div>
24
24
  <div>
25
25
  <label class="d-b mb-sm"><strong>Page Name</strong></label>
26
- <input type="text" id="dlg-move-name" class="full" .value="${i}">
26
+ <input type="text" id="dlg-move-name" class="full" .value="${s}">
27
27
  <p class="mt-sm muted"><small id="dlg-move-preview"></small></p>
28
28
  </div>
29
29
  </div>
30
- `,{title:"Move Page",confirmText:"Save and Move Page",confirmAction:async()=>{const e=u.querySelector("#dlg-move-dir").value.trim()||"/",t=p(u.querySelector("#dlg-move-name").value.trim());if(!t)return void d.error("Page name is required");if(!m(e))return void d.error("Invalid directory path");const a=g(e),n="."===a?`${t}.page.html`:`${a}/${t}.page.html`;this.saving=!0;const[i]=await s({file:this.file,...this.getFormState()});if(this.saving=!1,i)return void d.error(i.msg||"Failed to save page");const[l,o]=await r({file:this.file,newFile:n});l?d.error(l.msg||"Failed to move page"):(d.success("Page saved and moved"),setTimeout(()=>{window.location.href=`/admin/content/pages/edit?path=${encodeURIComponent(o.file.replace(/\.page\.html$/,""))}`},1e3))},cancelText:"Cancel"}),v=()=>{const e=g(u.querySelector("#dlg-move-dir").value||"/"),t=p(u.querySelector("#dlg-move-name").value),a="."===e?"/":"/"+e+"/";u.querySelector("#dlg-move-preview").textContent=t?a+t:""};u.querySelector("#dlg-move-dir").addEventListener("input",e=>{u.querySelector("#dlg-move-dir-error").classList.toggle("d-n",m(e.target.value||"/")),v()}),u.querySelector("#dlg-move-name").addEventListener("input",v),v()};handleReset=()=>{c.confirm("Reset this page? Any unsaved changes will be lost.",e=>{e&&(this.contents=this.buildContents(this.page.template,this.page.contents),this.page={...this.page})})};handleSave=async()=>{this.saving=!0;const e=this.getFormState(),[t,a]=await s({file:this.file,...e});this.saving=!1,t?d.error(t.msg||"Failed to save page"):(this.contents=this.contents.map(t=>{const a=e.contents.find(e=>e.location===t.location);return a?{...t,content:a.content}:t}),this.page={...this.page,...e,contents:e.contents.filter(e=>e.content.trim()),updatedAt:a.updatedAt},document.title=`Edit: ${e.name||e.title||this.file} - Admin`,d.success("Page saved"))};render(){if(this.loading)return t`<div>Loading page...</div>`;if(this.error)return t`
30
+ `,{title:"Move Page",confirmText:"Save and Move Page",confirmAction:async()=>{const e=h.querySelector("#dlg-move-dir").value.trim()||"/",t=u(h.querySelector("#dlg-move-name").value.trim());if(!t)return void p.error("Page name is required");if(!o(e))return void p.error("Invalid directory path");const a=r(e),n="."===a?`${t}.page.html`:`${a}/${t}.page.html`;this.saving=!0;const[s]=await l({file:this.file,...this.getFormState()});if(this.saving=!1,s)return void p.error(s.msg||"Failed to save page");const[i,c]=await d({file:this.file,newFile:n});i?p.error(i.msg||"Failed to move page"):(p.success("Page saved and moved"),setTimeout(()=>{window.location.href=`/admin/content/pages/edit?path=${encodeURIComponent(c.file.replace(/\.page\.html$/,""))}`},1e3))},cancelText:"Cancel"}),v=()=>{const e=r(h.querySelector("#dlg-move-dir").value||"/"),t=u(h.querySelector("#dlg-move-name").value),a="."===e?"/":"/"+e+"/";h.querySelector("#dlg-move-preview").textContent=t?a+t:""};h.querySelector("#dlg-move-dir").addEventListener("input",e=>{h.querySelector("#dlg-move-dir-error").classList.toggle("d-n",o(e.target.value||"/")),v()}),h.querySelector("#dlg-move-name").addEventListener("input",v),v()};handleReset=()=>{m.confirm("Reset this page? Any unsaved changes will be lost.",e=>{e&&(this.contents=this.buildContents(this.page.template,this.page.contents),this.page={...this.page})})};handleSave=async()=>{this.saving=!0;const e=this.getFormState(),[t,a]=await l({file:this.file,...e});this.saving=!1,t?p.error(t.msg||"Failed to save page"):(this.contents=this.contents.map(t=>{const a=e.contents.find(e=>e.location===t.location);return a?{...t,content:a.content}:t}),this.page={...this.page,...e,contents:e.contents.filter(e=>e.content.trim()),updatedAt:a.updatedAt},document.title=`Edit: ${e.name||e.title||this.file} - Admin`,p.success("Page saved"))};render(){if(this.loading)return t`<div>Loading page...</div>`;if(this.error)return t`
31
31
  <div>
32
32
  <p>Page not found.</p>
33
33
  <a href="/admin/pages">Back to Pages</a>
34
34
  </div>
35
- `;const{page:e,templates:n,contents:s}=this,i="system"===e.owner,l=e.locked;return t`
36
- ${l?t`
35
+ `;const{page:e,templates:i,contents:l}=this,o="system"===e.owner,r=e.locked;return t`
36
+ ${r?t`
37
37
  <div class="p r mb bc-warning tc-warning">
38
38
  <k-icon name="lock"></k-icon> This page is locked and cannot be edited. ${"custom"===e.owner?"Locked by developer":`Managed by: ${e.owner||"external system"}`}
39
39
  </div>
@@ -49,13 +49,13 @@ import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,repeat a
49
49
  </div>
50
50
 
51
51
  <div class="flex"></div>
52
- ${i||l?"":t`
52
+ ${o||r?"":t`
53
53
  <div class="btn-grp mrh mb">
54
54
  <button class="danger" @click="${this.handleDelete}"><k-icon name="delete"></k-icon> Delete</button>
55
55
  <button @click="${this.handleMove}"><k-icon name="drive_file_move"></k-icon> Move Page</button>
56
56
  </div>
57
57
  `}
58
- ${l?"":t`
58
+ ${r?"":t`
59
59
  <div class="btn-grp mrh mb">
60
60
  <button @click="${this.handleReset}"><k-icon name="restart_alt"></k-icon> Reset</button>
61
61
  <button id="saveBtn" class="primary" ?disabled="${this.saving}" @click="${this.handleSave}">
@@ -74,17 +74,17 @@ import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,repeat a
74
74
  <div class="p">
75
75
  <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
76
76
  <label style="min-width: 120px;"><strong>Name</strong></label>
77
- <input type="text" id="metaName" class="flex" .value="${e.name||""}" ?disabled="${i||l}">
77
+ <input type="text" id="metaName" class="flex" .value="${e.name||""}" ?disabled="${o||r}">
78
78
  </div>
79
79
  <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
80
80
  <label style="min-width: 120px;"><strong>Title</strong></label>
81
- <input type="text" id="metaTitle" class="flex" .value="${e.title||""}" ?disabled="${l}">
81
+ <input type="text" id="metaTitle" class="flex" .value="${e.title||""}" ?disabled="${r}">
82
82
  </div>
83
83
  <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
84
84
  <label style="min-width: 120px;"><strong>Description</strong></label>
85
- <input type="text" id="metaDescription" class="flex" .value="${e.description||""}" ?disabled="${i||l}">
85
+ <input type="text" id="metaDescription" class="flex" .value="${e.description||""}" ?disabled="${o||r}">
86
86
  </div>
87
- ${i?"":t`
87
+ ${o?"":t`
88
88
  <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
89
89
  <label style="min-width: 120px;"><strong>Author</strong></label>
90
90
  <span class="muted">${e.author||""}</span>
@@ -92,10 +92,10 @@ import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,repeat a
92
92
  `}
93
93
  <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
94
94
  <label style="min-width: 120px;"><strong>Template</strong></label>
95
- ${(()=>{const a=this.file.replace(/\.page\.html$/,"").split("/").slice(0,-1).join("/")||".",s=n.filter(e=>{const t="."===e.directory?"":e.directory,n="."===a?"":a;return""===t||n===t||n.startsWith(t+"/")}),i=s.some(t=>t.name===e.template);return t`
96
- <select id="metaTemplate" class="flex" @change="${this.handleTemplateChange}" ?disabled="${l}">
97
- ${i?"":t`<option value="${e.template}" selected>${e.template} (Not Found, using default)</option>`}
98
- ${s.map(a=>t`
95
+ ${(()=>{const a=this.file.replace(/\.page\.html$/,"").split("/").slice(0,-1).join("/")||".",n=i.filter(e=>{const t="."===e.directory?"":e.directory,n="."===a?"":a;return""===t||n===t||n.startsWith(t+"/")}),s=n.some(t=>t.name===e.template);return t`
96
+ <select id="metaTemplate" class="flex" @change="${this.handleTemplateChange}" ?disabled="${r}">
97
+ ${s?"":t`<option value="${e.template}" selected>${e.template} (Not Found, using default)</option>`}
98
+ ${n.map(a=>t`
99
99
  <option value="${a.name}" ?selected="${a.name===e.template}">${(e=>"."===e.directory?`/${e.name}`:`/${e.directory}/${e.name}`)(a)}</option>
100
100
  `)}
101
101
  </select>
@@ -129,24 +129,24 @@ import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,repeat a
129
129
  </div>
130
130
  </k-accordion-panel>
131
131
  `:""}
132
- ${a(s,e=>e.location,({location:e,label:a,content:n,orphaned:s})=>{const i="default"===e,o=a||(i?"Content":e.slice(0,1).toUpperCase()+e.slice(1));return t`
133
- <k-accordion-header for-panel="content-${e}" ?active="${i}">
134
- ${o}${s?t` <span class="tc-warning"><small>(not in template)</small></span>`:""}
132
+ ${a(l,e=>e.location,({location:e,label:a,content:i,orphaned:l})=>{const o="default"===e,c=a||(o?"Content":e.slice(0,1).toUpperCase()+e.slice(1)),d=s(g("page-content-editor","k-html-editor"));return t`
133
+ <k-accordion-header for-panel="content-${e}" ?active="${o}">
134
+ ${c}${l?t` <span class="tc-warning"><small>(not in template)</small></span>`:""}
135
135
  </k-accordion-header>
136
- <k-accordion-panel name="content-${e}" ?active="${i}">
136
+ <k-accordion-panel name="content-${e}" ?active="${o}">
137
137
  <div class="p">
138
- <k-html-editor
138
+ ${n`<${d}
139
139
  class="r b"
140
140
  data-location="${e}"
141
141
  controls="full"
142
- .value="${n}"
143
- mode="${m.has(e)?"code":"visual"}"
142
+ .value="${i}"
143
+ mode="${h.has(e)?"code":"visual"}"
144
144
  style="height: 400px;"
145
- ?disabled="${l}"
146
- ></k-html-editor>
145
+ ?disabled="${r}"
146
+ ></${d}>`}
147
147
  </div>
148
148
  </k-accordion-panel>
149
149
  `})}
150
150
  </k-accordion>
151
- `}}customElements.define("admin-page-editor",g);
151
+ `}}customElements.define("admin-page-editor",v);
152
152
  //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\PageEditor.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"src\\admin\\components\\PageEditor.js","names":["ShadowComponent","html","repeat","getPage","updatePage","listTemplates","deletePages","listDirectories","movePage","Dialog","Toast","CODE_MODE_LOCATIONS","Set","slugify","name","toLowerCase","trim","replace","PageEditor","static","loading","state","error","saving","page","templates","contents","constructor","super","this","file","connectedCallback","document","addEventListener","handleKeyDown","URLSearchParams","window","location","search","get","err","tmplData","Promise","all","buildContents","template","title","disconnectedCallback","removeEventListener","getPageUrl","split","filter","Boolean","join","templateName","existingContents","tmpl","find","t","locations","label","map","l","result","block","b","content","orphaned","some","push","getFormState","root","shadowRoot","editorMap","Map","querySelectorAll","ed","dataset","getValue","querySelector","value","description","e","ctrlKey","metaKey","key","preventDefault","locked","handleSave","handleTemplateChange","newTemplate","target","currentContents","handleDelete","confirm","async","confirmed","msg","success","setTimeout","href","handleMove","dirData","directories","parts","currentName","pop","currentDir","isValidDir","p","test","includes","dirToInternal","currentDirDisplay","$dialog","create","d","alert","confirmText","confirmAction","dirInput","newName","newDir","newFile","saveError","moveError","moved","encodeURIComponent","cancelText","updatePreview","dir","prefix","textContent","classList","toggle","handleReset","saved","c","current","x","updatedAt","render","isSystem","owner","isLocked","author","pageDir","slice","reachable","tDir","directory","pDir","startsWith","currentFound","templateLabel","createdAt","Date","toLocaleString","Object","keys","extraMetadata","length","entries","isDefault","headerTitle","toUpperCase","has","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html, repeat } from '/kempo-ui/lit-all.min.js';\nimport { getPage, updatePage, listTemplates, deletePages, listDirectories, movePage } from '/kempo/sdk.js';\nimport Dialog from '/kempo-ui/components/Dialog.js';\nimport Toast from '/kempo-ui/components/Toast.js';\nimport '/kempo-ui/components/Icon.js';\nimport '/kempo-ui/components/Accordion.js';\nimport '/kempo-ui/components/HtmlEditor.js';\n\nconst CODE_MODE_LOCATIONS = new Set(['head', 'scripts']);\n\nconst slugify = name => name\n .toLowerCase()\n .trim()\n .replace(/[^\\w\\s-]/g, '')\n .replace(/[\\s_]+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n\nexport default class PageEditor extends ShadowComponent {\n static properties = {\n loading: { state: true },\n error: { state: true },\n saving: { state: true },\n page: { state: true },\n templates: { state: true },\n contents: { state: true }\n };\n\n constructor() {\n super();\n this.loading = true;\n this.error = false;\n this.saving = false;\n this.page = null;\n this.templates = [];\n this.contents = [];\n this.file = '';\n }\n\n async connectedCallback() {\n super.connectedCallback();\n document.addEventListener('keydown', this.handleKeyDown);\n this.file = new URLSearchParams(window.location.search).get('path') + '.page.html';\n\n if (!this.file || this.file === '.page.html') {\n this.loading = false;\n this.error = true;\n return;\n }\n\n const [[err, page], [, tmplData]] = await Promise.all([\n getPage(this.file),\n listTemplates()\n ]);\n\n this.loading = false;\n\n if (err) {\n this.error = true;\n return;\n }\n\n this.templates = tmplData?.templates || [];\n this.page = page;\n this.contents = this.buildContents(page.template, page.contents);\n document.title = `Edit: ${page.name || page.title || this.file} - Admin`;\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n document.removeEventListener('keydown', this.handleKeyDown);\n }\n\n /*\n Helpers\n */\n\n getPageUrl() {\n const path = this.file.replace(/\\.page\\.html$/, '');\n return '/' + path.split('/').filter(Boolean).join('/');\n }\n\n buildContents(templateName, existingContents) {\n const tmpl = this.templates.find(t => t.name === templateName);\n const raw = tmpl?.locations || [{ name: 'default', label: null }];\n const locations = raw.map(l => typeof l === 'string' ? { name: l, label: null } : l);\n const result = locations.map(({ name, label }) => {\n const block = existingContents.find(b => b.location === name);\n return { location: name, label: label || null, content: block?.content || '', orphaned: false };\n });\n for (const block of existingContents) {\n if (!locations.some(l => l.name === block.location))\n result.push({ location: block.location, label: null, content: block.content, orphaned: true });\n }\n return result;\n }\n\n getFormState() {\n const root = this.shadowRoot;\n const editorMap = new Map(\n [...root.querySelectorAll('k-html-editor[data-location]')].map(ed => [ed.dataset.location, ed.getValue()])\n );\n return {\n name: root.querySelector('#metaName').value,\n title: root.querySelector('#metaTitle').value,\n description: root.querySelector('#metaDescription').value,\n template: root.querySelector('#metaTemplate').value,\n contents: this.contents.map(({ location, content, orphaned }) => ({\n location,\n content: orphaned\n ? (editorMap.get(location)?.trim() || content)\n : (editorMap.get(location) ?? content)\n }))\n };\n }\n\n /*\n Event Handlers\n */\n\n handleKeyDown = e => {\n if((e.ctrlKey || e.metaKey) && e.key === 's'){\n e.preventDefault();\n if(!this.saving && !this.page?.locked) this.handleSave();\n }\n };\n\n handleTemplateChange = e => {\n if(this.page?.locked) return;\n const newTemplate = e.target.value;\n const editorMap = new Map(\n [...this.shadowRoot.querySelectorAll('k-html-editor[data-location]')].map(ed => [ed.dataset.location, ed.getValue()])\n );\n // Fall back to state content for any editor that hasn't initialized (Monaco lazy-loads)\n const currentContents = this.contents.map(({ location, content }) => ({\n location,\n content: editorMap.get(location)?.trim() || content\n }));\n this.contents = this.buildContents(newTemplate, currentContents);\n this.page = { ...this.page, template: newTemplate };\n };\n\n handleDelete = () => {\n Dialog.confirm('Delete this page? This action cannot be undone.', async confirmed => {\n if (!confirmed) return;\n const [error] = await deletePages([this.file]);\n if (error) {\n Toast.error(error.msg || 'Failed to delete page');\n return;\n }\n Toast.success('Page deleted');\n setTimeout(() => { window.location.href = '/admin/pages'; }, 1000);\n });\n };\n\n handleMove = async () => {\n const [, dirData] = await listDirectories();\n const directories = dirData?.directories || ['.'];\n\n const parts = this.file.replace(/\\.page\\.html$/, '').split('/');\n const currentName = parts.pop();\n const currentDir = parts.join('/') || '.';\n\n const isValidDir = p => /^\\/[a-zA-Z0-9_\\-\\[\\]\\/]*$/.test(p) && !p.includes('..') && !p.includes('//');\n const dirToInternal = p => { const t = (p || '/').replace(/^\\//, '').replace(/\\/$/, ''); return t || '.'; };\n const currentDirDisplay = currentDir === '.' ? '/' : '/' + currentDir;\n\n const openLearnMore = () => Dialog.alert(html`\n <div class=\"p\">\n <p>Directory paths define where a page lives in the URL structure.</p>\n <p><strong>Examples:</strong></p>\n <ul>\n <li><code>/</code> — root, e.g. <code>/about</code></li>\n <li><code>/blog</code> — e.g. <code>/blog/my-post</code></li>\n <li><code>/blog/[slug]</code> — dynamic segment; <code>[slug]</code> matches any value</li>\n </ul>\n <p>Dynamic segments like <code>[token]</code> allow a single page to handle many URLs. The segment name is available to the page as a URL parameter.</p>\n <p>You can nest segments: <code>/store/[category]/[id]</code></p>\n </div>\n `, null, { title: 'About Directory Paths' });\n\n const $dialog = Dialog.create(html`\n <div class=\"p\">\n <div class=\"mb\">\n <label class=\"d-b mb-sm\"><strong>Location</strong></label>\n <input type=\"text\" id=\"dlg-move-dir\" class=\"full\" list=\"dlg-move-dir-list\" .value=\"${currentDirDisplay}\" placeholder=\"/\">\n <datalist id=\"dlg-move-dir-list\">\n ${directories.map(d => html`<option value=\"${d === '.' ? '/' : '/' + d}\">`)} \n </datalist>\n <p id=\"dlg-move-dir-error\" class=\"mt-sm tc-danger d-n\"><small>Invalid path — use format: /path/to/directory</small></p>\n <p class=\"mt-sm muted\"><small>Supports dynamic segments e.g. <code>/blog/[slug]</code>. <button class=\"link\" @click=\"${openLearnMore}\">Learn more</button></small></p>\n </div>\n <div>\n <label class=\"d-b mb-sm\"><strong>Page Name</strong></label>\n <input type=\"text\" id=\"dlg-move-name\" class=\"full\" .value=\"${currentName}\">\n <p class=\"mt-sm muted\"><small id=\"dlg-move-preview\"></small></p>\n </div>\n </div>\n `, {\n title: 'Move Page',\n confirmText: 'Save and Move Page',\n confirmAction: async () => {\n const dirInput = $dialog.querySelector('#dlg-move-dir').value.trim() || '/';\n const newName = slugify($dialog.querySelector('#dlg-move-name').value.trim());\n if (!newName) {\n Toast.error('Page name is required');\n return;\n }\n if (!isValidDir(dirInput)) {\n Toast.error('Invalid directory path');\n return;\n }\n const newDir = dirToInternal(dirInput);\n const newFile = newDir === '.'\n ? `${newName}.page.html`\n : `${newDir}/${newName}.page.html`;\n\n this.saving = true;\n const [saveError] = await updatePage({ file: this.file, ...this.getFormState() });\n this.saving = false;\n if (saveError) {\n Toast.error(saveError.msg || 'Failed to save page');\n return;\n }\n\n const [moveError, moved] = await movePage({ file: this.file, newFile });\n if (moveError) {\n Toast.error(moveError.msg || 'Failed to move page');\n return;\n }\n\n Toast.success('Page saved and moved');\n setTimeout(() => {\n window.location.href = `/admin/content/pages/edit?path=${encodeURIComponent(moved.file.replace(/\\.page\\.html$/, ''))}`;\n }, 1000);\n },\n cancelText: 'Cancel'\n });\n\n const updatePreview = () => {\n const dir = dirToInternal($dialog.querySelector('#dlg-move-dir').value || '/');\n const name = slugify($dialog.querySelector('#dlg-move-name').value);\n const prefix = dir === '.' ? '/' : '/' + dir + '/';\n $dialog.querySelector('#dlg-move-preview').textContent = name ? prefix + name : '';\n };\n $dialog.querySelector('#dlg-move-dir').addEventListener('input', e => {\n $dialog.querySelector('#dlg-move-dir-error').classList.toggle('d-n', isValidDir(e.target.value || '/'));\n updatePreview();\n });\n $dialog.querySelector('#dlg-move-name').addEventListener('input', updatePreview);\n updatePreview();\n };\n\n handleReset = () => {\n Dialog.confirm('Reset this page? Any unsaved changes will be lost.', confirmed => {\n if (!confirmed) return;\n this.contents = this.buildContents(this.page.template, this.page.contents);\n this.page = { ...this.page };\n });\n };\n\n handleSave = async () => {\n this.saving = true;\n const state = this.getFormState();\n const [saveError, saved] = await updatePage({ file: this.file, ...state });\n this.saving = false;\n\n if (saveError) {\n Toast.error(saveError.msg || 'Failed to save page');\n return;\n }\n\n this.contents = this.contents.map(c => {\n const current = state.contents.find(x => x.location === c.location);\n return current ? { ...c, content: current.content } : c;\n });\n this.page = {\n ...this.page,\n ...state,\n contents: state.contents.filter(b => b.content.trim()),\n updatedAt: saved.updatedAt\n };\n\n document.title = `Edit: ${state.name || state.title || this.file} - Admin`;\n Toast.success('Page saved');\n };\n\n /*\n Render\n */\n\n render() {\n if (this.loading) return html`<div>Loading page...</div>`;\n\n if (this.error) return html`\n <div>\n <p>Page not found.</p>\n <a href=\"/admin/pages\">Back to Pages</a>\n </div>\n `;\n\n const { page, templates, contents } = this;\n const isSystem = page.owner === 'system';\n const isLocked = page.locked;\n\n return html`\n ${isLocked ? html`\n <div class=\"p r mb bc-warning tc-warning\">\n <k-icon name=\"lock\"></k-icon> This page is locked and cannot be edited. ${page.owner === 'custom' ? 'Locked by developer' : `Managed by: ${page.owner || 'external system'}`}\n </div>\n ` : ''}\n <div class=\"d-f mb\">\n <div class=\"btn-grp mrh mb\">\n <a href=\"/admin/content/pages\" class=\"btn\">\n <k-icon name=\"arrow\" direction=\"left\"></k-icon> Pages\n </a>\n <a href=\"${this.getPageUrl()}\" id=\"page-link\" class=\"btn\">\n <k-icon name=\"visibility\"></k-icon> View Page\n </a>\n </div>\n \n <div class=\"flex\"></div>\n ${!isSystem && !isLocked ? html`\n <div class=\"btn-grp mrh mb\">\n <button class=\"danger\" @click=\"${this.handleDelete}\"><k-icon name=\"delete\"></k-icon> Delete</button>\n <button @click=\"${this.handleMove}\"><k-icon name=\"drive_file_move\"></k-icon> Move Page</button>\n </div>\n ` : ''}\n ${!isLocked ? html`\n <div class=\"btn-grp mrh mb\">\n <button @click=\"${this.handleReset}\"><k-icon name=\"restart_alt\"></k-icon> Reset</button>\n <button id=\"saveBtn\" class=\"primary\" ?disabled=\"${this.saving}\" @click=\"${this.handleSave}\">\n <k-icon name=\"save\"></k-icon> Save\n </button>\n </div>\n ` : ''}\n </div>\n <h1 class=\"mb0\">Page: ${page.name || page.title || this.file}</h1>\n\n <a href=\"${this.getPageUrl()}\" class=\"d-b mb\"><code>${this.getPageUrl()}</code></a>\n\n <k-accordion>\n <k-accordion-header for-panel=\"metadata\">Metadata</k-accordion-header>\n <k-accordion-panel name=\"metadata\">\n <div class=\"p\">\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\"><strong>Name</strong></label>\n <input type=\"text\" id=\"metaName\" class=\"flex\" .value=\"${page.name || ''}\" ?disabled=\"${isSystem || isLocked}\">\n </div>\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\"><strong>Title</strong></label>\n <input type=\"text\" id=\"metaTitle\" class=\"flex\" .value=\"${page.title || ''}\" ?disabled=\"${isLocked}\">\n </div>\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\"><strong>Description</strong></label>\n <input type=\"text\" id=\"metaDescription\" class=\"flex\" .value=\"${page.description || ''}\" ?disabled=\"${isSystem || isLocked}\">\n </div>\n ${!isSystem ? html`\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\"><strong>Author</strong></label>\n <span class=\"muted\">${page.author || ''}</span>\n </div>\n ` : ''}\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\"><strong>Template</strong></label>\n ${(() => {\n const pageDir = this.file.replace(/\\.page\\.html$/, '').split('/').slice(0, -1).join('/') || '.';\n const reachable = templates.filter(t => {\n const tDir = t.directory === '.' ? '' : t.directory;\n const pDir = pageDir === '.' ? '' : pageDir;\n return tDir === '' || pDir === tDir || pDir.startsWith(tDir + '/');\n });\n const templateLabel = t => t.directory === '.' ? `/${t.name}` : `/${t.directory}/${t.name}`;\n const currentFound = reachable.some(t => t.name === page.template);\n return html`\n <select id=\"metaTemplate\" class=\"flex\" @change=\"${this.handleTemplateChange}\" ?disabled=\"${isLocked}\">\n ${!currentFound ? html`<option value=\"${page.template}\" selected>${page.template} (Not Found, using default)</option>` : ''}\n ${reachable.map(t => html`\n <option value=\"${t.name}\" ?selected=\"${t.name === page.template}\">${templateLabel(t)}</option>\n `)}\n </select>\n `;\n })()}\n </div>\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\"><strong>Owner</strong></label>\n <span class=\"muted\">${page.owner || ''}</span>\n </div>\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\"><strong>Created</strong></label>\n <span class=\"muted\">${page.createdAt ? new Date(page.createdAt).toLocaleString() : ''}</span>\n </div>\n <div class=\"d-f\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\"><strong>Updated</strong></label>\n <span class=\"muted\">${page.updatedAt ? new Date(page.updatedAt).toLocaleString() : ''}</span>\n </div>\n </div>\n </k-accordion-panel>\n ${Object.keys(page.extraMetadata || {}).length ? html`\n <k-accordion-header for-panel=\"extra-metadata\">Extra Metadata</k-accordion-header>\n <k-accordion-panel name=\"extra-metadata\">\n <div class=\"p\">\n ${Object.entries(page.extraMetadata).map(([key, value]) => html`\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\" class=\"muted\"><strong>${key}</strong></label>\n <span>${value}</span>\n </div>\n `)}\n <p class=\"muted mt0\"><small>Extra metadata is managed by extensions and cannot be edited here.</small></p>\n </div>\n </k-accordion-panel>\n ` : ''}\n ${repeat(contents, c => c.location, ({ location, label, content, orphaned }) => {\n const isDefault = location === 'default';\n const headerTitle = label || (isDefault ? 'Content' : location.slice(0, 1).toUpperCase() + location.slice(1));\n return html`\n <k-accordion-header for-panel=\"content-${location}\" ?active=\"${isDefault}\">\n ${headerTitle}${orphaned ? html` <span class=\"tc-warning\"><small>(not in template)</small></span>` : ''}\n </k-accordion-header>\n <k-accordion-panel name=\"content-${location}\" ?active=\"${isDefault}\">\n <div class=\"p\">\n <k-html-editor\n class=\"r b\"\n data-location=\"${location}\"\n controls=\"full\"\n .value=\"${content}\"\n mode=\"${CODE_MODE_LOCATIONS.has(location) ? 'code' : 'visual'}\"\n style=\"height: 400px;\"\n ?disabled=\"${isLocked}\"\n ></k-html-editor>\n </div>\n </k-accordion-panel>\n `;\n })}\n </k-accordion>\n `;\n }\n}\n\ncustomElements.define('admin-page-editor', PageEditor);\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"src\\admin\\components\\PageEditor.js","names":["ShadowComponent","html","repeat","staticHtml","unsafeStatic","getPage","updatePage","listTemplates","deletePages","listDirectories","movePage","Dialog","Toast","getComponentOverride","CODE_MODE_LOCATIONS","Set","slugify","name","toLowerCase","trim","replace","PageEditor","static","loading","state","error","saving","page","templates","contents","constructor","super","this","file","connectedCallback","document","addEventListener","handleKeyDown","URLSearchParams","window","location","search","get","err","tmplData","Promise","all","buildContents","template","title","disconnectedCallback","removeEventListener","getPageUrl","split","filter","Boolean","join","templateName","existingContents","tmpl","find","t","locations","label","map","l","result","block","b","content","orphaned","some","push","getFormState","root","shadowRoot","editorMap","Map","querySelectorAll","ed","dataset","getValue","querySelector","value","description","e","ctrlKey","metaKey","key","preventDefault","locked","handleSave","handleTemplateChange","newTemplate","target","currentContents","handleDelete","confirm","async","confirmed","msg","success","setTimeout","href","handleMove","dirData","directories","parts","currentName","pop","currentDir","isValidDir","p","test","includes","dirToInternal","currentDirDisplay","$dialog","create","d","alert","confirmText","confirmAction","dirInput","newName","newDir","newFile","saveError","moveError","moved","encodeURIComponent","cancelText","updatePreview","dir","prefix","textContent","classList","toggle","handleReset","saved","c","current","x","updatedAt","render","isSystem","owner","isLocked","author","pageDir","slice","reachable","tDir","directory","pDir","startsWith","currentFound","templateLabel","createdAt","Date","toLocaleString","Object","keys","extraMetadata","length","entries","isDefault","headerTitle","toUpperCase","editorTag","has","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html, repeat, staticHtml, unsafeStatic } from '/kempo-ui/lit-all.min.js';\nimport { getPage, updatePage, listTemplates, deletePages, listDirectories, movePage } from '/kempo/sdk.js';\nimport Dialog from '/kempo-ui/components/Dialog.js';\nimport Toast from '/kempo-ui/components/Toast.js';\nimport { getComponentOverride } from '../component-overrides.js';\nimport '/kempo-ui/components/Icon.js';\nimport '/kempo-ui/components/Accordion.js';\nimport '/kempo-ui/components/HtmlEditor.js';\n\nconst CODE_MODE_LOCATIONS = new Set(['head', 'scripts']);\n\nconst slugify = name => name\n .toLowerCase()\n .trim()\n .replace(/[^\\w\\s-]/g, '')\n .replace(/[\\s_]+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n\nexport default class PageEditor extends ShadowComponent {\n static properties = {\n loading: { state: true },\n error: { state: true },\n saving: { state: true },\n page: { state: true },\n templates: { state: true },\n contents: { state: true }\n };\n\n constructor() {\n super();\n this.loading = true;\n this.error = false;\n this.saving = false;\n this.page = null;\n this.templates = [];\n this.contents = [];\n this.file = '';\n }\n\n async connectedCallback() {\n super.connectedCallback();\n document.addEventListener('keydown', this.handleKeyDown);\n this.file = new URLSearchParams(window.location.search).get('path') + '.page.html';\n\n if (!this.file || this.file === '.page.html') {\n this.loading = false;\n this.error = true;\n return;\n }\n\n const [[err, page], [, tmplData]] = await Promise.all([\n getPage(this.file),\n listTemplates()\n ]);\n\n this.loading = false;\n\n if (err) {\n this.error = true;\n return;\n }\n\n this.templates = tmplData?.templates || [];\n this.page = page;\n this.contents = this.buildContents(page.template, page.contents);\n document.title = `Edit: ${page.name || page.title || this.file} - Admin`;\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n document.removeEventListener('keydown', this.handleKeyDown);\n }\n\n /*\n Helpers\n */\n\n getPageUrl() {\n const path = this.file.replace(/\\.page\\.html$/, '');\n return '/' + path.split('/').filter(Boolean).join('/');\n }\n\n buildContents(templateName, existingContents) {\n const tmpl = this.templates.find(t => t.name === templateName);\n const raw = tmpl?.locations || [{ name: 'default', label: null }];\n const locations = raw.map(l => typeof l === 'string' ? { name: l, label: null } : l);\n const result = locations.map(({ name, label }) => {\n const block = existingContents.find(b => b.location === name);\n return { location: name, label: label || null, content: block?.content || '', orphaned: false };\n });\n for (const block of existingContents) {\n if (!locations.some(l => l.name === block.location))\n result.push({ location: block.location, label: null, content: block.content, orphaned: true });\n }\n return result;\n }\n\n getFormState() {\n const root = this.shadowRoot;\n const editorMap = new Map(\n [...root.querySelectorAll('[data-location]')].map(ed => [ed.dataset.location, ed.getValue()])\n );\n return {\n name: root.querySelector('#metaName').value,\n title: root.querySelector('#metaTitle').value,\n description: root.querySelector('#metaDescription').value,\n template: root.querySelector('#metaTemplate').value,\n contents: this.contents.map(({ location, content, orphaned }) => ({\n location,\n content: orphaned\n ? (editorMap.get(location)?.trim() || content)\n : (editorMap.get(location) ?? content)\n }))\n };\n }\n\n /*\n Event Handlers\n */\n\n handleKeyDown = e => {\n if((e.ctrlKey || e.metaKey) && e.key === 's'){\n e.preventDefault();\n if(!this.saving && !this.page?.locked) this.handleSave();\n }\n };\n\n handleTemplateChange = e => {\n if(this.page?.locked) return;\n const newTemplate = e.target.value;\n const editorMap = new Map(\n [...this.shadowRoot.querySelectorAll('[data-location]')].map(ed => [ed.dataset.location, ed.getValue()])\n );\n // Fall back to state content for any editor that hasn't initialized (Monaco lazy-loads)\n const currentContents = this.contents.map(({ location, content }) => ({\n location,\n content: editorMap.get(location)?.trim() || content\n }));\n this.contents = this.buildContents(newTemplate, currentContents);\n this.page = { ...this.page, template: newTemplate };\n };\n\n handleDelete = () => {\n Dialog.confirm('Delete this page? This action cannot be undone.', async confirmed => {\n if (!confirmed) return;\n const [error] = await deletePages([this.file]);\n if (error) {\n Toast.error(error.msg || 'Failed to delete page');\n return;\n }\n Toast.success('Page deleted');\n setTimeout(() => { window.location.href = '/admin/pages'; }, 1000);\n });\n };\n\n handleMove = async () => {\n const [, dirData] = await listDirectories();\n const directories = dirData?.directories || ['.'];\n\n const parts = this.file.replace(/\\.page\\.html$/, '').split('/');\n const currentName = parts.pop();\n const currentDir = parts.join('/') || '.';\n\n const isValidDir = p => /^\\/[a-zA-Z0-9_\\-\\[\\]\\/]*$/.test(p) && !p.includes('..') && !p.includes('//');\n const dirToInternal = p => { const t = (p || '/').replace(/^\\//, '').replace(/\\/$/, ''); return t || '.'; };\n const currentDirDisplay = currentDir === '.' ? '/' : '/' + currentDir;\n\n const openLearnMore = () => Dialog.alert(html`\n <div class=\"p\">\n <p>Directory paths define where a page lives in the URL structure.</p>\n <p><strong>Examples:</strong></p>\n <ul>\n <li><code>/</code> — root, e.g. <code>/about</code></li>\n <li><code>/blog</code> — e.g. <code>/blog/my-post</code></li>\n <li><code>/blog/[slug]</code> — dynamic segment; <code>[slug]</code> matches any value</li>\n </ul>\n <p>Dynamic segments like <code>[token]</code> allow a single page to handle many URLs. The segment name is available to the page as a URL parameter.</p>\n <p>You can nest segments: <code>/store/[category]/[id]</code></p>\n </div>\n `, null, { title: 'About Directory Paths' });\n\n const $dialog = Dialog.create(html`\n <div class=\"p\">\n <div class=\"mb\">\n <label class=\"d-b mb-sm\"><strong>Location</strong></label>\n <input type=\"text\" id=\"dlg-move-dir\" class=\"full\" list=\"dlg-move-dir-list\" .value=\"${currentDirDisplay}\" placeholder=\"/\">\n <datalist id=\"dlg-move-dir-list\">\n ${directories.map(d => html`<option value=\"${d === '.' ? '/' : '/' + d}\">`)} \n </datalist>\n <p id=\"dlg-move-dir-error\" class=\"mt-sm tc-danger d-n\"><small>Invalid path — use format: /path/to/directory</small></p>\n <p class=\"mt-sm muted\"><small>Supports dynamic segments e.g. <code>/blog/[slug]</code>. <button class=\"link\" @click=\"${openLearnMore}\">Learn more</button></small></p>\n </div>\n <div>\n <label class=\"d-b mb-sm\"><strong>Page Name</strong></label>\n <input type=\"text\" id=\"dlg-move-name\" class=\"full\" .value=\"${currentName}\">\n <p class=\"mt-sm muted\"><small id=\"dlg-move-preview\"></small></p>\n </div>\n </div>\n `, {\n title: 'Move Page',\n confirmText: 'Save and Move Page',\n confirmAction: async () => {\n const dirInput = $dialog.querySelector('#dlg-move-dir').value.trim() || '/';\n const newName = slugify($dialog.querySelector('#dlg-move-name').value.trim());\n if (!newName) {\n Toast.error('Page name is required');\n return;\n }\n if (!isValidDir(dirInput)) {\n Toast.error('Invalid directory path');\n return;\n }\n const newDir = dirToInternal(dirInput);\n const newFile = newDir === '.'\n ? `${newName}.page.html`\n : `${newDir}/${newName}.page.html`;\n\n this.saving = true;\n const [saveError] = await updatePage({ file: this.file, ...this.getFormState() });\n this.saving = false;\n if (saveError) {\n Toast.error(saveError.msg || 'Failed to save page');\n return;\n }\n\n const [moveError, moved] = await movePage({ file: this.file, newFile });\n if (moveError) {\n Toast.error(moveError.msg || 'Failed to move page');\n return;\n }\n\n Toast.success('Page saved and moved');\n setTimeout(() => {\n window.location.href = `/admin/content/pages/edit?path=${encodeURIComponent(moved.file.replace(/\\.page\\.html$/, ''))}`;\n }, 1000);\n },\n cancelText: 'Cancel'\n });\n\n const updatePreview = () => {\n const dir = dirToInternal($dialog.querySelector('#dlg-move-dir').value || '/');\n const name = slugify($dialog.querySelector('#dlg-move-name').value);\n const prefix = dir === '.' ? '/' : '/' + dir + '/';\n $dialog.querySelector('#dlg-move-preview').textContent = name ? prefix + name : '';\n };\n $dialog.querySelector('#dlg-move-dir').addEventListener('input', e => {\n $dialog.querySelector('#dlg-move-dir-error').classList.toggle('d-n', isValidDir(e.target.value || '/'));\n updatePreview();\n });\n $dialog.querySelector('#dlg-move-name').addEventListener('input', updatePreview);\n updatePreview();\n };\n\n handleReset = () => {\n Dialog.confirm('Reset this page? Any unsaved changes will be lost.', confirmed => {\n if (!confirmed) return;\n this.contents = this.buildContents(this.page.template, this.page.contents);\n this.page = { ...this.page };\n });\n };\n\n handleSave = async () => {\n this.saving = true;\n const state = this.getFormState();\n const [saveError, saved] = await updatePage({ file: this.file, ...state });\n this.saving = false;\n\n if (saveError) {\n Toast.error(saveError.msg || 'Failed to save page');\n return;\n }\n\n this.contents = this.contents.map(c => {\n const current = state.contents.find(x => x.location === c.location);\n return current ? { ...c, content: current.content } : c;\n });\n this.page = {\n ...this.page,\n ...state,\n contents: state.contents.filter(b => b.content.trim()),\n updatedAt: saved.updatedAt\n };\n\n document.title = `Edit: ${state.name || state.title || this.file} - Admin`;\n Toast.success('Page saved');\n };\n\n /*\n Render\n */\n\n render() {\n if (this.loading) return html`<div>Loading page...</div>`;\n\n if (this.error) return html`\n <div>\n <p>Page not found.</p>\n <a href=\"/admin/pages\">Back to Pages</a>\n </div>\n `;\n\n const { page, templates, contents } = this;\n const isSystem = page.owner === 'system';\n const isLocked = page.locked;\n\n return html`\n ${isLocked ? html`\n <div class=\"p r mb bc-warning tc-warning\">\n <k-icon name=\"lock\"></k-icon> This page is locked and cannot be edited. ${page.owner === 'custom' ? 'Locked by developer' : `Managed by: ${page.owner || 'external system'}`}\n </div>\n ` : ''}\n <div class=\"d-f mb\">\n <div class=\"btn-grp mrh mb\">\n <a href=\"/admin/content/pages\" class=\"btn\">\n <k-icon name=\"arrow\" direction=\"left\"></k-icon> Pages\n </a>\n <a href=\"${this.getPageUrl()}\" id=\"page-link\" class=\"btn\">\n <k-icon name=\"visibility\"></k-icon> View Page\n </a>\n </div>\n \n <div class=\"flex\"></div>\n ${!isSystem && !isLocked ? html`\n <div class=\"btn-grp mrh mb\">\n <button class=\"danger\" @click=\"${this.handleDelete}\"><k-icon name=\"delete\"></k-icon> Delete</button>\n <button @click=\"${this.handleMove}\"><k-icon name=\"drive_file_move\"></k-icon> Move Page</button>\n </div>\n ` : ''}\n ${!isLocked ? html`\n <div class=\"btn-grp mrh mb\">\n <button @click=\"${this.handleReset}\"><k-icon name=\"restart_alt\"></k-icon> Reset</button>\n <button id=\"saveBtn\" class=\"primary\" ?disabled=\"${this.saving}\" @click=\"${this.handleSave}\">\n <k-icon name=\"save\"></k-icon> Save\n </button>\n </div>\n ` : ''}\n </div>\n <h1 class=\"mb0\">Page: ${page.name || page.title || this.file}</h1>\n\n <a href=\"${this.getPageUrl()}\" class=\"d-b mb\"><code>${this.getPageUrl()}</code></a>\n\n <k-accordion>\n <k-accordion-header for-panel=\"metadata\">Metadata</k-accordion-header>\n <k-accordion-panel name=\"metadata\">\n <div class=\"p\">\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\"><strong>Name</strong></label>\n <input type=\"text\" id=\"metaName\" class=\"flex\" .value=\"${page.name || ''}\" ?disabled=\"${isSystem || isLocked}\">\n </div>\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\"><strong>Title</strong></label>\n <input type=\"text\" id=\"metaTitle\" class=\"flex\" .value=\"${page.title || ''}\" ?disabled=\"${isLocked}\">\n </div>\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\"><strong>Description</strong></label>\n <input type=\"text\" id=\"metaDescription\" class=\"flex\" .value=\"${page.description || ''}\" ?disabled=\"${isSystem || isLocked}\">\n </div>\n ${!isSystem ? html`\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\"><strong>Author</strong></label>\n <span class=\"muted\">${page.author || ''}</span>\n </div>\n ` : ''}\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\"><strong>Template</strong></label>\n ${(() => {\n const pageDir = this.file.replace(/\\.page\\.html$/, '').split('/').slice(0, -1).join('/') || '.';\n const reachable = templates.filter(t => {\n const tDir = t.directory === '.' ? '' : t.directory;\n const pDir = pageDir === '.' ? '' : pageDir;\n return tDir === '' || pDir === tDir || pDir.startsWith(tDir + '/');\n });\n const templateLabel = t => t.directory === '.' ? `/${t.name}` : `/${t.directory}/${t.name}`;\n const currentFound = reachable.some(t => t.name === page.template);\n return html`\n <select id=\"metaTemplate\" class=\"flex\" @change=\"${this.handleTemplateChange}\" ?disabled=\"${isLocked}\">\n ${!currentFound ? html`<option value=\"${page.template}\" selected>${page.template} (Not Found, using default)</option>` : ''}\n ${reachable.map(t => html`\n <option value=\"${t.name}\" ?selected=\"${t.name === page.template}\">${templateLabel(t)}</option>\n `)}\n </select>\n `;\n })()}\n </div>\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\"><strong>Owner</strong></label>\n <span class=\"muted\">${page.owner || ''}</span>\n </div>\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\"><strong>Created</strong></label>\n <span class=\"muted\">${page.createdAt ? new Date(page.createdAt).toLocaleString() : ''}</span>\n </div>\n <div class=\"d-f\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\"><strong>Updated</strong></label>\n <span class=\"muted\">${page.updatedAt ? new Date(page.updatedAt).toLocaleString() : ''}</span>\n </div>\n </div>\n </k-accordion-panel>\n ${Object.keys(page.extraMetadata || {}).length ? html`\n <k-accordion-header for-panel=\"extra-metadata\">Extra Metadata</k-accordion-header>\n <k-accordion-panel name=\"extra-metadata\">\n <div class=\"p\">\n ${Object.entries(page.extraMetadata).map(([key, value]) => html`\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer);\">\n <label style=\"min-width: 120px;\" class=\"muted\"><strong>${key}</strong></label>\n <span>${value}</span>\n </div>\n `)}\n <p class=\"muted mt0\"><small>Extra metadata is managed by extensions and cannot be edited here.</small></p>\n </div>\n </k-accordion-panel>\n ` : ''}\n ${repeat(contents, c => c.location, ({ location, label, content, orphaned }) => {\n const isDefault = location === 'default';\n const headerTitle = label || (isDefault ? 'Content' : location.slice(0, 1).toUpperCase() + location.slice(1));\n const editorTag = unsafeStatic(getComponentOverride('page-content-editor', 'k-html-editor'));\n return html`\n <k-accordion-header for-panel=\"content-${location}\" ?active=\"${isDefault}\">\n ${headerTitle}${orphaned ? html` <span class=\"tc-warning\"><small>(not in template)</small></span>` : ''}\n </k-accordion-header>\n <k-accordion-panel name=\"content-${location}\" ?active=\"${isDefault}\">\n <div class=\"p\">\n ${staticHtml`<${editorTag}\n class=\"r b\"\n data-location=\"${location}\"\n controls=\"full\"\n .value=\"${content}\"\n mode=\"${CODE_MODE_LOCATIONS.has(location) ? 'code' : 'visual'}\"\n style=\"height: 400px;\"\n ?disabled=\"${isLocked}\"\n ></${editorTag}>`}\n </div>\n </k-accordion-panel>\n `;\n })}\n </k-accordion>\n `;\n }\n}\n\ncustomElements.define('admin-page-editor', PageEditor);\n"],"mappings":"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","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kempo",
3
- "version": "4.2.33",
3
+ "version": "4.2.35",
4
4
  "type": "module",
5
5
  "description": "A fullstack CMS framework for creating websites with user authentication",
6
6
  "scripts": {