@sevenfold/setto-client 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
- import { useState, useRef, useEffect, useMemo, useSyncExternalStore, useLayoutEffect, createContext, useContext, useCallback, forwardRef, Fragment as Fragment$1, useId, useInsertionEffect } from "react";
3
+ import { useState, useRef, useEffect, useMemo, createContext, useContext, useCallback, useLayoutEffect, useSyncExternalStore, forwardRef, Fragment as Fragment$1, useId, useInsertionEffect } from "react";
4
4
  import { useTranslation } from "react-i18next";
5
5
  import * as ReactDOM from "react-dom";
6
6
  import { createPortal } from "react-dom";
@@ -21431,11 +21431,11 @@ function useSettoChromeSlot(name) {
21431
21431
  slotListeners.delete(listener);
21432
21432
  };
21433
21433
  }, []);
21434
- const Offered = slots.get(name) ?? null;
21434
+ const Offered = slots.get(name);
21435
21435
  return useMemo(() => {
21436
21436
  if (!Offered) return null;
21437
- return function SettoChromeSlot() {
21438
- return /* @__PURE__ */ jsx(ChromeBoundary, { onError: () => retireSlot(name), children: /* @__PURE__ */ jsx(Offered, {}) });
21437
+ return function SettoChromeSlot(props) {
21438
+ return /* @__PURE__ */ jsx(ChromeBoundary, { onError: () => retireSlot(name), children: React.createElement(Offered, props) });
21439
21439
  };
21440
21440
  }, [Offered, name]);
21441
21441
  }
@@ -21452,6 +21452,7 @@ function SettoChrome() {
21452
21452
  currentSite: null,
21453
21453
  sitesLoaded: false,
21454
21454
  editMode: false,
21455
+ guestEdit: false,
21455
21456
  authLoading: true
21456
21457
  }));
21457
21458
  stateRef.current = () => ({
@@ -21464,6 +21465,7 @@ function SettoChrome() {
21464
21465
  } : null,
21465
21466
  sitesLoaded: setto.sites !== null,
21466
21467
  editMode: setto.editMode,
21468
+ guestEdit: setto.guestEdit,
21467
21469
  authLoading: setto.authLoading
21468
21470
  });
