@obelism/content 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,158 @@
1
+ !/*! Obelism Content bridge (@obelism/content), MIT. https://content.obelism.studio/docs/visual-editor */function(){"use strict";var e;let t="data-cms-uid",r="data-cms-entry",n="cms_environment",o="#d58a1f",a=(e="")=>`${e}[${t}]{cursor:pointer;transition:outline-color .15s}${e}[${t}]:hover{outline:2px dashed ${o};outline-offset:2px}${e}[${t}][data-cms-selected]{outline:2px solid ${o};outline-offset:2px}`,i={features:[],selectedUid:null,prompting:!1,canUndo:!1,unsaved:!1,saving:!1,publishing:!1,selecting:!1},s=()=>{let e={update:new Set,saved:new Set,published:new Set,change:new Set};return{listeners:e,on:(t,r)=>(e[t].add(r),()=>{e[t].delete(r)}),emit(t,r){for(let n of e[t])try{n(r)}catch(e){console.error("[@obelism/content bridge]",e)}}}},l=()=>Promise.reject(Error("Save and publish in the visual editor itself")),d={prompt:()=>Promise.reject(Error("Prompts only work inside the visual editor")),createPage:()=>Promise.reject(Error("New pages are made on the site itself")),undo:()=>{},save:l,publish:l,signOut:()=>{},select:()=>{},setSelecting:()=>{}},c=e=>`[${t}="${e.replace(/["\\]/g,"\\$&")}"]`,p=(e,t)=>{let r=s();e.on("saved",()=>r.emit("saved",{})),e.on("published",()=>r.emit("published",{}));let o=!1;return{isEditing:e.isEditing,mode:e.isEditing?"editor":null,entryId:e.entryId,environment:t.get(n),on:(t,n)=>("update"!==t||o||(o=!0,e.on("update",e=>{let t=e?.entry;t&&r.emit("update",{entry:t,selectedUid:null})})),r.on(t,n)),getState:()=>i,...d,destroy:()=>{}}},g={open:"Edit with a prompt",placeholder:"Describe a change, e.g. “make the heading shorter”",send:"Apply",undo:"Undo",close:"Close",scopeEntry:"Whole page",scopeBlock:"Selected block",clearBlock:"Edit the whole page instead",working:"Working on it…",save:"Save",publish:"Publish",saving:"Saving…",publishing:"Publishing…",saved:"Saved as a draft",published:"Published",unsaved:"Unsaved changes",signOut:"Exit editing",viewingDrafts:"Viewing drafts",selectHint:"Click a block to change only that block",newPage:"New page",newPageCancel:"Edit this page instead",newPagePlaceholder:"Describe the new page, e.g. “a pricing page for agencies”",path:"Path",pathPlaceholder:"/pricing",creating:"Writing the page…",created:"Opening the new page…"},u={page:'<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>',newPage:'<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M9 15h6"/><path d="M12 18v-6"/>',block:'<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M7 8h10"/><path d="M7 12h6"/>',undo:'<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>',close:'<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',send:'<path d="M3.714 3.048a.498.498 0 0 0-.683.627l2.843 7.627a2 2 0 0 1 0 1.396l-2.842 7.627a.498.498 0 0 0 .682.627l18-8.5a.5.5 0 0 0 0-.904z"/><path d="M6 12h16"/>'},h=`
2
+ :host { all: initial; }
3
+ * { box-sizing: border-box; font: inherit; color: inherit; margin: 0; }
4
+ .root {
5
+ --color-white-rgb: 248 248 248; --color-beige-rgb: 238 236 233; --color-beige-off-rgb: 228 225 217;
6
+ --color-grey-light-rgb: 132 132 132; --color-grey-rgb: 108 108 108; --color-grey-mid-rgb: 100 100 100; --color-grey-dark-rgb: 40 40 40;
7
+ --color-black-off-rgb: 20 20 20; --color-black-rgb: 12 12 12;
8
+ --color-orange-rgb: 212 130 30; --color-red-rgb: 240 93 93;
9
+ --theme-foreground-rgb: var(--color-black-rgb);
10
+ --theme-background-alt-rgb: var(--color-white-rgb);
11
+ --theme-background-actions-rgb: var(--color-beige-off-rgb);
12
+ --theme-border-rgb: var(--color-grey-light-rgb);
13
+ --theme-accent-rgb: var(--color-orange-rgb);
14
+ --theme-foreground-muted-rgb: var(--color-grey-mid-rgb);
15
+ --theme-error: #b3261e;
16
+ --border-radius-medium: 20px; --border-radius-pill: 5em;
17
+ --ease-roll: cubic-bezier(0.65, 0, 0.35, 1);
18
+ --shadow: 0 18px 40px -16px rgb(var(--color-black-rgb) / .3), 0 2px 6px rgb(var(--color-black-rgb) / .05);
19
+ --font-inter: 'InterVariable', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
20
+ Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
21
+ position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%);
22
+ z-index: 2147483647; width: min(640px, calc(100vw - 32px)); color-scheme: light dark;
23
+ font: 400 14px/1.4 var(--font-inter); color: rgb(var(--theme-foreground-rgb));
24
+ -webkit-font-smoothing: antialiased;
25
+ }
26
+ @media (prefers-color-scheme: dark) {
27
+ .root {
28
+ --theme-foreground-rgb: var(--color-white-rgb);
29
+ --theme-background-alt-rgb: var(--color-black-off-rgb);
30
+ --theme-background-actions-rgb: var(--color-grey-dark-rgb);
31
+ --theme-border-rgb: var(--color-grey-rgb);
32
+ --theme-foreground-muted-rgb: var(--color-grey-light-rgb);
33
+ --theme-error: rgb(var(--color-red-rgb));
34
+ --shadow: 0 18px 40px -14px rgb(0 0 0 / .7), 0 2px 6px rgb(0 0 0 / .3);
35
+ }
36
+ }
37
+ .icon { display: inline-flex; flex: none; }
38
+ .icon svg {
39
+ width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.75;
40
+ stroke-linecap: round; stroke-linejoin: round;
41
+ }
42
+ .visually-hidden {
43
+ position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
44
+ }
45
+ button { cursor: pointer; background: none; border: 0; padding: 0; }
46
+ button:disabled { cursor: not-allowed; }
47
+ button:focus-visible { outline: 2px solid rgb(var(--theme-accent-rgb)); outline-offset: 2px; }
48
+
49
+ /* Closed: one floating pill that reads like an empty field, with the mark. */
50
+ .bar { display: flex; justify-content: center; }
51
+ .pill {
52
+ display: flex; align-items: center; gap: 12px; width: min(560px, 100%); height: 56px;
53
+ padding: 0 22px 0 26px; border-radius: var(--border-radius-pill);
54
+ background: rgb(var(--theme-background-alt-rgb));
55
+ border: 1px solid rgb(var(--theme-border-rgb) / .25); box-shadow: var(--shadow);
56
+ color: rgb(var(--theme-foreground-muted-rgb)); font-size: 16px; text-align: left;
57
+ transition: transform .25s var(--ease-roll), color .25s var(--ease-roll);
58
+ }
59
+ button.pill:hover { color: rgb(var(--theme-foreground-rgb)); transform: translateY(-2px); }
60
+ .pill > span:first-child { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
61
+ .mark { color: rgb(var(--theme-accent-rgb)); font-size: 22px; line-height: 1; }
62
+ .pill.compact { width: auto; height: 40px; padding: 0 18px; font-size: 14px; gap: 4px; }
63
+
64
+ /* Open: a floating card, the text on top and a row of pill buttons below. */
65
+ .panel {
66
+ display: grid; background: rgb(var(--theme-background-alt-rgb));
67
+ border: 1px solid rgb(var(--theme-border-rgb) / .25); border-radius: var(--border-radius-medium);
68
+ box-shadow: var(--shadow); overflow: hidden;
69
+ transition: border-color .25s var(--ease-roll), box-shadow .25s var(--ease-roll);
70
+ animation: rise .25s var(--ease-roll);
71
+ }
72
+ .panel:focus-within {
73
+ border-color: rgb(var(--theme-accent-rgb) / .6);
74
+ box-shadow: 0 0 0 3px rgb(var(--theme-accent-rgb) / .18), var(--shadow);
75
+ }
76
+ @keyframes rise { from { opacity: 0; transform: translateY(8px) scale(.98); } }
77
+ @keyframes spin { to { transform: rotate(1turn); } }
78
+ @media (prefers-reduced-motion: reduce) {
79
+ .panel { animation: none; }
80
+ .pill { transition: none; }
81
+ }
82
+ .status {
83
+ padding: 16px 22px 0; font-size: 14px; color: rgb(var(--theme-foreground-muted-rgb));
84
+ display: flex; align-items: center; gap: 8px;
85
+ }
86
+ .status.working::before {
87
+ content: ''; width: 12px; height: 12px; flex: none; border-radius: 50%;
88
+ border: 2px solid rgb(var(--theme-border-rgb) / .3); border-top-color: rgb(var(--theme-accent-rgb));
89
+ animation: spin .7s linear infinite;
90
+ }
91
+ .status.error { color: var(--theme-error); }
92
+ textarea {
93
+ display: block; width: 100%; resize: none; min-height: 76px; max-height: 200px;
94
+ padding: 16px 22px 6px; border: 0; outline: 0; background: transparent;
95
+ font-size: 16px; line-height: 1.6; color: rgb(var(--theme-foreground-rgb));
96
+ }
97
+ textarea::placeholder { color: rgb(var(--theme-foreground-muted-rgb)); }
98
+ /* New page: where it goes, above what it is for. */
99
+ .path {
100
+ display: flex; align-items: center; gap: 10px; padding: 16px 22px 0;
101
+ color: rgb(var(--theme-foreground-muted-rgb));
102
+ }
103
+ .path input {
104
+ flex: 1; min-width: 0; height: 34px; padding: 0 14px; outline: 0;
105
+ border: 1px solid rgb(var(--theme-border-rgb) / .25); border-radius: var(--border-radius-pill);
106
+ background: rgb(var(--theme-background-actions-rgb) / .45); color: rgb(var(--theme-foreground-rgb));
107
+ transition: border-color .25s var(--ease-roll);
108
+ }
109
+ .path input:focus-visible { border-color: rgb(var(--theme-accent-rgb)); }
110
+ .path input::placeholder { color: rgb(var(--theme-foreground-muted-rgb)); }
111
+ .path input:disabled { opacity: .5; }
112
+ textarea:disabled { opacity: .5; }
113
+ .toolbar { display: flex; align-items: center; gap: 6px; padding: 8px 12px 12px; }
114
+ .spacer { flex: 1; }
115
+ .chip {
116
+ display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: .4em 1.1em .4em .9em;
117
+ border: 1px solid rgb(var(--theme-border-rgb) / .2); border-radius: var(--border-radius-pill);
118
+ background: rgb(var(--theme-background-actions-rgb) / .6); white-space: nowrap;
119
+ transition: background-color .25s var(--ease-roll);
120
+ }
121
+ button.chip:hover:not(:disabled) { background: rgb(var(--theme-background-actions-rgb)); }
122
+ button.chip:disabled { opacity: .5; }
123
+ .scope { min-width: 0; background: transparent; }
124
+ .scope > span:not(.icon) { overflow: hidden; text-overflow: ellipsis; }
125
+ .scope button {
126
+ display: inline-flex; margin-right: -6px; padding: 3px; border-radius: 50%;
127
+ color: rgb(var(--theme-foreground-muted-rgb));
128
+ }
129
+ .scope button:hover { background: rgb(var(--theme-background-actions-rgb)); color: rgb(var(--theme-foreground-rgb)); }
130
+ .scope button svg { width: 14px; height: 14px; }
131
+ .icon-button {
132
+ display: inline-flex; align-items: center; justify-content: center;
133
+ width: 34px; height: 34px; border-radius: 50%; color: rgb(var(--theme-foreground-muted-rgb));
134
+ transition: background-color .25s var(--ease-roll), color .25s var(--ease-roll), opacity .25s var(--ease-roll);
135
+ }
136
+ .icon-button:hover:not(:disabled) { background: rgb(var(--theme-background-actions-rgb)); color: rgb(var(--theme-foreground-rgb)); }
137
+ /* The one call to action: white on orange. */
138
+ .icon-button.primary { background: rgb(var(--theme-accent-rgb)); color: #fff; }
139
+ .icon-button.primary:hover:not(:disabled) { background: rgb(var(--theme-accent-rgb)); color: #fff; opacity: .88; }
140
+ .icon-button.primary:disabled { opacity: .35; }
141
+ .divider { width: 1px; height: 20px; background: rgb(var(--theme-border-rgb) / .3); margin: 0 4px; }
142
+
143
+ /* On the site: storing the draft, and the way out. */
144
+ .footer {
145
+ display: flex; align-items: center; gap: 8px; padding: 10px 12px 10px 22px;
146
+ border-top: 1px solid rgb(var(--theme-border-rgb) / .2);
147
+ background: rgb(var(--theme-background-actions-rgb) / .45);
148
+ }
149
+ .footer .chip { height: 30px; padding: .4em 1.2em; background: rgb(var(--theme-background-alt-rgb)); }
150
+ .unsaved { color: rgb(var(--theme-foreground-muted-rgb)); display: inline-flex; align-items: center; }
151
+ .link { color: rgb(var(--theme-foreground-muted-rgb)); text-decoration: underline; text-underline-offset: 3px; padding: 0 6px; }
152
+ .link:hover { color: rgb(var(--theme-foreground-rgb)); }
153
+ .dot {
154
+ display: inline-block; width: 7px; height: 7px; border-radius: 50%;
155
+ background: rgb(var(--theme-accent-rgb)); margin-right: 8px;
156
+ }
157
+ [hidden] { display: none !important; }
158
+ `,b=(e,t,r={},...n)=>{let o=e.createElement(t);for(let t of(Object.assign(o,r),n))o.appendChild("string"==typeof t?e.createTextNode(t):t);return o},m=(e,t)=>{let r=b(e,"span",{className:"icon"});return r.setAttribute("aria-hidden","true"),r.innerHTML=`<svg viewBox="0 0 24 24">${u[t]}</svg>`,r},f=(e,t,r={})=>{let n={...g,...r.labels},o=b(e,"div");o.setAttribute("data-obelism-content-prompt","");let a=o.attachShadow({mode:"open"}),i="site"===t.mode,s=b(e,"span",{textContent:n.open}),l=b(e,"button",{className:"pill",type:"button"},s,b(e,"span",{className:"mark",textContent:"⁜"})),d=b(e,"span",{className:"pill compact"},`${n.viewingDrafts} \xb7 `),c=b(e,"button",{className:"link",type:"button",textContent:n.signOut});d.appendChild(c);let p=b(e,"p",{className:"status"});p.setAttribute("role","status");let u=b(e,"textarea",{placeholder:n.placeholder,rows:1});u.setAttribute("aria-label",n.open);let f=m(e,"page"),v=m(e,"block"),y=b(e,"span"),x=b(e,"button",{type:"button",title:n.clearBlock});x.setAttribute("aria-label",n.clearBlock),x.appendChild(m(e,"close"));let w=b(e,"span",{className:"chip scope"},f,v,y,x),k=b(e,"button",{className:"action chip",type:"button"},m(e,"undo"),n.undo),E=b(e,"button",{className:"action chip",type:"button"},m(e,"newPage"),n.newPage),S=b(e,"button",{type:"button",title:n.newPageCancel});S.setAttribute("aria-label",n.newPageCancel),S.appendChild(m(e,"close"));let C=b(e,"span",{className:"chip scope"},m(e,"newPage"),b(e,"span",{textContent:n.newPage}),S),N=b(e,"input",{type:"text",placeholder:n.pathPlaceholder,autocomplete:"off",spellcheck:!1}),U=b(e,"label",{className:"path"},b(e,"span",{textContent:n.path}),N),P=b(e,"button",{className:"close icon-button",type:"button",title:n.close},m(e,"close"));P.setAttribute("aria-label",n.close);let L=b(e,"button",{className:"action primary icon-button",type:"submit"},b(e,"span",{className:"visually-hidden",textContent:n.send}),m(e,"send"));L.title=n.send;let $=b(e,"span",{className:"unsaved"});$.appendChild(b(e,"span",{className:"dot"})),$.appendChild(e.createTextNode(n.unsaved));let A=b(e,"button",{className:"link",type:"button",textContent:n.signOut}),O=b(e,"button",{className:"action chip",type:"button",textContent:n.save}),I=b(e,"button",{className:"action chip",type:"button",textContent:n.publish}),M=b(e,"div",{className:"footer"},$,b(e,"span",{className:"spacer"}),A,O,I),j=b(e,"form",{className:"panel"},p,U,u,b(e,"div",{className:"toolbar"},w,C,k,E,b(e,"span",{className:"spacer"}),P,b(e,"span",{className:"divider"}),L),M),R=b(e,"div",{className:"root"},b(e,"div",{className:"bar"},l,d),j);a.append(b(e,"style",{textContent:h}),R);let z=!1,D=!1,T=null,B=e=>{let r=e.features.includes("prompt"),o=e.saving||e.publishing,a=e.prompting||o;R.hidden=!r&&!i,l.hidden=z||!r,d.hidden=r||!i,j.hidden=!z||!r;let c=i&&e.features.includes("create"),g=i&&null===t.entryId,h=g&&c;c||(D=!1),h&&(D=!0),s.textContent=h?n.newPage:n.open,S.hidden=h,w.hidden=D,C.hidden=!D,E.hidden=D||!c,E.disabled=a,U.hidden=!D,N.disabled=a,u.placeholder=D?n.newPagePlaceholder:n.placeholder,y.textContent=e.selectedUid?n.scopeBlock:n.scopeEntry,x.hidden=!e.selectedUid,f.hidden=!!e.selectedUid,v.hidden=!e.selectedUid,k.hidden=D||!e.canUndo,k.disabled=a,L.disabled=a||""===u.value.trim()||D&&""===N.value.trim(),u.disabled=a,O.hidden=!e.features.includes("save")||g,O.disabled=a||!e.unsaved,O.textContent=e.saving?n.saving:n.save,I.hidden=!e.features.includes("publish")||g,I.disabled=a,I.textContent=e.publishing?n.publishing:n.publish,A.hidden=!i,M.hidden=!i,$.hidden=!e.unsaved;let b=e.prompting?D?n.creating:n.working:T?.text??(!i||D||e.selectedUid?"":n.selectHint);p.textContent=b,p.hidden=!b,p.className=`status${e.prompting?" working":""}${!a&&T?.error?" error":""}`},q=()=>B(t.getState()),H=()=>{u.style.height="auto",u.style.height=`${u.scrollHeight}px`},Y=async()=>{let e=u.value.trim();if(e&&!t.getState().prompting){T=null;try{if(D){let r=N.value.trim();if(!r)return N.focus();await t.createPage(r,e),T={text:n.created,error:!1}}else{let{summary:r}=await t.prompt(e);T={text:r,error:!1}}u.value="",H()}catch(e){T={text:e instanceof Error?e.message:String(e),error:!0}}q(),u.focus()}},_=e=>{z=e,q(),i&&t.setSelecting(z&&!D)},V=e=>{D=e,T=null,t.setSelecting(z&&!D),D&&t.select(null),q(),(D?N:u).focus()},W=async e=>{T=null;try{await t[e](),T={text:"save"===e?n.saved:n.published,error:!1}}catch(e){T={text:e instanceof Error?e.message:String(e),error:!0}}q()};l.addEventListener("click",()=>{_(!0),(D?N:u).focus()}),P.addEventListener("click",()=>_(!1)),O.addEventListener("click",()=>void W("save")),I.addEventListener("click",()=>void W("publish")),A.addEventListener("click",()=>t.signOut()),c.addEventListener("click",()=>t.signOut()),x.addEventListener("click",()=>t.select(null)),E.addEventListener("click",()=>V(!0)),S.addEventListener("click",()=>V(!1)),N.addEventListener("input",q),N.addEventListener("keydown",e=>{"Enter"!==e.key||e.isComposing?"Escape"===e.key&&_(!1):(e.preventDefault(),u.focus())}),k.addEventListener("click",()=>{T=null,t.undo()}),j.addEventListener("submit",e=>{e.preventDefault(),Y()}),u.addEventListener("input",()=>{H(),q()}),u.addEventListener("keydown",e=>{"Enter"!==e.key||e.shiftKey||e.isComposing?"Escape"===e.key&&_(!1):(e.preventDefault(),Y())});let F=t.on("change",B);return q(),e.body.appendChild(o),()=>{F(),o.remove()}},v="https://content.obelism.studio",y=(e,t)=>JSON.stringify(e)===JSON.stringify(t),x=Symbol.for("@obelism/content/bridge"),w=p({isEditing:!1,entryId:null,on:()=>{}},new URLSearchParams),k=({url:e,prompt:o=!0,pagePath:l}={})=>{let g;if("u"<typeof window)return w;let u=window;if(u[x])return u[x];if(u.ObelismBridge)g=p(u.ObelismBridge,new URLSearchParams(u.location.search));else{let p=(g=((e,r)=>{let o=new URLSearchParams(e.location.search),l=e.parent!==e&&o.has("cms_preview"),p=s(),g=[],u={...i,selecting:!0},h=e=>{u={...u,...e},p.emit("change",u)},b={isEditing:l,mode:l?"editor":null,entryId:o.get("cms_entry"),environment:o.get(n),on:p.on,getState:()=>u,...d,destroy:()=>{g.splice(0).forEach(e=>e())}};if(!l)return b;let m=e.document,f=(t,n={})=>{e.parent.postMessage({source:"obelism-content-bridge",type:t,...n},r)},v=m.createElement("style");v.textContent=a(),m.head.appendChild(v),g.push(()=>v.remove());let y=(e,t)=>{if(m.querySelectorAll("[data-cms-selected]").forEach(e=>{e.removeAttribute("data-cms-selected")}),u.selectedUid!==(e??null)&&h({selectedUid:e??null}),!e)return;let r=m.querySelector(c(e));r&&(r.setAttribute("data-cms-selected",""),t&&r.scrollIntoView({behavior:"smooth",block:"center"}))},x=e=>{let r=e.target,n=r?.closest?.(`[${t}]`);if(!n)return;e.preventDefault(),e.stopPropagation();let o=n.getAttribute(t);y(o,!1),f("select",{uid:o})};m.addEventListener("click",x,!0),g.push(()=>m.removeEventListener("click",x,!0));let w=()=>f("ready",{url:e.location.href}),k=new Map,E=0,S=e=>{let t=k.get(e);return t?(clearTimeout(t.timer),k.delete(e),0===k.size&&h({prompting:!1}),t):null};b.prompt=(e,t={})=>{let r=e.trim();if(!r)return Promise.reject(Error("Say what to change"));if(!u.features.includes("prompt"))return Promise.reject(Error("The editor does not offer prompts here"));let n=`prompt_${++E}`,o=void 0===t.uid?u.selectedUid:t.uid;return new Promise((e,t)=>{let a=setTimeout(()=>{S(n)?.reject(Error("The editor did not answer in time"))},12e4);k.set(n,{resolve:e,reject:t,timer:a}),h({prompting:!0}),f("prompt",{id:n,prompt:r,uid:o})})},b.undo=()=>f("undo"),b.select=e=>{y(e,!1),f("select",{uid:e})},g.push(()=>{for(let e of k.keys())S(e)?.reject(Error("The bridge was destroyed"))});let C=t=>{if(t.origin!==r||t.source!==e.parent)return;let n=t.data;if(n&&"obelism-content-editor"===n.source){if("connect"===n.type)w();else if("update"===n.type&&n.entry)p.emit("update",{entry:n.entry,selectedUid:n.selectedUid??null}),e.requestAnimationFrame(()=>y(n.selectedUid,!1));else if("select"===n.type)y(n.uid,!0);else if("saved"===n.type||"published"===n.type)p.emit(n.type,{}),0===p.listeners.update.size&&e.location.reload();else if("features"===n.type&&Array.isArray(n.features))h({features:n.features.filter(e=>"string"==typeof e)});else if("promptState"===n.type)h({canUndo:!0===n.canUndo});else if("promptResult"===n.type&&"string"==typeof n.id){let e=S(n.id);if(!e)return;!0===n.ok?e.resolve({summary:"string"==typeof n.summary?n.summary:""}):e.reject(Error("string"==typeof n.error?n.error:"The prompt failed"))}}};return e.addEventListener("message",C),g.push(()=>e.removeEventListener("message",C)),"loading"===m.readyState?(m.addEventListener("DOMContentLoaded",w,{once:!0}),g.push(()=>m.removeEventListener("DOMContentLoaded",w))):w(),b})(u,new URL(e||v).origin)).isEditing||u.parent!==u?null:(e=>{for(let t of e.cookie.split(";")){let[e,...r]=t.trim().split("=");if("obelism_content_editing"===e)try{let e=decodeURIComponent(r.join("="));return e.startsWith("/")&&!e.startsWith("//")?e:null}catch{break}}return null})(u.document);if(p&&(g=((e,n,{pagePath:o=e=>`/${e}`}={})=>{let l=e.document,d=s(),p=[],g=new Map,u=[],h=new Set,b=null,m=i,f=e=>{m={...m,...e},d.emit("change",m)},v=()=>f({unsaved:[...g.values()].some(({content:e,saved:t})=>!y(e,t)),canUndo:u.length>0}),x=async(t,r)=>{let o=await e.fetch(`${n}/api/${t}`,void 0===r?{credentials:"same-origin"}:{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","x-obelism-content":"1"},body:JSON.stringify(r)}),a=await o.json().catch(()=>({}));if(401===o.status)throw f({features:[]}),Error("You were signed out. Sign in again to edit.");if(!o.ok)throw Error("string"==typeof a.error?a.error:`Obelism Content answered ${o.status}`);return a},w=()=>l.querySelector(`[${r}]`)?.getAttribute(r)??null,k=()=>{if(!b)return;let e=b.space.locales.map(({code:e})=>e),t=l.documentElement.lang;if(e.includes(t))return t;let r=t.split("-")[0]??"";return e.includes(r)?r:b.space.defaultLocale},E=async e=>{let t=g.get(e);if(t)return t;let r=await x(`entries/${encodeURIComponent(e)}`),n={content:r.content,saved:r.content,revision:r.draftRevision};return g.set(e,n),n},S=e=>{l.querySelectorAll("[data-cms-selected]").forEach(e=>{e.removeAttribute("data-cms-selected")}),m.selectedUid!==e&&f({selectedUid:e}),e&&l.querySelector(c(e))?.setAttribute("data-cms-selected","")},C=async t=>{if(0===d.listeners.update.size)return;let r=g.get(t)?.content,{entry:n}=await x(`entries/${encodeURIComponent(t)}/preview`,{content:r,locale:k()});d.emit("update",{entry:n,selectedUid:m.selectedUid}),e.requestAnimationFrame(()=>S(m.selectedUid))},N=t=>{d.emit(t,{}),0===d.listeners.update.size&&e.location.reload()},U=async()=>{for(let[e,t]of g){if(y(t.content,t.saved))continue;let{draftRevision:r}=await x(`entries/${encodeURIComponent(e)}/save`,{content:t.content,baseRevision:t.revision});t.saved=t.content,t.revision=r,h.add(e)}u.length=0,v()},P={isEditing:!0,mode:"site",get entryId(){return w()},environment:null,on:d.on,getState:()=>m,async prompt(e,t={}){let n,o=e.trim();if(!o)throw Error("Say what to change");if(!m.features.includes("prompt"))throw Error("You may not edit content here");let a=void 0===t.uid?m.selectedUid:t.uid,i=(n=a?l.querySelector(c(a)):null,n?.closest(`[${r}]`)?.getAttribute(r)??w());if(!i)throw Error("Nothing on this page is marked as an entry: add editable(entry)");f({prompting:!0});try{let e=await E(i),t=await x(`entries/${encodeURIComponent(i)}/prompt`,{prompt:o,content:e.content,locale:k(),uid:a});return u.push({entryId:i,content:e.content}),e.content=t.content,v(),await C(i),{summary:t.summary}}finally{f({prompting:!1})}},async createPage(t,r){let n=r.trim();if(!n)throw Error("Say what the page is for");if(!t.trim())throw Error("Say where the page goes, like /pricing");if(!m.features.includes("create"))throw Error("You may not add pages here");if(m.unsaved)throw Error("Save or undo your changes before adding a page");f({prompting:!0});try{let r=await x("entries",{path:t,prompt:n,locale:k(),from:w()}),a=o(r.path);return e.location.assign(a),{summary:r.summary,entryId:r.entryId,path:r.path,url:a}}finally{f({prompting:!1})}},undo(){let e=u.pop(),t=e&&g.get(e.entryId);e&&t&&(t.content=e.content,v(),C(e.entryId).catch(e=>console.error("[@obelism/content bridge]",e)))},async save(){if(!m.features.includes("save"))throw Error("You may not edit content here");f({saving:!0});try{await U()}finally{f({saving:!1})}N("saved")},async publish(){if(!m.features.includes("publish"))throw Error("You may not publish here");f({publishing:!0});try{await U();let e=w(),t=h.size>0?[...h]:e?[e]:[];if(0===t.length)throw Error("Nothing on this page is marked as an entry: add editable(entry)");for(let e of t)await x(`entries/${encodeURIComponent(e)}/publish`,{}),h.delete(e)}finally{f({publishing:!1})}N("published")},signOut(){if(m.unsaved&&!e.confirm("Sign out and lose the changes you have not saved?"))return;g.clear(),v();let t=e.location.pathname+e.location.search;e.location.href=`${n}/logout?return_to=${encodeURIComponent(t)}`},select(e){S(e)},setSelecting(e){e?l.documentElement.setAttribute("data-cms-selecting",""):l.documentElement.removeAttribute("data-cms-selecting"),f({selecting:e})},destroy(){p.splice(0).forEach(e=>e())}},L=l.createElement("style");L.textContent=a("html[data-cms-selecting] "),l.head.appendChild(L),p.push(()=>L.remove()),p.push(()=>l.documentElement.removeAttribute("data-cms-selecting"));let $=e=>{if(!m.selecting)return;let r=e.target,n=r?.closest?.(`[${t}]`);n&&(e.preventDefault(),e.stopPropagation(),S(n.getAttribute(t)))};l.addEventListener("click",$,!0),p.push(()=>l.removeEventListener("click",$,!0));let A=e=>{m.unsaved&&(e.preventDefault(),e.returnValue="")};return e.addEventListener("beforeunload",A),p.push(()=>e.removeEventListener("beforeunload",A)),x("session").then(e=>{b=e,f({features:e.features})}).catch(e=>console.error("[@obelism/content bridge]",e)),P})(u,p,{pagePath:l})),g.isEditing&&o){let e=f(u.document,g,!0===o?{}:o),t=g.destroy;g.destroy=()=>{e(),t()}}}return u[x]=g,g};(e=window).ObelismBridge=k((e=>{let t=e?.dataset.url,r=t||v;if(!t&&e?.src){let t=new URL(e.src);"/bridge.js"===t.pathname&&(r=t.origin)}return{url:r,prompt:e?.dataset.prompt!=="false"}})(document.currentScript)),e.ObelismContent={getContentBridge:k,mountPromptBox:f}}();
package/dist/bridge.js CHANGED
@@ -64,6 +64,7 @@ const onlyOnSite = ()=>Promise.reject(new Error('Save and publish in the visual
64
64
  // What every bridge without a feature does for it.
65
65
  const INERT_METHODS = {
66
66
  prompt: inertPrompt,
67
+ createPage: ()=>Promise.reject(new Error('New pages are made on the site itself')),
67
68
  undo: ()=>{},
68
69
  save: onlyOnSite,
69
70
  publish: onlyOnSite,
@@ -306,11 +307,19 @@ const DEFAULT_PROMPT_LABELS = {
306
307
  unsaved: 'Unsaved changes',
307
308
  signOut: 'Exit editing',
308
309
  viewingDrafts: 'Viewing drafts',
309
- selectHint: 'Click a block to change only that block'
310
+ selectHint: 'Click a block to change only that block',
311
+ newPage: 'New page',
312
+ newPageCancel: 'Edit this page instead',
313
+ newPagePlaceholder: 'Describe the new page, e.g. “a pricing page for agencies”',
314
+ path: 'Path',
315
+ pathPlaceholder: '/pricing',
316
+ creating: 'Writing the page…',
317
+ created: 'Opening the new page…'
310
318
  };
311
319
  // Icons (Lucide), drawn in the text colour.
312
320
  const ICONS = {
313
321
  page: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>',
322
+ newPage: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M9 15h6"/><path d="M12 18v-6"/>',
314
323
  block: '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M7 8h10"/><path d="M7 12h6"/>',
315
324
  undo: '<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>',
316
325
  close: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
@@ -415,6 +424,20 @@ textarea {
415
424
  font-size: 16px; line-height: 1.6; color: rgb(var(--theme-foreground-rgb));
416
425
  }
417
426
  textarea::placeholder { color: rgb(var(--theme-foreground-muted-rgb)); }
427
+ /* New page: where it goes, above what it is for. */
428
+ .path {
429
+ display: flex; align-items: center; gap: 10px; padding: 16px 22px 0;
430
+ color: rgb(var(--theme-foreground-muted-rgb));
431
+ }
432
+ .path input {
433
+ flex: 1; min-width: 0; height: 34px; padding: 0 14px; outline: 0;
434
+ border: 1px solid rgb(var(--theme-border-rgb) / .25); border-radius: var(--border-radius-pill);
435
+ background: rgb(var(--theme-background-actions-rgb) / .45); color: rgb(var(--theme-foreground-rgb));
436
+ transition: border-color .25s var(--ease-roll);
437
+ }
438
+ .path input:focus-visible { border-color: rgb(var(--theme-accent-rgb)); }
439
+ .path input::placeholder { color: rgb(var(--theme-foreground-muted-rgb)); }
440
+ .path input:disabled { opacity: .5; }
418
441
  textarea:disabled { opacity: .5; }
419
442
  .toolbar { display: flex; align-items: center; gap: 6px; padding: 8px 12px 12px; }
420
443
  .spacer { flex: 1; }
@@ -492,12 +515,13 @@ const mountPromptBox = (doc, bridge, options = {})=>{
492
515
  mode: 'open'
493
516
  });
494
517
  const site = bridge.mode === 'site';
518
+ const pillText = element(doc, 'span', {
519
+ textContent: labels.open
520
+ });
495
521
  const pill = element(doc, 'button', {
496
522
  className: 'pill',
497
523
  type: 'button'
498
- }, element(doc, 'span', {
499
- textContent: labels.open
500
- }), // The Obelism mark.
524
+ }, pillText, // The Obelism mark.
501
525
  element(doc, 'span', {
502
526
  className: 'mark',
503
527
  textContent: '⁜'
@@ -540,6 +564,34 @@ const mountPromptBox = (doc, bridge, options = {})=>{
540
564
  className: 'action chip',
541
565
  type: 'button'
542
566
  }, icon(doc, 'undo'), labels.undo);
567
+ // New page (on the site, for people who may add pages): the scope becomes
568
+ // the new page, with a way back, and a path field shows above the prompt.
569
+ const newPage = element(doc, 'button', {
570
+ className: 'action chip',
571
+ type: 'button'
572
+ }, icon(doc, 'newPage'), labels.newPage);
573
+ const cancelNew = element(doc, 'button', {
574
+ type: 'button',
575
+ title: labels.newPageCancel
576
+ });
577
+ cancelNew.setAttribute('aria-label', labels.newPageCancel);
578
+ cancelNew.appendChild(icon(doc, 'close'));
579
+ const newScope = element(doc, 'span', {
580
+ className: 'chip scope'
581
+ }, icon(doc, 'newPage'), element(doc, 'span', {
582
+ textContent: labels.newPage
583
+ }), cancelNew);
584
+ const pathInput = element(doc, 'input', {
585
+ type: 'text',
586
+ placeholder: labels.pathPlaceholder,
587
+ autocomplete: 'off',
588
+ spellcheck: false
589
+ });
590
+ const pathRow = element(doc, 'label', {
591
+ className: 'path'
592
+ }, element(doc, 'span', {
593
+ textContent: labels.path
594
+ }), pathInput);
543
595
  const close = element(doc, 'button', {
544
596
  className: 'close icon-button',
545
597
  type: 'button',
@@ -583,9 +635,9 @@ const mountPromptBox = (doc, bridge, options = {})=>{
583
635
  }), signOut, save, publish);
584
636
  const panel = element(doc, 'form', {
585
637
  className: 'panel'
586
- }, status, input, element(doc, 'div', {
638
+ }, status, pathRow, input, element(doc, 'div', {
587
639
  className: 'toolbar'
588
- }, scope, undo, element(doc, 'span', {
640
+ }, scope, newScope, undo, newPage, element(doc, 'span', {
589
641
  className: 'spacer'
590
642
  }), close, element(doc, 'span', {
591
643
  className: 'divider'
@@ -599,6 +651,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
599
651
  textContent: CSS
600
652
  }), root);
601
653
  let open = false;
654
+ let creating = false;
602
655
  let message = null;
603
656
  const render = (state)=>{
604
657
  const available = state.features.includes('prompt');
@@ -608,24 +661,40 @@ const mountPromptBox = (doc, bridge, options = {})=>{
608
661
  pill.hidden = open || !available;
609
662
  viewing.hidden = available || !site;
610
663
  panel.hidden = !open || !available;
664
+ const canCreate = site && state.features.includes('create');
665
+ // A page of the site that isn't an entry (no `editable(entry)`, like a
666
+ // hand-written /about) has nothing to edit: only New page.
667
+ const pageless = site && bridge.entryId === null;
668
+ const onlyNew = pageless && canCreate;
669
+ if (!canCreate) creating = false;
670
+ if (onlyNew) creating = true;
671
+ pillText.textContent = onlyNew ? labels.newPage : labels.open;
672
+ cancelNew.hidden = onlyNew;
673
+ scope.hidden = creating;
674
+ newScope.hidden = !creating;
675
+ newPage.hidden = creating || !canCreate;
676
+ newPage.disabled = busy;
677
+ pathRow.hidden = !creating;
678
+ pathInput.disabled = busy;
679
+ input.placeholder = creating ? labels.newPagePlaceholder : labels.placeholder;
611
680
  scopeText.textContent = state.selectedUid ? labels.scopeBlock : labels.scopeEntry;
612
681
  clearBlock.hidden = !state.selectedUid;
613
682
  pageIcon.hidden = !!state.selectedUid;
614
683
  blockIcon.hidden = !state.selectedUid;
615
- undo.hidden = !state.canUndo;
684
+ undo.hidden = creating || !state.canUndo;
616
685
  undo.disabled = busy;
617
- send.disabled = busy || input.value.trim() === '';
686
+ send.disabled = busy || input.value.trim() === '' || creating && pathInput.value.trim() === '';
618
687
  input.disabled = busy;
619
- save.hidden = !state.features.includes('save');
688
+ save.hidden = !state.features.includes('save') || pageless;
620
689
  save.disabled = busy || !state.unsaved;
621
690
  save.textContent = state.saving ? labels.saving : labels.save;
622
- publish.hidden = !state.features.includes('publish');
691
+ publish.hidden = !state.features.includes('publish') || pageless;
623
692
  publish.disabled = busy;
624
693
  publish.textContent = state.publishing ? labels.publishing : labels.publish;
625
694
  signOut.hidden = !site;
626
695
  footer.hidden = !site;
627
696
  unsaved.hidden = !state.unsaved;
628
- const text = state.prompting ? labels.working : message?.text ?? (site && !state.selectedUid ? labels.selectHint : '');
697
+ const text = state.prompting ? creating ? labels.creating : labels.working : message?.text ?? (site && !creating && !state.selectedUid ? labels.selectHint : '');
629
698
  status.textContent = text;
630
699
  status.hidden = !text;
631
700
  status.className = `status${state.prompting ? ' working' : ''}${!busy && message?.error ? ' error' : ''}`;
@@ -640,11 +709,22 @@ const mountPromptBox = (doc, bridge, options = {})=>{
640
709
  if (!prompt || bridge.getState().prompting) return;
641
710
  message = null;
642
711
  try {
643
- const { summary } = await bridge.prompt(prompt);
644
- message = {
645
- text: summary,
646
- error: false
647
- };
712
+ if (creating) {
713
+ const path = pathInput.value.trim();
714
+ if (!path) return pathInput.focus();
715
+ // The bridge opens the new page; this shows until it does.
716
+ await bridge.createPage(path, prompt);
717
+ message = {
718
+ text: labels.created,
719
+ error: false
720
+ };
721
+ } else {
722
+ const { summary } = await bridge.prompt(prompt);
723
+ message = {
724
+ text: summary,
725
+ error: false
726
+ };
727
+ }
648
728
  input.value = '';
649
729
  grow();
650
730
  } catch (error) {
@@ -658,8 +738,16 @@ const mountPromptBox = (doc, bridge, options = {})=>{
658
738
  };
659
739
  const setOpen = (next)=>{
660
740
  open = next;
661
- if (site) bridge.setSelecting(open);
662
741
  refresh();
742
+ if (site) bridge.setSelecting(open && !creating);
743
+ };
744
+ const setCreating = (next)=>{
745
+ creating = next;
746
+ message = null;
747
+ bridge.setSelecting(open && !creating);
748
+ if (creating) bridge.select(null);
749
+ refresh();
750
+ (creating ? pathInput : input).focus();
663
751
  };
664
752
  const store = async (action)=>{
665
753
  message = null;
@@ -679,7 +767,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
679
767
  };
680
768
  pill.addEventListener('click', ()=>{
681
769
  setOpen(true);
682
- input.focus();
770
+ (creating ? pathInput : input).focus();
683
771
  });
684
772
  close.addEventListener('click', ()=>setOpen(false));
685
773
  save.addEventListener('click', ()=>void store('save'));
@@ -687,6 +775,17 @@ const mountPromptBox = (doc, bridge, options = {})=>{
687
775
  signOut.addEventListener('click', ()=>bridge.signOut());
688
776
  pillSignOut.addEventListener('click', ()=>bridge.signOut());
689
777
  clearBlock.addEventListener('click', ()=>bridge.select(null));
778
+ newPage.addEventListener('click', ()=>setCreating(true));
779
+ cancelNew.addEventListener('click', ()=>setCreating(false));
780
+ pathInput.addEventListener('input', refresh);
781
+ pathInput.addEventListener('keydown', (event)=>{
782
+ if (event.key === 'Enter' && !event.isComposing) {
783
+ event.preventDefault();
784
+ input.focus();
785
+ } else if (event.key === 'Escape') {
786
+ setOpen(false);
787
+ }
788
+ });
690
789
  undo.addEventListener('click', ()=>{
691
790
  message = null;
692
791
  bridge.undo();
@@ -740,7 +839,7 @@ const readSiteEditing = (doc)=>{
740
839
  return null;
741
840
  };
742
841
  const same = (a, b)=>JSON.stringify(a) === JSON.stringify(b);
743
- const createSiteBridge = (win, basePath)=>{
842
+ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})=>{
744
843
  const doc = win.document;
745
844
  const emitter = createEmitter();
746
845
  const cleanup = [];
@@ -903,6 +1002,39 @@ const createSiteBridge = (win, basePath)=>{
903
1002
  });
904
1003
  }
905
1004
  },
1005
+ async createPage (path, prompt) {
1006
+ const text = prompt.trim();
1007
+ if (!text) throw new Error('Say what the page is for');
1008
+ if (!path.trim()) throw new Error('Say where the page goes, like /pricing');
1009
+ if (!has('create')) throw new Error('You may not add pages here');
1010
+ // Opening the new page would lose them.
1011
+ if (state.unsaved) {
1012
+ throw new Error('Save or undo your changes before adding a page');
1013
+ }
1014
+ setState({
1015
+ prompting: true
1016
+ });
1017
+ try {
1018
+ const created = await api('entries', {
1019
+ path,
1020
+ prompt: text,
1021
+ locale: locale(),
1022
+ from: pageEntryId()
1023
+ });
1024
+ const url = pagePath(created.path);
1025
+ win.location.assign(url);
1026
+ return {
1027
+ summary: created.summary,
1028
+ entryId: created.entryId,
1029
+ path: created.path,
1030
+ url
1031
+ };
1032
+ } finally{
1033
+ setState({
1034
+ prompting: false
1035
+ });
1036
+ }
1037
+ },
906
1038
  undo () {
907
1039
  const last = history.pop();
908
1040
  const entry = last && working.get(last.entryId);
@@ -1029,7 +1161,7 @@ const peekContentBridge = ()=>typeof window === 'undefined' ? null : window[KEY]
1029
1161
  // if (bridge.isEditing) bridge.on('update', ({ entry }) => render(entry))
1030
1162
  //
1031
1163
  // The first call's options win: there is one bridge per page.
1032
- const getContentBridge = ({ url, prompt = true } = {})=>{
1164
+ const getContentBridge = ({ url, prompt = true, pagePath } = {})=>{
1033
1165
  if (typeof window === 'undefined') return SERVER_BRIDGE;
1034
1166
  const win = window;
1035
1167
  if (win[KEY]) return win[KEY];
@@ -1041,7 +1173,9 @@ const getContentBridge = ({ url, prompt = true } = {})=>{
1041
1173
  // Not in the visual editor, but signed in to edit through the site's
1042
1174
  // own `/cms/login` (@obelism/content/site).
1043
1175
  const sitePath = bridge.isEditing || win.parent !== win ? null : readSiteEditing(win.document);
1044
- if (sitePath) bridge = createSiteBridge(win, sitePath);
1176
+ if (sitePath) bridge = createSiteBridge(win, sitePath, {
1177
+ pagePath
1178
+ });
1045
1179
  if (bridge.isEditing && prompt) {
1046
1180
  const unmount = mountPromptBox(win.document, bridge, prompt === true ? {} : prompt);
1047
1181
  const destroy = bridge.destroy;
@@ -1058,4 +1192,5 @@ const getContentBridge = ({ url, prompt = true } = {})=>{
1058
1192
  exports.DEFAULT_PROMPT_LABELS = DEFAULT_PROMPT_LABELS;
1059
1193
  exports.INITIAL_STATE = INITIAL_STATE;
1060
1194
  exports.getContentBridge = getContentBridge;
1195
+ exports.mountPromptBox = mountPromptBox;
1061
1196
  exports.peekContentBridge = peekContentBridge;