@obelism/content 0.26.0 → 0.27.1

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