21469
21471
  useEffect(() => {
@@ -21550,7 +21552,7 @@ function withoutCheckoutIntent() {
21550
21552
  }
21551
21553
  const FAB_SIZE = 52;
21552
21554
  const FAB_INSET = 16;
21553
- const STYLE_ID$3 = "setto-edit-layout";
21555
+ const STYLE_ID$2 = "setto-edit-layout";
21554
21556
  const HTML_CLASS = "setto-edit-mode";
21555
21557
  function useSettoDocumentLayout(active) {
21556
21558
  useEffect(() => {
@@ -21559,19 +21561,24 @@ function useSettoDocumentLayout(active) {
21559
21561
  html.classList.add(HTML_CLASS);
21560
21562
  html.style.setProperty("--setto-fab-size", `${FAB_SIZE}px`);
21561
21563
  html.style.setProperty("--setto-fab-inset", `${FAB_INSET}px`);
21562
- let styleEl = document.getElementById(STYLE_ID$3);
21564
+ let styleEl = document.getElementById(STYLE_ID$2);
21563
21565
  if (!styleEl) {
21564
21566
  styleEl = document.createElement("style");
21565
- styleEl.id = STYLE_ID$3;
21567
+ styleEl.id = STYLE_ID$2;
21566
21568
  styleEl.textContent = `
21567
21569
  html.setto-edit-mode {
21568
21570
  width: 100%;
21569
21571
  overflow-x: clip;
21570
21572
  }
21571
21573
 
21574
+ /*
21575
+ * Anchored by its right edge, which is also what makes the menu open
21576
+ * leftwards: the pill grows and this corner does not move, so the
21577
+ * button somebody just tapped stays under their thumb.
21578
+ */
21572
21579
  html.setto-edit-mode #setto-toolbar-root {
21573
21580
  position: fixed;
21574
- left: var(--setto-fab-inset);
21581
+ right: var(--setto-fab-inset);
21575
21582
  bottom: var(--setto-fab-inset);
21576
21583
  z-index: 2147483647;
21577
21584
  }
@@ -21625,492 +21632,6 @@ function notifyLayoutChange() {
21625
21632
  window.dispatchEvent(new Event("resize"));
21626
21633
  });
21627
21634
  }
21628
- function isLinkContext(el) {
21629
- const key = el.getAttribute("data-setto-key");
21630
- if (key?.startsWith("lang.")) return false;
21631
- if (el.closest("[data-setto-no-hint]")) return false;
21632
- return !!(el.closest("a[href]") || el.closest("button"));
21633
- }
21634
- function activateLinkContext(el) {
21635
- const anchor = el.closest("a[href]");
21636
- if (anchor instanceof HTMLAnchorElement) {
21637
- anchor.click();
21638
- return;
21639
- }
21640
- const button = el.closest("button");
21641
- if (button instanceof HTMLButtonElement) {
21642
- button.click();
21643
- }
21644
- }
21645
- const QUERY = "(hover: none) and (pointer: coarse)";
21646
- function subscribe(onChange) {
21647
- const mq = window.matchMedia(QUERY);
21648
- mq.addEventListener("change", onChange);
21649
- return () => mq.removeEventListener("change", onChange);
21650
- }
21651
- function getSnapshot() {
21652
- return window.matchMedia(QUERY).matches;
21653
- }
21654
- function useCoarsePointer() {
21655
- return useSyncExternalStore(subscribe, getSnapshot, () => false);
21656
- }
21657
- const MARGIN$2 = 10;
21658
- const OFFSET$1 = 14;
21659
- function hintLabel() {
21660
- const isMac = navigator.platform.toUpperCase().includes("MAC");
21661
- return isMac ? "⌘+klikk for å åpne" : "Ctrl+klikk for å åpne";
21662
- }
21663
- function placeTooltip(anchor, width, height) {
21664
- const vw = window.innerWidth;
21665
- const vh = window.innerHeight;
21666
- let left = anchor.x + OFFSET$1;
21667
- let top = anchor.y + OFFSET$1;
21668
- if (left + width > vw - MARGIN$2) {
21669
- left = anchor.x - width - OFFSET$1;
21670
- }
21671
- if (left < MARGIN$2) {
21672
- left = MARGIN$2;
21673
- }
21674
- if (top + height > vh - MARGIN$2) {
21675
- top = anchor.y - height - OFFSET$1;
21676
- }
21677
- if (top < MARGIN$2) {
21678
- top = MARGIN$2;
21679
- }
21680
- return { left, top };
21681
- }
21682
- function EditLinkHint() {
21683
- const coarsePointer = useCoarsePointer();
21684
- const [anchor, setAnchor] = useState(null);
21685
- const [position, setPosition] = useState(null);
21686
- const tipRef = useRef(null);
21687
- const text = hintLabel();
21688
- useEffect(() => {
21689
- if (coarsePointer) return;
21690
- const onMove = (e) => {
21691
- const target = e.target;
21692
- if (!(target instanceof Element)) {
21693
- setAnchor(null);
21694
- return;
21695
- }
21696
- const editable = target.closest("[data-setto-key]");
21697
- if (!(editable instanceof HTMLElement) || !isLinkContext(editable)) {
21698
- setAnchor(null);
21699
- return;
21700
- }
21701
- setAnchor({ x: e.clientX, y: e.clientY });
21702
- };
21703
- document.addEventListener("mousemove", onMove, true);
21704
- return () => document.removeEventListener("mousemove", onMove, true);
21705
- }, [coarsePointer]);
21706
- useLayoutEffect(() => {
21707
- if (!anchor || !tipRef.current) {
21708
- setPosition(null);
21709
- return;
21710
- }
21711
- const { width, height } = tipRef.current.getBoundingClientRect();
21712
- setPosition(placeTooltip(anchor, width, height));
21713
- }, [anchor, text]);
21714
- if (coarsePointer || !anchor) return null;
21715
- const style = {
21716
- position: "fixed",
21717
- left: position?.left ?? anchor.x + OFFSET$1,
21718
- top: position?.top ?? anchor.y + OFFSET$1,
21719
- visibility: position ? "visible" : "hidden",
21720
- background: "#1a1a1a",
21721
- color: "#fff",
21722
- fontSize: 11,
21723
- padding: "4px 8px",
21724
- borderRadius: 4,
21725
- pointerEvents: "none",
21726
- whiteSpace: "nowrap",
21727
- fontFamily: "system-ui, sans-serif",
21728
- boxShadow: "0 2px 8px rgba(0,0,0,0.15)"
21729
- };
21730
- return /* @__PURE__ */ jsx("div", { ref: tipRef, style, children: text });
21731
- }
21732
- function colorsMatch(a, b) {
21733
- const na = normalizeColor(a);
21734
- const nb = normalizeColor(b);
21735
- if (!na || !nb) return false;
21736
- return na === nb;
21737
- }
21738
- function normalizeColor(color) {
21739
- const trimmed = color.trim().toLowerCase();
21740
- if (/^#[0-9a-f]{3}$/.test(trimmed)) {
21741
- const [, r, g, b] = trimmed.match(/^#(.)(.)(.)$/) ?? [];
21742
- return `#${r}${r}${g}${g}${b}${b}`;
21743
- }
21744
- if (/^#[0-9a-f]{6}$/.test(trimmed)) return trimmed;
21745
- const rgb = trimmed.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
21746
- if (rgb) {
21747
- const [, r, g, b] = rgb;
21748
- if (r === void 0 || g === void 0 || b === void 0) return null;
21749
- const hex = (n) => Number(n).toString(16).padStart(2, "0");
21750
- return `#${hex(r)}${hex(g)}${hex(b)}`;
21751
- }
21752
- return null;
21753
- }
21754
- function swatchBorder(color) {
21755
- const n = normalizeColor(color);
21756
- if (n === "#ffffff" || n === "#e6dccf") return "#ccc";
21757
- return "transparent";
21758
- }
21759
- const SectionEditContext = createContext(null);
21760
- function SectionEditProvider({ children }) {
21761
- const [selectedId, setSelectedId] = useState(null);
21762
- const selectSection = useCallback((id) => {
21763
- setSelectedId(id);
21764
- }, []);
21765
- const value = useMemo(
21766
- () => ({ selectedId, selectSection }),
21767
- [selectedId, selectSection]
21768
- );
21769
- return /* @__PURE__ */ jsx(SectionEditContext.Provider, { value, children });
21770
- }
21771
- function useSectionEdit() {
21772
- const ctx = useContext(SectionEditContext);
21773
- if (!ctx) {
21774
- throw new Error("useSectionEdit() must be used inside SectionEditProvider");
21775
- }
21776
- return ctx;
21777
- }
21778
- const SECTION_TOOLBAR_HEIGHT = 44;
21779
- const GAP = 8;
21780
- const MARGIN$1 = 8;
21781
- function computePosition(targetEl, toolbarW, toolbarH) {
21782
- const rect = targetEl.getBoundingClientRect();
21783
- let left = rect.left + (rect.width - toolbarW) / 2;
21784
- left = Math.max(MARGIN$1, Math.min(left, window.innerWidth - toolbarW - MARGIN$1));
21785
- const top = Math.max(MARGIN$1, rect.top - toolbarH - GAP);
21786
- return { top, left };
21787
- }
21788
- function BrandColorPicker({
21789
- label,
21790
- value,
21791
- brandColors,
21792
- open,
21793
- onToggle,
21794
- onSelect,
21795
- popoverPlacement
21796
- }) {
21797
- const buttonRef = useRef(null);
21798
- const popoverRef = useRef(null);
21799
- const [popoverPos, setPopoverPos] = useState(null);
21800
- const matched = brandColors.find((b) => colorsMatch(value, b.value));
21801
- const displayColor = matched?.value ?? (value || "#cccccc");
21802
- const displayLabel = matched?.label ?? label;
21803
- useLayoutEffect(() => {
21804
- if (!open || !buttonRef.current) {
21805
- setPopoverPos(null);
21806
- return;
21807
- }
21808
- const update = () => {
21809
- const btn = buttonRef.current;
21810
- const pop = popoverRef.current;
21811
- if (!btn) return;
21812
- const rect = btn.getBoundingClientRect();
21813
- const popoverH = pop?.offsetHeight ?? 120;
21814
- const popoverW = pop?.offsetWidth ?? 180;
21815
- let top = popoverPlacement === "above" ? rect.top - popoverH - 6 : rect.bottom + 6;
21816
- let left = rect.left;
21817
- if (left + popoverW > window.innerWidth - 8) {
21818
- left = window.innerWidth - popoverW - 8;
21819
- }
21820
- if (left < 8) left = 8;
21821
- if (top < 8) top = rect.bottom + 6;
21822
- if (top + popoverH > window.innerHeight - 8) {
21823
- top = rect.top - popoverH - 6;
21824
- }
21825
- setPopoverPos({ top, left });
21826
- };
21827
- update();
21828
- requestAnimationFrame(update);
21829
- window.addEventListener("scroll", update, true);
21830
- window.addEventListener("resize", update);
21831
- return () => {
21832
- window.removeEventListener("scroll", update, true);
21833
- window.removeEventListener("resize", update);
21834
- };
21835
- }, [open, popoverPlacement, brandColors.length]);
21836
- const popover = open ? /* @__PURE__ */ jsx(
21837
- "div",
21838
- {
21839
- ref: popoverRef,
21840
- role: "listbox",
21841
- "aria-label": label,
21842
- "data-setto-ui": true,
21843
- style: {
21844
- position: "fixed",
21845
- top: popoverPos?.top ?? -9999,
21846
- left: popoverPos?.left ?? 0,
21847
- visibility: popoverPos ? "visible" : "hidden",
21848
- zIndex: 2147483647,
21849
- display: "grid",
21850
- gridTemplateColumns: "repeat(4, 1fr)",
21851
- gap: 4,
21852
- padding: 8,
21853
- background: "#fff",
21854
- border: "1px solid #e0e0e0",
21855
- borderRadius: 8,
21856
- boxShadow: "0 8px 24px rgba(0,0,0,0.15)",
21857
- minWidth: 160
21858
- },
21859
- onMouseDown: (e) => e.stopPropagation(),
21860
- onClick: (e) => e.stopPropagation(),
21861
- children: brandColors.map((brand) => {
21862
- const selected = colorsMatch(value, brand.value);
21863
- return /* @__PURE__ */ jsxs(
21864
- "button",
21865
- {
21866
- type: "button",
21867
- role: "option",
21868
- "aria-selected": selected,
21869
- title: brand.label,
21870
- onClick: () => onSelect(brand.value),
21871
- style: {
21872
- display: "flex",
21873
- flexDirection: "column",
21874
- alignItems: "center",
21875
- gap: 4,
21876
- padding: 6,
21877
- background: selected ? "#f5f3ff" : "transparent",
21878
- border: selected ? "1px solid #640AFF" : "1px solid transparent",
21879
- borderRadius: 6,
21880
- cursor: "pointer"
21881
- },
21882
- children: [
21883
- /* @__PURE__ */ jsx(
21884
- "span",
21885
- {
21886
- "aria-hidden": true,
21887
- style: {
21888
- width: 24,
21889
- height: 24,
21890
- borderRadius: "50%",
21891
- background: brand.value,
21892
- border: `1px solid ${swatchBorder(brand.value)}`
21893
- }
21894
- }
21895
- ),
21896
- /* @__PURE__ */ jsx("span", { style: { fontSize: 10, color: "#666", lineHeight: 1.2 }, children: brand.label })
21897
- ]
21898
- },
21899
- brand.value
21900
- );
21901
- })
21902
- }
21903
- ) : null;
21904
- return /* @__PURE__ */ jsxs(Fragment, { children: [
21905
- /* @__PURE__ */ jsxs(
21906
- "button",
21907
- {
21908
- ref: buttonRef,
21909
- type: "button",
21910
- "data-setto-ui": true,
21911
- onMouseDown: (e) => e.stopPropagation(),
21912
- onClick: (e) => {
21913
- e.stopPropagation();
21914
- onToggle();
21915
- },
21916
- "aria-expanded": open,
21917
- "aria-haspopup": "listbox",
21918
- "aria-label": `${label}: ${displayLabel}`,
21919
- title: `${label}: ${displayLabel}`,
21920
- style: {
21921
- display: "inline-flex",
21922
- alignItems: "center",
21923
- gap: 6,
21924
- padding: "4px 8px 4px 6px",
21925
- background: open ? "#f5f3ff" : "#f7f7f7",
21926
- border: open ? "1px solid #640AFF" : "1px solid #e0e0e0",
21927
- borderRadius: 6,
21928
- cursor: "pointer",
21929
- fontSize: 11,
21930
- color: "#444",
21931
- whiteSpace: "nowrap",
21932
- flexShrink: 0
21933
- },
21934
- children: [
21935
- /* @__PURE__ */ jsx(
21936
- "span",
21937
- {
21938
- "aria-hidden": true,
21939
- style: {
21940
- width: 18,
21941
- height: 18,
21942
- borderRadius: "50%",
21943
- background: displayColor,
21944
- border: `1px solid ${swatchBorder(displayColor)}`,
21945
- flexShrink: 0
21946
- }
21947
- }
21948
- ),
21949
- /* @__PURE__ */ jsx("span", { children: label }),
21950
- /* @__PURE__ */ jsx("span", { "aria-hidden": true, style: { color: "#999", fontSize: 10, lineHeight: 1 }, children: "▾" })
21951
- ]
21952
- }
21953
- ),
21954
- popover ? createPortal(popover, document.body) : null
21955
- ] });
21956
- }
21957
- function SectionToolbar() {
21958
- const { themeStore, config } = useSetto();
21959
- const { selectedId, selectSection } = useSectionEdit();
21960
- const [pos, setPos] = useState(null);
21961
- const [openField, setOpenField] = useState(null);
21962
- const barRef = useRef(null);
21963
- const schema = selectedId ? config.sectionSchemas?.[selectedId] : null;
21964
- const brandColors = config.brandColors ?? [];
21965
- useLayoutEffect(() => {
21966
- if (!selectedId) {
21967
- setPos(null);
21968
- return;
21969
- }
21970
- const el = document.querySelector(
21971
- `[data-setto-section="${selectedId}"]`
21972
- );
21973
- if (!el) {
21974
- setPos(null);
21975
- return;
21976
- }
21977
- const update = () => {
21978
- const bar = barRef.current;
21979
- if (!bar) return;
21980
- const w = bar.offsetWidth;
21981
- const h = bar.offsetHeight || SECTION_TOOLBAR_HEIGHT;
21982
- setPos(computePosition(el, w, h));
21983
- };
21984
- update();
21985
- requestAnimationFrame(update);
21986
- window.addEventListener("scroll", update, true);
21987
- window.addEventListener("resize", update);
21988
- return () => {
21989
- window.removeEventListener("scroll", update, true);
21990
- window.removeEventListener("resize", update);
21991
- };
21992
- }, [selectedId, openField, schema?.fields.length]);
21993
- useEffect(() => {
21994
- setOpenField(null);
21995
- }, [selectedId]);
21996
- useEffect(() => {
21997
- if (!openField) return;
21998
- const close = (e) => {
21999
- const target = e.target;
22000
- if (barRef.current?.contains(target)) return;
22001
- if (target instanceof Element && target.closest("[data-setto-ui]")) return;
22002
- setOpenField(null);
22003
- };
22004
- document.addEventListener("mousedown", close);
22005
- return () => document.removeEventListener("mousedown", close);
22006
- }, [openField]);
22007
- useEffect(() => {
22008
- if (!selectedId) return;
22009
- const onKey = (e) => {
22010
- if (e.key !== "Escape") return;
22011
- if (openField) {
22012
- setOpenField(null);
22013
- return;
22014
- }
22015
- selectSection(null);
22016
- };
22017
- window.addEventListener("keydown", onKey);
22018
- return () => window.removeEventListener("keydown", onKey);
22019
- }, [selectedId, selectSection, openField]);
22020
- if (!selectedId || !schema || !themeStore || brandColors.length === 0) {
22021
- return null;
22022
- }
22023
- const colors = themeStore.getSection(selectedId);
22024
- return /* @__PURE__ */ jsxs(
22025
- "div",
22026
- {
22027
- ref: barRef,
22028
- role: "toolbar",
22029
- "data-setto-ui": true,
22030
- "data-setto-theme-id": selectedId,
22031
- "aria-label": `Farger — ${schema.label}`,
22032
- style: {
22033
- position: "fixed",
22034
- top: pos?.top ?? -9999,
22035
- left: pos?.left ?? 0,
22036
- width: "max-content",
22037
- maxWidth: `calc(100vw - ${MARGIN$1 * 2}px)`,
22038
- minHeight: SECTION_TOOLBAR_HEIGHT,
22039
- zIndex: 2147483646,
22040
- display: "flex",
22041
- alignItems: "center",
22042
- gap: 8,
22043
- padding: "0 12px",
22044
- background: "#ffffff",
22045
- border: "1px solid #e0e0e0",
22046
- borderRadius: 8,
22047
- boxShadow: "0 4px 20px rgba(0,0,0,0.12)",
22048
- fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
22049
- fontSize: 12,
22050
- pointerEvents: "auto",
22051
- userSelect: "none",
22052
- visibility: pos ? "visible" : "hidden"
22053
- },
22054
- onMouseDown: (e) => e.stopPropagation(),
22055
- onClick: (e) => e.stopPropagation(),
22056
- children: [
22057
- /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, whiteSpace: "nowrap", flexShrink: 0 }, children: schema.label }),
22058
- /* @__PURE__ */ jsx("span", { style: { color: "#ccc", flexShrink: 0 }, children: "·" }),
22059
- /* @__PURE__ */ jsx(
22060
- "div",
22061
- {
22062
- style: {
22063
- display: "flex",
22064
- alignItems: "center",
22065
- gap: 6,
22066
- flexWrap: "nowrap"
22067
- },
22068
- children: schema.fields.map((field) => /* @__PURE__ */ jsx(
22069
- BrandColorPicker,
22070
- {
22071
- label: field.label,
22072
- value: colors[field.key] ?? "",
22073
- brandColors,
22074
- open: openField === field.key,
22075
- popoverPlacement: "below",
22076
- onToggle: () => setOpenField((prev) => prev === field.key ? null : field.key),
22077
- onSelect: (v) => {
22078
- themeStore.set(selectedId, field.key, v);
22079
- setOpenField(null);
22080
- }
22081
- },
22082
- field.key
22083
- ))
22084
- }
22085
- ),
22086
- /* @__PURE__ */ jsx(
22087
- "button",
22088
- {
22089
- type: "button",
22090
- "data-setto-ui": true,
22091
- onMouseDown: (e) => e.stopPropagation(),
22092
- onClick: (e) => {
22093
- e.stopPropagation();
22094
- selectSection(null);
22095
- },
22096
- "aria-label": "Lukk",
22097
- style: {
22098
- flexShrink: 0,
22099
- background: "transparent",
22100
- border: "none",
22101
- color: "#999",
22102
- cursor: "pointer",
22103
- fontSize: 16,
22104
- lineHeight: 1,
22105
- padding: "2px 6px"
22106
- },
22107
- children: "×"
22108
- }
22109
- )
22110
- ]
22111
- }
22112
- );
22113
- }
22114
21635
  function SettoMark({
22115
21636
  size = 24,
22116
21637
  letterColor = "#F2EBDF",
@@ -22175,6 +21696,8 @@ const pillStyle = {
22175
21696
  function EditModeShell({ children, themeStore, active }) {
22176
21697
  useSettoDocumentLayout(active);
22177
21698
  const Toolbar = useSettoChromeSlot("toolbar");
21699
+ const SectionTools = useSettoChromeSlot("section-toolbar");
21700
+ const LinkHint = useSettoChromeSlot("link-hint");
22178
21701
  const [toolbarRoot, setToolbarRoot] = useState(null);
22179
21702
  const [hintRoot, setHintRoot] = useState(null);
22180
21703
  const [sectionToolbarRoot, setSectionToolbarRoot] = useState(null);
@@ -22208,8 +21731,8 @@ function EditModeShell({ children, themeStore, active }) {
22208
21731
  return /* @__PURE__ */ jsxs(Fragment, { children: [
22209
21732
  children,
22210
21733
  active && toolbarRoot ? createPortal(Toolbar ? /* @__PURE__ */ jsx(Toolbar, {}) : /* @__PURE__ */ jsx(ToolbarUnavailable, {}), toolbarRoot) : null,
22211
- active && hintRoot ? createPortal(/* @__PURE__ */ jsx(EditLinkHint, {}), hintRoot) : null,
22212
- active && sectionToolbarRoot && themeStore ? createPortal(/* @__PURE__ */ jsx(SectionToolbar, {}), sectionToolbarRoot) : null
21734
+ active && hintRoot && LinkHint ? createPortal(/* @__PURE__ */ jsx(LinkHint, {}), hintRoot) : null,
21735
+ active && sectionToolbarRoot && themeStore && SectionTools ? createPortal(/* @__PURE__ */ jsx(SectionTools, {}), sectionToolbarRoot) : null
22213
21736
  ] });
22214
21737
  }
22215
21738
  const SettoContext = createContext(null);
@@ -22256,6 +21779,16 @@ function SettoProvider({ config, children }) {
22256
21779
  setStoreReady(true);
22257
21780
  }, [i18n, config.theme]);
22258
21781
  const editMode = !!session && !!access.currentSite && !onAdminRoute && !authLoading && !access.loading;
21782
+ const [selectedSectionId, setSelectedSectionId] = useState(null);
21783
+ const selectSection = useCallback((id) => setSelectedSectionId(id), []);
21784
+ const [guestEdit, setGuestEdit] = useState(false);
21785
+ useEffect(() => {
21786
+ if (editMode && guestEdit) setGuestEdit(false);
21787
+ }, [editMode, guestEdit]);
21788
+ useEffect(() => {
21789
+ document.documentElement.classList.toggle("setto-guest-editing", guestEdit);
21790
+ return () => document.documentElement.classList.remove("setto-guest-editing");
21791
+ }, [guestEdit]);
22259
21792
  const value = {
22260
21793
  config,
22261
21794
  supabase,
@@ -22267,10 +21800,14 @@ function SettoProvider({ config, children }) {
22267
21800
  sites: access.sites,
22268
21801
  store: storeReady ? storeRef.current : null,
22269
21802
  themeStore: storeReady ? themeStoreRef.current : null,
22270
- assetStore: storeReady ? assetStoreRef.current : null
21803
+ assetStore: storeReady ? assetStoreRef.current : null,
21804
+ selectedSectionId,
21805
+ selectSection,
21806
+ guestEdit,
21807
+ setGuestEdit
22271
21808
  };
22272
21809
  const editShellActive = editMode && !!value.store;
22273
- return /* @__PURE__ */ jsx(SettoContext.Provider, { value, children: /* @__PURE__ */ jsxs(SectionEditProvider, { children: [
21810
+ return /* @__PURE__ */ jsx(SettoContext.Provider, { value, children: /* @__PURE__ */ jsxs(Fragment, { children: [
22274
21811
  /* @__PURE__ */ jsx(EditModeShell, { active: editShellActive, themeStore: value.themeStore, children }),
22275
21812
  /* @__PURE__ */ jsx(SettoChrome, {})
22276
21813
  ] }) });
@@ -22282,233 +21819,42 @@ function useSetto() {
22282
21819
  }
22283
21820
  return ctx;
22284
21821
  }
22285
- const GuestEditContext = createContext(null);
22286
21822
  function GuestEditProvider({ children }) {
22287
- const { editMode, themeStore } = useSetto();
22288
- const { selectSection } = useSectionEdit();
22289
- const [active, setActive] = useState(false);
22290
- useEffect(() => {
22291
- if (editMode && active) setActive(false);
22292
- }, [editMode, active]);
22293
- useEffect(() => {
22294
- document.documentElement.classList.toggle("setto-guest-editing", active);
22295
- return () => document.documentElement.classList.remove("setto-guest-editing");
22296
- }, [active]);
22297
- const start = useCallback(() => setActive(true), []);
22298
- const stop = useCallback(() => {
22299
- setActive(false);
22300
- selectSection(null);
22301
- }, [selectSection]);
22302
- return /* @__PURE__ */ jsxs(GuestEditContext.Provider, { value: { active, start, stop }, children: [
21823
+ const { guestEdit, themeStore } = useSetto();
21824
+ const SectionTools = useSettoChromeSlot("section-toolbar");
21825
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
22303
21826
  children,
22304
- active && themeStore ? createPortal(/* @__PURE__ */ jsx(SectionToolbar, {}), document.body) : null
21827
+ guestEdit && themeStore && SectionTools ? createPortal(/* @__PURE__ */ jsx(SectionTools, {}), document.body) : null
22305
21828
  ] });
22306
21829
  }
22307
21830
  function useGuestEdit() {
22308
- const ctx = useContext(GuestEditContext);
22309
- if (!ctx) {
22310
- return { active: false, start: () => {
22311
- }, stop: () => {
22312
- } };
22313
- }
22314
- return ctx;
22315
- }
22316
- const PREVIEW_HOST = /(^|\.)site\.setto\.no$/i;
22317
- function isPreviewHost(hostname) {
22318
- return PREVIEW_HOST.test(hostname);
22319
- }
22320
- function previewBarState(input) {
22321
- if (!input.onPreview || input.authLoading) return "hidden";
22322
- if (input.hasSession && !input.sitesLoaded) return "hidden";
22323
- if (!input.hasSession && input.soldPublic === null) return "hidden";
22324
- const bought = input.paidMarker || input.currentSiteIsLive || input.soldPublic === true;
22325
- if (bought) return "hidden";
22326
- return input.hasSession && input.hasCurrentSite ? "owner" : "visitor";
22327
- }
22328
- function SettoPreviewBar() {
22329
- const { supabase, api, config, session, authLoading, currentSite, sites, editMode } = useSetto();
22330
- const [onPreview, setOnPreview] = useState(false);
22331
- const [busy, setBusy] = useState(false);
22332
- const [error, setError] = useState(null);
22333
- const [paid, setPaid] = useState(false);
22334
- const [soldPublic, setSoldPublic] = useState(null);
22335
- useEffect(() => {
22336
- setOnPreview(isPreviewHost(window.location.hostname));
22337
- setPaid(new URLSearchParams(window.location.search).get("betaling") === "ok");
22338
- }, []);
22339
- useEffect(() => {
22340
- if (!onPreview || session) return;
22341
- let cancelled = false;
22342
- fetch(`${config.apiUrl}/onboarding/sites/${config.siteId}/sold`, {
22343
- headers: { Accept: "application/json" }
22344
- }).then((r) => r.ok ? r.json() : null).then((d) => {
22345
- if (!cancelled && d) setSoldPublic(d.sold === true);
22346
- }).catch(() => {
22347
- });
22348
- return () => {
22349
- cancelled = true;
22350
- };
22351
- }, [onPreview, session, config.apiUrl, config.siteId]);
22352
- const signIn = () => {
22353
- window.location.href = checkoutIntentUrl();
22354
- };
22355
- const next = () => {
22356
- if (window.parent !== window) {
22357
- window.parent.postMessage({ type: "setto:next", siteId: config.siteId }, "*");
22358
- return;
22359
- }
22360
- signIn();
22361
- };
22362
- const publish = async () => {
22363
- setBusy(true);
22364
- setError(null);
22365
- try {
22366
- const token = (await supabase.auth.getSession()).data.session?.access_token;
22367
- if (!token) {
22368
- signIn();
22369
- return;
22370
- }
22371
- const data = await api.request(
22372
- `/onboarding/sites/${config.siteId}/checkout`,
22373
- {
22374
- method: "POST",
22375
- // Back to the page they were looking at, not to a chat window.
22376
- body: { returnTo: window.location.origin + window.location.pathname }
22377
- },
22378
- "checkout"
22379
- );
22380
- if (!data.url) throw new Error("checkout_failed");
22381
- window.location.href = data.url;
22382
- } catch (err) {
22383
- const detail = err instanceof Error ? err.message : "";
22384
- setError(
22385
- detail.includes("already_paid") ? "Denne siden er allerede betalt." : detail.includes("not_found") ? "Fant ingen bestilling for denne siden på kontoen din." : detail.includes("stripe_not_configured") ? "Betaling er ikke satt opp ennå." : "Klarte ikke starte betalingen. Prøv igjen."
22386
- );
22387
- setBusy(false);
21831
+ const { guestEdit, setGuestEdit, selectSection } = useSetto();
21832
+ return {
21833
+ active: guestEdit,
21834
+ start: () => setGuestEdit(true),
21835
+ stop: () => {
21836
+ setGuestEdit(false);
21837
+ selectSection(null);
22388
21838
  }
22389
21839
  };
22390
- const resumed = useRef(false);
22391
- useEffect(() => {
22392
- if (resumed.current || !onPreview || !session || !currentSite) return;
22393
- if (!wantsCheckout()) return;
22394
- if (paid || currentSite.status === "live") return;
22395
- resumed.current = true;
22396
- window.history.replaceState(null, "", withoutCheckoutIntent());
22397
- void publish();
22398
- }, [onPreview, session, currentSite, paid]);
22399
- const state = previewBarState({
22400
- onPreview,
22401
- authLoading,
22402
- hasSession: !!session,
22403
- sitesLoaded: sites !== null,
22404
- hasCurrentSite: !!currentSite,
22405
- currentSiteIsLive: currentSite?.status === "live",
22406
- soldPublic,
22407
- paidMarker: paid
22408
- });
22409
- if (state === "hidden") return null;
22410
- const owner = state === "owner";
22411
- const name = currentSite?.display_name?.trim() || config.displayName?.trim() || config.siteId;
22412
- return createPortal(
22413
- /*
22414
- * Lifted above the editor's floating button when edit mode is on, which
22415
- * sits in the same corner band. Sideways would collide again the moment
22416
- * its toolbar opens, since that expands rightward.
22417
- */
22418
- /* @__PURE__ */ jsx(
22419
- "div",
22420
- {
22421
- style: editMode ? { ...barStyle, bottom: "calc(max(16px, env(safe-area-inset-bottom, 0px)) + 68px)" } : barStyle,
22422
- "data-setto-preview-bar": "",
22423
- children: owner ? /* @__PURE__ */ jsxs(Fragment, { children: [
22424
- /* @__PURE__ */ jsx("span", { style: dotStyle, "aria-hidden": true }),
22425
- /* @__PURE__ */ jsxs("span", { style: labelStyle, children: [
22426
- "Du forhåndsviser nå ",
22427
- /* @__PURE__ */ jsx("strong", { style: { fontWeight: 600 }, children: name })
22428
- ] }),
22429
- error && /* @__PURE__ */ jsx("span", { style: errorStyle, children: error }),
22430
- /* @__PURE__ */ jsx("button", { type: "button", style: primaryStyle, onClick: publish, disabled: busy, children: busy ? "Åpner betaling …" : "Publiser" })
22431
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
22432
- /* @__PURE__ */ jsx("span", { style: dotStyle, "aria-hidden": true }),
22433
- /* @__PURE__ */ jsx("span", { style: labelStyle, children: "Forhåndsvisning" }),
22434
- /* @__PURE__ */ jsx("button", { type: "button", style: primaryStyle, onClick: next, children: "Neste" })
22435
- ] })
22436
- }
22437
- ),
22438
- document.body
22439
- );
22440
21840
  }
22441
- const barStyle = {
22442
- position: "fixed",
22443
- left: "50%",
22444
- bottom: "max(16px, env(safe-area-inset-bottom, 0px))",
22445
- transform: "translateX(-50%)",
22446
- zIndex: 2147483e3,
22447
- display: "flex",
22448
- alignItems: "center",
22449
- gap: 14,
22450
- maxWidth: "calc(100vw - 24px)",
22451
- padding: "10px 10px 10px 18px",
22452
- borderRadius: 999,
22453
- background: "#211711",
22454
- color: "#fffdf8",
22455
- fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
22456
- fontSize: 14,
22457
- lineHeight: 1.3,
22458
- boxShadow: "0 18px 40px -18px rgba(0, 0, 0, 0.65)",
22459
- /*
22460
- * A frosted rim, so the pill reads as an object on any page.
22461
- *
22462
- * The bar sits on sites Setto did not choose the colours for: on a dark hero
22463
- * a dark pill disappears, and on a pale section its shadow is all that holds
22464
- * it. A translucent white border with `padding-box` clipping lets the page
22465
- * show through the rim itself, which separates the pill from either ground
22466
- * without picking a side.
22467
- */
22468
- border: "5px solid rgba(255, 255, 255, 0.22)",
22469
- backgroundClip: "padding-box",
22470
- backdropFilter: "blur(10px)",
22471
- WebkitBackdropFilter: "blur(10px)"
22472
- };
22473
- const dotStyle = {
22474
- width: 7,
22475
- height: 7,
22476
- borderRadius: "50%",
22477
- background: "#c4502a",
22478
- flex: "none"
22479
- };
22480
- const labelStyle = {
22481
- whiteSpace: "nowrap",
22482
- overflow: "hidden",
22483
- textOverflow: "ellipsis"
22484
- };
22485
- const primaryStyle = {
22486
- flex: "none",
22487
- padding: "9px 18px",
22488
- border: 0,
22489
- borderRadius: 999,
22490
- background: "#c4502a",
22491
- color: "#fffdf8",
22492
- font: "inherit",
22493
- fontWeight: 600,
22494
- cursor: "pointer"
22495
- };
22496
- const errorStyle = {
22497
- color: "#ffb4a2",
22498
- fontSize: 13,
22499
- /*
22500
- * Shrinkable, unlike the label beside it.
22501
- *
22502
- * `nowrap` on a flex child whose `min-width` is `auto` cannot shrink below
22503
- * its own text, so on a 320px phone the error shouldered «Publiser» 19px
22504
- * past the right edge of the screen and the bar clipped it — the button
22505
- * that takes the money, unreachable, exactly when something had gone wrong.
22506
- */
22507
- minWidth: 0,
22508
- overflow: "hidden",
22509
- textOverflow: "ellipsis",
22510
- whiteSpace: "nowrap"
22511
- };
21841
+ function isLinkContext(el) {
21842
+ const key = el.getAttribute("data-setto-key");
21843
+ if (key?.startsWith("lang.")) return false;
21844
+ if (el.closest("[data-setto-no-hint]")) return false;
21845
+ return !!(el.closest("a[href]") || el.closest("button"));
21846
+ }
21847
+ function activateLinkContext(el) {
21848
+ const anchor = el.closest("a[href]");
21849
+ if (anchor instanceof HTMLAnchorElement) {
21850
+ anchor.click();
21851
+ return;
21852
+ }
21853
+ const button = el.closest("button");
21854
+ if (button instanceof HTMLButtonElement) {
21855
+ button.click();
21856
+ }
21857
+ }
22512
21858
  const MARGIN = 10;
22513
21859
  const OFFSET = 8;
22514
21860
  const MIN_TOUCH = 44;
@@ -22623,6 +21969,18 @@ function rangeFromPoint(doc, x, y) {
22623
21969
  range.collapse(true);
22624
21970
  return range;
22625
21971
  }
21972
+ const QUERY = "(hover: none) and (pointer: coarse)";
21973
+ function subscribe(onChange) {
21974
+ const mq = window.matchMedia(QUERY);
21975
+ mq.addEventListener("change", onChange);
21976
+ return () => mq.removeEventListener("change", onChange);
21977
+ }
21978
+ function getSnapshot() {
21979
+ return window.matchMedia(QUERY).matches;
21980
+ }
21981
+ function useCoarsePointer() {
21982
+ return useSyncExternalStore(subscribe, getSnapshot, () => false);
21983
+ }
22626
21984
  function T({ k }) {
22627
21985
  const { t, i18n } = useTranslation();
22628
21986
  const { editMode, store } = useSetto();
@@ -22745,6 +22103,10 @@ function toBcp47(code) {
22745
22103
  if (base === "en") return "en";
22746
22104
  return code;
22747
22105
  }
22106
+ function useSectionEdit() {
22107
+ const { selectedSectionId, selectSection } = useSetto();
22108
+ return { selectedId: selectedSectionId, selectSection };
22109
+ }
22748
22110
  function isTextEditClick(target) {
22749
22111
  if (target.closest("[data-setto-key]")) return true;
22750
22112
  if (target.closest("[data-setto-ui]")) return true;
@@ -23281,12 +22643,12 @@ function SettoImage({ srcKey, alt, className, style, ...rest }) {
23281
22643
  )
23282
22644
  ] });
23283
22645
  }
23284
- const STYLE_ID$2 = "setto-repeater-styles";
22646
+ const STYLE_ID$1 = "setto-repeater-styles";
23285
22647
  function useRepeaterStyles() {
23286
22648
  useEffect(() => {
23287
- if (document.getElementById(STYLE_ID$2)) return;
22649
+ if (document.getElementById(STYLE_ID$1)) return;
23288
22650
  const el = document.createElement("style");
23289
- el.id = STYLE_ID$2;
22651
+ el.id = STYLE_ID$1;
23290
22652
  el.textContent = `
23291
22653
  .setto-repeater-remove {
23292
22654
  opacity: 0;
@@ -23571,276 +22933,6 @@ function buildEnvelope(fields, values, extra) {
23571
22933
  function settoSubmissionUrl(apiUrl, siteId, formId) {
23572
22934
  return `${apiUrl.replace(/\/+$/, "")}/sites/${encodeURIComponent(siteId)}/forms/${encodeURIComponent(formId)}/submissions`;
23573
22935
  }
23574
- const STYLE_ID$1 = "setto-form-edit-styles";
23575
- function useFormEditStyles() {
23576
- useEffect(() => {
23577
- if (document.getElementById(STYLE_ID$1)) return;
23578
- const el = document.createElement("style");
23579
- el.id = STYLE_ID$1;
23580
- el.textContent = `
23581
- .setto-form-field-tools {
23582
- opacity: 0;
23583
- transition: opacity 120ms ease;
23584
- }
23585
- .setto-form__field:hover .setto-form-field-tools,
23586
- .setto-form__field:focus-within .setto-form-field-tools,
23587
- .setto-form-field-tools[data-open] {
23588
- opacity: 1;
23589
- }
23590
- @media (hover: none) {
23591
- .setto-form-field-tools { opacity: 1; }
23592
- }
23593
- `;
23594
- document.head.appendChild(el);
23595
- }, []);
23596
- }
23597
- function FormFieldTools({
23598
- field,
23599
- index,
23600
- count,
23601
- autoOpen,
23602
- onMove,
23603
- onRemove,
23604
- onChange
23605
- }) {
23606
- useFormEditStyles();
23607
- const [open, setOpen] = useState(autoOpen === true);
23608
- const gearRef = useRef(null);
23609
- return /* @__PURE__ */ jsxs(Fragment, { children: [
23610
- /* @__PURE__ */ jsxs(
23611
- "div",
23612
- {
23613
- "data-setto-ui": true,
23614
- className: "setto-form-field-tools",
23615
- style: toolsStyle,
23616
- ...open ? { "data-open": "" } : {},
23617
- onMouseDown: (e) => e.stopPropagation(),
23618
- onClick: (e) => e.stopPropagation(),
23619
- children: [
23620
- /* @__PURE__ */ jsx(ToolButton, { label: "Flytt opp", disabled: index === 0, onClick: () => onMove(-1), children: /* @__PURE__ */ jsx(ArrowIcon, { direction: "up" }) }),
23621
- /* @__PURE__ */ jsx(
23622
- ToolButton,
23623
- {
23624
- label: "Flytt ned",
23625
- disabled: index === count - 1,
23626
- onClick: () => onMove(1),
23627
- children: /* @__PURE__ */ jsx(ArrowIcon, { direction: "down" })
23628
- }
23629
- ),
23630
- /* @__PURE__ */ jsx(
23631
- ToolButton,
23632
- {
23633
- ref: gearRef,
23634
- label: "Feltinnstillinger",
23635
- active: open,
23636
- onClick: () => setOpen((o) => !o),
23637
- children: /* @__PURE__ */ jsx(GearIcon, {})
23638
- }
23639
- ),
23640
- /* @__PURE__ */ jsx(ToolButton, { label: "Fjern felt", disabled: count <= 1, onClick: onRemove, danger: true, children: "×" })
23641
- ]
23642
- }
23643
- ),
23644
- /* @__PURE__ */ jsx(
23645
- FloatingPopover,
23646
- {
23647
- open,
23648
- anchorRef: gearRef,
23649
- onClose: () => setOpen(false),
23650
- ariaLabel: "Feltinnstillinger",
23651
- minWidth: 300,
23652
- style: { width: 300, padding: 14 },
23653
- children: /* @__PURE__ */ jsx(FieldSettings, { field, onChange })
23654
- }
23655
- )
23656
- ] });
23657
- }
23658
- function FieldSettings({ field, onChange }) {
23659
- const [label, setLabel] = useState(field.label);
23660
- const [placeholder, setPlaceholder] = useState(field.placeholder);
23661
- const [options, setOptions] = useState(field.options.join("\n"));
23662
- const [name, setName] = useState(field.name);
23663
- useEffect(() => setLabel(field.label), [field.label]);
23664
- useEffect(() => setPlaceholder(field.placeholder), [field.placeholder]);
23665
- useEffect(() => setName(field.name), [field.name]);
23666
- return /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 10, fontSize: 13, color: "#222" }, children: [
23667
- /* @__PURE__ */ jsx(Row, { label: "Type", children: /* @__PURE__ */ jsx(
23668
- "select",
23669
- {
23670
- value: field.type,
23671
- onChange: (e) => onChange({ type: e.target.value }),
23672
- style: inputStyle$1,
23673
- children: FORM_FIELD_TYPES.map((type) => /* @__PURE__ */ jsx("option", { value: type, children: FORM_FIELD_TYPE_LABELS[type] }, type))
23674
- }
23675
- ) }),
23676
- /* @__PURE__ */ jsx(Row, { label: "Etikett", children: /* @__PURE__ */ jsx(
23677
- "input",
23678
- {
23679
- type: "text",
23680
- value: label,
23681
- onChange: (e) => setLabel(e.target.value),
23682
- onBlur: () => label !== field.label && onChange({ label }),
23683
- style: inputStyle$1
23684
- }
23685
- ) }),
23686
- fieldTypeHasPlaceholder(field.type) ? /* @__PURE__ */ jsx(Row, { label: "Plassholder", children: /* @__PURE__ */ jsx(
23687
- "input",
23688
- {
23689
- type: "text",
23690
- value: placeholder,
23691
- onChange: (e) => setPlaceholder(e.target.value),
23692
- onBlur: () => placeholder !== field.placeholder && onChange({ placeholder }),
23693
- style: inputStyle$1
23694
- }
23695
- ) }) : null,
23696
- fieldTypeHasOptions(field.type) ? /* @__PURE__ */ jsx(Row, { label: "Alternativer", hint: "Ett per linje", children: /* @__PURE__ */ jsx(
23697
- "textarea",
23698
- {
23699
- value: options,
23700
- rows: 4,
23701
- onChange: (e) => setOptions(e.target.value),
23702
- onBlur: () => options !== field.options.join("\n") && onChange({ options }),
23703
- style: { ...inputStyle$1, resize: "vertical", fontFamily: "inherit" }
23704
- }
23705
- ) }) : null,
23706
- /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }, children: [
23707
- /* @__PURE__ */ jsx(
23708
- "input",
23709
- {
23710
- type: "checkbox",
23711
- checked: field.required,
23712
- onChange: (e) => onChange({ required: e.target.checked })
23713
- }
23714
- ),
23715
- "Må fylles ut"
23716
- ] }),
23717
- /* @__PURE__ */ jsx(Row, { label: "Teknisk navn", hint: "Brukes i e-posten og i historikken", children: /* @__PURE__ */ jsx(
23718
- "input",
23719
- {
23720
- type: "text",
23721
- value: name,
23722
- onChange: (e) => setName(e.target.value),
23723
- onBlur: () => {
23724
- const next = name.trim();
23725
- if (next && next !== field.name) onChange({ name: next });
23726
- else setName(field.name);
23727
- },
23728
- style: { ...inputStyle$1, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" }
23729
- }
23730
- ) })
23731
- ] });
23732
- }
23733
- function Row({ label, hint, children }) {
23734
- return /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 4 }, children: [
23735
- /* @__PURE__ */ jsxs("span", { style: { fontSize: 12, color: "#666" }, children: [
23736
- label,
23737
- hint ? /* @__PURE__ */ jsxs("span", { style: { color: "#999" }, children: [
23738
- " · ",
23739
- hint
23740
- ] }) : null
23741
- ] }),
23742
- children
23743
- ] });
23744
- }
23745
- function ToolButton({ label, onClick, disabled, active, danger, children, ref }) {
23746
- return /* @__PURE__ */ jsx(
23747
- "button",
23748
- {
23749
- ref,
23750
- type: "button",
23751
- "data-setto-ui": true,
23752
- "aria-label": label,
23753
- title: label,
23754
- disabled,
23755
- onClick,
23756
- style: {
23757
- ...toolBtnStyle,
23758
- ...active ? { background: "#640AFF", color: "#fff", borderColor: "#640AFF" } : {},
23759
- ...danger ? { fontSize: 16 } : {},
23760
- ...disabled ? { opacity: 0.35, cursor: "default" } : {}
23761
- },
23762
- children
23763
- }
23764
- );
23765
- }
23766
- function ArrowIcon({ direction }) {
23767
- return /* @__PURE__ */ jsx(
23768
- "svg",
23769
- {
23770
- width: "12",
23771
- height: "12",
23772
- viewBox: "0 0 24 24",
23773
- fill: "none",
23774
- stroke: "currentColor",
23775
- strokeWidth: "2.5",
23776
- strokeLinecap: "round",
23777
- strokeLinejoin: "round",
23778
- "aria-hidden": "true",
23779
- style: direction === "down" ? { transform: "rotate(180deg)" } : void 0,
23780
- children: /* @__PURE__ */ jsx("path", { d: "M12 19V5M5 12l7-7 7 7" })
23781
- }
23782
- );
23783
- }
23784
- function GearIcon() {
23785
- return /* @__PURE__ */ jsxs(
23786
- "svg",
23787
- {
23788
- width: "13",
23789
- height: "13",
23790
- viewBox: "0 0 24 24",
23791
- fill: "none",
23792
- stroke: "currentColor",
23793
- strokeWidth: "2",
23794
- strokeLinecap: "round",
23795
- strokeLinejoin: "round",
23796
- "aria-hidden": "true",
23797
- children: [
23798
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
23799
- /* @__PURE__ */ jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" })
23800
- ]
23801
- }
23802
- );
23803
- }
23804
- const toolsStyle = {
23805
- position: "absolute",
23806
- top: -12,
23807
- right: 0,
23808
- zIndex: 2,
23809
- display: "flex",
23810
- gap: 4,
23811
- padding: 3,
23812
- background: "#ffffff",
23813
- border: "1px solid #e0e0e0",
23814
- borderRadius: 999,
23815
- boxShadow: "0 1px 4px rgba(0,0,0,0.18)",
23816
- fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
23817
- };
23818
- const toolBtnStyle = {
23819
- display: "flex",
23820
- alignItems: "center",
23821
- justifyContent: "center",
23822
- width: 24,
23823
- height: 24,
23824
- padding: 0,
23825
- background: "transparent",
23826
- border: "1px solid transparent",
23827
- borderRadius: "50%",
23828
- fontSize: 13,
23829
- lineHeight: 1,
23830
- color: "#444",
23831
- cursor: "pointer"
23832
- };
23833
- const inputStyle$1 = {
23834
- width: "100%",
23835
- boxSizing: "border-box",
23836
- padding: "6px 8px",
23837
- border: "1px solid #d8d8d8",
23838
- borderRadius: 6,
23839
- fontSize: 13,
23840
- color: "#222",
23841
- background: "#fff",
23842
- fontFamily: "inherit"
23843
- };
23844
22936
  const HONEYPOT_NAME = "website";
23845
22937
  const warnedEndpointKeys = /* @__PURE__ */ new Set();
23846
22938
  function cx(...parts) {
@@ -23856,6 +22948,7 @@ function SettoForm({
23856
22948
  }) {
23857
22949
  const { t, i18n } = useTranslation();
23858
22950
  const { config, editMode, store } = useSetto();
22951
+ const FieldTools = useSettoChromeSlot("form-field-tools");
23859
22952
  const guestEdit = useGuestEdit();
23860
22953
  const editable = editMode || guestEdit.active;
23861
22954
  const lng = i18n.language;
@@ -24103,8 +23196,8 @@ function SettoForm({
24103
23196
  }
24104
23197
  ),
24105
23198
  error ? /* @__PURE__ */ jsx("p", { id: errorId, className: cx("setto-form__field-error", classNames.fieldError), children: t(`form.validation.${error}`, { defaultValue: FIELD_ERROR_DEFAULTS[error] }) }) : null,
24106
- editable && store ? /* @__PURE__ */ jsx(
24107
- FormFieldTools,
23199
+ editable && store && FieldTools ? /* @__PURE__ */ jsx(
23200
+ FieldTools,
24108
23201
  {
24109
23202
  field,
24110
23203
  index,
@@ -25505,204 +24598,6 @@ function collectRelativeKeys(nodes, prefix, out) {
25505
24598
  }
25506
24599
  }
25507
24600
  }
25508
- const IN_PROGRESS_STATUSES = ["pending", "building"];
25509
- function isDeploymentInProgress(status) {
25510
- return IN_PROGRESS_STATUSES.includes(status);
25511
- }
25512
- const STALE_PENDING_MS = 20 * 60 * 1e3;
25513
- const STALE_BUILDING_MS = 45 * 60 * 1e3;
25514
- function isDeploymentStale(row) {
25515
- if (row.status !== "pending" && row.status !== "building") return false;
25516
- const now = Date.now();
25517
- if (row.status === "pending") {
25518
- return now - new Date(row.started_at).getTime() > STALE_PENDING_MS;
25519
- }
25520
- return now - new Date(row.updated_at).getTime() > STALE_BUILDING_MS;
25521
- }
25522
- async function fetchInProgressDeploymentId(supabase, siteId) {
25523
- const { data } = await supabase.from("deployments").select("id, status, started_at, updated_at").eq("site_id", siteId).in("status", [...IN_PROGRESS_STATUSES]).order("started_at", { ascending: false }).limit(5);
25524
- const rows = data ?? [];
25525
- const fresh = rows.find((r) => !isDeploymentStale(r));
25526
- return fresh?.id ?? null;
25527
- }
25528
- async function waitForDeploymentByCommit(supabase, siteId, commitSha, timeoutMs = 6e4) {
25529
- const deadline = Date.now() + timeoutMs;
25530
- while (Date.now() < deadline) {
25531
- const { data } = await supabase.from("deployments").select("id").eq("site_id", siteId).eq("commit_sha", commitSha).order("started_at", { ascending: false }).limit(1).maybeSingle();
25532
- if (data?.id) return data.id;
25533
- await new Promise((r) => setTimeout(r, 2e3));
25534
- }
25535
- return null;
25536
- }
25537
- function useDeploymentStatus(supabase, deploymentId) {
25538
- const [row, setRow] = useState(null);
25539
- useEffect(() => {
25540
- if (!deploymentId) {
25541
- setRow(null);
25542
- return;
25543
- }
25544
- let channel = null;
25545
- let cancelled = false;
25546
- const load = async () => {
25547
- const { data } = await supabase.from("deployments").select("*").eq("id", deploymentId).maybeSingle();
25548
- if (!cancelled && data) setRow(data);
25549
- };
25550
- void load();
25551
- channel = supabase.channel(`setto-dep-${deploymentId}`).on(
25552
- "postgres_changes",
25553
- {
25554
- event: "UPDATE",
25555
- schema: "public",
25556
- table: "deployments",
25557
- filter: `id=eq.${deploymentId}`
25558
- },
25559
- (payload) => {
25560
- setRow(payload.new);
25561
- }
25562
- ).subscribe();
25563
- return () => {
25564
- cancelled = true;
25565
- if (channel) supabase.removeChannel(channel);
25566
- };
25567
- }, [supabase, deploymentId]);
25568
- return { row, status: row?.status ?? null };
25569
- }
25570
- function deploymentStepIndex(status) {
25571
- switch (status) {
25572
- case "pending":
25573
- return 0;
25574
- case "building":
25575
- return 1;
25576
- case "ready":
25577
- return 2;
25578
- default:
25579
- return -1;
25580
- }
25581
- }
25582
- function isDeploymentStepReached(status, i) {
25583
- return deploymentStepIndex(status) >= i;
25584
- }
25585
- const STEPS = ["Committed", "Building", "Deployed"];
25586
- const PROGRESS_STYLE_ID = "setto-publish-progress-styles";
25587
- function usePublishProgressStyles() {
25588
- useEffect(() => {
25589
- if (document.getElementById(PROGRESS_STYLE_ID)) return;
25590
- const el = document.createElement("style");
25591
- el.id = PROGRESS_STYLE_ID;
25592
- el.textContent = `
25593
- @keyframes setto-step-pulse {
25594
- 0%, 100% { opacity: 1; transform: scaleY(1); }
25595
- 50% { opacity: 0.72; transform: scaleY(0.82); }
25596
- }
25597
- @keyframes setto-step-shimmer {
25598
- 0% { background-position: 120% 0; }
25599
- 100% { background-position: -120% 0; }
25600
- }
25601
- .setto-step-bar-active {
25602
- animation: setto-step-pulse 1.5s ease-in-out infinite;
25603
- transform-origin: center bottom;
25604
- }
25605
- .setto-step-bar-shimmer {
25606
- background: linear-gradient(
25607
- 90deg,
25608
- #640aff 0%,
25609
- #8b5cf6 45%,
25610
- #640aff 90%
25611
- ) !important;
25612
- background-size: 220% 100% !important;
25613
- animation: setto-step-shimmer 2.2s ease-in-out infinite;
25614
- }
25615
- .setto-step-label-active {
25616
- animation: setto-step-pulse 1.5s ease-in-out infinite;
25617
- }
25618
- `;
25619
- document.head.appendChild(el);
25620
- }, []);
25621
- }
25622
- function PublishProgressSteps({
25623
- status,
25624
- compact,
25625
- buildingElapsed
25626
- }) {
25627
- usePublishProgressStyles();
25628
- const scopeId = useId().replace(/:/g, "");
25629
- const errored = status === "error" || status === "canceled";
25630
- const activeIndex = deploymentStepIndex(status);
25631
- return /* @__PURE__ */ jsx(
25632
- "div",
25633
- {
25634
- className: `setto-progress-${scopeId}`,
25635
- style: {
25636
- display: "flex",
25637
- gap: compact ? 3 : 8,
25638
- fontSize: compact ? 11 : 12,
25639
- minWidth: compact ? 100 : 200
25640
- },
25641
- children: STEPS.map((label, i) => {
25642
- const reached = isDeploymentStepReached(status, i);
25643
- const active = activeIndex === i && !errored;
25644
- const barClass = active ? "setto-step-bar-active setto-step-bar-shimmer" : void 0;
25645
- return /* @__PURE__ */ jsxs("div", { style: { flex: 1, textAlign: "center", minWidth: 0 }, children: [
25646
- /* @__PURE__ */ jsx(
25647
- "div",
25648
- {
25649
- className: barClass,
25650
- style: {
25651
- height: compact ? 3 : 4,
25652
- borderRadius: 2,
25653
- background: errored ? "#fca5a5" : reached ? "#640AFF" : "#e5e5e5",
25654
- marginBottom: compact ? 0 : 6,
25655
- transition: "background 0.35s ease"
25656
- }
25657
- }
25658
- ),
25659
- !compact ? /* @__PURE__ */ jsxs(
25660
- "span",
25661
- {
25662
- className: active ? "setto-step-label-active" : void 0,
25663
- style: {
25664
- color: active ? "#1a1a1a" : "#888",
25665
- fontWeight: active ? 500 : 400,
25666
- display: "inline-block"
25667
- },
25668
- children: [
25669
- label,
25670
- active && status === "building" && buildingElapsed != null ? ` (${buildingElapsed}s)` : active ? " …" : ""
25671
- ]
25672
- }
25673
- ) : null
25674
- ] }, label);
25675
- })
25676
- }
25677
- );
25678
- }
25679
- function PublishProgressBar({ status, compact, href }) {
25680
- const inner = /* @__PURE__ */ jsx(PublishProgressSteps, { status, compact });
25681
- const wrapStyle = {
25682
- fontSize: compact ? 11 : 12,
25683
- minWidth: compact ? 100 : 200
25684
- };
25685
- if (href) {
25686
- return /* @__PURE__ */ jsx(
25687
- "a",
25688
- {
25689
- href,
25690
- title: "Se publiseringsstatus",
25691
- style: {
25692
- ...wrapStyle,
25693
- display: "block",
25694
- textDecoration: "none",
25695
- color: "inherit",
25696
- cursor: "pointer",
25697
- borderRadius: 4,
25698
- padding: compact ? "4px 2px" : void 0
25699
- },
25700
- children: inner
25701
- }
25702
- );
25703
- }
25704
- return /* @__PURE__ */ jsx("div", { style: wrapStyle, children: inner });
25705
- }
25706
24601
  export {
25707
24602
  AuthGate,
25708
24603
  BLOCK_REGISTRY,
@@ -25713,14 +24608,10 @@ export {
25713
24608
  FAB_SIZE,
25714
24609
  FORM_FIELD_TYPES,
25715
24610
  FORM_FIELD_TYPE_LABELS,
24611
+ FloatingPopover,
25716
24612
  GuestEditProvider,
25717
- IN_PROGRESS_STATUSES,
25718
- PublishProgressBar,
25719
- PublishProgressSteps,
25720
24613
  SETTO_BASE,
25721
24614
  SETTO_MODULES,
25722
- STALE_BUILDING_MS,
25723
- STALE_PENDING_MS,
25724
24615
  SettoAdminApp,
25725
24616
  SettoAnimation,
25726
24617
  SettoBlock,
@@ -25731,7 +24622,6 @@ export {
25731
24622
  SettoIcon,
25732
24623
  SettoImage,
25733
24624
  SettoMark,
25734
- SettoPreviewBar,
25735
24625
  SettoProvider,
25736
24626
  SettoRepeater,
25737
24627
  SettoSection,
@@ -25742,28 +24632,25 @@ export {
25742
24632
  checkoutIntentUrl,
25743
24633
  currentAdminView,
25744
24634
  extractContentKeys,
25745
- fetchInProgressDeploymentId,
24635
+ fieldTypeHasOptions,
24636
+ fieldTypeHasPlaceholder,
25746
24637
  formFieldsPrefix,
25747
24638
  isAdminDeploymentView,
25748
24639
  isAdminPanelView,
25749
24640
  isAdminRoute,
25750
- isDeploymentInProgress,
25751
- isDeploymentStale,
25752
24641
  isFormFieldType,
25753
- isPreviewHost,
24642
+ isLinkContext,
25754
24643
  nodeChildren,
25755
24644
  parseBlockTree,
25756
- previewBarState,
25757
24645
  readLocaleFields,
25758
24646
  settoSubmissionUrl,
25759
24647
  siteHomeUrl,
25760
- useDeploymentStatus,
24648
+ useCoarsePointer,
25761
24649
  useGuestEdit,
25762
24650
  useSectionTheme,
25763
24651
  useSetto,
25764
24652
  useSettoChromeSlot,
25765
24653
  validateBlockTree,
25766
- waitForDeploymentByCommit,
25767
24654
  wantsCheckout,
25768
24655
  withoutCheckoutIntent
25769
24656
  };