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