@obelism/content 0.26.0 → 0.27.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.
- package/README.md +39 -10
- package/dist/bin/content.js +1 -1
- package/dist/bridge.d.mts +55 -11
- package/dist/bridge.d.mts.map +1 -1
- package/dist/bridge.d.ts +55 -11
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.global.js +347 -29
- package/dist/bridge.js +1799 -313
- package/dist/bridge.mjs +1798 -314
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/{types-m08zzac6.d.ts → types-c7464prw.d.ts} +22 -2
- package/dist/{types-m08zzac6.d.ts.map → types-c7464prw.d.ts.map} +1 -1
- package/package.json +1 -1
package/dist/bridge.mjs
CHANGED
|
@@ -15,15 +15,15 @@ const DEFAULT_STATS_LABELS = {
|
|
|
15
15
|
clicked: '{rate}% clicked',
|
|
16
16
|
title: '{clickers} of {viewers} visitors of its page clicked it in the last 30 days ({clicks} clicks)'
|
|
17
17
|
};
|
|
18
|
-
const fill = (template, values)=>template.replace(/\{(\w+)\}/g, (match, key)=>key in values ? String(values[key]) : match);
|
|
19
|
-
// The same tokens as the prompt box:
|
|
18
|
+
const fill$1 = (template, values)=>template.replace(/\{(\w+)\}/g, (match, key)=>key in values ? String(values[key]) : match);
|
|
19
|
+
// The same tokens as the prompt box: an ink pill, paper text, the orange
|
|
20
20
|
// accent for the rate.
|
|
21
|
-
const CSS$
|
|
21
|
+
const CSS$2 = `
|
|
22
22
|
:host { all: initial; }
|
|
23
23
|
.layer {
|
|
24
24
|
position: fixed; inset: 0; pointer-events: none;
|
|
25
25
|
z-index: 2147483646;
|
|
26
|
-
font: 600 12px/1 '
|
|
26
|
+
font: 600 12px/1 'Geist', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
27
27
|
-webkit-font-smoothing: antialiased;
|
|
28
28
|
}
|
|
29
29
|
.badge {
|
|
@@ -31,8 +31,8 @@ const CSS$1 = `
|
|
|
31
31
|
display: inline-flex; gap: 6px; align-items: center;
|
|
32
32
|
padding: 6px 10px; white-space: nowrap; cursor: pointer;
|
|
33
33
|
pointer-events: auto; border: 0;
|
|
34
|
-
color: rgb(
|
|
35
|
-
border-radius: 5em; box-shadow: 0 6px 16px -8px rgb(
|
|
34
|
+
color: rgb(242 240 236); background: rgb(23 22 20 / .88);
|
|
35
|
+
border-radius: 5em; box-shadow: 0 6px 16px -8px rgb(23 22 20 / .5);
|
|
36
36
|
font: inherit;
|
|
37
37
|
}
|
|
38
38
|
.badge::before {
|
|
@@ -81,7 +81,7 @@ const mountStatsOverlay = (win, { selector, onSelect, labels: labelOverrides })=
|
|
|
81
81
|
mode: 'open'
|
|
82
82
|
});
|
|
83
83
|
const style = doc.createElement('style');
|
|
84
|
-
style.textContent = CSS$
|
|
84
|
+
style.textContent = CSS$2;
|
|
85
85
|
const layer = doc.createElement('div');
|
|
86
86
|
layer.className = 'layer';
|
|
87
87
|
shadow.append(style, layer);
|
|
@@ -106,10 +106,10 @@ const mountStatsOverlay = (win, { selector, onSelect, labels: labelOverrides })=
|
|
|
106
106
|
badge.type = 'button';
|
|
107
107
|
badge.className = 'badge';
|
|
108
108
|
badge.dataset.uid = uid;
|
|
109
|
-
badge.textContent = fill(labels.clicked, {
|
|
109
|
+
badge.textContent = fill$1(labels.clicked, {
|
|
110
110
|
rate: Math.round(block.rate * 100)
|
|
111
111
|
});
|
|
112
|
-
badge.title = fill(labels.title, {
|
|
112
|
+
badge.title = fill$1(labels.title, {
|
|
113
113
|
clickers: block.clickers,
|
|
114
114
|
viewers: block.viewers,
|
|
115
115
|
clicks: block.clicks
|
|
@@ -615,29 +615,128 @@ const wrapScriptBridge = (script, params)=>{
|
|
|
615
615
|
};
|
|
616
616
|
};
|
|
617
617
|
|
|
618
|
-
|
|
618
|
+
// Plain DOM helpers for the bridge's shadow-root UIs (the prompt box, the
|
|
619
|
+
// on-site editor).
|
|
620
|
+
const element = (doc, tag, props = {}, ...children)=>{
|
|
621
|
+
const node = doc.createElement(tag);
|
|
622
|
+
Object.assign(node, props);
|
|
623
|
+
// appendChild, not append: Workers types redefine Element.append, and a
|
|
624
|
+
// site's server code may well have them loaded.
|
|
625
|
+
for (const child of children){
|
|
626
|
+
node.appendChild(typeof child === 'string' ? doc.createTextNode(child) : child);
|
|
627
|
+
}
|
|
628
|
+
return node;
|
|
629
|
+
};
|
|
630
|
+
// The obelism marks (U+205C dotted cross, the logo; U+2059 five dot
|
|
631
|
+
// punctuation, working), as the design system draws them: filled, on
|
|
632
|
+
// currentColor, 24px grid.
|
|
633
|
+
const MARKS = {
|
|
634
|
+
obelism: '<rect x="10" y="2" width="4" height="20"/><rect x="2" y="10" width="20" height="4"/><circle cx="6" cy="6" r="2.6"/><circle cx="18" cy="6" r="2.6"/><circle cx="6" cy="18" r="2.6"/><circle cx="18" cy="18" r="2.6"/>',
|
|
635
|
+
working: '<circle cx="6" cy="6" r="2.2"/><circle cx="18" cy="6" r="2.2"/><circle cx="12" cy="12" r="2.2" class="accent"/><circle cx="6" cy="18" r="2.2"/><circle cx="18" cy="18" r="2.2"/>'
|
|
636
|
+
};
|
|
637
|
+
// A span with an SVG, hidden from screen readers: a word beside it says
|
|
638
|
+
// what it is.
|
|
639
|
+
const svgSpan = (doc, className, paths)=>{
|
|
640
|
+
const span = element(doc, 'span', {
|
|
641
|
+
className
|
|
642
|
+
});
|
|
643
|
+
span.setAttribute('aria-hidden', 'true');
|
|
644
|
+
span.innerHTML = `<svg viewBox="0 0 24 24">${paths}</svg>`;
|
|
645
|
+
return span;
|
|
646
|
+
};
|
|
647
|
+
// `{name}` style placeholders.
|
|
648
|
+
const fill = (template, values)=>template.replace(/\{(\w+)\}/g, (match, key)=>key in values ? String(values[key]) : match);
|
|
649
|
+
// Line icons (Lucide), drawn in the text colour.
|
|
650
|
+
const ICONS = {
|
|
651
|
+
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"/>',
|
|
652
|
+
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"/>',
|
|
653
|
+
block: '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M7 8h10"/><path d="M7 12h6"/>',
|
|
654
|
+
undo: '<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>',
|
|
655
|
+
close: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
|
656
|
+
minimize: '<path d="m6 9 6 6 6-6"/>',
|
|
657
|
+
expand: '<path d="m18 15-6-6-6 6"/>',
|
|
658
|
+
chevrons: '<path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/>',
|
|
659
|
+
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
|
|
660
|
+
check: '<path d="M20 6 9 17l-5-5"/>',
|
|
661
|
+
mark: `<g fill="currentColor" stroke="none">${MARKS.obelism}</g>`,
|
|
662
|
+
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"/>',
|
|
663
|
+
arrowUp: '<path d="M12 19V5"/><path d="m5 12 7-7 7 7"/>',
|
|
664
|
+
dash: '<path d="M5 12h14"/>',
|
|
665
|
+
eye: '<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/>',
|
|
666
|
+
eyeOff: '<path d="M10.7 5.1A10 10 0 0 1 12 5c6.5 0 10 7 10 7a17 17 0 0 1-2.2 3.2"/><path d="M6.6 6.6A17 17 0 0 0 2 12s3.5 7 10 7a9.7 9.7 0 0 0 5.4-1.6"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2"/><path d="m2 2 20 20"/>',
|
|
667
|
+
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
|
|
668
|
+
scope: '<rect x="3" y="3" width="18" height="18" rx="3" stroke-dasharray="3 3"/>',
|
|
669
|
+
alert: '<circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/>'
|
|
670
|
+
};
|
|
671
|
+
const icon$1 = (doc, name)=>svgSpan(doc, 'icon', ICONS[name]);
|
|
672
|
+
|
|
673
|
+
// The CMS's form (`bridge.openForm`) in a container, while shown: a fresh
|
|
674
|
+
// one, and one-time address, every time it mounts.
|
|
675
|
+
const createFormSlot = (bridge, container, title, { onReady, onError, onEscape })=>{
|
|
676
|
+
let handle = null;
|
|
677
|
+
let loading = false;
|
|
678
|
+
let error = null;
|
|
679
|
+
return {
|
|
680
|
+
get mounted () {
|
|
681
|
+
return handle !== null;
|
|
682
|
+
},
|
|
683
|
+
get loading () {
|
|
684
|
+
return loading;
|
|
685
|
+
},
|
|
686
|
+
get error () {
|
|
687
|
+
return error;
|
|
688
|
+
},
|
|
689
|
+
// Callbacks come later (the address is fetched first), never during
|
|
690
|
+
// the caller's render.
|
|
691
|
+
mount () {
|
|
692
|
+
loading = true;
|
|
693
|
+
error = null;
|
|
694
|
+
const opened = bridge.openForm(container, {
|
|
695
|
+
title,
|
|
696
|
+
onReady: ()=>{
|
|
697
|
+
if (handle !== opened) return;
|
|
698
|
+
loading = false;
|
|
699
|
+
onReady();
|
|
700
|
+
},
|
|
701
|
+
onError: (message)=>{
|
|
702
|
+
if (handle !== opened) return;
|
|
703
|
+
loading = false;
|
|
704
|
+
error = message;
|
|
705
|
+
onError(message);
|
|
706
|
+
},
|
|
707
|
+
onEscape: ()=>{
|
|
708
|
+
if (handle === opened) onEscape?.();
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
handle = opened;
|
|
712
|
+
},
|
|
713
|
+
unmount () {
|
|
714
|
+
handle?.close();
|
|
715
|
+
handle = null;
|
|
716
|
+
loading = false;
|
|
717
|
+
error = null;
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
// The words of the prompt box and the on-site editor. What both say is
|
|
723
|
+
// `SharedLabels`; each adds its own.
|
|
724
|
+
const SHARED_LABELS = {
|
|
619
725
|
open: 'Edit with a prompt',
|
|
620
|
-
placeholder: 'Describe a change,
|
|
726
|
+
placeholder: 'Describe a change, like “make it warmer”',
|
|
621
727
|
send: 'Apply',
|
|
622
728
|
undo: 'Undo',
|
|
623
|
-
close: 'Close',
|
|
624
729
|
minimize: 'Minimize, to see the page',
|
|
625
|
-
expand: 'Expand',
|
|
626
730
|
scopeEntry: 'Whole page',
|
|
627
|
-
scopeNoPage: 'Whole page',
|
|
628
|
-
scopeBlock: 'Selected block',
|
|
629
|
-
clearBlock: 'Edit the whole page instead',
|
|
630
731
|
working: 'Working on it…',
|
|
631
|
-
save: 'Save',
|
|
632
732
|
publish: 'Publish',
|
|
633
733
|
saving: 'Saving…',
|
|
634
734
|
publishing: 'Publishing…',
|
|
635
735
|
saved: 'Saved as a draft',
|
|
636
736
|
published: 'Published',
|
|
637
737
|
unsaved: 'Unsaved changes',
|
|
638
|
-
signOut: 'Exit
|
|
738
|
+
signOut: 'Exit',
|
|
639
739
|
viewingDrafts: 'Viewing drafts',
|
|
640
|
-
selectHint: 'Click a block to change only that block',
|
|
641
740
|
newPage: 'New page',
|
|
642
741
|
newPageCancel: 'Edit this page instead',
|
|
643
742
|
newPagePlaceholder: 'Describe the new page, e.g. “a pricing page for agencies”',
|
|
@@ -646,8 +745,8 @@ const DEFAULT_PROMPT_LABELS = {
|
|
|
646
745
|
creating: 'Writing the page…',
|
|
647
746
|
created: 'Opening the new page…',
|
|
648
747
|
modes: 'Edit with',
|
|
649
|
-
modePrompt: '
|
|
650
|
-
form: '
|
|
748
|
+
modePrompt: 'Ask',
|
|
749
|
+
form: 'Edit fields',
|
|
651
750
|
formTitle: 'The content form',
|
|
652
751
|
formLoading: 'Opening the form…',
|
|
653
752
|
pages: 'Pages',
|
|
@@ -666,6 +765,258 @@ const DEFAULT_PROMPT_LABELS = {
|
|
|
666
765
|
addedCollectionPage: 'Opening the collection page…',
|
|
667
766
|
collectionPageHint: 'This collection has no page yet. Add one for what goes around its products.'
|
|
668
767
|
};
|
|
768
|
+
const DEFAULT_PROMPT_LABELS = {
|
|
769
|
+
...SHARED_LABELS,
|
|
770
|
+
close: 'Close',
|
|
771
|
+
expand: 'Expand',
|
|
772
|
+
scopeNoPage: 'Whole page',
|
|
773
|
+
scopeBlock: 'Selected block',
|
|
774
|
+
clearBlock: 'Edit the whole page instead',
|
|
775
|
+
save: 'Save',
|
|
776
|
+
selectHint: 'Click a block to change only that block'
|
|
777
|
+
};
|
|
778
|
+
const DEFAULT_SITE_EDITOR_LABELS = {
|
|
779
|
+
...SHARED_LABELS,
|
|
780
|
+
editor: 'Obelism editor',
|
|
781
|
+
editing: 'Editing',
|
|
782
|
+
hideOutlines: 'Show the page without outlines',
|
|
783
|
+
environment: 'Environment',
|
|
784
|
+
envProduction: 'live',
|
|
785
|
+
scopeNoteBlock: 'Working on {name}. Click another block to switch.',
|
|
786
|
+
scopeNotePage: 'Working on the whole page. Click a block to change only that block.',
|
|
787
|
+
block: 'Block',
|
|
788
|
+
previewChip: '{name} · preview',
|
|
789
|
+
askIntro: 'Describe a change and Obelism shows it on the page. Nothing is stored until you keep it and save.',
|
|
790
|
+
you: 'You',
|
|
791
|
+
obelism: 'Obelism',
|
|
792
|
+
suggested: 'Suggested by Obelism',
|
|
793
|
+
keep: 'Keep it',
|
|
794
|
+
retry: 'Try another',
|
|
795
|
+
retryPrompt: '{prompt}\n\nWrite a different version than the last one.',
|
|
796
|
+
kept: 'Kept',
|
|
797
|
+
undone: 'Undone',
|
|
798
|
+
before: 'Before',
|
|
799
|
+
after: 'After',
|
|
800
|
+
emptyValue: 'Empty',
|
|
801
|
+
moreChanges: '{count} more changed',
|
|
802
|
+
keepFirst: 'Keep or undo the suggestion to save',
|
|
803
|
+
keepBeforeFields: 'Keep or undo the suggestion before editing fields.',
|
|
804
|
+
saveDraft: 'Save draft',
|
|
805
|
+
upToDate: 'No unsaved changes',
|
|
806
|
+
saveFailed: 'Couldn’t save',
|
|
807
|
+
publishFailed: 'Couldn’t publish',
|
|
808
|
+
tryAgain: 'Try again'
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
const createPagesPicker = (doc, bridge, labels, { onLoad, onClose })=>{
|
|
812
|
+
const search = element(doc, 'input', {
|
|
813
|
+
type: 'text',
|
|
814
|
+
placeholder: labels.pagesSearch,
|
|
815
|
+
autocomplete: 'off',
|
|
816
|
+
spellcheck: false
|
|
817
|
+
});
|
|
818
|
+
search.setAttribute('role', 'combobox');
|
|
819
|
+
search.setAttribute('aria-label', labels.pagesSearch);
|
|
820
|
+
search.setAttribute('aria-autocomplete', 'list');
|
|
821
|
+
search.setAttribute('aria-expanded', 'true');
|
|
822
|
+
search.setAttribute('aria-controls', 'obelism-pages');
|
|
823
|
+
const searchRow = element(doc, 'label', {
|
|
824
|
+
className: 'search'
|
|
825
|
+
}, icon$1(doc, 'search'), search);
|
|
826
|
+
const list = element(doc, 'ul', {
|
|
827
|
+
className: 'options',
|
|
828
|
+
id: 'obelism-pages'
|
|
829
|
+
});
|
|
830
|
+
list.setAttribute('role', 'listbox');
|
|
831
|
+
list.setAttribute('aria-label', labels.pages);
|
|
832
|
+
const note = element(doc, 'p', {
|
|
833
|
+
className: 'empty'
|
|
834
|
+
});
|
|
835
|
+
let pages = null;
|
|
836
|
+
let load = null;
|
|
837
|
+
let error = null;
|
|
838
|
+
let active = 0;
|
|
839
|
+
const matching = ()=>{
|
|
840
|
+
const query = search.value.trim().toLowerCase();
|
|
841
|
+
return (pages ?? []).filter((page)=>!query || page.name.toLowerCase().includes(query) || page.url.toLowerCase().includes(query) || `/${page.path}`.toLowerCase().includes(query));
|
|
842
|
+
};
|
|
843
|
+
const choose = (page)=>{
|
|
844
|
+
if (page.entryId === bridge.entryId) return onClose();
|
|
845
|
+
// Asks first when changes aren't saved; stays here if told no.
|
|
846
|
+
bridge.openPage(page);
|
|
847
|
+
};
|
|
848
|
+
const render = ()=>{
|
|
849
|
+
const options = matching();
|
|
850
|
+
active = Math.max(0, Math.min(active, options.length - 1));
|
|
851
|
+
list.textContent = '';
|
|
852
|
+
options.forEach((page, index)=>{
|
|
853
|
+
const option = element(doc, 'li', {
|
|
854
|
+
className: `option${index === active ? ' active' : ''}`,
|
|
855
|
+
id: `obelism-page-${index}`
|
|
856
|
+
}, element(doc, 'span', {
|
|
857
|
+
className: 'option-text'
|
|
858
|
+
}, element(doc, 'span', {
|
|
859
|
+
textContent: page.name
|
|
860
|
+
}), element(doc, 'span', {
|
|
861
|
+
className: 'option-path',
|
|
862
|
+
textContent: page.url
|
|
863
|
+
})));
|
|
864
|
+
option.setAttribute('role', 'option');
|
|
865
|
+
option.setAttribute('aria-selected', String(index === active));
|
|
866
|
+
if (page.status !== 'published') {
|
|
867
|
+
option.appendChild(element(doc, 'span', {
|
|
868
|
+
className: 'badge',
|
|
869
|
+
textContent: page.status === 'draft' ? labels.pageDraft : labels.pageChanged
|
|
870
|
+
}));
|
|
871
|
+
}
|
|
872
|
+
if (page.entryId === bridge.entryId) {
|
|
873
|
+
option.setAttribute('aria-current', 'page');
|
|
874
|
+
const check = icon$1(doc, 'check');
|
|
875
|
+
check.removeAttribute('aria-hidden');
|
|
876
|
+
check.setAttribute('role', 'img');
|
|
877
|
+
check.setAttribute('aria-label', labels.pageCurrent);
|
|
878
|
+
option.appendChild(check);
|
|
879
|
+
}
|
|
880
|
+
option.addEventListener('click', ()=>choose(page));
|
|
881
|
+
list.appendChild(option);
|
|
882
|
+
});
|
|
883
|
+
const activeOption = list.children[active];
|
|
884
|
+
if (activeOption) {
|
|
885
|
+
search.setAttribute('aria-activedescendant', activeOption.id);
|
|
886
|
+
activeOption.scrollIntoView?.({
|
|
887
|
+
block: 'nearest'
|
|
888
|
+
});
|
|
889
|
+
} else {
|
|
890
|
+
search.removeAttribute('aria-activedescendant');
|
|
891
|
+
}
|
|
892
|
+
const text = error ?? (pages ? '' : labels.pagesLoading);
|
|
893
|
+
note.textContent = text || (options.length === 0 ? labels.pagesEmpty : '');
|
|
894
|
+
note.hidden = !note.textContent;
|
|
895
|
+
note.className = `empty${error ? ' error' : ''}`;
|
|
896
|
+
};
|
|
897
|
+
const fetch = ()=>{
|
|
898
|
+
load ??= bridge.pages().then((list)=>{
|
|
899
|
+
pages = list;
|
|
900
|
+
error = null;
|
|
901
|
+
render();
|
|
902
|
+
onLoad();
|
|
903
|
+
}, (reason)=>{
|
|
904
|
+
load = null;
|
|
905
|
+
error = reason instanceof Error ? reason.message : String(reason);
|
|
906
|
+
render();
|
|
907
|
+
onLoad();
|
|
908
|
+
});
|
|
909
|
+
return load;
|
|
910
|
+
};
|
|
911
|
+
search.addEventListener('input', ()=>{
|
|
912
|
+
active = 0;
|
|
913
|
+
render();
|
|
914
|
+
});
|
|
915
|
+
search.addEventListener('keydown', (event)=>{
|
|
916
|
+
const count = list.children.length;
|
|
917
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
918
|
+
event.preventDefault();
|
|
919
|
+
if (count === 0) return;
|
|
920
|
+
active = (active + (event.key === 'ArrowDown' ? 1 : -1) + count) % count;
|
|
921
|
+
render();
|
|
922
|
+
} else if (event.key === 'Enter' && !event.isComposing) {
|
|
923
|
+
event.preventDefault();
|
|
924
|
+
const page = matching()[active];
|
|
925
|
+
if (page) choose(page);
|
|
926
|
+
} else if (event.key === 'Escape') {
|
|
927
|
+
event.preventDefault();
|
|
928
|
+
onClose();
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
return {
|
|
932
|
+
searchRow,
|
|
933
|
+
search,
|
|
934
|
+
list,
|
|
935
|
+
note,
|
|
936
|
+
get pages () {
|
|
937
|
+
return pages;
|
|
938
|
+
},
|
|
939
|
+
get error () {
|
|
940
|
+
return error;
|
|
941
|
+
},
|
|
942
|
+
current: ()=>pages?.find(({ entryId })=>entryId === bridge.entryId) ?? null,
|
|
943
|
+
fetch,
|
|
944
|
+
forget () {
|
|
945
|
+
pages = null;
|
|
946
|
+
load = null;
|
|
947
|
+
},
|
|
948
|
+
reset () {
|
|
949
|
+
search.value = '';
|
|
950
|
+
active = Math.max(0, (pages ?? []).findIndex(({ entryId })=>entryId === bridge.entryId));
|
|
951
|
+
void fetch();
|
|
952
|
+
render();
|
|
953
|
+
},
|
|
954
|
+
render
|
|
955
|
+
};
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
// What the prompt box and the on-site editor decide alike on the site.
|
|
959
|
+
const failure = (error)=>error instanceof Error ? error.message : String(error);
|
|
960
|
+
const pageContext = (bridge, state, promptWithoutPage)=>{
|
|
961
|
+
const site = bridge.mode === 'site';
|
|
962
|
+
const canCreate = site && state.features.includes('create');
|
|
963
|
+
// A page of the site that isn't an entry (no `editable(entry)`, like a
|
|
964
|
+
// hand-written /about) has nothing to edit: only New page…
|
|
965
|
+
const pageless = site && bridge.entryId === null;
|
|
966
|
+
// …unless it shows a product (collection): then Add a product
|
|
967
|
+
// (collection) page first.
|
|
968
|
+
const shopHere = !pageless || !canCreate ? null : bridge.product ? 'product' : bridge.collection ? 'collection' : null;
|
|
969
|
+
const onlyNew = pageless && canCreate && !shopHere && !promptWithoutPage;
|
|
970
|
+
return {
|
|
971
|
+
site,
|
|
972
|
+
canCreate,
|
|
973
|
+
pageless,
|
|
974
|
+
shopHere,
|
|
975
|
+
onlyNew
|
|
976
|
+
};
|
|
977
|
+
};
|
|
978
|
+
const shopCopy = (labels, shopHere)=>shopHere === 'collection' ? {
|
|
979
|
+
add: labels.addCollectionPage,
|
|
980
|
+
adding: labels.addingCollectionPage,
|
|
981
|
+
hint: labels.collectionPageHint
|
|
982
|
+
} : {
|
|
983
|
+
add: labels.addProductPage,
|
|
984
|
+
adding: labels.addingProductPage,
|
|
985
|
+
hint: labels.productPageHint
|
|
986
|
+
};
|
|
987
|
+
// Makes the page's product (collection) page; the bridge then reloads, and
|
|
988
|
+
// the answer shows until it does.
|
|
989
|
+
const addShopPage = async (bridge, labels)=>{
|
|
990
|
+
try {
|
|
991
|
+
if (bridge.product) {
|
|
992
|
+
await bridge.createProductPage();
|
|
993
|
+
return {
|
|
994
|
+
text: labels.addedProductPage,
|
|
995
|
+
error: false
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
await bridge.createCollectionPage();
|
|
999
|
+
return {
|
|
1000
|
+
text: labels.addedCollectionPage,
|
|
1001
|
+
error: false
|
|
1002
|
+
};
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
return {
|
|
1005
|
+
text: failure(error),
|
|
1006
|
+
error: true
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
// Saves or publishes; the reason when that failed, else null.
|
|
1011
|
+
const storeChanges = async (bridge, action)=>{
|
|
1012
|
+
try {
|
|
1013
|
+
await bridge[action]();
|
|
1014
|
+
return null;
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
return failure(error);
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
|
|
669
1020
|
const SIDE_KEY = 'obelism-content-prompt-side';
|
|
670
1021
|
// How far the pointer moves before it drags the box, not clicks it.
|
|
671
1022
|
const DRAG_THRESHOLD_PX = 6;
|
|
@@ -678,47 +1029,28 @@ const FLICK_MAX_PX = 120;
|
|
|
678
1029
|
const FLICK_STALE_MS = 80;
|
|
679
1030
|
// Its distance from the side of the screen, as in the CSS.
|
|
680
1031
|
const EDGE_PX = 16;
|
|
681
|
-
// Icons (Lucide), drawn in the text colour.
|
|
682
|
-
const ICONS = {
|
|
683
|
-
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"/>',
|
|
684
|
-
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"/>',
|
|
685
|
-
block: '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M7 8h10"/><path d="M7 12h6"/>',
|
|
686
|
-
undo: '<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>',
|
|
687
|
-
close: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
|
688
|
-
minimize: '<path d="m6 9 6 6 6-6"/>',
|
|
689
|
-
expand: '<path d="m18 15-6-6-6 6"/>',
|
|
690
|
-
chevrons: '<path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/>',
|
|
691
|
-
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
|
|
692
|
-
check: '<path d="M20 6 9 17l-5-5"/>',
|
|
693
|
-
mark: '<path d="M12 3v18M3 12h18"/><g fill="currentColor" stroke="none"><circle cx="7" cy="7" r="1.75"/><circle cx="17" cy="7" r="1.75"/><circle cx="7" cy="17" r="1.75"/><circle cx="17" cy="17" r="1.75"/></g>',
|
|
694
|
-
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"/>'
|
|
695
|
-
};
|
|
696
1032
|
// Obelism's design tokens (obelism.studio/design.md), under their own names:
|
|
697
|
-
//
|
|
698
|
-
const CSS = `
|
|
1033
|
+
// warm paper and ink, one orange accent, Geist, pill buttons.
|
|
1034
|
+
const CSS$1 = `
|
|
699
1035
|
:host { all: initial; }
|
|
700
1036
|
* { box-sizing: border-box; font: inherit; color: inherit; margin: 0; }
|
|
701
1037
|
.root {
|
|
702
|
-
--
|
|
703
|
-
--
|
|
704
|
-
--
|
|
705
|
-
--
|
|
706
|
-
--theme-
|
|
707
|
-
--theme-
|
|
708
|
-
--theme-
|
|
709
|
-
--theme-border-rgb: var(--color-grey-light-rgb);
|
|
710
|
-
--theme-accent-rgb: var(--color-orange-rgb);
|
|
711
|
-
--theme-foreground-muted-rgb: var(--color-grey-mid-rgb);
|
|
712
|
-
--theme-error: #b3261e;
|
|
1038
|
+
--theme-foreground-rgb: 23 22 20;
|
|
1039
|
+
--theme-background-alt-rgb: 255 255 255;
|
|
1040
|
+
--theme-background-actions-rgb: 232 229 223;
|
|
1041
|
+
--theme-border-rgb: 138 132 124;
|
|
1042
|
+
--theme-accent-rgb: 212 130 30;
|
|
1043
|
+
--theme-foreground-muted-rgb: 105 100 94;
|
|
1044
|
+
--theme-error: rgb(184 46 46);
|
|
713
1045
|
--border-radius-medium: 20px; --border-radius-pill: 5em;
|
|
714
1046
|
--ease-roll: cubic-bezier(0.65, 0, 0.35, 1);
|
|
715
|
-
--shadow: 0
|
|
716
|
-
--font-
|
|
717
|
-
Roboto, 'Helvetica Neue', Arial,
|
|
1047
|
+
--shadow: 0 0 0 1px rgb(23 22 20 / .08), 0 12px 32px rgb(23 22 20 / .14);
|
|
1048
|
+
--font-base: 'Geist', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
|
1049
|
+
Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
718
1050
|
position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%);
|
|
719
1051
|
transition: translate .45s cubic-bezier(0.34, 1.3, 0.64, 1);
|
|
720
1052
|
z-index: 2147483647; width: min(640px, calc(100vw - 32px)); color-scheme: light dark;
|
|
721
|
-
font: 400 14px/1.4 var(--font-
|
|
1053
|
+
font: 400 14px/1.4 var(--font-base); color: rgb(var(--theme-foreground-rgb));
|
|
722
1054
|
-webkit-font-smoothing: antialiased;
|
|
723
1055
|
}
|
|
724
1056
|
/* Dragged to a side of the screen, it stays there, springing into place. */
|
|
@@ -732,13 +1064,13 @@ const CSS = `
|
|
|
732
1064
|
.pill { touch-action: none; }
|
|
733
1065
|
@media (prefers-color-scheme: dark) {
|
|
734
1066
|
.root {
|
|
735
|
-
--theme-foreground-rgb:
|
|
736
|
-
--theme-background-alt-rgb:
|
|
737
|
-
--theme-background-actions-rgb:
|
|
738
|
-
--theme-border-rgb:
|
|
739
|
-
--theme-foreground-muted-rgb:
|
|
740
|
-
--theme-error: rgb(
|
|
741
|
-
--shadow: 0
|
|
1067
|
+
--theme-foreground-rgb: 242 240 236;
|
|
1068
|
+
--theme-background-alt-rgb: 27 26 24;
|
|
1069
|
+
--theme-background-actions-rgb: 45 43 40;
|
|
1070
|
+
--theme-border-rgb: 112 108 102;
|
|
1071
|
+
--theme-foreground-muted-rgb: 160 154 146;
|
|
1072
|
+
--theme-error: rgb(240 110 110);
|
|
1073
|
+
--shadow: 0 0 0 1px rgb(255 255 255 / .06), 0 12px 32px rgb(0 0 0 / .5);
|
|
742
1074
|
}
|
|
743
1075
|
}
|
|
744
1076
|
.icon { display: inline-flex; flex: none; }
|
|
@@ -879,7 +1211,7 @@ button.chip:disabled { opacity: .5; }
|
|
|
879
1211
|
.modes button:hover { color: rgb(var(--theme-foreground-rgb)); }
|
|
880
1212
|
.modes button[aria-pressed='true'] {
|
|
881
1213
|
background: rgb(var(--theme-background-alt-rgb)); color: rgb(var(--theme-foreground-rgb));
|
|
882
|
-
box-shadow: 0 1px 2px rgb(
|
|
1214
|
+
box-shadow: 0 1px 2px rgb(23 22 20 / .08);
|
|
883
1215
|
}
|
|
884
1216
|
.divider { width: 1px; height: 20px; background: rgb(var(--theme-border-rgb) / .3); margin: 0 4px; }
|
|
885
1217
|
|
|
@@ -931,24 +1263,7 @@ button.chip:disabled { opacity: .5; }
|
|
|
931
1263
|
.empty.error { color: var(--theme-error); }
|
|
932
1264
|
[hidden] { display: none !important; }
|
|
933
1265
|
`;
|
|
934
|
-
const
|
|
935
|
-
const node = doc.createElement(tag);
|
|
936
|
-
Object.assign(node, props);
|
|
937
|
-
// appendChild, not append: Workers types redefine Element.append, and a
|
|
938
|
-
// site's server code may well have them loaded.
|
|
939
|
-
for (const child of children){
|
|
940
|
-
node.appendChild(typeof child === 'string' ? doc.createTextNode(child) : child);
|
|
941
|
-
}
|
|
942
|
-
return node;
|
|
943
|
-
};
|
|
944
|
-
const icon = (doc, name)=>{
|
|
945
|
-
const span = element(doc, 'span', {
|
|
946
|
-
className: 'icon'
|
|
947
|
-
});
|
|
948
|
-
span.setAttribute('aria-hidden', 'true');
|
|
949
|
-
span.innerHTML = `<svg viewBox="0 0 24 24">${ICONS[name]}</svg>`;
|
|
950
|
-
return span;
|
|
951
|
-
};
|
|
1266
|
+
const icon = (doc, name)=>icon$1(doc, name);
|
|
952
1267
|
const mark = (doc)=>{
|
|
953
1268
|
const span = icon(doc, 'mark');
|
|
954
1269
|
span.classList.add('mark');
|
|
@@ -1115,31 +1430,13 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1115
1430
|
pageChip.setAttribute('aria-haspopup', 'listbox');
|
|
1116
1431
|
pageChip.setAttribute('aria-expanded', 'false');
|
|
1117
1432
|
pageChip.setAttribute('aria-controls', 'obelism-pages');
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
autocomplete: 'off',
|
|
1122
|
-
spellcheck: false
|
|
1123
|
-
});
|
|
1124
|
-
search.setAttribute('role', 'combobox');
|
|
1125
|
-
search.setAttribute('aria-label', labels.pagesSearch);
|
|
1126
|
-
search.setAttribute('aria-autocomplete', 'list');
|
|
1127
|
-
search.setAttribute('aria-expanded', 'true');
|
|
1128
|
-
search.setAttribute('aria-controls', 'obelism-pages');
|
|
1129
|
-
const optionList = element(doc, 'ul', {
|
|
1130
|
-
className: 'options',
|
|
1131
|
-
id: 'obelism-pages'
|
|
1132
|
-
});
|
|
1133
|
-
optionList.setAttribute('role', 'listbox');
|
|
1134
|
-
optionList.setAttribute('aria-label', labels.pages);
|
|
1135
|
-
const pagesNote = element(doc, 'p', {
|
|
1136
|
-
className: 'empty'
|
|
1433
|
+
const picker = createPagesPicker(doc, bridge, labels, {
|
|
1434
|
+
onLoad: ()=>refresh(),
|
|
1435
|
+
onClose: ()=>setPicking(false)
|
|
1137
1436
|
});
|
|
1138
1437
|
const pagesSection = element(doc, 'div', {
|
|
1139
1438
|
className: 'pages'
|
|
1140
|
-
},
|
|
1141
|
-
className: 'search'
|
|
1142
|
-
}, icon(doc, 'search'), search), optionList, pagesNote);
|
|
1439
|
+
}, picker.searchRow, picker.list, picker.note);
|
|
1143
1440
|
// The page's own controls (`actions`), in the footer while open and next
|
|
1144
1441
|
// to the pill while closed.
|
|
1145
1442
|
const actionsInFooter = element(doc, 'slot');
|
|
@@ -1186,7 +1483,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1186
1483
|
className: 'bar'
|
|
1187
1484
|
}, pill, viewing, actionsInBar), panel);
|
|
1188
1485
|
shadow.append(element(doc, 'style', {
|
|
1189
|
-
textContent: CSS
|
|
1486
|
+
textContent: CSS$1
|
|
1190
1487
|
}), root);
|
|
1191
1488
|
// Dragged sideways, the box moves to the left, the middle or the right
|
|
1192
1489
|
// of the screen, whichever third it is let go (or flicked) in; this
|
|
@@ -1281,20 +1578,23 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1281
1578
|
let minimized = false;
|
|
1282
1579
|
let creating = false;
|
|
1283
1580
|
let message = null;
|
|
1284
|
-
// Form mode, and the form while mounted: a fresh one (and one-time
|
|
1285
|
-
// address) every time it shows.
|
|
1286
1581
|
let form = false;
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1582
|
+
const formSlot = createFormSlot(bridge, formBox, labels.formTitle, {
|
|
1583
|
+
onReady: ()=>{
|
|
1584
|
+
message = null;
|
|
1585
|
+
refresh();
|
|
1586
|
+
},
|
|
1587
|
+
onError: (text)=>{
|
|
1588
|
+
message = {
|
|
1589
|
+
text,
|
|
1590
|
+
error: true
|
|
1591
|
+
};
|
|
1592
|
+
refresh();
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1291
1595
|
let picking = false;
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
let pagesError = null;
|
|
1295
|
-
let active = 0;
|
|
1296
|
-
const failure = (error)=>({
|
|
1297
|
-
text: error instanceof Error ? error.message : String(error),
|
|
1596
|
+
const failure$1 = (error)=>({
|
|
1597
|
+
text: failure(error),
|
|
1298
1598
|
error: true
|
|
1299
1599
|
});
|
|
1300
1600
|
const render = (state)=>{
|
|
@@ -1305,34 +1605,19 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1305
1605
|
pill.hidden = open || !available;
|
|
1306
1606
|
viewing.hidden = available || !site;
|
|
1307
1607
|
panel.hidden = !open || !available;
|
|
1308
|
-
const canCreate =
|
|
1309
|
-
// A page of the site that isn't an entry (no `editable(entry)`, like a
|
|
1310
|
-
// hand-written /about) has nothing to edit: only New page.
|
|
1311
|
-
const pageless = site && bridge.entryId === null;
|
|
1312
|
-
// …unless it shows a product (collection): then Add a product
|
|
1313
|
-
// (collection) page first.
|
|
1314
|
-
const shopHere = !pageless || !canCreate ? null : bridge.product ? 'product' : bridge.collection ? 'collection' : null;
|
|
1608
|
+
const { canCreate, pageless, shopHere, onlyNew } = pageContext(bridge, state, options.promptWithoutPage);
|
|
1315
1609
|
const productHere = shopHere !== null;
|
|
1316
|
-
const shopCopy = shopHere
|
|
1317
|
-
|
|
1318
|
-
adding: labels.addingCollectionPage,
|
|
1319
|
-
hint: labels.collectionPageHint
|
|
1320
|
-
} : {
|
|
1321
|
-
add: labels.addProductPage,
|
|
1322
|
-
adding: labels.addingProductPage,
|
|
1323
|
-
hint: labels.productPageHint
|
|
1324
|
-
};
|
|
1325
|
-
addProduct.lastChild.textContent = shopCopy.add;
|
|
1326
|
-
const onlyNew = pageless && canCreate && !productHere && !options.promptWithoutPage;
|
|
1610
|
+
const shopCopy$1 = shopCopy(labels, shopHere);
|
|
1611
|
+
addProduct.lastChild.textContent = shopCopy$1.add;
|
|
1327
1612
|
if (!canCreate) creating = false;
|
|
1328
1613
|
if (onlyNew) creating = true;
|
|
1329
1614
|
if (creating || pageless || !site) form = false;
|
|
1330
1615
|
// The form is there only while shown in the open panel.
|
|
1331
1616
|
const showForm = open && available && form;
|
|
1332
|
-
if (showForm && !
|
|
1333
|
-
else if (!showForm &&
|
|
1617
|
+
if (showForm && !formSlot.mounted) formSlot.mount();
|
|
1618
|
+
else if (!showForm && formSlot.mounted) formSlot.unmount();
|
|
1334
1619
|
const offerProduct = productHere && !creating;
|
|
1335
|
-
pillText.textContent = offerProduct ? shopCopy.add : onlyNew ? labels.newPage : labels.open;
|
|
1620
|
+
pillText.textContent = offerProduct ? shopCopy$1.add : onlyNew ? labels.newPage : labels.open;
|
|
1336
1621
|
cancelNew.hidden = onlyNew;
|
|
1337
1622
|
scope.hidden = creating || offerProduct;
|
|
1338
1623
|
newScope.hidden = !creating;
|
|
@@ -1345,8 +1630,8 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1345
1630
|
pathInput.disabled = busy;
|
|
1346
1631
|
input.hidden = form || picking || minimized || offerProduct;
|
|
1347
1632
|
formBox.hidden = !form || picking || minimized;
|
|
1348
|
-
formBox.classList.toggle('loading',
|
|
1349
|
-
formLoader.hidden = !
|
|
1633
|
+
formBox.classList.toggle('loading', formSlot.loading);
|
|
1634
|
+
formLoader.hidden = !formSlot.loading;
|
|
1350
1635
|
toolbar.hidden = picking;
|
|
1351
1636
|
pagesSection.hidden = !picking;
|
|
1352
1637
|
input.placeholder = creating ? labels.newPagePlaceholder : labels.placeholder;
|
|
@@ -1389,11 +1674,11 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1389
1674
|
unsaved.hidden = !state.unsaved;
|
|
1390
1675
|
pageChip.hidden = !site;
|
|
1391
1676
|
pageChip.setAttribute('aria-expanded', String(picking));
|
|
1392
|
-
pageChipText.textContent =
|
|
1393
|
-
const loadingForm = form &&
|
|
1677
|
+
pageChipText.textContent = picker.current()?.name ?? labels.pages;
|
|
1678
|
+
const loadingForm = form && formSlot.loading && !picking;
|
|
1394
1679
|
const working = state.prompting;
|
|
1395
1680
|
// A form opening says so in its own place.
|
|
1396
|
-
const note = state.prompting ? creating ? labels.creating : offerProduct ? shopCopy.adding : labels.working : loadingForm ? '' : message?.text ?? (offerProduct ? shopCopy.hint : site && !creating && !picking && !state.selectedUid ? labels.selectHint : '');
|
|
1681
|
+
const note = state.prompting ? creating ? labels.creating : offerProduct ? shopCopy$1.adding : labels.working : loadingForm ? '' : message?.text ?? (offerProduct ? shopCopy$1.hint : site && !creating && !picking && !state.selectedUid ? labels.selectHint : '');
|
|
1397
1682
|
status.textContent = note;
|
|
1398
1683
|
status.hidden = !note || minimized;
|
|
1399
1684
|
status.className = `status${working ? ' working' : ''}${!busy && !loadingForm && message?.error ? ' error' : ''}`;
|
|
@@ -1403,36 +1688,6 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1403
1688
|
input.style.height = 'auto';
|
|
1404
1689
|
input.style.height = `${input.scrollHeight}px`;
|
|
1405
1690
|
};
|
|
1406
|
-
// Callbacks come later (the address is fetched first), never during a
|
|
1407
|
-
// render.
|
|
1408
|
-
const mountForm = ()=>{
|
|
1409
|
-
formLoading = true;
|
|
1410
|
-
formBox.classList.add('loading');
|
|
1411
|
-
const handle = bridge.openForm(formBox, {
|
|
1412
|
-
title: labels.formTitle,
|
|
1413
|
-
onReady: ()=>{
|
|
1414
|
-
if (formHandle !== handle) return;
|
|
1415
|
-
formLoading = false;
|
|
1416
|
-
message = null;
|
|
1417
|
-
refresh();
|
|
1418
|
-
},
|
|
1419
|
-
onError: (text)=>{
|
|
1420
|
-
if (formHandle !== handle) return;
|
|
1421
|
-
formLoading = false;
|
|
1422
|
-
message = {
|
|
1423
|
-
text,
|
|
1424
|
-
error: true
|
|
1425
|
-
};
|
|
1426
|
-
refresh();
|
|
1427
|
-
}
|
|
1428
|
-
});
|
|
1429
|
-
formHandle = handle;
|
|
1430
|
-
};
|
|
1431
|
-
const unmountForm = ()=>{
|
|
1432
|
-
formHandle?.close();
|
|
1433
|
-
formHandle = null;
|
|
1434
|
-
formLoading = false;
|
|
1435
|
-
};
|
|
1436
1691
|
const setForm = (next)=>{
|
|
1437
1692
|
if (form === next) return;
|
|
1438
1693
|
form = next;
|
|
@@ -1451,8 +1706,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1451
1706
|
if (!path) return pathInput.focus();
|
|
1452
1707
|
// The bridge opens the new page; this shows until it does.
|
|
1453
1708
|
await bridge.createPage(path, prompt);
|
|
1454
|
-
|
|
1455
|
-
pagesLoad = null;
|
|
1709
|
+
picker.forget();
|
|
1456
1710
|
message = {
|
|
1457
1711
|
text: labels.created,
|
|
1458
1712
|
error: false
|
|
@@ -1467,94 +1721,17 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1467
1721
|
input.value = '';
|
|
1468
1722
|
grow();
|
|
1469
1723
|
} catch (error) {
|
|
1470
|
-
message = failure(error);
|
|
1724
|
+
message = failure$1(error);
|
|
1471
1725
|
}
|
|
1472
1726
|
refresh();
|
|
1473
1727
|
input.focus();
|
|
1474
1728
|
};
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
}, (error)=>{
|
|
1482
|
-
pagesLoad = null;
|
|
1483
|
-
pagesError = failure(error).text;
|
|
1484
|
-
renderPages();
|
|
1485
|
-
});
|
|
1486
|
-
return pagesLoad;
|
|
1487
|
-
};
|
|
1488
|
-
const matching = ()=>{
|
|
1489
|
-
const query = search.value.trim().toLowerCase();
|
|
1490
|
-
return (pages ?? []).filter((page)=>!query || page.name.toLowerCase().includes(query) || page.url.toLowerCase().includes(query) || `/${page.path}`.toLowerCase().includes(query));
|
|
1491
|
-
};
|
|
1492
|
-
const renderPages = ()=>{
|
|
1493
|
-
const list = matching();
|
|
1494
|
-
active = Math.max(0, Math.min(active, list.length - 1));
|
|
1495
|
-
optionList.textContent = '';
|
|
1496
|
-
list.forEach((page, index)=>{
|
|
1497
|
-
const current = page.entryId === bridge.entryId;
|
|
1498
|
-
const option = element(doc, 'li', {
|
|
1499
|
-
className: `option${index === active ? ' active' : ''}`,
|
|
1500
|
-
id: `obelism-page-${index}`
|
|
1501
|
-
}, element(doc, 'span', {
|
|
1502
|
-
className: 'option-text'
|
|
1503
|
-
}, element(doc, 'span', {
|
|
1504
|
-
textContent: page.name
|
|
1505
|
-
}), element(doc, 'span', {
|
|
1506
|
-
className: 'option-path',
|
|
1507
|
-
textContent: page.url
|
|
1508
|
-
})));
|
|
1509
|
-
option.setAttribute('role', 'option');
|
|
1510
|
-
option.setAttribute('aria-selected', String(index === active));
|
|
1511
|
-
if (page.status !== 'published') {
|
|
1512
|
-
option.appendChild(element(doc, 'span', {
|
|
1513
|
-
className: 'badge',
|
|
1514
|
-
textContent: page.status === 'draft' ? labels.pageDraft : labels.pageChanged
|
|
1515
|
-
}));
|
|
1516
|
-
}
|
|
1517
|
-
if (current) {
|
|
1518
|
-
option.setAttribute('aria-current', 'page');
|
|
1519
|
-
const check = icon(doc, 'check');
|
|
1520
|
-
check.removeAttribute('aria-hidden');
|
|
1521
|
-
check.setAttribute('role', 'img');
|
|
1522
|
-
check.setAttribute('aria-label', labels.pageCurrent);
|
|
1523
|
-
option.appendChild(check);
|
|
1524
|
-
}
|
|
1525
|
-
option.addEventListener('click', ()=>choose(page));
|
|
1526
|
-
optionList.appendChild(option);
|
|
1527
|
-
});
|
|
1528
|
-
const activeOption = optionList.children[active];
|
|
1529
|
-
if (activeOption) {
|
|
1530
|
-
search.setAttribute('aria-activedescendant', activeOption.id);
|
|
1531
|
-
activeOption.scrollIntoView?.({
|
|
1532
|
-
block: 'nearest'
|
|
1533
|
-
});
|
|
1534
|
-
} else {
|
|
1535
|
-
search.removeAttribute('aria-activedescendant');
|
|
1536
|
-
}
|
|
1537
|
-
const note = pagesError ?? (pages ? '' : labels.pagesLoading);
|
|
1538
|
-
pagesNote.textContent = note || (list.length === 0 ? labels.pagesEmpty : '');
|
|
1539
|
-
pagesNote.hidden = !pagesNote.textContent;
|
|
1540
|
-
pagesNote.className = `empty${pagesError ? ' error' : ''}`;
|
|
1541
|
-
};
|
|
1542
|
-
const setPicking = (next)=>{
|
|
1543
|
-
picking = next;
|
|
1544
|
-
message = null;
|
|
1545
|
-
if (picking) {
|
|
1546
|
-
search.value = '';
|
|
1547
|
-
active = Math.max(0, (pages ?? []).findIndex(({ entryId })=>entryId === bridge.entryId));
|
|
1548
|
-
void fetchPages();
|
|
1549
|
-
renderPages();
|
|
1550
|
-
}
|
|
1551
|
-
refresh();
|
|
1552
|
-
(picking ? search : pageChip).focus();
|
|
1553
|
-
};
|
|
1554
|
-
const choose = (page)=>{
|
|
1555
|
-
if (page.entryId === bridge.entryId) return setPicking(false);
|
|
1556
|
-
// Asks first when changes aren't saved; stays here if told no.
|
|
1557
|
-
bridge.openPage(page);
|
|
1729
|
+
const setPicking = (next)=>{
|
|
1730
|
+
picking = next;
|
|
1731
|
+
message = null;
|
|
1732
|
+
if (picking) picker.reset();
|
|
1733
|
+
refresh();
|
|
1734
|
+
(picking ? picker.search : pageChip).focus();
|
|
1558
1735
|
};
|
|
1559
1736
|
const setOpen = (next)=>{
|
|
1560
1737
|
open = next;
|
|
@@ -1564,7 +1741,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1564
1741
|
}
|
|
1565
1742
|
refresh();
|
|
1566
1743
|
if (site) bridge.setSelecting(open && !creating);
|
|
1567
|
-
if (open && site) void
|
|
1744
|
+
if (open && site) void picker.fetch();
|
|
1568
1745
|
};
|
|
1569
1746
|
const setCreating = (next)=>{
|
|
1570
1747
|
creating = next;
|
|
@@ -1576,15 +1753,14 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1576
1753
|
};
|
|
1577
1754
|
const store = async (action)=>{
|
|
1578
1755
|
message = null;
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
}
|
|
1756
|
+
const error = await storeChanges(bridge, action);
|
|
1757
|
+
message = error ? {
|
|
1758
|
+
text: error,
|
|
1759
|
+
error: true
|
|
1760
|
+
} : {
|
|
1761
|
+
text: action === 'save' ? labels.saved : labels.published,
|
|
1762
|
+
error: false
|
|
1763
|
+
};
|
|
1588
1764
|
refresh();
|
|
1589
1765
|
};
|
|
1590
1766
|
pill.addEventListener('click', ()=>{
|
|
@@ -1614,24 +1790,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1614
1790
|
newPage.addEventListener('click', ()=>setCreating(true));
|
|
1615
1791
|
addProduct.addEventListener('click', async ()=>{
|
|
1616
1792
|
message = null;
|
|
1617
|
-
|
|
1618
|
-
// The bridge reloads the page; this shows until it does.
|
|
1619
|
-
if (bridge.product) {
|
|
1620
|
-
await bridge.createProductPage();
|
|
1621
|
-
message = {
|
|
1622
|
-
text: labels.addedProductPage,
|
|
1623
|
-
error: false
|
|
1624
|
-
};
|
|
1625
|
-
} else {
|
|
1626
|
-
await bridge.createCollectionPage();
|
|
1627
|
-
message = {
|
|
1628
|
-
text: labels.addedCollectionPage,
|
|
1629
|
-
error: false
|
|
1630
|
-
};
|
|
1631
|
-
}
|
|
1632
|
-
} catch (error) {
|
|
1633
|
-
message = failure(error);
|
|
1634
|
-
}
|
|
1793
|
+
message = await addShopPage(bridge, labels);
|
|
1635
1794
|
refresh();
|
|
1636
1795
|
});
|
|
1637
1796
|
cancelNew.addEventListener('click', ()=>setCreating(false));
|
|
@@ -1647,26 +1806,6 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1647
1806
|
setOpen(false);
|
|
1648
1807
|
}
|
|
1649
1808
|
});
|
|
1650
|
-
search.addEventListener('input', ()=>{
|
|
1651
|
-
active = 0;
|
|
1652
|
-
renderPages();
|
|
1653
|
-
});
|
|
1654
|
-
search.addEventListener('keydown', (event)=>{
|
|
1655
|
-
const count = optionList.children.length;
|
|
1656
|
-
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
1657
|
-
event.preventDefault();
|
|
1658
|
-
if (count === 0) return;
|
|
1659
|
-
active = (active + (event.key === 'ArrowDown' ? 1 : -1) + count) % count;
|
|
1660
|
-
renderPages();
|
|
1661
|
-
} else if (event.key === 'Enter' && !event.isComposing) {
|
|
1662
|
-
event.preventDefault();
|
|
1663
|
-
const page = matching()[active];
|
|
1664
|
-
if (page) choose(page);
|
|
1665
|
-
} else if (event.key === 'Escape') {
|
|
1666
|
-
event.preventDefault();
|
|
1667
|
-
setPicking(false);
|
|
1668
|
-
}
|
|
1669
|
-
});
|
|
1670
1809
|
undo.addEventListener('click', ()=>{
|
|
1671
1810
|
message = null;
|
|
1672
1811
|
bridge.undo();
|
|
@@ -1692,7 +1831,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1692
1831
|
doc.body.appendChild(host);
|
|
1693
1832
|
return ()=>{
|
|
1694
1833
|
unsubscribe();
|
|
1695
|
-
|
|
1834
|
+
formSlot.unmount();
|
|
1696
1835
|
host.remove();
|
|
1697
1836
|
};
|
|
1698
1837
|
};
|
|
@@ -1705,11 +1844,13 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1705
1844
|
// Messages are only exchanged with the URL's origin, and only with the
|
|
1706
1845
|
// iframe's window:
|
|
1707
1846
|
// - form → bridge: { source: 'obelism-content-form', type: 'ready' |
|
|
1708
|
-
// 'loaded' | 'change' | 'error' | 'height', loading?, content?,
|
|
1709
|
-
// height? } — `ready` with `loading: true` when the form still loads the
|
|
1847
|
+
// 'loaded' | 'change' | 'error' | 'height' | 'escape', loading?, content?,
|
|
1848
|
+
// message?, height? } — `ready` with `loading: true` when the form still loads the
|
|
1710
1849
|
// entry after it and says `loaded` once it shows (a CMS from before says
|
|
1711
1850
|
// only `ready`), `change` carries the whole entry content after an edit,
|
|
1712
|
-
// `height` the form's own height in px, which the iframe takes (up to 60vh)
|
|
1851
|
+
// `height` the form's own height in px, which the iframe takes (up to 60vh),
|
|
1852
|
+
// `escape` that Escape was pressed in it (focus there never reaches the
|
|
1853
|
+
// page's keydown)
|
|
1713
1854
|
// - bridge → form: { source: 'obelism-content-bridge', type: 'content',
|
|
1714
1855
|
// entryId, content, locale, selectedUid }
|
|
1715
1856
|
const FORM_SOURCE = 'obelism-content-form';
|
|
@@ -1773,6 +1914,8 @@ const mountFormFrame = (win, container, url, title, handlers)=>{
|
|
|
1773
1914
|
const px = Math.round(Math.min(Math.max(height, 80), 4000));
|
|
1774
1915
|
iframe.style.height = `min(${px}px, 60vh)`;
|
|
1775
1916
|
}
|
|
1917
|
+
} else if (data.type === 'escape') {
|
|
1918
|
+
handlers.onEscape?.();
|
|
1776
1919
|
} else if (data.type === 'error') {
|
|
1777
1920
|
handlers.onError(typeof data.message === 'string' && data.message ? data.message : 'The form failed');
|
|
1778
1921
|
}
|
|
@@ -1866,7 +2009,8 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
1866
2009
|
unsaved: [
|
|
1867
2010
|
...working.values()
|
|
1868
2011
|
].some(({ content, saved })=>!same(content, saved)),
|
|
1869
|
-
canUndo: history.length > 0
|
|
2012
|
+
canUndo: history.length > 0,
|
|
2013
|
+
undoSteps: history.length
|
|
1870
2014
|
});
|
|
1871
2015
|
const api = async (path, body)=>{
|
|
1872
2016
|
const response = await win.fetch(`${basePath}/api/${path}`, body === undefined ? {
|
|
@@ -2026,7 +2170,9 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2026
2170
|
get collection () {
|
|
2027
2171
|
return pageCollection();
|
|
2028
2172
|
},
|
|
2029
|
-
environment
|
|
2173
|
+
get environment () {
|
|
2174
|
+
return session?.environment ?? null;
|
|
2175
|
+
},
|
|
2030
2176
|
on: emitter.on,
|
|
2031
2177
|
getState: ()=>state,
|
|
2032
2178
|
async prompt (prompt, options = {}) {
|
|
@@ -2202,7 +2348,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2202
2348
|
}
|
|
2203
2349
|
},
|
|
2204
2350
|
openForm (container, options = {}) {
|
|
2205
|
-
const { title = 'Content form', onReady, onError } = options;
|
|
2351
|
+
const { title = 'Content form', onReady, onError, onEscape } = options;
|
|
2206
2352
|
closeForm?.();
|
|
2207
2353
|
let closed = false;
|
|
2208
2354
|
let frame = null;
|
|
@@ -2276,7 +2422,8 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2276
2422
|
frame = mountFormFrame(win, container, address, title, {
|
|
2277
2423
|
onReady: ()=>onReady?.(),
|
|
2278
2424
|
onChange: change,
|
|
2279
|
-
onError: (message)=>onError?.(message)
|
|
2425
|
+
onError: (message)=>onError?.(message),
|
|
2426
|
+
onEscape: ()=>onEscape?.()
|
|
2280
2427
|
});
|
|
2281
2428
|
unsubscribe = emitter.on('change', show);
|
|
2282
2429
|
show();
|
|
@@ -2431,6 +2578,1342 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2431
2578
|
return bridge;
|
|
2432
2579
|
};
|
|
2433
2580
|
|
|
2581
|
+
// What the site gives up beside the panel: its 404px and a 16px gap on
|
|
2582
|
+
// either side.
|
|
2583
|
+
const DOCK_WIDTH_PX = 436;
|
|
2584
|
+
// Narrower than this the panel is a sheet over the page instead.
|
|
2585
|
+
const DOCK_QUERY = '(min-width: 768px)';
|
|
2586
|
+
const OPEN_KEY = 'obelism-content-editor';
|
|
2587
|
+
// Changed fields an answer shows; the rest are counted.
|
|
2588
|
+
const MAX_DIFFS = 3;
|
|
2589
|
+
// Obelism's tokens (the design system's foundation/tokens.css), copied: a
|
|
2590
|
+
// shadow root can't reach the site's, nor the site's fonts but Geist's
|
|
2591
|
+
// family name.
|
|
2592
|
+
const CSS = `
|
|
2593
|
+
:host { all: initial; }
|
|
2594
|
+
* { box-sizing: border-box; font: inherit; color: inherit; margin: 0; }
|
|
2595
|
+
.root {
|
|
2596
|
+
--bg: 255 255 255; --subtle: 250 249 247; --paper: 242 240 236; --actions: 232 229 223;
|
|
2597
|
+
--fg: 23 22 20; --soft: 68 64 60; --muted: 105 100 94;
|
|
2598
|
+
--hairline: 236 233 228; --border-soft: 220 216 209; --border: 138 132 124;
|
|
2599
|
+
--accent: 212 130 30; --focus: 176 98 0; --ink: 23 22 20;
|
|
2600
|
+
--positive: 30 112 45; --negative: 184 46 46; --warning: 150 88 0;
|
|
2601
|
+
--coral: 251 217 203; --coral-wash: 255 243 236; --coral-ink: 122 46 20;
|
|
2602
|
+
--mint-wash: 237 247 240; --mint-ink: 20 83 45;
|
|
2603
|
+
--butter-wash: 253 248 228; --butter-ink: 107 78 7;
|
|
2604
|
+
--env-develop: 22 163 74; --env-staging: 217 119 6; --env-live: 220 38 38;
|
|
2605
|
+
--tile: 23 22 20;
|
|
2606
|
+
--shadow-small: 0 1px 2px rgb(23 22 20 / .06);
|
|
2607
|
+
--shadow-medium: 0 0 0 1px rgb(23 22 20 / .08), 0 12px 32px rgb(23 22 20 / .14);
|
|
2608
|
+
--shadow-large: 0 0 0 1px rgb(23 22 20 / .08), 0 32px 80px rgb(23 22 20 / .28);
|
|
2609
|
+
--ease: cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
2610
|
+
--font-base: 'Geist', 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
|
2611
|
+
'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
2612
|
+
font: 400 14px/20px var(--font-base); color: rgb(var(--fg)); color-scheme: light dark;
|
|
2613
|
+
-webkit-font-smoothing: antialiased;
|
|
2614
|
+
}
|
|
2615
|
+
@media (prefers-color-scheme: dark) {
|
|
2616
|
+
.root {
|
|
2617
|
+
--bg: 27 26 24; --subtle: 33 31 29; --paper: 17 16 15; --actions: 45 43 40;
|
|
2618
|
+
--fg: 242 240 236; --soft: 214 210 204; --muted: 160 154 146;
|
|
2619
|
+
--hairline: 44 42 39; --border-soft: 64 61 57; --border: 112 108 102;
|
|
2620
|
+
--focus: 212 130 30;
|
|
2621
|
+
--positive: 125 190 122; --negative: 240 110 110; --warning: 230 160 60;
|
|
2622
|
+
--coral: 92 46 30; --coral-wash: 46 30 24; --coral-ink: 251 196 172;
|
|
2623
|
+
--mint-wash: 24 40 30; --mint-ink: 176 226 190;
|
|
2624
|
+
--butter-wash: 42 36 20; --butter-ink: 245 222 140;
|
|
2625
|
+
--env-develop: 74 222 128; --env-staging: 251 191 36; --env-live: 248 113 113;
|
|
2626
|
+
--tile: 45 43 40;
|
|
2627
|
+
--shadow-small: 0 1px 2px rgb(0 0 0 / .4);
|
|
2628
|
+
--shadow-medium: 0 0 0 1px rgb(255 255 255 / .06), 0 12px 32px rgb(0 0 0 / .5);
|
|
2629
|
+
--shadow-large: 0 0 0 1px rgb(255 255 255 / .08), 0 32px 80px rgb(0 0 0 / .6);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
[hidden] { display: none !important; }
|
|
2633
|
+
.visually-hidden {
|
|
2634
|
+
position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
|
|
2635
|
+
}
|
|
2636
|
+
.icon { display: inline-flex; flex: none; }
|
|
2637
|
+
.icon svg {
|
|
2638
|
+
width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.5;
|
|
2639
|
+
stroke-linecap: round; stroke-linejoin: round;
|
|
2640
|
+
}
|
|
2641
|
+
.mark { display: inline-flex; flex: none; color: rgb(var(--accent)); }
|
|
2642
|
+
.mark svg { width: 16px; height: 16px; fill: currentColor; }
|
|
2643
|
+
.working svg { width: 16px; height: 16px; fill: rgb(var(--muted)); }
|
|
2644
|
+
.working .accent { fill: rgb(var(--accent)); }
|
|
2645
|
+
.working svg { animation: pulse 1.2s ease-in-out infinite; }
|
|
2646
|
+
@keyframes pulse { 50% { opacity: .35; } }
|
|
2647
|
+
button { cursor: pointer; background: none; border: 0; padding: 0; position: relative; }
|
|
2648
|
+
button:disabled { cursor: not-allowed; }
|
|
2649
|
+
:focus-visible { outline: 2px solid rgb(var(--focus)); outline-offset: 2px; }
|
|
2650
|
+
/* 44px targets around smaller-looking controls. */
|
|
2651
|
+
.hit::after { content: ''; position: absolute; inset: -4px; }
|
|
2652
|
+
|
|
2653
|
+
.panel {
|
|
2654
|
+
position: fixed; top: 16px; right: 16px; bottom: 16px; width: 404px; z-index: 2147483647;
|
|
2655
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
2656
|
+
background: rgb(var(--bg)); border-radius: 22px; box-shadow: var(--shadow-large);
|
|
2657
|
+
transition: transform .3s var(--ease), opacity .3s var(--ease), visibility 0s;
|
|
2658
|
+
}
|
|
2659
|
+
.panel[data-closed] {
|
|
2660
|
+
transform: translateX(calc(100% + 24px)); opacity: 0; visibility: hidden;
|
|
2661
|
+
transition: transform .3s var(--ease), opacity .3s var(--ease), visibility 0s .3s;
|
|
2662
|
+
}
|
|
2663
|
+
@media (max-width: 767px) {
|
|
2664
|
+
.panel { top: auto; left: 8px; right: 8px; bottom: 64px; width: auto; height: min(60vh, 520px); }
|
|
2665
|
+
.panel[data-closed] { transform: translateY(24px); }
|
|
2666
|
+
}
|
|
2667
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2668
|
+
.panel, .panel[data-closed] { transform: none; transition: opacity .2s linear, visibility 0s; }
|
|
2669
|
+
.panel[data-closed] { transition: opacity .2s linear, visibility 0s .2s; }
|
|
2670
|
+
.working svg { animation: none; }
|
|
2671
|
+
}
|
|
2672
|
+
.head {
|
|
2673
|
+
display: flex; align-items: center; gap: 8px; padding: 12px 12px 12px 14px;
|
|
2674
|
+
border-bottom: 1px solid rgb(var(--hairline));
|
|
2675
|
+
}
|
|
2676
|
+
.tile {
|
|
2677
|
+
display: inline-flex; align-items: center; justify-content: center; flex: none;
|
|
2678
|
+
width: 28px; height: 28px; border-radius: 8px; background: rgb(var(--tile));
|
|
2679
|
+
}
|
|
2680
|
+
.tile .mark svg { width: 15px; height: 15px; }
|
|
2681
|
+
.page-button, .page-name {
|
|
2682
|
+
display: inline-flex; align-items: center; gap: 6px; min-width: 0; max-width: 190px; height: 32px;
|
|
2683
|
+
padding: 0 10px; border-radius: 999px; background: rgb(var(--paper)); font-size: 13px; font-weight: 500;
|
|
2684
|
+
}
|
|
2685
|
+
.page-button:hover { background: rgb(var(--actions)); }
|
|
2686
|
+
.page-button > span:first-child, .page-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2687
|
+
.page-name { display: block; line-height: 32px; }
|
|
2688
|
+
.page-button .icon svg { width: 12px; height: 12px; stroke-width: 1.75; }
|
|
2689
|
+
.env {
|
|
2690
|
+
display: inline-flex; align-items: center; gap: 5px; height: 22px; padding: 0 8px; flex: none;
|
|
2691
|
+
border-radius: 11px; font-size: 12px; font-weight: 500;
|
|
2692
|
+
background: rgb(var(--paper)); color: rgb(var(--soft));
|
|
2693
|
+
}
|
|
2694
|
+
.env::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: rgb(var(--muted)); }
|
|
2695
|
+
.env[data-env='develop'] { background: rgb(var(--mint-wash)); color: rgb(var(--mint-ink)); }
|
|
2696
|
+
.env[data-env='develop']::before { background: rgb(var(--env-develop)); }
|
|
2697
|
+
.env[data-env='staging'] { background: rgb(var(--butter-wash)); color: rgb(var(--butter-ink)); }
|
|
2698
|
+
.env[data-env='staging']::before { background: rgb(var(--env-staging)); }
|
|
2699
|
+
.env[data-env='production'] { background: rgb(var(--negative) / .1); color: rgb(var(--negative)); }
|
|
2700
|
+
.env[data-env='production']::before { background: rgb(var(--env-live)); }
|
|
2701
|
+
.spacer { flex: 1; }
|
|
2702
|
+
.icon-button {
|
|
2703
|
+
display: inline-flex; align-items: center; justify-content: center; flex: none;
|
|
2704
|
+
width: 36px; height: 36px; border-radius: 50%; color: rgb(var(--muted));
|
|
2705
|
+
}
|
|
2706
|
+
.icon-button:hover { background: rgb(var(--paper)); color: rgb(var(--fg)); }
|
|
2707
|
+
|
|
2708
|
+
.pages {
|
|
2709
|
+
position: absolute; top: 60px; left: 12px; right: 12px; z-index: 1; max-height: calc(100% - 140px);
|
|
2710
|
+
display: flex; flex-direction: column; gap: 8px; padding: 10px;
|
|
2711
|
+
background: rgb(var(--bg)); border-radius: 16px; box-shadow: var(--shadow-medium);
|
|
2712
|
+
}
|
|
2713
|
+
.search {
|
|
2714
|
+
display: flex; align-items: center; gap: 8px; height: 40px; padding: 0 12px; flex: none;
|
|
2715
|
+
border: 1px solid rgb(var(--border)); border-radius: 999px; color: rgb(var(--muted));
|
|
2716
|
+
}
|
|
2717
|
+
.search:focus-within { border-color: rgb(var(--accent)); }
|
|
2718
|
+
.search input {
|
|
2719
|
+
flex: 1; min-width: 0; height: 100%; border: 0; outline: 0; background: transparent; color: rgb(var(--fg));
|
|
2720
|
+
}
|
|
2721
|
+
.search input::placeholder, textarea::placeholder, .path input::placeholder { color: rgb(var(--muted)); }
|
|
2722
|
+
.options { list-style: none; padding: 0; overflow-y: auto; display: grid; gap: 2px; min-height: 0; }
|
|
2723
|
+
.option {
|
|
2724
|
+
display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 6px 10px; border-radius: 12px; cursor: pointer;
|
|
2725
|
+
}
|
|
2726
|
+
.option:hover, .option.active { background: rgb(var(--paper)); }
|
|
2727
|
+
.option-text { flex: 1; min-width: 0; display: grid; }
|
|
2728
|
+
.option-text > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2729
|
+
.option-path { font-size: 12px; color: rgb(var(--muted)); }
|
|
2730
|
+
.option .icon { color: rgb(var(--accent)); }
|
|
2731
|
+
.badge {
|
|
2732
|
+
flex: none; font-size: 12px; padding: 0 8px; border-radius: 999px;
|
|
2733
|
+
background: rgb(var(--paper)); color: rgb(var(--soft));
|
|
2734
|
+
}
|
|
2735
|
+
.empty { padding: 4px 10px; color: rgb(var(--muted)); }
|
|
2736
|
+
.empty.error { color: rgb(var(--negative)); }
|
|
2737
|
+
.new-page { justify-content: flex-start; }
|
|
2738
|
+
|
|
2739
|
+
.tabs {
|
|
2740
|
+
display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2px; flex: none;
|
|
2741
|
+
margin: 12px 14px 0; padding: 3px; border-radius: 999px; background: rgb(var(--actions));
|
|
2742
|
+
}
|
|
2743
|
+
.tab {
|
|
2744
|
+
display: flex; align-items: center; justify-content: center; gap: 6px; height: 38px;
|
|
2745
|
+
border-radius: 999px; font-size: 13px; font-weight: 500; color: rgb(var(--soft));
|
|
2746
|
+
}
|
|
2747
|
+
.tab:hover { color: rgb(var(--fg)); }
|
|
2748
|
+
.tab[aria-selected='true'] { background: rgb(var(--bg)); color: rgb(var(--fg)); box-shadow: var(--shadow-small); }
|
|
2749
|
+
.tab .mark svg { width: 12px; height: 12px; }
|
|
2750
|
+
.ask, .fields { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
2751
|
+
.scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 16px 14px; display: flex; flex-direction: column; gap: 14px; }
|
|
2752
|
+
.note {
|
|
2753
|
+
display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: 12px;
|
|
2754
|
+
background: rgb(var(--coral-wash)); color: rgb(var(--coral-ink)); font-size: 12px; line-height: 18px;
|
|
2755
|
+
}
|
|
2756
|
+
.note > span:not(.icon) { flex: 1; min-width: 0; }
|
|
2757
|
+
.note strong { font-weight: 600; }
|
|
2758
|
+
.note button {
|
|
2759
|
+
flex: none; font-size: 12px; font-weight: 500; text-decoration: underline; text-underline-offset: 2px;
|
|
2760
|
+
}
|
|
2761
|
+
.note button::after { content: ''; position: absolute; inset: -13px -6px; }
|
|
2762
|
+
.intro { color: rgb(var(--muted)); font-size: 13px; }
|
|
2763
|
+
.gate {
|
|
2764
|
+
display: grid; gap: 8px; margin: 10px 14px 0; padding: 10px 12px; border-radius: 12px;
|
|
2765
|
+
background: rgb(var(--coral-wash)); color: rgb(var(--coral-ink)); font-size: 12px; line-height: 18px;
|
|
2766
|
+
}
|
|
2767
|
+
.shop { display: grid; gap: 10px; justify-items: start; padding: 12px; border-radius: 14px; background: rgb(var(--subtle)); }
|
|
2768
|
+
.log { display: flex; flex-direction: column; gap: 14px; }
|
|
2769
|
+
.bubble {
|
|
2770
|
+
align-self: flex-end; max-width: 80%; padding: 10px 12px; border-radius: 16px 16px 4px 16px;
|
|
2771
|
+
background: rgb(var(--fg)); color: rgb(var(--bg)); white-space: pre-wrap; overflow-wrap: anywhere;
|
|
2772
|
+
}
|
|
2773
|
+
.answer { display: flex; gap: 10px; align-items: flex-start; }
|
|
2774
|
+
.answer > .chip {
|
|
2775
|
+
display: inline-flex; align-items: center; justify-content: center; flex: none;
|
|
2776
|
+
width: 26px; height: 26px; border-radius: 50%; background: rgb(var(--tile));
|
|
2777
|
+
}
|
|
2778
|
+
.answer > .chip .mark svg { width: 12px; height: 12px; }
|
|
2779
|
+
.card { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 10px; }
|
|
2780
|
+
.answer[data-status='pending'] .card { padding: 12px; border-radius: 16px; background: rgb(var(--coral-wash)); }
|
|
2781
|
+
.eyebrow { font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: rgb(var(--coral-ink)); }
|
|
2782
|
+
.answer:not([data-status='pending']) .eyebrow { color: rgb(var(--muted)); }
|
|
2783
|
+
.summary { color: rgb(var(--soft)); overflow-wrap: anywhere; }
|
|
2784
|
+
.answer[data-status='undone'] .summary { color: rgb(var(--muted)); }
|
|
2785
|
+
.status-line { display: flex; align-items: center; gap: 8px; color: rgb(var(--muted)); }
|
|
2786
|
+
.failure { display: flex; align-items: flex-start; gap: 8px; color: rgb(var(--negative)); overflow-wrap: anywhere; }
|
|
2787
|
+
.failure .icon { margin-top: 2px; }
|
|
2788
|
+
.diff-field { display: grid; gap: 4px; }
|
|
2789
|
+
.diff-label { font-size: 12px; font-weight: 500; color: rgb(var(--muted)); }
|
|
2790
|
+
.diff { border-radius: 14px; overflow: hidden; box-shadow: inset 0 0 0 1px rgb(var(--hairline)); }
|
|
2791
|
+
.diff > div { display: grid; gap: 2px; padding: 10px 12px; }
|
|
2792
|
+
.diff .before { background: rgb(var(--negative) / .07); color: rgb(var(--negative)); }
|
|
2793
|
+
.diff .after { background: rgb(var(--mint-wash)); color: rgb(var(--mint-ink)); }
|
|
2794
|
+
.diff .tag { font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }
|
|
2795
|
+
.diff .value {
|
|
2796
|
+
white-space: pre-wrap; overflow-wrap: anywhere; display: -webkit-box; -webkit-box-orient: vertical;
|
|
2797
|
+
-webkit-line-clamp: 6; overflow: hidden;
|
|
2798
|
+
}
|
|
2799
|
+
.diff .before .value { text-decoration: line-through; text-decoration-color: rgb(var(--negative) / .5); }
|
|
2800
|
+
.diff .after .value { font-weight: 500; }
|
|
2801
|
+
.diff .value.none { text-decoration: none; font-style: italic; font-weight: 400; color: rgb(var(--muted)); }
|
|
2802
|
+
.more { font-size: 12px; color: rgb(var(--muted)); }
|
|
2803
|
+
.actions { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
2804
|
+
|
|
2805
|
+
.button {
|
|
2806
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 36px;
|
|
2807
|
+
padding: 0 14px; border-radius: 999px; font-size: 13px; font-weight: 500; white-space: nowrap;
|
|
2808
|
+
}
|
|
2809
|
+
.button::after { content: ''; position: absolute; inset: -4px 0; }
|
|
2810
|
+
.button.primary { background: rgb(var(--fg)); color: rgb(var(--bg)); }
|
|
2811
|
+
.button.primary:hover:not(:disabled) { background: rgb(var(--soft)); }
|
|
2812
|
+
.button.secondary { background: rgb(var(--bg)); box-shadow: inset 0 0 0 1px rgb(var(--border-soft)); }
|
|
2813
|
+
.button.secondary:hover:not(:disabled) { background: rgb(var(--paper)); }
|
|
2814
|
+
.button.quiet { color: rgb(var(--soft)); padding: 0 10px; }
|
|
2815
|
+
.button.quiet:hover:not(:disabled) { background: rgb(var(--paper)); color: rgb(var(--fg)); }
|
|
2816
|
+
.button:disabled { background: rgb(var(--actions)); color: rgb(var(--muted)); box-shadow: none; }
|
|
2817
|
+
.button.quiet:disabled { background: none; }
|
|
2818
|
+
|
|
2819
|
+
.composer { flex: none; display: grid; gap: 8px; padding: 10px 14px 12px; }
|
|
2820
|
+
.path { display: flex; align-items: center; gap: 10px; color: rgb(var(--muted)); font-size: 13px; }
|
|
2821
|
+
.path input {
|
|
2822
|
+
flex: 1; min-width: 0; height: 40px; padding: 0 14px; outline: 0; color: rgb(var(--fg));
|
|
2823
|
+
border: 1px solid rgb(var(--border)); border-radius: 999px; background: rgb(var(--bg));
|
|
2824
|
+
}
|
|
2825
|
+
.path input:focus-visible { border-color: rgb(var(--accent)); }
|
|
2826
|
+
.bar {
|
|
2827
|
+
display: flex; align-items: flex-end; gap: 6px; padding: 0 0 0 14px; border-radius: 18px;
|
|
2828
|
+
background: rgb(var(--paper)); box-shadow: inset 0 0 0 1px rgb(var(--hairline));
|
|
2829
|
+
}
|
|
2830
|
+
.bar:focus-within { box-shadow: inset 0 0 0 1px rgb(var(--accent)), 0 0 0 3px rgb(var(--accent) / .18); }
|
|
2831
|
+
.bar > .mark { height: 44px; align-items: center; }
|
|
2832
|
+
/* The bar's ring shows focus. */
|
|
2833
|
+
textarea:focus-visible, .search input:focus-visible { outline: none; }
|
|
2834
|
+
textarea {
|
|
2835
|
+
flex: 1; min-width: 0; resize: none; min-height: 44px; max-height: 160px; padding: 12px 0;
|
|
2836
|
+
border: 0; outline: 0; background: transparent; line-height: 20px; color: rgb(var(--fg));
|
|
2837
|
+
}
|
|
2838
|
+
textarea:disabled, .path input:disabled { opacity: .6; }
|
|
2839
|
+
.send { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; flex: none; }
|
|
2840
|
+
.send::before {
|
|
2841
|
+
content: ''; position: absolute; width: 34px; height: 34px; border-radius: 50%; background: rgb(var(--accent));
|
|
2842
|
+
}
|
|
2843
|
+
.send > .icon, .send > .working { position: relative; color: rgb(var(--ink)); }
|
|
2844
|
+
.send:disabled::before { opacity: .4; }
|
|
2845
|
+
.send .icon svg { stroke-width: 2; }
|
|
2846
|
+
.send .working svg, .send .working .accent { fill: rgb(var(--ink)); }
|
|
2847
|
+
|
|
2848
|
+
.form { position: relative; flex: 1; min-height: 0; margin: 12px 14px 0; }
|
|
2849
|
+
.form iframe {
|
|
2850
|
+
display: block; width: 100%; height: 100% !important; border: 0; border-radius: 12px; background: rgb(var(--subtle));
|
|
2851
|
+
}
|
|
2852
|
+
.form.loading iframe { visibility: hidden; }
|
|
2853
|
+
.form-note {
|
|
2854
|
+
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 8px;
|
|
2855
|
+
padding: 16px; text-align: center; border-radius: 12px; background: rgb(var(--subtle)); color: rgb(var(--muted));
|
|
2856
|
+
}
|
|
2857
|
+
.form-note.error { color: rgb(var(--negative)); }
|
|
2858
|
+
|
|
2859
|
+
.foot {
|
|
2860
|
+
display: flex; align-items: center; gap: 8px; flex: none; padding: 12px 14px;
|
|
2861
|
+
border-top: 1px solid rgb(var(--hairline)); background: rgb(var(--subtle));
|
|
2862
|
+
}
|
|
2863
|
+
.save-state { flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; font-size: 12px; line-height: 16px; color: rgb(var(--muted)); }
|
|
2864
|
+
.save-state .dot { width: 7px; height: 7px; flex: none; border-radius: 50%; background: rgb(var(--warning)); }
|
|
2865
|
+
.save-state[data-state='unsaved'] { color: rgb(var(--warning)); }
|
|
2866
|
+
.save-state[data-state='saved'] { color: rgb(var(--positive)); }
|
|
2867
|
+
.save-state[data-state='error'] { color: rgb(var(--negative)); }
|
|
2868
|
+
.save-state .icon svg { width: 14px; height: 14px; }
|
|
2869
|
+
.save-state .link { text-decoration: underline; text-underline-offset: 2px; font-weight: 500; flex: none; }
|
|
2870
|
+
.save-state .link::after { content: ''; position: absolute; inset: -14px -6px; }
|
|
2871
|
+
|
|
2872
|
+
.launcher {
|
|
2873
|
+
position: fixed; left: 20px; bottom: 20px; z-index: 2147483647;
|
|
2874
|
+
display: flex; align-items: center; gap: 2px; padding: 2px; border-radius: 999px;
|
|
2875
|
+
background: rgb(23 22 20); color: rgb(214 211 204); box-shadow: 0 8px 24px rgb(0 0 0 / .3);
|
|
2876
|
+
}
|
|
2877
|
+
@media (prefers-color-scheme: dark) { .launcher { box-shadow: 0 0 0 1px rgb(255 255 255 / .12), 0 8px 24px rgb(0 0 0 / .5); } }
|
|
2878
|
+
@media (max-width: 767px) { .launcher { left: 8px; bottom: 8px; } }
|
|
2879
|
+
.launcher button { height: 44px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; }
|
|
2880
|
+
.launcher button:hover { background: rgb(255 255 255 / .08); color: rgb(244 243 240); }
|
|
2881
|
+
.launcher :focus-visible { outline-color: rgb(212 130 30); }
|
|
2882
|
+
.launch { gap: 8px; padding: 0 12px 0 6px; color: rgb(244 243 240); font-size: 13px; font-weight: 500; }
|
|
2883
|
+
.launch-tile, .reading .launch-tile {
|
|
2884
|
+
display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 50%;
|
|
2885
|
+
background: rgb(44 42 39);
|
|
2886
|
+
}
|
|
2887
|
+
.launch-tile .mark svg { width: 15px; height: 15px; }
|
|
2888
|
+
.reading { display: inline-flex; align-items: center; gap: 8px; padding: 0 8px 0 6px; color: rgb(244 243 240); font-size: 13px; font-weight: 500; }
|
|
2889
|
+
.eye { width: 44px; }
|
|
2890
|
+
.exit { padding: 0 14px; font-size: 13px; }
|
|
2891
|
+
|
|
2892
|
+
.layer { position: fixed; inset: 0; pointer-events: none; z-index: 2147483645; }
|
|
2893
|
+
.box { position: absolute; border-radius: 4px; }
|
|
2894
|
+
.box.hover { border: 1.5px dashed rgb(var(--border)); }
|
|
2895
|
+
.box.preview { border: 2px solid rgb(var(--accent)); }
|
|
2896
|
+
/* Its own layer, multiplied onto the page (a blend inside the marks layer
|
|
2897
|
+
wouldn't reach it): the wash tints the block's background, not its text. */
|
|
2898
|
+
.wash { position: fixed; inset: 0; pointer-events: none; z-index: 2147483644; mix-blend-mode: multiply; }
|
|
2899
|
+
.box.fill { background: rgb(255 243 236); }
|
|
2900
|
+
.name-tab {
|
|
2901
|
+
position: absolute; height: 20px; padding: 0 8px; border-radius: 6px 6px 0 0; white-space: nowrap;
|
|
2902
|
+
background: rgb(105 100 94); color: rgb(255 255 255); font-size: 11px; font-weight: 500; line-height: 20px;
|
|
2903
|
+
}
|
|
2904
|
+
.name-tab.inside { border-radius: 0 0 6px 0; }
|
|
2905
|
+
.chips { position: absolute; display: flex; gap: 4px; }
|
|
2906
|
+
.chips span {
|
|
2907
|
+
display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 10px; border-radius: 999px;
|
|
2908
|
+
font-size: 12px; white-space: nowrap;
|
|
2909
|
+
}
|
|
2910
|
+
.chips .accent-chip { background: rgb(212 130 30); color: rgb(23 22 20); font-weight: 600; }
|
|
2911
|
+
.chips .accent-chip .mark { color: rgb(23 22 20); }
|
|
2912
|
+
.chips .accent-chip .mark svg { width: 11px; height: 11px; }
|
|
2913
|
+
.chips .ink-chip { background: rgb(23 22 20); color: rgb(255 255 255); font-weight: 500; }
|
|
2914
|
+
`;
|
|
2915
|
+
// Outlines on the page: the selection in the accent, hover drawn by the
|
|
2916
|
+
// layer instead (only the innermost block, with its name).
|
|
2917
|
+
// A site's own fixed or sticky elements stay on the viewport: they make room
|
|
2918
|
+
// for the panel with `right: var(--obelism-dock, 0px)` in their CSS, or with
|
|
2919
|
+
// the `data-obelism-dock` attribute.
|
|
2920
|
+
const pageCss = ()=>{
|
|
2921
|
+
const block = `[${UID_ATTRIBUTE}]`;
|
|
2922
|
+
return `html[data-obelism-editor]{transition:margin-right .3s cubic-bezier(.2,.8,.2,1)}` + `html[data-obelism-docked]{margin-right:${DOCK_WIDTH_PX}px!important}` + `html[data-obelism-docked] [data-obelism-dock]{right:${DOCK_WIDTH_PX}px!important;max-width:calc(100vw - ${DOCK_WIDTH_PX}px)!important}` + `html[data-obelism-editor][data-cms-selecting] ${block}:hover:not([data-cms-selected]){outline:none}` + `html[data-obelism-editor][data-cms-selecting] ${block}[data-cms-selected]{outline:2px solid rgb(212 130 30);outline-offset:2px}` + `@media (prefers-reduced-motion:reduce){html[data-obelism-editor]{transition:none}}`;
|
|
2923
|
+
};
|
|
2924
|
+
// 'heroTitle', 'hero_title' → 'Hero title': block types are apiIds, and the
|
|
2925
|
+
// site API doesn't send their names.
|
|
2926
|
+
const humanize = (apiId)=>{
|
|
2927
|
+
const words = apiId.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ').trim().toLowerCase();
|
|
2928
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
2929
|
+
};
|
|
2930
|
+
const isRecord = (value)=>!!value && typeof value === 'object' && !Array.isArray(value);
|
|
2931
|
+
// Every text in a view (bridge.view()), by a path that follows blocks by
|
|
2932
|
+
// uid, so a block moving or one added doesn't read as everything changed.
|
|
2933
|
+
const leaves = (value, label, path, out)=>{
|
|
2934
|
+
if (value === null || value === undefined || value === '') return;
|
|
2935
|
+
if (typeof value !== 'object') {
|
|
2936
|
+
out.set(path, {
|
|
2937
|
+
label,
|
|
2938
|
+
text: String(value)
|
|
2939
|
+
});
|
|
2940
|
+
return;
|
|
2941
|
+
}
|
|
2942
|
+
if (Array.isArray(value)) {
|
|
2943
|
+
if (value.every((item)=>item === null || typeof item !== 'object')) {
|
|
2944
|
+
if (value.length) out.set(path, {
|
|
2945
|
+
label,
|
|
2946
|
+
text: value.join(', ')
|
|
2947
|
+
});
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
value.forEach((item, index)=>{
|
|
2951
|
+
const uid = isRecord(item) && typeof item._uid === 'string' ? item._uid : index;
|
|
2952
|
+
const type = isRecord(item) && typeof item._type === 'string' ? item._type : null;
|
|
2953
|
+
leaves(item, type ? humanize(type) : label, `${path}/${uid}`, out);
|
|
2954
|
+
});
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
for (const [key, child] of Object.entries(value)){
|
|
2958
|
+
if (key === '_uid' || key === '_type') continue;
|
|
2959
|
+
leaves(child, label ? `${label} · ${humanize(key)}` : humanize(key), `${path}/${key}`, out);
|
|
2960
|
+
}
|
|
2961
|
+
};
|
|
2962
|
+
// What a prompt changed, from the view before and after it.
|
|
2963
|
+
const diffViews = (before, after)=>{
|
|
2964
|
+
const was = new Map();
|
|
2965
|
+
const is = new Map();
|
|
2966
|
+
leaves(before, '', '', was);
|
|
2967
|
+
leaves(after, '', '', is);
|
|
2968
|
+
const diffs = [];
|
|
2969
|
+
for (const path of new Set([
|
|
2970
|
+
...was.keys(),
|
|
2971
|
+
...is.keys()
|
|
2972
|
+
])){
|
|
2973
|
+
const old = was.get(path);
|
|
2974
|
+
const now = is.get(path);
|
|
2975
|
+
if (old?.text === now?.text) continue;
|
|
2976
|
+
diffs.push({
|
|
2977
|
+
label: (now ?? old).label,
|
|
2978
|
+
before: old?.text ?? '',
|
|
2979
|
+
after: now?.text ?? ''
|
|
2980
|
+
});
|
|
2981
|
+
}
|
|
2982
|
+
return diffs;
|
|
2983
|
+
};
|
|
2984
|
+
const readOpen = (win)=>{
|
|
2985
|
+
try {
|
|
2986
|
+
return win?.localStorage.getItem(OPEN_KEY) !== 'minimised';
|
|
2987
|
+
} catch {
|
|
2988
|
+
return true;
|
|
2989
|
+
}
|
|
2990
|
+
};
|
|
2991
|
+
// Mounts the editor on the page; returns a function that removes it.
|
|
2992
|
+
const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
2993
|
+
const labels = {
|
|
2994
|
+
...DEFAULT_SITE_EDITOR_LABELS,
|
|
2995
|
+
...options.labels
|
|
2996
|
+
};
|
|
2997
|
+
const win = doc.defaultView;
|
|
2998
|
+
const html = doc.documentElement;
|
|
2999
|
+
const icon = (name)=>icon$1(doc, name);
|
|
3000
|
+
const mark = ()=>svgSpan(doc, 'mark', MARKS.obelism);
|
|
3001
|
+
const working = ()=>svgSpan(doc, 'working', MARKS.working);
|
|
3002
|
+
const button = (className, ...children)=>element(doc, 'button', {
|
|
3003
|
+
className,
|
|
3004
|
+
type: 'button'
|
|
3005
|
+
}, ...children);
|
|
3006
|
+
const host = element(doc, 'div');
|
|
3007
|
+
host.setAttribute('data-obelism-content-editor', '');
|
|
3008
|
+
const shadow = host.attachShadow({
|
|
3009
|
+
mode: 'open'
|
|
3010
|
+
});
|
|
3011
|
+
const pageStyle = element(doc, 'style', {
|
|
3012
|
+
textContent: pageCss()
|
|
3013
|
+
});
|
|
3014
|
+
const tile = element(doc, 'span', {
|
|
3015
|
+
className: 'tile'
|
|
3016
|
+
}, mark());
|
|
3017
|
+
const pageButtonText = element(doc, 'span');
|
|
3018
|
+
const pageButton = button('page-button hit', pageButtonText, element(doc, 'span', {
|
|
3019
|
+
className: 'visually-hidden',
|
|
3020
|
+
textContent: `, ${labels.pages}`
|
|
3021
|
+
}), icon('chevrons'));
|
|
3022
|
+
pageButton.setAttribute('aria-haspopup', 'listbox');
|
|
3023
|
+
pageButton.setAttribute('aria-expanded', 'false');
|
|
3024
|
+
pageButton.setAttribute('aria-controls', 'obelism-pages');
|
|
3025
|
+
const pageName = element(doc, 'span', {
|
|
3026
|
+
className: 'page-name'
|
|
3027
|
+
});
|
|
3028
|
+
const envText = element(doc, 'span');
|
|
3029
|
+
const env = element(doc, 'span', {
|
|
3030
|
+
className: 'env'
|
|
3031
|
+
}, element(doc, 'span', {
|
|
3032
|
+
className: 'visually-hidden',
|
|
3033
|
+
textContent: `${labels.environment}: `
|
|
3034
|
+
}), envText);
|
|
3035
|
+
const minimise = button('icon-button hit', icon('dash'));
|
|
3036
|
+
minimise.setAttribute('aria-label', labels.minimize);
|
|
3037
|
+
minimise.title = labels.minimize;
|
|
3038
|
+
const head = element(doc, 'div', {
|
|
3039
|
+
className: 'head'
|
|
3040
|
+
}, tile, pageButton, pageName, env, element(doc, 'span', {
|
|
3041
|
+
className: 'spacer'
|
|
3042
|
+
}), minimise);
|
|
3043
|
+
const picker = createPagesPicker(doc, bridge, labels, {
|
|
3044
|
+
onLoad: ()=>refresh(),
|
|
3045
|
+
onClose: ()=>setPicking(false)
|
|
3046
|
+
});
|
|
3047
|
+
const newPage = button('button quiet new-page', icon('plus'), labels.newPage);
|
|
3048
|
+
const pagesBox = element(doc, 'div', {
|
|
3049
|
+
className: 'pages'
|
|
3050
|
+
}, picker.searchRow, picker.list, picker.note, newPage);
|
|
3051
|
+
const tab = (id, controls, ...children)=>{
|
|
3052
|
+
const node = button('tab', ...children);
|
|
3053
|
+
node.id = id;
|
|
3054
|
+
node.setAttribute('role', 'tab');
|
|
3055
|
+
node.setAttribute('aria-controls', controls);
|
|
3056
|
+
return node;
|
|
3057
|
+
};
|
|
3058
|
+
const askTab = tab('obelism-tab-ask', 'obelism-ask', mark(), labels.modePrompt);
|
|
3059
|
+
const fieldsTab = tab('obelism-tab-fields', 'obelism-fields', labels.form);
|
|
3060
|
+
const tabs = element(doc, 'div', {
|
|
3061
|
+
className: 'tabs'
|
|
3062
|
+
}, askTab, fieldsTab);
|
|
3063
|
+
tabs.setAttribute('role', 'tablist');
|
|
3064
|
+
tabs.setAttribute('aria-label', labels.modes);
|
|
3065
|
+
// Edit fields asked for while a suggestion waits: it's kept or undone
|
|
3066
|
+
// first, never by the switch itself.
|
|
3067
|
+
const gateKeep = button('button primary', labels.keep);
|
|
3068
|
+
const gateUndo = button('button quiet', labels.undo);
|
|
3069
|
+
const gate = element(doc, 'div', {
|
|
3070
|
+
className: 'gate'
|
|
3071
|
+
}, element(doc, 'p', {
|
|
3072
|
+
textContent: labels.keepBeforeFields
|
|
3073
|
+
}), element(doc, 'div', {
|
|
3074
|
+
className: 'actions'
|
|
3075
|
+
}, gateKeep, gateUndo));
|
|
3076
|
+
gate.setAttribute('role', 'alert');
|
|
3077
|
+
const noteText = element(doc, 'span');
|
|
3078
|
+
const wholePage = button('', labels.scopeEntry);
|
|
3079
|
+
const cancelNew = button('', labels.newPageCancel);
|
|
3080
|
+
const note = element(doc, 'div', {
|
|
3081
|
+
className: 'note'
|
|
3082
|
+
}, icon('scope'), noteText, wholePage, cancelNew);
|
|
3083
|
+
const intro = element(doc, 'p', {
|
|
3084
|
+
className: 'intro',
|
|
3085
|
+
textContent: labels.askIntro
|
|
3086
|
+
});
|
|
3087
|
+
const shopText = element(doc, 'p', {
|
|
3088
|
+
className: 'summary'
|
|
3089
|
+
});
|
|
3090
|
+
const shopButtonText = element(doc, 'span');
|
|
3091
|
+
const shopButton = button('button primary', icon('plus'), shopButtonText);
|
|
3092
|
+
const shop = element(doc, 'div', {
|
|
3093
|
+
className: 'shop'
|
|
3094
|
+
}, shopText, shopButton);
|
|
3095
|
+
const log = element(doc, 'div', {
|
|
3096
|
+
className: 'log'
|
|
3097
|
+
});
|
|
3098
|
+
log.setAttribute('role', 'log');
|
|
3099
|
+
const scroll = element(doc, 'div', {
|
|
3100
|
+
className: 'scroll'
|
|
3101
|
+
}, note, intro, shop, log);
|
|
3102
|
+
const pathInput = element(doc, 'input', {
|
|
3103
|
+
type: 'text',
|
|
3104
|
+
placeholder: labels.pathPlaceholder,
|
|
3105
|
+
autocomplete: 'off',
|
|
3106
|
+
spellcheck: false
|
|
3107
|
+
});
|
|
3108
|
+
const pathRow = element(doc, 'label', {
|
|
3109
|
+
className: 'path'
|
|
3110
|
+
}, element(doc, 'span', {
|
|
3111
|
+
textContent: labels.path
|
|
3112
|
+
}), pathInput);
|
|
3113
|
+
const input = element(doc, 'textarea', {
|
|
3114
|
+
rows: 1
|
|
3115
|
+
});
|
|
3116
|
+
input.setAttribute('aria-label', labels.open);
|
|
3117
|
+
const sendIcon = icon('arrowUp');
|
|
3118
|
+
const sendWorking = working();
|
|
3119
|
+
const send = element(doc, 'button', {
|
|
3120
|
+
className: 'send',
|
|
3121
|
+
type: 'submit',
|
|
3122
|
+
title: labels.send
|
|
3123
|
+
}, element(doc, 'span', {
|
|
3124
|
+
className: 'visually-hidden',
|
|
3125
|
+
textContent: labels.send
|
|
3126
|
+
}), sendIcon, sendWorking);
|
|
3127
|
+
const bar = element(doc, 'div', {
|
|
3128
|
+
className: 'bar'
|
|
3129
|
+
}, mark(), input, send);
|
|
3130
|
+
const composer = element(doc, 'form', {
|
|
3131
|
+
className: 'composer'
|
|
3132
|
+
}, pathRow, bar);
|
|
3133
|
+
const ask = element(doc, 'div', {
|
|
3134
|
+
className: 'ask',
|
|
3135
|
+
id: 'obelism-ask'
|
|
3136
|
+
}, scroll, composer);
|
|
3137
|
+
ask.setAttribute('role', 'tabpanel');
|
|
3138
|
+
ask.setAttribute('aria-labelledby', askTab.id);
|
|
3139
|
+
const formNote = element(doc, 'p', {
|
|
3140
|
+
className: 'form-note'
|
|
3141
|
+
});
|
|
3142
|
+
formNote.setAttribute('role', 'status');
|
|
3143
|
+
const formBox = element(doc, 'div', {
|
|
3144
|
+
className: 'form'
|
|
3145
|
+
}, formNote);
|
|
3146
|
+
const fields = element(doc, 'div', {
|
|
3147
|
+
className: 'fields',
|
|
3148
|
+
id: 'obelism-fields'
|
|
3149
|
+
}, formBox);
|
|
3150
|
+
fields.setAttribute('role', 'tabpanel');
|
|
3151
|
+
fields.setAttribute('aria-labelledby', fieldsTab.id);
|
|
3152
|
+
const saveState = element(doc, 'p', {
|
|
3153
|
+
className: 'save-state'
|
|
3154
|
+
});
|
|
3155
|
+
saveState.setAttribute('aria-live', 'polite');
|
|
3156
|
+
const footerActions = element(doc, 'slot');
|
|
3157
|
+
footerActions.name = 'actions-footer';
|
|
3158
|
+
const undo = button('button quiet', icon('undo'), labels.undo);
|
|
3159
|
+
const save = button('button secondary', labels.saveDraft);
|
|
3160
|
+
const publish = button('button primary', labels.publish);
|
|
3161
|
+
const foot = element(doc, 'div', {
|
|
3162
|
+
className: 'foot'
|
|
3163
|
+
}, saveState, footerActions, undo, save, publish);
|
|
3164
|
+
const announcer = element(doc, 'p', {
|
|
3165
|
+
className: 'visually-hidden'
|
|
3166
|
+
});
|
|
3167
|
+
announcer.setAttribute('role', 'status');
|
|
3168
|
+
const panel = element(doc, 'aside', {
|
|
3169
|
+
className: 'panel',
|
|
3170
|
+
id: 'obelism-editor'
|
|
3171
|
+
}, head, pagesBox, tabs, gate, ask, fields, foot, announcer);
|
|
3172
|
+
panel.setAttribute('aria-label', labels.editor);
|
|
3173
|
+
const launch = button('launch', element(doc, 'span', {
|
|
3174
|
+
className: 'launch-tile'
|
|
3175
|
+
}, mark()), labels.editing);
|
|
3176
|
+
launch.setAttribute('aria-controls', panel.id);
|
|
3177
|
+
const reading = element(doc, 'span', {
|
|
3178
|
+
className: 'reading'
|
|
3179
|
+
}, element(doc, 'span', {
|
|
3180
|
+
className: 'launch-tile'
|
|
3181
|
+
}, mark()), labels.viewingDrafts);
|
|
3182
|
+
const eyeOn = icon('eye');
|
|
3183
|
+
const eyeOff = icon('eyeOff');
|
|
3184
|
+
const eye = button('eye', eyeOn, eyeOff);
|
|
3185
|
+
eye.setAttribute('aria-label', labels.hideOutlines);
|
|
3186
|
+
eye.title = labels.hideOutlines;
|
|
3187
|
+
const barActions = element(doc, 'slot');
|
|
3188
|
+
barActions.name = 'actions-bar';
|
|
3189
|
+
const exit = button('exit', labels.signOut);
|
|
3190
|
+
const launcher = element(doc, 'div', {
|
|
3191
|
+
className: 'launcher'
|
|
3192
|
+
}, launch, reading, eye, barActions, exit);
|
|
3193
|
+
if (options.actions) host.appendChild(options.actions);
|
|
3194
|
+
const layer = element(doc, 'div', {
|
|
3195
|
+
className: 'layer'
|
|
3196
|
+
});
|
|
3197
|
+
layer.setAttribute('aria-hidden', 'true');
|
|
3198
|
+
const wash = element(doc, 'div', {
|
|
3199
|
+
className: 'wash'
|
|
3200
|
+
});
|
|
3201
|
+
wash.setAttribute('aria-hidden', 'true');
|
|
3202
|
+
const root = element(doc, 'div', {
|
|
3203
|
+
className: 'root'
|
|
3204
|
+
}, wash, layer, panel, launcher);
|
|
3205
|
+
shadow.append(element(doc, 'style', {
|
|
3206
|
+
textContent: CSS
|
|
3207
|
+
}), root);
|
|
3208
|
+
let open = readOpen(win);
|
|
3209
|
+
let mode = 'ask';
|
|
3210
|
+
let gated = false;
|
|
3211
|
+
let creating = false;
|
|
3212
|
+
let outlines = true;
|
|
3213
|
+
let picking = false;
|
|
3214
|
+
let stored = null;
|
|
3215
|
+
let wasUnsaved = false;
|
|
3216
|
+
let shopMessage = null;
|
|
3217
|
+
const answers = [];
|
|
3218
|
+
let wide = true;
|
|
3219
|
+
let hoverUid = null;
|
|
3220
|
+
// Block names by uid, from the views; null when it couldn't be read.
|
|
3221
|
+
const names = new Map();
|
|
3222
|
+
let pageNames = 'idle';
|
|
3223
|
+
let destroyed = false;
|
|
3224
|
+
const formSlot = createFormSlot(bridge, formBox, labels.formTitle, {
|
|
3225
|
+
onReady: ()=>refresh(),
|
|
3226
|
+
onError: ()=>refresh(),
|
|
3227
|
+
onEscape: ()=>stepBack()
|
|
3228
|
+
});
|
|
3229
|
+
const pending = ()=>answers.filter(({ status })=>status === 'pending');
|
|
3230
|
+
const latestPending = ()=>pending().slice(-1)[0] ?? null;
|
|
3231
|
+
// Decided on while nothing else runs: undo would take the running one's
|
|
3232
|
+
// step.
|
|
3233
|
+
const decidable = ()=>answers.some(({ status })=>status === 'working') ? null : latestPending();
|
|
3234
|
+
const announce = (text)=>{
|
|
3235
|
+
announcer.textContent = '';
|
|
3236
|
+
announcer.textContent = text;
|
|
3237
|
+
};
|
|
3238
|
+
const nameOf = (uid)=>{
|
|
3239
|
+
if (!names.has(uid) && pageNames !== 'loading') {
|
|
3240
|
+
names.set(uid, null);
|
|
3241
|
+
bridge.view({
|
|
3242
|
+
uid
|
|
3243
|
+
}).then((view)=>{
|
|
3244
|
+
if (typeof view._type === 'string') names.set(uid, humanize(view._type));
|
|
3245
|
+
refresh();
|
|
3246
|
+
}, ()=>{});
|
|
3247
|
+
}
|
|
3248
|
+
return names.get(uid) ?? labels.block;
|
|
3249
|
+
};
|
|
3250
|
+
const readNames = (value)=>{
|
|
3251
|
+
if (Array.isArray(value)) value.forEach(readNames);
|
|
3252
|
+
else if (isRecord(value)) {
|
|
3253
|
+
if (typeof value._uid === 'string' && typeof value._type === 'string') names.set(value._uid, humanize(value._type));
|
|
3254
|
+
Object.values(value).forEach(readNames);
|
|
3255
|
+
}
|
|
3256
|
+
};
|
|
3257
|
+
const readPageNames = ()=>{
|
|
3258
|
+
if (pageNames !== 'idle' || bridge.entryId === null) return;
|
|
3259
|
+
pageNames = 'loading';
|
|
3260
|
+
const done = ()=>{
|
|
3261
|
+
pageNames = 'done';
|
|
3262
|
+
refresh();
|
|
3263
|
+
};
|
|
3264
|
+
bridge.view({
|
|
3265
|
+
uid: null
|
|
3266
|
+
}).then((view)=>{
|
|
3267
|
+
readNames(view);
|
|
3268
|
+
done();
|
|
3269
|
+
}, done);
|
|
3270
|
+
};
|
|
3271
|
+
// The page's own title: its entry's name, else the document's.
|
|
3272
|
+
const knownTitle = ()=>picker.current()?.name || doc.title.trim();
|
|
3273
|
+
const pageTitle = ()=>knownTitle() || labels.pages;
|
|
3274
|
+
let frame = null;
|
|
3275
|
+
const drawMarks = (state)=>{
|
|
3276
|
+
layer.replaceChildren();
|
|
3277
|
+
wash.replaceChildren();
|
|
3278
|
+
if (!marksShown(state)) return;
|
|
3279
|
+
const rectOf = (selector)=>{
|
|
3280
|
+
const target = doc.querySelector(selector);
|
|
3281
|
+
const rect = target?.getBoundingClientRect();
|
|
3282
|
+
return rect && (rect.width || rect.height) ? rect : null;
|
|
3283
|
+
};
|
|
3284
|
+
const box = (rect, kind, into = layer)=>{
|
|
3285
|
+
const node = element(doc, 'div', {
|
|
3286
|
+
className: `box ${kind}`
|
|
3287
|
+
});
|
|
3288
|
+
Object.assign(node.style, {
|
|
3289
|
+
top: `${rect.top - 3}px`,
|
|
3290
|
+
left: `${rect.left - 3}px`,
|
|
3291
|
+
width: `${rect.width + 6}px`,
|
|
3292
|
+
height: `${rect.height + 6}px`
|
|
3293
|
+
});
|
|
3294
|
+
into.appendChild(node);
|
|
3295
|
+
};
|
|
3296
|
+
const chips = (rect, ...nodes)=>{
|
|
3297
|
+
const row = element(doc, 'div', {
|
|
3298
|
+
className: 'chips'
|
|
3299
|
+
}, ...nodes);
|
|
3300
|
+
// Above the block, or inside its top when the screen's edge is
|
|
3301
|
+
// there.
|
|
3302
|
+
const top = rect.top - 34 < 0 ? rect.top + 6 : rect.top - 34;
|
|
3303
|
+
Object.assign(row.style, {
|
|
3304
|
+
top: `${top}px`,
|
|
3305
|
+
left: `${rect.left - 3}px`
|
|
3306
|
+
});
|
|
3307
|
+
layer.appendChild(row);
|
|
3308
|
+
};
|
|
3309
|
+
const accentChip = (text, preview)=>{
|
|
3310
|
+
const chip = element(doc, 'span', {
|
|
3311
|
+
className: 'accent-chip'
|
|
3312
|
+
});
|
|
3313
|
+
if (preview) chip.appendChild(mark());
|
|
3314
|
+
chip.appendChild(doc.createTextNode(text));
|
|
3315
|
+
return chip;
|
|
3316
|
+
};
|
|
3317
|
+
const previewing = latestPending();
|
|
3318
|
+
const previewSelector = previewing ? previewing.uid ? blockSelector(previewing.uid) : `[${ENTRY_ATTRIBUTE}]` : null;
|
|
3319
|
+
const previewRect = previewSelector ? rectOf(previewSelector) : null;
|
|
3320
|
+
if (previewRect) box(previewRect, 'fill', wash);
|
|
3321
|
+
if (hoverUid && hoverUid !== state.selectedUid) {
|
|
3322
|
+
const rect = rectOf(blockSelector(hoverUid));
|
|
3323
|
+
if (rect) {
|
|
3324
|
+
box(rect, 'hover');
|
|
3325
|
+
const inside = rect.top - 23 < 0;
|
|
3326
|
+
const tab = element(doc, 'span', {
|
|
3327
|
+
className: `name-tab${inside ? ' inside' : ''}`,
|
|
3328
|
+
textContent: nameOf(hoverUid)
|
|
3329
|
+
});
|
|
3330
|
+
Object.assign(tab.style, {
|
|
3331
|
+
top: `${inside ? rect.top - 1.5 : rect.top - 23}px`,
|
|
3332
|
+
left: `${rect.left - 3}px`
|
|
3333
|
+
});
|
|
3334
|
+
layer.appendChild(tab);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
const selectedPreviewing = !!previewing && previewing.uid === state.selectedUid;
|
|
3338
|
+
if (state.selectedUid) {
|
|
3339
|
+
const rect = rectOf(blockSelector(state.selectedUid));
|
|
3340
|
+
if (rect) {
|
|
3341
|
+
const name = nameOf(state.selectedUid);
|
|
3342
|
+
const title = knownTitle();
|
|
3343
|
+
chips(rect, accentChip(selectedPreviewing ? fill(labels.previewChip, {
|
|
3344
|
+
name
|
|
3345
|
+
}) : name, selectedPreviewing), ...title ? [
|
|
3346
|
+
element(doc, 'span', {
|
|
3347
|
+
className: 'ink-chip',
|
|
3348
|
+
textContent: title
|
|
3349
|
+
})
|
|
3350
|
+
] : []);
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
if (previewRect && !selectedPreviewing) {
|
|
3354
|
+
box(previewRect, 'preview');
|
|
3355
|
+
const name = previewing.uid ? nameOf(previewing.uid) : pageTitle();
|
|
3356
|
+
chips(previewRect, accentChip(fill(labels.previewChip, {
|
|
3357
|
+
name
|
|
3358
|
+
}), true));
|
|
3359
|
+
}
|
|
3360
|
+
};
|
|
3361
|
+
const scheduleMarks = ()=>{
|
|
3362
|
+
if (frame !== null || !win) return;
|
|
3363
|
+
frame = win.requestAnimationFrame(()=>{
|
|
3364
|
+
frame = null;
|
|
3365
|
+
drawMarks(bridge.getState());
|
|
3366
|
+
});
|
|
3367
|
+
};
|
|
3368
|
+
const panelShown = (state)=>state.features.includes('prompt') && open;
|
|
3369
|
+
const marksShown = (state)=>panelShown(state) && outlines && !creating;
|
|
3370
|
+
const value = (text)=>element(doc, 'span', {
|
|
3371
|
+
className: `value${text ? '' : ' none'}`,
|
|
3372
|
+
textContent: text || labels.emptyValue
|
|
3373
|
+
});
|
|
3374
|
+
// Fills an answer's card once it's answered; afterwards only its eyebrow
|
|
3375
|
+
// and actions change, so the log reads out only what's new.
|
|
3376
|
+
const answered = (answer)=>{
|
|
3377
|
+
const card = answer.node.querySelector('.card');
|
|
3378
|
+
card.querySelector('.status-line')?.remove();
|
|
3379
|
+
if (answer.status === 'failed') {
|
|
3380
|
+
card.appendChild(element(doc, 'p', {
|
|
3381
|
+
className: 'failure'
|
|
3382
|
+
}, icon('alert'), answer.summary));
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
card.appendChild(answer.eyebrow);
|
|
3386
|
+
if (answer.summary) card.appendChild(element(doc, 'p', {
|
|
3387
|
+
className: 'summary',
|
|
3388
|
+
textContent: answer.summary
|
|
3389
|
+
}));
|
|
3390
|
+
for (const diff of answer.diffs.slice(0, MAX_DIFFS)){
|
|
3391
|
+
const field = element(doc, 'div', {
|
|
3392
|
+
className: 'diff-field'
|
|
3393
|
+
}, element(doc, 'span', {
|
|
3394
|
+
className: 'diff-label',
|
|
3395
|
+
textContent: diff.label
|
|
3396
|
+
}), element(doc, 'div', {
|
|
3397
|
+
className: 'diff'
|
|
3398
|
+
}, element(doc, 'div', {
|
|
3399
|
+
className: 'before'
|
|
3400
|
+
}, element(doc, 'span', {
|
|
3401
|
+
className: 'tag',
|
|
3402
|
+
textContent: labels.before
|
|
3403
|
+
}), value(diff.before)), element(doc, 'div', {
|
|
3404
|
+
className: 'after'
|
|
3405
|
+
}, element(doc, 'span', {
|
|
3406
|
+
className: 'tag',
|
|
3407
|
+
textContent: labels.after
|
|
3408
|
+
}), value(diff.after))));
|
|
3409
|
+
answer.details.push(field);
|
|
3410
|
+
card.appendChild(field);
|
|
3411
|
+
}
|
|
3412
|
+
if (answer.diffs.length > MAX_DIFFS) {
|
|
3413
|
+
const more = element(doc, 'p', {
|
|
3414
|
+
className: 'more',
|
|
3415
|
+
textContent: fill(labels.moreChanges, {
|
|
3416
|
+
count: answer.diffs.length - MAX_DIFFS
|
|
3417
|
+
})
|
|
3418
|
+
});
|
|
3419
|
+
answer.details.push(more);
|
|
3420
|
+
card.appendChild(more);
|
|
3421
|
+
}
|
|
3422
|
+
if (answer.status === 'pending') {
|
|
3423
|
+
const keep = button('button primary', labels.keep);
|
|
3424
|
+
const retry = button('button secondary', labels.retry);
|
|
3425
|
+
const back = button('button quiet', labels.undo);
|
|
3426
|
+
keep.addEventListener('click', ()=>keepAll());
|
|
3427
|
+
retry.addEventListener('click', ()=>void tryAnother(answer));
|
|
3428
|
+
back.addEventListener('click', ()=>undoAnswer(answer));
|
|
3429
|
+
answer.actions = element(doc, 'div', {
|
|
3430
|
+
className: 'actions'
|
|
3431
|
+
}, keep, retry, back);
|
|
3432
|
+
card.appendChild(answer.actions);
|
|
3433
|
+
}
|
|
3434
|
+
};
|
|
3435
|
+
// Brings each card's eyebrow, actions and details in line with its
|
|
3436
|
+
// status, touching only what changed.
|
|
3437
|
+
const syncAnswers = ()=>{
|
|
3438
|
+
const deciding = decidable();
|
|
3439
|
+
for (const answer of answers){
|
|
3440
|
+
if (answer.status === 'working' || answer.status === 'failed') continue;
|
|
3441
|
+
answer.node.dataset.status = answer.status;
|
|
3442
|
+
const eyebrow = answer.status === 'pending' ? labels.suggested : answer.status === 'kept' ? labels.kept : labels.undone;
|
|
3443
|
+
if (answer.eyebrow.textContent !== eyebrow) answer.eyebrow.textContent = eyebrow;
|
|
3444
|
+
if (answer.actions) answer.actions.hidden = answer !== deciding;
|
|
3445
|
+
for (const detail of answer.details)detail.hidden = answer.status === 'undone';
|
|
3446
|
+
}
|
|
3447
|
+
};
|
|
3448
|
+
const addAnswer = (prompt, uid)=>{
|
|
3449
|
+
const card = element(doc, 'div', {
|
|
3450
|
+
className: 'card'
|
|
3451
|
+
}, element(doc, 'span', {
|
|
3452
|
+
className: 'visually-hidden',
|
|
3453
|
+
textContent: `${labels.obelism}: `
|
|
3454
|
+
}), element(doc, 'p', {
|
|
3455
|
+
className: 'status-line'
|
|
3456
|
+
}, working(), creating ? labels.creating : labels.working));
|
|
3457
|
+
const node = element(doc, 'div', {
|
|
3458
|
+
className: 'answer'
|
|
3459
|
+
}, element(doc, 'span', {
|
|
3460
|
+
className: 'chip'
|
|
3461
|
+
}, mark()), card);
|
|
3462
|
+
node.dataset.status = 'working';
|
|
3463
|
+
const answer = {
|
|
3464
|
+
prompt,
|
|
3465
|
+
uid,
|
|
3466
|
+
status: 'working',
|
|
3467
|
+
summary: '',
|
|
3468
|
+
diffs: [],
|
|
3469
|
+
step: null,
|
|
3470
|
+
node,
|
|
3471
|
+
eyebrow: element(doc, 'p', {
|
|
3472
|
+
className: 'eyebrow'
|
|
3473
|
+
}),
|
|
3474
|
+
actions: null,
|
|
3475
|
+
details: []
|
|
3476
|
+
};
|
|
3477
|
+
answers.push(answer);
|
|
3478
|
+
log.appendChild(node);
|
|
3479
|
+
return answer;
|
|
3480
|
+
};
|
|
3481
|
+
const render = (state)=>{
|
|
3482
|
+
if (destroyed) return;
|
|
3483
|
+
const available = state.features.includes('prompt');
|
|
3484
|
+
const storing = state.saving || state.publishing;
|
|
3485
|
+
const busy = state.prompting || storing;
|
|
3486
|
+
const shown = available && open;
|
|
3487
|
+
const { site, canCreate, pageless, shopHere, onlyNew } = pageContext(bridge, state, options.promptWithoutPage);
|
|
3488
|
+
if (!canCreate) creating = false;
|
|
3489
|
+
if (onlyNew) creating = true;
|
|
3490
|
+
const hasForm = site && !creating && !pageless && state.features.includes('form');
|
|
3491
|
+
if (!hasForm) mode = 'ask';
|
|
3492
|
+
const waiting = pending().length > 0;
|
|
3493
|
+
if (!waiting || mode === 'fields') gated = false;
|
|
3494
|
+
// Steps undone elsewhere (or saved away) aren't any answer's any more.
|
|
3495
|
+
for (const answer of answers)if (answer.step !== null && answer.step > (state.undoSteps ?? 0)) answer.step = null;
|
|
3496
|
+
root.hidden = !available && !site;
|
|
3497
|
+
panel.toggleAttribute('data-closed', !shown);
|
|
3498
|
+
panel.inert = !shown;
|
|
3499
|
+
launch.hidden = !available;
|
|
3500
|
+
launch.setAttribute('aria-expanded', String(shown));
|
|
3501
|
+
reading.hidden = available;
|
|
3502
|
+
eye.hidden = !shown;
|
|
3503
|
+
eye.setAttribute('aria-pressed', String(!outlines));
|
|
3504
|
+
eyeOn.hidden = !outlines;
|
|
3505
|
+
eyeOff.hidden = outlines;
|
|
3506
|
+
exit.hidden = options.signOut === false;
|
|
3507
|
+
// The site beside the panel, wide enough; a sheet over it otherwise.
|
|
3508
|
+
const docked = shown && wide;
|
|
3509
|
+
html.toggleAttribute('data-obelism-docked', docked);
|
|
3510
|
+
html.style.setProperty('--obelism-dock', `${docked ? DOCK_WIDTH_PX : 0}px`);
|
|
3511
|
+
const selecting = shown && outlines && !creating;
|
|
3512
|
+
if (state.selecting !== selecting) bridge.setSelecting(selecting);
|
|
3513
|
+
const showForm = shown && mode === 'fields';
|
|
3514
|
+
if (showForm && !formSlot.mounted) formSlot.mount();
|
|
3515
|
+
else if (!showForm && formSlot.mounted) formSlot.unmount();
|
|
3516
|
+
if (picking && !shown) picking = false;
|
|
3517
|
+
pageButtonText.textContent = pageTitle();
|
|
3518
|
+
pageButton.hidden = picker.pages === null && picker.error !== null;
|
|
3519
|
+
pageName.hidden = !pageButton.hidden;
|
|
3520
|
+
pageName.textContent = pageTitle();
|
|
3521
|
+
pageButton.setAttribute('aria-expanded', String(picking));
|
|
3522
|
+
pagesBox.hidden = !picking;
|
|
3523
|
+
newPage.hidden = !canCreate || creating;
|
|
3524
|
+
newPage.disabled = busy;
|
|
3525
|
+
const environment = bridge.environment;
|
|
3526
|
+
env.hidden = !environment;
|
|
3527
|
+
env.dataset.env = environment ?? '';
|
|
3528
|
+
envText.textContent = environment === 'production' ? labels.envProduction : environment ?? '';
|
|
3529
|
+
tabs.hidden = !hasForm;
|
|
3530
|
+
askTab.setAttribute('aria-selected', String(mode === 'ask'));
|
|
3531
|
+
fieldsTab.setAttribute('aria-selected', String(mode === 'fields'));
|
|
3532
|
+
askTab.disabled = fieldsTab.disabled = busy;
|
|
3533
|
+
askTab.tabIndex = mode === 'ask' ? 0 : -1;
|
|
3534
|
+
fieldsTab.tabIndex = mode === 'fields' ? 0 : -1;
|
|
3535
|
+
gate.hidden = !gated;
|
|
3536
|
+
gateKeep.disabled = gateUndo.disabled = busy;
|
|
3537
|
+
ask.hidden = mode !== 'ask';
|
|
3538
|
+
fields.hidden = mode !== 'fields';
|
|
3539
|
+
const offerShop = shopHere !== null && !creating;
|
|
3540
|
+
if (creating) {
|
|
3541
|
+
noteText.textContent = labels.newPage;
|
|
3542
|
+
} else if (state.selectedUid) {
|
|
3543
|
+
const [beforeName, afterName = ''] = labels.scopeNoteBlock.split('{name}');
|
|
3544
|
+
noteText.replaceChildren(beforeName ?? '', element(doc, 'strong', {
|
|
3545
|
+
textContent: nameOf(state.selectedUid)
|
|
3546
|
+
}), afterName);
|
|
3547
|
+
} else {
|
|
3548
|
+
noteText.textContent = labels.scopeNotePage;
|
|
3549
|
+
}
|
|
3550
|
+
note.hidden = offerShop || pageless && !creating && !options.promptWithoutPage;
|
|
3551
|
+
wholePage.hidden = creating || !state.selectedUid;
|
|
3552
|
+
cancelNew.hidden = !creating || onlyNew;
|
|
3553
|
+
intro.hidden = answers.length > 0 || offerShop || creating;
|
|
3554
|
+
shop.hidden = !offerShop;
|
|
3555
|
+
const shopWords = shopCopy(labels, shopHere);
|
|
3556
|
+
shopButtonText.textContent = state.prompting && offerShop ? shopWords.adding : shopWords.add;
|
|
3557
|
+
shopButton.disabled = busy;
|
|
3558
|
+
shopText.textContent = shopMessage?.text ?? shopWords.hint;
|
|
3559
|
+
shopText.className = shopMessage?.error ? 'failure' : 'summary';
|
|
3560
|
+
composer.hidden = offerShop;
|
|
3561
|
+
pathRow.hidden = !creating;
|
|
3562
|
+
pathInput.disabled = busy;
|
|
3563
|
+
input.placeholder = creating ? labels.newPagePlaceholder : labels.placeholder;
|
|
3564
|
+
input.disabled = busy;
|
|
3565
|
+
send.disabled = busy || input.value.trim() === '' || creating && pathInput.value.trim() === '';
|
|
3566
|
+
send.setAttribute('aria-busy', String(state.prompting));
|
|
3567
|
+
sendIcon.hidden = state.prompting;
|
|
3568
|
+
sendWorking.hidden = !state.prompting;
|
|
3569
|
+
undo.hidden = waiting || !state.canUndo || creating;
|
|
3570
|
+
undo.disabled = busy;
|
|
3571
|
+
save.hidden = !state.features.includes('save') || pageless;
|
|
3572
|
+
save.disabled = busy || waiting || !state.unsaved;
|
|
3573
|
+
publish.hidden = !state.features.includes('publish') || pageless;
|
|
3574
|
+
publish.disabled = busy || waiting;
|
|
3575
|
+
if (options.actions) options.actions.slot = shown ? 'actions-footer' : 'actions-bar';
|
|
3576
|
+
foot.hidden = !site;
|
|
3577
|
+
// New changes make the last save's word stale.
|
|
3578
|
+
if (state.unsaved && !wasUnsaved) stored = null;
|
|
3579
|
+
wasUnsaved = state.unsaved;
|
|
3580
|
+
const saveIcon = storing ? working() : stored?.kind === 'error' ? icon('alert') : waiting || state.unsaved ? element(doc, 'span', {
|
|
3581
|
+
className: 'dot'
|
|
3582
|
+
}) : icon('check');
|
|
3583
|
+
const saveText = state.saving ? labels.saving : state.publishing ? labels.publishing : stored?.kind === 'error' ? `${stored.action === 'save' ? labels.saveFailed : labels.publishFailed}: ${stored.text}` : waiting ? labels.keepFirst : state.unsaved ? labels.unsaved : stored?.kind === 'saved' ? labels.saved : stored?.kind === 'published' ? labels.published : labels.upToDate;
|
|
3584
|
+
const saveKind = storing ? 'saving' : stored?.kind === 'error' ? 'error' : waiting || state.unsaved ? 'unsaved' : 'saved';
|
|
3585
|
+
// Rewritten only when it says something new: it's a live region.
|
|
3586
|
+
if (saveState.dataset.state !== saveKind || saveState.dataset.text !== saveText) {
|
|
3587
|
+
saveState.dataset.state = saveKind;
|
|
3588
|
+
saveState.dataset.text = saveText;
|
|
3589
|
+
saveState.replaceChildren(saveIcon, element(doc, 'span', {
|
|
3590
|
+
textContent: saveText
|
|
3591
|
+
}));
|
|
3592
|
+
if (stored?.kind === 'error' && !storing) {
|
|
3593
|
+
const action = stored.action;
|
|
3594
|
+
const retry = button('link', labels.tryAgain);
|
|
3595
|
+
retry.addEventListener('click', ()=>void store(action));
|
|
3596
|
+
saveState.appendChild(retry);
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
formBox.classList.toggle('loading', formSlot.loading || formSlot.error !== null);
|
|
3600
|
+
formNote.hidden = !formSlot.loading && formSlot.error === null;
|
|
3601
|
+
formNote.className = `form-note${formSlot.error !== null ? ' error' : ''}`;
|
|
3602
|
+
formNote.textContent = formSlot.error ?? labels.formLoading;
|
|
3603
|
+
syncAnswers();
|
|
3604
|
+
if (shown) {
|
|
3605
|
+
readPageNames();
|
|
3606
|
+
if (picker.pages === null && picker.error === null) void picker.fetch();
|
|
3607
|
+
}
|
|
3608
|
+
scheduleMarks();
|
|
3609
|
+
};
|
|
3610
|
+
const refresh = ()=>render(bridge.getState());
|
|
3611
|
+
const grow = ()=>{
|
|
3612
|
+
input.style.height = 'auto';
|
|
3613
|
+
input.style.height = `${input.scrollHeight}px`;
|
|
3614
|
+
};
|
|
3615
|
+
const scrollDown = ()=>{
|
|
3616
|
+
scroll.scrollTop = scroll.scrollHeight;
|
|
3617
|
+
};
|
|
3618
|
+
const remember = ()=>{
|
|
3619
|
+
try {
|
|
3620
|
+
win?.localStorage.setItem(OPEN_KEY, open ? 'open' : 'minimised');
|
|
3621
|
+
} catch {}
|
|
3622
|
+
};
|
|
3623
|
+
const setOpen = (next)=>{
|
|
3624
|
+
open = next;
|
|
3625
|
+
remember();
|
|
3626
|
+
if (!open) picking = false;
|
|
3627
|
+
refresh();
|
|
3628
|
+
if (open) {
|
|
3629
|
+
(mode === 'fields' ? fieldsTab : creating ? pathInput : input).focus();
|
|
3630
|
+
} else {
|
|
3631
|
+
launch.focus();
|
|
3632
|
+
}
|
|
3633
|
+
};
|
|
3634
|
+
const setMode = (next)=>{
|
|
3635
|
+
if (next === 'fields' && pending().length > 0) {
|
|
3636
|
+
gated = true;
|
|
3637
|
+
refresh();
|
|
3638
|
+
gateKeep.focus();
|
|
3639
|
+
return;
|
|
3640
|
+
}
|
|
3641
|
+
gated = false;
|
|
3642
|
+
if (mode === next) return refresh();
|
|
3643
|
+
mode = next;
|
|
3644
|
+
refresh();
|
|
3645
|
+
};
|
|
3646
|
+
const setCreating = (next)=>{
|
|
3647
|
+
creating = next;
|
|
3648
|
+
picking = false;
|
|
3649
|
+
if (creating) bridge.select(null);
|
|
3650
|
+
refresh();
|
|
3651
|
+
(creating ? pathInput : input).focus();
|
|
3652
|
+
};
|
|
3653
|
+
const setPicking = (next)=>{
|
|
3654
|
+
picking = next;
|
|
3655
|
+
if (picking) picker.reset();
|
|
3656
|
+
refresh();
|
|
3657
|
+
(picking ? picker.search : pageButton).focus();
|
|
3658
|
+
};
|
|
3659
|
+
// Escape, on the page or in the form: from the pages, from a block to the
|
|
3660
|
+
// whole page. True when it did something.
|
|
3661
|
+
const stepBack = ()=>{
|
|
3662
|
+
const state = bridge.getState();
|
|
3663
|
+
if (!panelShown(state)) return false;
|
|
3664
|
+
if (picking) setPicking(false);
|
|
3665
|
+
else if (state.selectedUid) bridge.select(null);
|
|
3666
|
+
else return false;
|
|
3667
|
+
return true;
|
|
3668
|
+
};
|
|
3669
|
+
const keepAll = (focus = true)=>{
|
|
3670
|
+
const waiting = pending();
|
|
3671
|
+
if (!waiting.length || !decidable()) return;
|
|
3672
|
+
waiting.forEach((answer)=>{
|
|
3673
|
+
answer.status = 'kept';
|
|
3674
|
+
});
|
|
3675
|
+
refresh();
|
|
3676
|
+
if (focus) input.focus();
|
|
3677
|
+
};
|
|
3678
|
+
const undoAnswer = (answer, focus = true)=>{
|
|
3679
|
+
if (answer !== decidable()) return;
|
|
3680
|
+
bridge.undo();
|
|
3681
|
+
answer.status = 'undone';
|
|
3682
|
+
refresh();
|
|
3683
|
+
if (focus) input.focus();
|
|
3684
|
+
};
|
|
3685
|
+
// Runs a prompt on `uid` (null: the page) and shows what it changed. The
|
|
3686
|
+
// change is on the page straight away (the bridge's unsaved content):
|
|
3687
|
+
// that is the preview, until it's kept or undone.
|
|
3688
|
+
const runPrompt = async (answer, text)=>{
|
|
3689
|
+
const scope = {
|
|
3690
|
+
uid: answer.uid
|
|
3691
|
+
};
|
|
3692
|
+
const before = await bridge.view(scope).catch(()=>null);
|
|
3693
|
+
try {
|
|
3694
|
+
const { summary } = await bridge.prompt(text, scope);
|
|
3695
|
+
answer.step = bridge.getState().undoSteps ?? null;
|
|
3696
|
+
const after = before ? await bridge.view(scope).catch(()=>null) : null;
|
|
3697
|
+
answer.summary = summary;
|
|
3698
|
+
answer.diffs = before && after ? diffViews(before, after) : [];
|
|
3699
|
+
answer.status = 'pending';
|
|
3700
|
+
} catch (error) {
|
|
3701
|
+
answer.summary = failure(error);
|
|
3702
|
+
answer.status = 'failed';
|
|
3703
|
+
}
|
|
3704
|
+
answered(answer);
|
|
3705
|
+
refresh();
|
|
3706
|
+
scrollDown();
|
|
3707
|
+
};
|
|
3708
|
+
const tryAnother = async (answer)=>{
|
|
3709
|
+
if (answer !== decidable()) return;
|
|
3710
|
+
bridge.undo();
|
|
3711
|
+
answer.status = 'undone';
|
|
3712
|
+
const next = addAnswer(answer.prompt, answer.uid);
|
|
3713
|
+
refresh();
|
|
3714
|
+
scrollDown();
|
|
3715
|
+
input.focus();
|
|
3716
|
+
await runPrompt(next, fill(labels.retryPrompt, {
|
|
3717
|
+
prompt: answer.prompt
|
|
3718
|
+
}));
|
|
3719
|
+
};
|
|
3720
|
+
const submit = async ()=>{
|
|
3721
|
+
const prompt = input.value.trim();
|
|
3722
|
+
const state = bridge.getState();
|
|
3723
|
+
if (!prompt || state.prompting) return;
|
|
3724
|
+
const path = pathInput.value.trim();
|
|
3725
|
+
if (creating && !path) return pathInput.focus();
|
|
3726
|
+
const uid = creating ? null : state.selectedUid;
|
|
3727
|
+
const scopeName = creating ? `${labels.newPage} · ${path}` : uid ? nameOf(uid) : labels.scopeEntry;
|
|
3728
|
+
log.appendChild(element(doc, 'p', {
|
|
3729
|
+
className: 'bubble'
|
|
3730
|
+
}, element(doc, 'span', {
|
|
3731
|
+
className: 'visually-hidden',
|
|
3732
|
+
textContent: `${labels.you} (${scopeName}): `
|
|
3733
|
+
}), prompt));
|
|
3734
|
+
input.value = '';
|
|
3735
|
+
grow();
|
|
3736
|
+
const answer = addAnswer(prompt, uid);
|
|
3737
|
+
refresh();
|
|
3738
|
+
scrollDown();
|
|
3739
|
+
if (creating) {
|
|
3740
|
+
try {
|
|
3741
|
+
// The bridge opens the new page; this shows until it does.
|
|
3742
|
+
await bridge.createPage(path, prompt);
|
|
3743
|
+
picker.forget();
|
|
3744
|
+
answer.summary = labels.created;
|
|
3745
|
+
answer.status = 'kept';
|
|
3746
|
+
} catch (error) {
|
|
3747
|
+
answer.summary = failure(error);
|
|
3748
|
+
answer.status = 'failed';
|
|
3749
|
+
}
|
|
3750
|
+
answered(answer);
|
|
3751
|
+
refresh();
|
|
3752
|
+
} else {
|
|
3753
|
+
await runPrompt(answer, prompt);
|
|
3754
|
+
}
|
|
3755
|
+
input.focus();
|
|
3756
|
+
};
|
|
3757
|
+
const store = async (action)=>{
|
|
3758
|
+
stored = null;
|
|
3759
|
+
const error = await storeChanges(bridge, action);
|
|
3760
|
+
stored = error ? {
|
|
3761
|
+
kind: 'error',
|
|
3762
|
+
action,
|
|
3763
|
+
text: error
|
|
3764
|
+
} : {
|
|
3765
|
+
kind: action === 'save' ? 'saved' : 'published'
|
|
3766
|
+
};
|
|
3767
|
+
refresh();
|
|
3768
|
+
};
|
|
3769
|
+
launch.addEventListener('click', ()=>setOpen(!open));
|
|
3770
|
+
minimise.addEventListener('click', ()=>setOpen(false));
|
|
3771
|
+
eye.addEventListener('click', ()=>{
|
|
3772
|
+
outlines = !outlines;
|
|
3773
|
+
refresh();
|
|
3774
|
+
});
|
|
3775
|
+
exit.addEventListener('click', ()=>bridge.signOut());
|
|
3776
|
+
pageButton.addEventListener('click', ()=>setPicking(!picking));
|
|
3777
|
+
newPage.addEventListener('click', ()=>setCreating(true));
|
|
3778
|
+
cancelNew.addEventListener('click', ()=>setCreating(false));
|
|
3779
|
+
wholePage.addEventListener('click', ()=>{
|
|
3780
|
+
bridge.select(null);
|
|
3781
|
+
input.focus();
|
|
3782
|
+
});
|
|
3783
|
+
shopButton.addEventListener('click', async ()=>{
|
|
3784
|
+
shopMessage = null;
|
|
3785
|
+
shopMessage = await addShopPage(bridge, labels);
|
|
3786
|
+
refresh();
|
|
3787
|
+
});
|
|
3788
|
+
askTab.addEventListener('click', ()=>setMode('ask'));
|
|
3789
|
+
fieldsTab.addEventListener('click', ()=>setMode('fields'));
|
|
3790
|
+
// Arrows move focus between the tabs; Enter or Space (a click) opens one.
|
|
3791
|
+
tabs.addEventListener('keydown', (event)=>{
|
|
3792
|
+
if (![
|
|
3793
|
+
'ArrowLeft',
|
|
3794
|
+
'ArrowRight',
|
|
3795
|
+
'Home',
|
|
3796
|
+
'End'
|
|
3797
|
+
].includes(event.key)) return;
|
|
3798
|
+
event.preventDefault();
|
|
3799
|
+
const onAsk = shadow.activeElement === askTab;
|
|
3800
|
+
const target = event.key === 'Home' ? askTab : event.key === 'End' ? fieldsTab : onAsk ? fieldsTab : askTab;
|
|
3801
|
+
target.focus();
|
|
3802
|
+
});
|
|
3803
|
+
gateKeep.addEventListener('click', ()=>{
|
|
3804
|
+
keepAll(false);
|
|
3805
|
+
setMode('fields');
|
|
3806
|
+
fieldsTab.focus();
|
|
3807
|
+
});
|
|
3808
|
+
gateUndo.addEventListener('click', ()=>{
|
|
3809
|
+
for(let answer = decidable(); answer; answer = decidable())undoAnswer(answer, false);
|
|
3810
|
+
setMode('fields');
|
|
3811
|
+
fieldsTab.focus();
|
|
3812
|
+
});
|
|
3813
|
+
undo.addEventListener('click', ()=>{
|
|
3814
|
+
const step = bridge.getState().undoSteps;
|
|
3815
|
+
const answer = step === undefined ? undefined : answers.find((candidate)=>candidate.status === 'kept' && candidate.step === step);
|
|
3816
|
+
bridge.undo();
|
|
3817
|
+
if (answer) {
|
|
3818
|
+
answer.status = 'undone';
|
|
3819
|
+
refresh();
|
|
3820
|
+
announce(`${labels.undone}: ${answer.summary}`);
|
|
3821
|
+
}
|
|
3822
|
+
if (undo.hidden) input.focus();
|
|
3823
|
+
});
|
|
3824
|
+
save.addEventListener('click', ()=>void store('save'));
|
|
3825
|
+
publish.addEventListener('click', ()=>void store('publish'));
|
|
3826
|
+
composer.addEventListener('submit', (event)=>{
|
|
3827
|
+
event.preventDefault();
|
|
3828
|
+
void submit();
|
|
3829
|
+
});
|
|
3830
|
+
input.addEventListener('input', ()=>{
|
|
3831
|
+
grow();
|
|
3832
|
+
refresh();
|
|
3833
|
+
});
|
|
3834
|
+
input.addEventListener('keydown', (event)=>{
|
|
3835
|
+
if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
|
|
3836
|
+
event.preventDefault();
|
|
3837
|
+
void submit();
|
|
3838
|
+
}
|
|
3839
|
+
});
|
|
3840
|
+
pathInput.addEventListener('input', refresh);
|
|
3841
|
+
pathInput.addEventListener('keydown', (event)=>{
|
|
3842
|
+
if (event.key === 'Enter' && !event.isComposing) {
|
|
3843
|
+
event.preventDefault();
|
|
3844
|
+
input.focus();
|
|
3845
|
+
}
|
|
3846
|
+
});
|
|
3847
|
+
const onKeydown = (event)=>{
|
|
3848
|
+
if (event.key !== 'Escape' || event.defaultPrevented) return;
|
|
3849
|
+
if (stepBack()) event.preventDefault();
|
|
3850
|
+
};
|
|
3851
|
+
doc.addEventListener('keydown', onKeydown);
|
|
3852
|
+
const onPointerOver = (event)=>{
|
|
3853
|
+
const target = event.target;
|
|
3854
|
+
const block = target?.closest?.(`[${UID_ATTRIBUTE}]`);
|
|
3855
|
+
const uid = block?.getAttribute(UID_ATTRIBUTE) ?? null;
|
|
3856
|
+
if (uid === hoverUid) return;
|
|
3857
|
+
hoverUid = uid;
|
|
3858
|
+
scheduleMarks();
|
|
3859
|
+
};
|
|
3860
|
+
const onPointerLeave = ()=>{
|
|
3861
|
+
hoverUid = null;
|
|
3862
|
+
scheduleMarks();
|
|
3863
|
+
};
|
|
3864
|
+
doc.addEventListener('pointerover', onPointerOver);
|
|
3865
|
+
html.addEventListener('pointerleave', onPointerLeave);
|
|
3866
|
+
win?.addEventListener('scroll', scheduleMarks, {
|
|
3867
|
+
capture: true,
|
|
3868
|
+
passive: true
|
|
3869
|
+
});
|
|
3870
|
+
win?.addEventListener('resize', scheduleMarks);
|
|
3871
|
+
const Mutations = win?.MutationObserver;
|
|
3872
|
+
const mutations = typeof Mutations === 'function' ? new Mutations(scheduleMarks) : null;
|
|
3873
|
+
mutations?.observe(doc.body, {
|
|
3874
|
+
childList: true,
|
|
3875
|
+
subtree: true
|
|
3876
|
+
});
|
|
3877
|
+
// The page reflows as the panel docks.
|
|
3878
|
+
const Resizes = win?.ResizeObserver;
|
|
3879
|
+
const resizes = typeof Resizes === 'function' ? new Resizes(scheduleMarks) : null;
|
|
3880
|
+
resizes?.observe(html);
|
|
3881
|
+
const query = win?.matchMedia?.(DOCK_QUERY) ?? null;
|
|
3882
|
+
wide = query?.matches ?? true;
|
|
3883
|
+
const onQuery = ()=>{
|
|
3884
|
+
wide = query?.matches ?? true;
|
|
3885
|
+
refresh();
|
|
3886
|
+
};
|
|
3887
|
+
query?.addEventListener?.('change', onQuery);
|
|
3888
|
+
const unsubscribe = bridge.on('change', render);
|
|
3889
|
+
html.setAttribute('data-obelism-editor', '');
|
|
3890
|
+
doc.head.appendChild(pageStyle);
|
|
3891
|
+
doc.body.appendChild(host);
|
|
3892
|
+
refresh();
|
|
3893
|
+
return ()=>{
|
|
3894
|
+
destroyed = true;
|
|
3895
|
+
unsubscribe();
|
|
3896
|
+
formSlot.unmount();
|
|
3897
|
+
if (frame !== null) win?.cancelAnimationFrame?.(frame);
|
|
3898
|
+
doc.removeEventListener('keydown', onKeydown);
|
|
3899
|
+
doc.removeEventListener('pointerover', onPointerOver);
|
|
3900
|
+
html.removeEventListener('pointerleave', onPointerLeave);
|
|
3901
|
+
win?.removeEventListener('scroll', scheduleMarks, {
|
|
3902
|
+
capture: true
|
|
3903
|
+
});
|
|
3904
|
+
win?.removeEventListener('resize', scheduleMarks);
|
|
3905
|
+
query?.removeEventListener?.('change', onQuery);
|
|
3906
|
+
mutations?.disconnect();
|
|
3907
|
+
resizes?.disconnect();
|
|
3908
|
+
bridge.setSelecting(false);
|
|
3909
|
+
html.removeAttribute('data-obelism-editor');
|
|
3910
|
+
html.removeAttribute('data-obelism-docked');
|
|
3911
|
+
html.style.removeProperty('--obelism-dock');
|
|
3912
|
+
pageStyle.remove();
|
|
3913
|
+
host.remove();
|
|
3914
|
+
};
|
|
3915
|
+
};
|
|
3916
|
+
|
|
2434
3917
|
// Kept on the window, so two copies of this package on one page still share
|
|
2435
3918
|
// one bridge.
|
|
2436
3919
|
const KEY = Symbol.for('@obelism/content/bridge');
|
|
@@ -2472,9 +3955,10 @@ const getContentBridge = ({ url, prompt = true, pagePath, productPath, collectio
|
|
|
2472
3955
|
const doc = win.document;
|
|
2473
3956
|
let unmount = ()=>{};
|
|
2474
3957
|
let destroyed = false;
|
|
3958
|
+
const mountUi = sitePath ? mountSiteEditor : mountPromptBox;
|
|
2475
3959
|
const mount = ()=>{
|
|
2476
3960
|
if (destroyed) return;
|
|
2477
|
-
unmount =
|
|
3961
|
+
unmount = mountUi(doc, bridge, prompt === true ? {} : prompt);
|
|
2478
3962
|
};
|
|
2479
3963
|
// A script tag in <head> runs before there is a body to mount into.
|
|
2480
3964
|
if (doc.body) mount();
|
|
@@ -2494,4 +3978,4 @@ const getContentBridge = ({ url, prompt = true, pagePath, productPath, collectio
|
|
|
2494
3978
|
return bridge;
|
|
2495
3979
|
};
|
|
2496
3980
|
|
|
2497
|
-
export { DEFAULT_PROMPT_LABELS, DEFAULT_STATS_LABELS, INITIAL_STATE, getContentBridge, mountPromptBox, mountStatsOverlay, peekContentBridge };
|
|
3981
|
+
export { DEFAULT_PROMPT_LABELS, DEFAULT_SITE_EDITOR_LABELS, DEFAULT_STATS_LABELS, INITIAL_STATE, getContentBridge, mountPromptBox, mountSiteEditor, mountStatsOverlay, peekContentBridge };
|