@sevenfold/setto-client 0.15.0 → 0.17.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,9 +1,242 @@
1
- import { jsx, jsxs, Fragment } from "react/jsx-runtime";
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
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";
7
+ const STORAGE_KEY$1 = "setto.lang";
8
+ const DEFAULT_LANGUAGE = "no";
9
+ const ENDONYMS = {
10
+ no: "Norsk",
11
+ nb: "Norsk",
12
+ nn: "Nynorsk",
13
+ en: "English",
14
+ sv: "Svenska",
15
+ da: "Dansk",
16
+ de: "Deutsch",
17
+ fr: "Français",
18
+ es: "Español",
19
+ it: "Italiano",
20
+ nl: "Nederlands",
21
+ pl: "Polski",
22
+ pt: "Português",
23
+ fi: "Suomi",
24
+ is: "Íslenska",
25
+ lt: "Lietuvių",
26
+ uk: "Українська",
27
+ ru: "Русский",
28
+ ar: "العربية",
29
+ tr: "Türkçe",
30
+ so: "Soomaali",
31
+ ti: "ትግርኛ",
32
+ th: "ไทย",
33
+ vi: "Tiếng Việt",
34
+ zh: "中文",
35
+ ja: "日本語"
36
+ };
37
+ function languageName(tag) {
38
+ const base = tag.toLowerCase().split("-")[0] ?? tag;
39
+ return ENDONYMS[base] ?? tag.toUpperCase();
40
+ }
41
+ function siteLanguages(configured, primary = DEFAULT_LANGUAGE) {
42
+ const out = [primary];
43
+ for (const tag of configured ?? []) {
44
+ if (tag && !out.includes(tag)) out.push(tag);
45
+ }
46
+ return out;
47
+ }
48
+ function readStoredLanguage() {
49
+ try {
50
+ return window.localStorage.getItem(STORAGE_KEY$1);
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ function storeLanguage(tag) {
56
+ try {
57
+ window.localStorage.setItem(STORAGE_KEY$1, tag);
58
+ } catch {
59
+ }
60
+ }
61
+ function pickInitialLanguage(available, browserLanguages = typeof navigator === "undefined" ? [] : navigator.languages ?? [navigator.language], stored = null) {
62
+ const fallback = available[0] ?? DEFAULT_LANGUAGE;
63
+ if (stored && available.includes(stored)) return stored;
64
+ for (const wanted of browserLanguages) {
65
+ const base = wanted.toLowerCase().split("-")[0];
66
+ if (!base) continue;
67
+ const hit = available.find((tag) => tag.toLowerCase().split("-")[0] === base);
68
+ if (hit) return hit;
69
+ if ((base === "nb" || base === "nn" || base === "no") && available.includes("no")) {
70
+ return "no";
71
+ }
72
+ }
73
+ return fallback;
74
+ }
75
+ const INLINE_MARKER = "data-setto-languages-inline";
76
+ function SettoLanguages({ languages, inline = false }) {
77
+ const { i18n } = useTranslation();
78
+ const [open, setOpen] = useState(false);
79
+ const ref = useRef(null);
80
+ useEffect(() => {
81
+ if (!inline) return;
82
+ const root = document.documentElement;
83
+ const count = Number(root.getAttribute(INLINE_MARKER) ?? "0") + 1;
84
+ root.setAttribute(INLINE_MARKER, String(count));
85
+ return () => {
86
+ const left = Number(root.getAttribute(INLINE_MARKER) ?? "1") - 1;
87
+ if (left > 0) root.setAttribute(INLINE_MARKER, String(left));
88
+ else root.removeAttribute(INLINE_MARKER);
89
+ };
90
+ }, [inline]);
91
+ useEffect(() => {
92
+ if (!open) return;
93
+ const onDown = (e) => {
94
+ if (!ref.current?.contains(e.target)) setOpen(false);
95
+ };
96
+ const onKey = (e) => {
97
+ if (e.key === "Escape") setOpen(false);
98
+ };
99
+ document.addEventListener("mousedown", onDown);
100
+ document.addEventListener("keydown", onKey);
101
+ return () => {
102
+ document.removeEventListener("mousedown", onDown);
103
+ document.removeEventListener("keydown", onKey);
104
+ };
105
+ }, [open]);
106
+ if (languages.length < 2) return null;
107
+ const current = i18n.language;
108
+ const choose = (tag) => {
109
+ setOpen(false);
110
+ if (tag === current) return;
111
+ storeLanguage(tag);
112
+ void i18n.changeLanguage(tag);
113
+ document.documentElement.lang = tag;
114
+ };
115
+ return /* @__PURE__ */ jsxs("div", { ref, style: inline ? inlineWrapStyle : floatingWrapStyle, children: [
116
+ /* @__PURE__ */ jsxs(
117
+ "button",
118
+ {
119
+ type: "button",
120
+ onClick: () => setOpen((o) => !o),
121
+ "aria-expanded": open,
122
+ "aria-haspopup": "listbox",
123
+ "aria-label": `Språk: ${languageName(current)}`,
124
+ style: triggerStyle,
125
+ children: [
126
+ /* @__PURE__ */ jsx(GlobeIcon, {}),
127
+ languageName(current)
128
+ ]
129
+ }
130
+ ),
131
+ open ? /* @__PURE__ */ jsx("ul", { role: "listbox", style: inline ? inlineListStyle : floatingListStyle, children: languages.map((tag) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
132
+ "button",
133
+ {
134
+ type: "button",
135
+ role: "option",
136
+ "aria-selected": tag === current,
137
+ onClick: () => choose(tag),
138
+ style: {
139
+ ...optionStyle,
140
+ fontWeight: tag === current ? 600 : 400
141
+ },
142
+ children: languageName(tag)
143
+ }
144
+ ) }, tag)) }) : null
145
+ ] });
146
+ }
147
+ function AutoLanguageSwitcher({ languages }) {
148
+ const [inlineElsewhere, setInlineElsewhere] = useState(false);
149
+ useEffect(() => {
150
+ const check = () => setInlineElsewhere(document.documentElement.hasAttribute(INLINE_MARKER));
151
+ check();
152
+ const observer = new MutationObserver(check);
153
+ observer.observe(document.documentElement, {
154
+ attributes: true,
155
+ attributeFilter: [INLINE_MARKER]
156
+ });
157
+ return () => observer.disconnect();
158
+ }, []);
159
+ if (inlineElsewhere || languages.length < 2) return null;
160
+ return /* @__PURE__ */ jsx(SettoLanguages, { languages });
161
+ }
162
+ function GlobeIcon() {
163
+ return /* @__PURE__ */ jsxs(
164
+ "svg",
165
+ {
166
+ width: "15",
167
+ height: "15",
168
+ viewBox: "0 0 24 24",
169
+ fill: "none",
170
+ stroke: "currentColor",
171
+ strokeWidth: "1.8",
172
+ strokeLinecap: "round",
173
+ strokeLinejoin: "round",
174
+ "aria-hidden": "true",
175
+ children: [
176
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
177
+ /* @__PURE__ */ jsx("path", { d: "M3 12h18M12 3c2.4 2.6 3.6 5.6 3.6 9s-1.2 6.4-3.6 9c-2.4-2.6-3.6-5.6-3.6-9S9.6 5.6 12 3z" })
178
+ ]
179
+ }
180
+ );
181
+ }
182
+ const floatingWrapStyle = {
183
+ position: "fixed",
184
+ left: "max(16px, env(safe-area-inset-left))",
185
+ bottom: "max(16px, env(safe-area-inset-bottom))",
186
+ zIndex: 2147482e3,
187
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
188
+ };
189
+ const inlineWrapStyle = {
190
+ position: "relative",
191
+ display: "inline-block",
192
+ fontFamily: "inherit"
193
+ };
194
+ const triggerStyle = {
195
+ display: "inline-flex",
196
+ alignItems: "center",
197
+ gap: 7,
198
+ minHeight: 40,
199
+ padding: "8px 14px",
200
+ borderRadius: 999,
201
+ border: "1px solid rgba(127, 127, 127, 0.32)",
202
+ // Translucent over whatever the site put behind it, and legible either way.
203
+ background: "rgba(255, 255, 255, 0.82)",
204
+ backdropFilter: "blur(8px)",
205
+ WebkitBackdropFilter: "blur(8px)",
206
+ color: "#1a1a1a",
207
+ fontSize: 13.5,
208
+ fontWeight: 500,
209
+ lineHeight: 1,
210
+ cursor: "pointer",
211
+ boxShadow: "0 4px 14px rgba(0, 0, 0, 0.12)"
212
+ };
213
+ const listBase = {
214
+ position: "absolute",
215
+ margin: 0,
216
+ padding: 4,
217
+ listStyle: "none",
218
+ minWidth: 150,
219
+ borderRadius: 12,
220
+ border: "1px solid rgba(127, 127, 127, 0.24)",
221
+ background: "#ffffff",
222
+ boxShadow: "0 12px 32px rgba(0, 0, 0, 0.18)",
223
+ zIndex: 1
224
+ };
225
+ const floatingListStyle = { ...listBase, left: 0, bottom: "calc(100% + 8px)" };
226
+ const inlineListStyle = { ...listBase, left: 0, top: "calc(100% + 8px)" };
227
+ const optionStyle = {
228
+ display: "block",
229
+ width: "100%",
230
+ minHeight: 40,
231
+ padding: "9px 12px",
232
+ border: 0,
233
+ borderRadius: 8,
234
+ background: "transparent",
235
+ color: "#1a1a1a",
236
+ fontSize: 13.5,
237
+ textAlign: "left",
238
+ cursor: "pointer"
239
+ };
7
240
  function __rest(s, e) {
8
241
  var t = {};
9
242
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
@@ -21431,11 +21664,11 @@ function useSettoChromeSlot(name) {
21431
21664
  slotListeners.delete(listener);
21432
21665
  };
21433
21666
  }, []);
21434
- const Offered = slots.get(name) ?? null;
21667
+ const Offered = slots.get(name);
21435
21668
  return useMemo(() => {
21436
21669
  if (!Offered) return null;
21437
- return function SettoChromeSlot() {
21438
- return /* @__PURE__ */ jsx(ChromeBoundary, { onError: () => retireSlot(name), children: /* @__PURE__ */ jsx(Offered, {}) });
21670
+ return function SettoChromeSlot(props) {
21671
+ return /* @__PURE__ */ jsx(ChromeBoundary, { onError: () => retireSlot(name), children: React.createElement(Offered, props) });
21439
21672
  };
21440
21673
  }, [Offered, name]);
21441
21674
  }
@@ -21552,7 +21785,7 @@ function withoutCheckoutIntent() {
21552
21785
  }
21553
21786
  const FAB_SIZE = 52;
21554
21787
  const FAB_INSET = 16;
21555
- const STYLE_ID$3 = "setto-edit-layout";
21788
+ const STYLE_ID$2 = "setto-edit-layout";
21556
21789
  const HTML_CLASS = "setto-edit-mode";
21557
21790
  function useSettoDocumentLayout(active) {
21558
21791
  useEffect(() => {
@@ -21561,10 +21794,10 @@ function useSettoDocumentLayout(active) {
21561
21794
  html.classList.add(HTML_CLASS);
21562
21795
  html.style.setProperty("--setto-fab-size", `${FAB_SIZE}px`);
21563
21796
  html.style.setProperty("--setto-fab-inset", `${FAB_INSET}px`);
21564
- let styleEl = document.getElementById(STYLE_ID$3);
21797
+ let styleEl = document.getElementById(STYLE_ID$2);
21565
21798
  if (!styleEl) {
21566
21799
  styleEl = document.createElement("style");
21567
- styleEl.id = STYLE_ID$3;
21800
+ styleEl.id = STYLE_ID$2;
21568
21801
  styleEl.textContent = `
21569
21802
  html.setto-edit-mode {
21570
21803
  width: 100%;
@@ -21767,6 +22000,16 @@ function SettoProvider({ config, children }) {
21767
22000
  }, []);
21768
22001
  const access = useSiteAccess(supabase, session, config.siteId);
21769
22002
  const { i18n } = useTranslation();
22003
+ const languages = useMemo(
22004
+ () => siteLanguages(config.languages, config.languages?.[0]),
22005
+ [config.languages]
22006
+ );
22007
+ useEffect(() => {
22008
+ if (!i18n || languages.length < 2) return;
22009
+ const wanted = pickInitialLanguage(languages, void 0, readStoredLanguage());
22010
+ if (wanted && wanted !== i18n.language) void i18n.changeLanguage(wanted);
22011
+ document.documentElement.lang = wanted;
22012
+ }, [i18n, languages]);
21770
22013
  const storeRef = useRef(null);
21771
22014
  const themeStoreRef = useRef(null);
21772
22015
  const assetStoreRef = useRef(null);
@@ -21809,7 +22052,8 @@ function SettoProvider({ config, children }) {
21809
22052
  const editShellActive = editMode && !!value.store;
21810
22053
  return /* @__PURE__ */ jsx(SettoContext.Provider, { value, children: /* @__PURE__ */ jsxs(Fragment, { children: [
21811
22054
  /* @__PURE__ */ jsx(EditModeShell, { active: editShellActive, themeStore: value.themeStore, children }),
21812
- /* @__PURE__ */ jsx(SettoChrome, {})
22055
+ /* @__PURE__ */ jsx(SettoChrome, {}),
22056
+ /* @__PURE__ */ jsx(AutoLanguageSwitcher, { languages })
21813
22057
  ] }) });
21814
22058
  }
21815
22059
  function useSetto() {
@@ -21838,202 +22082,6 @@ function useGuestEdit() {
21838
22082
  }
21839
22083
  };
21840
22084
  }
21841
- const PREVIEW_HOST = /(^|\.)site\.setto\.no$/i;
21842
- function isPreviewHost(hostname) {
21843
- return PREVIEW_HOST.test(hostname);
21844
- }
21845
- function previewBarState(input) {
21846
- if (!input.onPreview || input.authLoading) return "hidden";
21847
- if (input.hasSession && !input.sitesLoaded) return "hidden";
21848
- if (!input.hasSession && input.soldPublic === null) return "hidden";
21849
- const bought = input.paidMarker || input.currentSiteIsLive || input.soldPublic === true;
21850
- if (bought) return "hidden";
21851
- return input.hasSession && input.hasCurrentSite ? "owner" : "visitor";
21852
- }
21853
- function SettoPreviewBar() {
21854
- const { supabase, api, config, session, authLoading, currentSite, sites, editMode } = useSetto();
21855
- const [onPreview, setOnPreview] = useState(false);
21856
- const [busy, setBusy] = useState(false);
21857
- const [error, setError] = useState(null);
21858
- const [paid, setPaid] = useState(false);
21859
- const [soldPublic, setSoldPublic] = useState(null);
21860
- useEffect(() => {
21861
- setOnPreview(isPreviewHost(window.location.hostname));
21862
- setPaid(new URLSearchParams(window.location.search).get("betaling") === "ok");
21863
- }, []);
21864
- useEffect(() => {
21865
- if (!onPreview || session) return;
21866
- let cancelled = false;
21867
- fetch(`${config.apiUrl}/onboarding/sites/${config.siteId}/sold`, {
21868
- headers: { Accept: "application/json" }
21869
- }).then((r) => r.ok ? r.json() : null).then((d) => {
21870
- if (!cancelled && d) setSoldPublic(d.sold === true);
21871
- }).catch(() => {
21872
- });
21873
- return () => {
21874
- cancelled = true;
21875
- };
21876
- }, [onPreview, session, config.apiUrl, config.siteId]);
21877
- const signIn = () => {
21878
- window.location.href = checkoutIntentUrl();
21879
- };
21880
- const next = () => {
21881
- if (window.parent !== window) {
21882
- window.parent.postMessage({ type: "setto:next", siteId: config.siteId }, "*");
21883
- return;
21884
- }
21885
- signIn();
21886
- };
21887
- const publish = async () => {
21888
- setBusy(true);
21889
- setError(null);
21890
- try {
21891
- const token = (await supabase.auth.getSession()).data.session?.access_token;
21892
- if (!token) {
21893
- signIn();
21894
- return;
21895
- }
21896
- const data = await api.request(
21897
- `/onboarding/sites/${config.siteId}/checkout`,
21898
- {
21899
- method: "POST",
21900
- // Back to the page they were looking at, not to a chat window.
21901
- body: { returnTo: window.location.origin + window.location.pathname }
21902
- },
21903
- "checkout"
21904
- );
21905
- if (!data.url) throw new Error("checkout_failed");
21906
- window.location.href = data.url;
21907
- } catch (err) {
21908
- const detail = err instanceof Error ? err.message : "";
21909
- setError(
21910
- 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."
21911
- );
21912
- setBusy(false);
21913
- }
21914
- };
21915
- const resumed = useRef(false);
21916
- useEffect(() => {
21917
- if (resumed.current || !onPreview || !session || !currentSite) return;
21918
- if (!wantsCheckout()) return;
21919
- if (paid || currentSite.status === "live") return;
21920
- resumed.current = true;
21921
- window.history.replaceState(null, "", withoutCheckoutIntent());
21922
- void publish();
21923
- }, [onPreview, session, currentSite, paid]);
21924
- const state = previewBarState({
21925
- onPreview,
21926
- authLoading,
21927
- hasSession: !!session,
21928
- sitesLoaded: sites !== null,
21929
- hasCurrentSite: !!currentSite,
21930
- currentSiteIsLive: currentSite?.status === "live",
21931
- soldPublic,
21932
- paidMarker: paid
21933
- });
21934
- if (state === "hidden") return null;
21935
- const owner = state === "owner";
21936
- const name = currentSite?.display_name?.trim() || config.displayName?.trim() || config.siteId;
21937
- return createPortal(
21938
- /*
21939
- * Lifted above the editor's floating button when edit mode is on, which
21940
- * sits in the same corner band. Sideways would collide again the moment
21941
- * its toolbar opens, since that expands rightward.
21942
- */
21943
- /* @__PURE__ */ jsx(
21944
- "div",
21945
- {
21946
- style: editMode ? { ...barStyle, bottom: "calc(max(16px, env(safe-area-inset-bottom, 0px)) + 68px)" } : barStyle,
21947
- "data-setto-preview-bar": "",
21948
- children: owner ? /* @__PURE__ */ jsxs(Fragment, { children: [
21949
- /* @__PURE__ */ jsx("span", { style: dotStyle, "aria-hidden": true }),
21950
- /* @__PURE__ */ jsxs("span", { style: labelStyle, children: [
21951
- "Du forhåndsviser nå ",
21952
- /* @__PURE__ */ jsx("strong", { style: { fontWeight: 600 }, children: name })
21953
- ] }),
21954
- error && /* @__PURE__ */ jsx("span", { style: errorStyle, children: error }),
21955
- /* @__PURE__ */ jsx("button", { type: "button", style: primaryStyle, onClick: publish, disabled: busy, children: busy ? "Åpner betaling …" : "Publiser" })
21956
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
21957
- /* @__PURE__ */ jsx("span", { style: dotStyle, "aria-hidden": true }),
21958
- /* @__PURE__ */ jsx("span", { style: labelStyle, children: "Forhåndsvisning" }),
21959
- /* @__PURE__ */ jsx("button", { type: "button", style: primaryStyle, onClick: next, children: "Neste" })
21960
- ] })
21961
- }
21962
- ),
21963
- document.body
21964
- );
21965
- }
21966
- const barStyle = {
21967
- position: "fixed",
21968
- left: "50%",
21969
- bottom: "max(16px, env(safe-area-inset-bottom, 0px))",
21970
- transform: "translateX(-50%)",
21971
- zIndex: 2147483e3,
21972
- display: "flex",
21973
- alignItems: "center",
21974
- gap: 14,
21975
- maxWidth: "calc(100vw - 24px)",
21976
- padding: "10px 10px 10px 18px",
21977
- borderRadius: 999,
21978
- background: "#211711",
21979
- color: "#fffdf8",
21980
- fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
21981
- fontSize: 14,
21982
- lineHeight: 1.3,
21983
- boxShadow: "0 18px 40px -18px rgba(0, 0, 0, 0.65)",
21984
- /*
21985
- * A frosted rim, so the pill reads as an object on any page.
21986
- *
21987
- * The bar sits on sites Setto did not choose the colours for: on a dark hero
21988
- * a dark pill disappears, and on a pale section its shadow is all that holds
21989
- * it. A translucent white border with `padding-box` clipping lets the page
21990
- * show through the rim itself, which separates the pill from either ground
21991
- * without picking a side.
21992
- */
21993
- border: "5px solid rgba(255, 255, 255, 0.22)",
21994
- backgroundClip: "padding-box",
21995
- backdropFilter: "blur(10px)",
21996
- WebkitBackdropFilter: "blur(10px)"
21997
- };
21998
- const dotStyle = {
21999
- width: 7,
22000
- height: 7,
22001
- borderRadius: "50%",
22002
- background: "#c4502a",
22003
- flex: "none"
22004
- };
22005
- const labelStyle = {
22006
- whiteSpace: "nowrap",
22007
- overflow: "hidden",
22008
- textOverflow: "ellipsis"
22009
- };
22010
- const primaryStyle = {
22011
- flex: "none",
22012
- padding: "9px 18px",
22013
- border: 0,
22014
- borderRadius: 999,
22015
- background: "#c4502a",
22016
- color: "#fffdf8",
22017
- font: "inherit",
22018
- fontWeight: 600,
22019
- cursor: "pointer"
22020
- };
22021
- const errorStyle = {
22022
- color: "#ffb4a2",
22023
- fontSize: 13,
22024
- /*
22025
- * Shrinkable, unlike the label beside it.
22026
- *
22027
- * `nowrap` on a flex child whose `min-width` is `auto` cannot shrink below
22028
- * its own text, so on a 320px phone the error shouldered «Publiser» 19px
22029
- * past the right edge of the screen and the bar clipped it — the button
22030
- * that takes the money, unreachable, exactly when something had gone wrong.
22031
- */
22032
- minWidth: 0,
22033
- overflow: "hidden",
22034
- textOverflow: "ellipsis",
22035
- whiteSpace: "nowrap"
22036
- };
22037
22085
  function isLinkContext(el) {
22038
22086
  const key = el.getAttribute("data-setto-key");
22039
22087
  if (key?.startsWith("lang.")) return false;
@@ -22839,12 +22887,12 @@ function SettoImage({ srcKey, alt, className, style, ...rest }) {
22839
22887
  )
22840
22888
  ] });
22841
22889
  }
22842
- const STYLE_ID$2 = "setto-repeater-styles";
22890
+ const STYLE_ID$1 = "setto-repeater-styles";
22843
22891
  function useRepeaterStyles() {
22844
22892
  useEffect(() => {
22845
- if (document.getElementById(STYLE_ID$2)) return;
22893
+ if (document.getElementById(STYLE_ID$1)) return;
22846
22894
  const el = document.createElement("style");
22847
- el.id = STYLE_ID$2;
22895
+ el.id = STYLE_ID$1;
22848
22896
  el.textContent = `
22849
22897
  .setto-repeater-remove {
22850
22898
  opacity: 0;
@@ -23129,276 +23177,6 @@ function buildEnvelope(fields, values, extra) {
23129
23177
  function settoSubmissionUrl(apiUrl, siteId, formId) {
23130
23178
  return `${apiUrl.replace(/\/+$/, "")}/sites/${encodeURIComponent(siteId)}/forms/${encodeURIComponent(formId)}/submissions`;
23131
23179
  }
23132
- const STYLE_ID$1 = "setto-form-edit-styles";
23133
- function useFormEditStyles() {
23134
- useEffect(() => {
23135
- if (document.getElementById(STYLE_ID$1)) return;
23136
- const el = document.createElement("style");
23137
- el.id = STYLE_ID$1;
23138
- el.textContent = `
23139
- .setto-form-field-tools {
23140
- opacity: 0;
23141
- transition: opacity 120ms ease;
23142
- }
23143
- .setto-form__field:hover .setto-form-field-tools,
23144
- .setto-form__field:focus-within .setto-form-field-tools,
23145
- .setto-form-field-tools[data-open] {
23146
- opacity: 1;
23147
- }
23148
- @media (hover: none) {
23149
- .setto-form-field-tools { opacity: 1; }
23150
- }
23151
- `;
23152
- document.head.appendChild(el);
23153
- }, []);
23154
- }
23155
- function FormFieldTools({
23156
- field,
23157
- index,
23158
- count,
23159
- autoOpen,
23160
- onMove,
23161
- onRemove,
23162
- onChange
23163
- }) {
23164
- useFormEditStyles();
23165
- const [open, setOpen] = useState(autoOpen === true);
23166
- const gearRef = useRef(null);
23167
- return /* @__PURE__ */ jsxs(Fragment, { children: [
23168
- /* @__PURE__ */ jsxs(
23169
- "div",
23170
- {
23171
- "data-setto-ui": true,
23172
- className: "setto-form-field-tools",
23173
- style: toolsStyle,
23174
- ...open ? { "data-open": "" } : {},
23175
- onMouseDown: (e) => e.stopPropagation(),
23176
- onClick: (e) => e.stopPropagation(),
23177
- children: [
23178
- /* @__PURE__ */ jsx(ToolButton, { label: "Flytt opp", disabled: index === 0, onClick: () => onMove(-1), children: /* @__PURE__ */ jsx(ArrowIcon, { direction: "up" }) }),
23179
- /* @__PURE__ */ jsx(
23180
- ToolButton,
23181
- {
23182
- label: "Flytt ned",
23183
- disabled: index === count - 1,
23184
- onClick: () => onMove(1),
23185
- children: /* @__PURE__ */ jsx(ArrowIcon, { direction: "down" })
23186
- }
23187
- ),
23188
- /* @__PURE__ */ jsx(
23189
- ToolButton,
23190
- {
23191
- ref: gearRef,
23192
- label: "Feltinnstillinger",
23193
- active: open,
23194
- onClick: () => setOpen((o) => !o),
23195
- children: /* @__PURE__ */ jsx(GearIcon, {})
23196
- }
23197
- ),
23198
- /* @__PURE__ */ jsx(ToolButton, { label: "Fjern felt", disabled: count <= 1, onClick: onRemove, danger: true, children: "×" })
23199
- ]
23200
- }
23201
- ),
23202
- /* @__PURE__ */ jsx(
23203
- FloatingPopover,
23204
- {
23205
- open,
23206
- anchorRef: gearRef,
23207
- onClose: () => setOpen(false),
23208
- ariaLabel: "Feltinnstillinger",
23209
- minWidth: 300,
23210
- style: { width: 300, padding: 14 },
23211
- children: /* @__PURE__ */ jsx(FieldSettings, { field, onChange })
23212
- }
23213
- )
23214
- ] });
23215
- }
23216
- function FieldSettings({ field, onChange }) {
23217
- const [label, setLabel] = useState(field.label);
23218
- const [placeholder, setPlaceholder] = useState(field.placeholder);
23219
- const [options, setOptions] = useState(field.options.join("\n"));
23220
- const [name, setName] = useState(field.name);
23221
- useEffect(() => setLabel(field.label), [field.label]);
23222
- useEffect(() => setPlaceholder(field.placeholder), [field.placeholder]);
23223
- useEffect(() => setName(field.name), [field.name]);
23224
- return /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 10, fontSize: 13, color: "#222" }, children: [
23225
- /* @__PURE__ */ jsx(Row, { label: "Type", children: /* @__PURE__ */ jsx(
23226
- "select",
23227
- {
23228
- value: field.type,
23229
- onChange: (e) => onChange({ type: e.target.value }),
23230
- style: inputStyle$1,
23231
- children: FORM_FIELD_TYPES.map((type) => /* @__PURE__ */ jsx("option", { value: type, children: FORM_FIELD_TYPE_LABELS[type] }, type))
23232
- }
23233
- ) }),
23234
- /* @__PURE__ */ jsx(Row, { label: "Etikett", children: /* @__PURE__ */ jsx(
23235
- "input",
23236
- {
23237
- type: "text",
23238
- value: label,
23239
- onChange: (e) => setLabel(e.target.value),
23240
- onBlur: () => label !== field.label && onChange({ label }),
23241
- style: inputStyle$1
23242
- }
23243
- ) }),
23244
- fieldTypeHasPlaceholder(field.type) ? /* @__PURE__ */ jsx(Row, { label: "Plassholder", children: /* @__PURE__ */ jsx(
23245
- "input",
23246
- {
23247
- type: "text",
23248
- value: placeholder,
23249
- onChange: (e) => setPlaceholder(e.target.value),
23250
- onBlur: () => placeholder !== field.placeholder && onChange({ placeholder }),
23251
- style: inputStyle$1
23252
- }
23253
- ) }) : null,
23254
- fieldTypeHasOptions(field.type) ? /* @__PURE__ */ jsx(Row, { label: "Alternativer", hint: "Ett per linje", children: /* @__PURE__ */ jsx(
23255
- "textarea",
23256
- {
23257
- value: options,
23258
- rows: 4,
23259
- onChange: (e) => setOptions(e.target.value),
23260
- onBlur: () => options !== field.options.join("\n") && onChange({ options }),
23261
- style: { ...inputStyle$1, resize: "vertical", fontFamily: "inherit" }
23262
- }
23263
- ) }) : null,
23264
- /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }, children: [
23265
- /* @__PURE__ */ jsx(
23266
- "input",
23267
- {
23268
- type: "checkbox",
23269
- checked: field.required,
23270
- onChange: (e) => onChange({ required: e.target.checked })
23271
- }
23272
- ),
23273
- "Må fylles ut"
23274
- ] }),
23275
- /* @__PURE__ */ jsx(Row, { label: "Teknisk navn", hint: "Brukes i e-posten og i historikken", children: /* @__PURE__ */ jsx(
23276
- "input",
23277
- {
23278
- type: "text",
23279
- value: name,
23280
- onChange: (e) => setName(e.target.value),
23281
- onBlur: () => {
23282
- const next = name.trim();
23283
- if (next && next !== field.name) onChange({ name: next });
23284
- else setName(field.name);
23285
- },
23286
- style: { ...inputStyle$1, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" }
23287
- }
23288
- ) })
23289
- ] });
23290
- }
23291
- function Row({ label, hint, children }) {
23292
- return /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 4 }, children: [
23293
- /* @__PURE__ */ jsxs("span", { style: { fontSize: 12, color: "#666" }, children: [
23294
- label,
23295
- hint ? /* @__PURE__ */ jsxs("span", { style: { color: "#999" }, children: [
23296
- " · ",
23297
- hint
23298
- ] }) : null
23299
- ] }),
23300
- children
23301
- ] });
23302
- }
23303
- function ToolButton({ label, onClick, disabled, active, danger, children, ref }) {
23304
- return /* @__PURE__ */ jsx(
23305
- "button",
23306
- {
23307
- ref,
23308
- type: "button",
23309
- "data-setto-ui": true,
23310
- "aria-label": label,
23311
- title: label,
23312
- disabled,
23313
- onClick,
23314
- style: {
23315
- ...toolBtnStyle,
23316
- ...active ? { background: "#640AFF", color: "#fff", borderColor: "#640AFF" } : {},
23317
- ...danger ? { fontSize: 16 } : {},
23318
- ...disabled ? { opacity: 0.35, cursor: "default" } : {}
23319
- },
23320
- children
23321
- }
23322
- );
23323
- }
23324
- function ArrowIcon({ direction }) {
23325
- return /* @__PURE__ */ jsx(
23326
- "svg",
23327
- {
23328
- width: "12",
23329
- height: "12",
23330
- viewBox: "0 0 24 24",
23331
- fill: "none",
23332
- stroke: "currentColor",
23333
- strokeWidth: "2.5",
23334
- strokeLinecap: "round",
23335
- strokeLinejoin: "round",
23336
- "aria-hidden": "true",
23337
- style: direction === "down" ? { transform: "rotate(180deg)" } : void 0,
23338
- children: /* @__PURE__ */ jsx("path", { d: "M12 19V5M5 12l7-7 7 7" })
23339
- }
23340
- );
23341
- }
23342
- function GearIcon() {
23343
- return /* @__PURE__ */ jsxs(
23344
- "svg",
23345
- {
23346
- width: "13",
23347
- height: "13",
23348
- viewBox: "0 0 24 24",
23349
- fill: "none",
23350
- stroke: "currentColor",
23351
- strokeWidth: "2",
23352
- strokeLinecap: "round",
23353
- strokeLinejoin: "round",
23354
- "aria-hidden": "true",
23355
- children: [
23356
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
23357
- /* @__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" })
23358
- ]
23359
- }
23360
- );
23361
- }
23362
- const toolsStyle = {
23363
- position: "absolute",
23364
- top: -12,
23365
- right: 0,
23366
- zIndex: 2,
23367
- display: "flex",
23368
- gap: 4,
23369
- padding: 3,
23370
- background: "#ffffff",
23371
- border: "1px solid #e0e0e0",
23372
- borderRadius: 999,
23373
- boxShadow: "0 1px 4px rgba(0,0,0,0.18)",
23374
- fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
23375
- };
23376
- const toolBtnStyle = {
23377
- display: "flex",
23378
- alignItems: "center",
23379
- justifyContent: "center",
23380
- width: 24,
23381
- height: 24,
23382
- padding: 0,
23383
- background: "transparent",
23384
- border: "1px solid transparent",
23385
- borderRadius: "50%",
23386
- fontSize: 13,
23387
- lineHeight: 1,
23388
- color: "#444",
23389
- cursor: "pointer"
23390
- };
23391
- const inputStyle$1 = {
23392
- width: "100%",
23393
- boxSizing: "border-box",
23394
- padding: "6px 8px",
23395
- border: "1px solid #d8d8d8",
23396
- borderRadius: 6,
23397
- fontSize: 13,
23398
- color: "#222",
23399
- background: "#fff",
23400
- fontFamily: "inherit"
23401
- };
23402
23180
  const HONEYPOT_NAME = "website";
23403
23181
  const warnedEndpointKeys = /* @__PURE__ */ new Set();
23404
23182
  function cx(...parts) {
@@ -23414,6 +23192,7 @@ function SettoForm({
23414
23192
  }) {
23415
23193
  const { t, i18n } = useTranslation();
23416
23194
  const { config, editMode, store } = useSetto();
23195
+ const FieldTools = useSettoChromeSlot("form-field-tools");
23417
23196
  const guestEdit = useGuestEdit();
23418
23197
  const editable = editMode || guestEdit.active;
23419
23198
  const lng = i18n.language;
@@ -23661,8 +23440,8 @@ function SettoForm({
23661
23440
  }
23662
23441
  ),
23663
23442
  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,
23664
- editable && store ? /* @__PURE__ */ jsx(
23665
- FormFieldTools,
23443
+ editable && store && FieldTools ? /* @__PURE__ */ jsx(
23444
+ FieldTools,
23666
23445
  {
23667
23446
  field,
23668
23447
  index,
@@ -25063,222 +24842,21 @@ function collectRelativeKeys(nodes, prefix, out) {
25063
24842
  }
25064
24843
  }
25065
24844
  }
25066
- const IN_PROGRESS_STATUSES = ["pending", "building"];
25067
- function isDeploymentInProgress(status) {
25068
- return IN_PROGRESS_STATUSES.includes(status);
25069
- }
25070
- const STALE_PENDING_MS = 20 * 60 * 1e3;
25071
- const STALE_BUILDING_MS = 45 * 60 * 1e3;
25072
- function isDeploymentStale(row) {
25073
- if (row.status !== "pending" && row.status !== "building") return false;
25074
- const now = Date.now();
25075
- if (row.status === "pending") {
25076
- return now - new Date(row.started_at).getTime() > STALE_PENDING_MS;
25077
- }
25078
- return now - new Date(row.updated_at).getTime() > STALE_BUILDING_MS;
25079
- }
25080
- async function fetchInProgressDeploymentId(supabase, siteId) {
25081
- 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);
25082
- const rows = data ?? [];
25083
- const fresh = rows.find((r) => !isDeploymentStale(r));
25084
- return fresh?.id ?? null;
25085
- }
25086
- async function waitForDeploymentByCommit(supabase, siteId, commitSha, timeoutMs = 6e4) {
25087
- const deadline = Date.now() + timeoutMs;
25088
- while (Date.now() < deadline) {
25089
- const { data } = await supabase.from("deployments").select("id").eq("site_id", siteId).eq("commit_sha", commitSha).order("started_at", { ascending: false }).limit(1).maybeSingle();
25090
- if (data?.id) return data.id;
25091
- await new Promise((r) => setTimeout(r, 2e3));
25092
- }
25093
- return null;
25094
- }
25095
- function useDeploymentStatus(supabase, deploymentId) {
25096
- const [row, setRow] = useState(null);
25097
- useEffect(() => {
25098
- if (!deploymentId) {
25099
- setRow(null);
25100
- return;
25101
- }
25102
- let channel = null;
25103
- let cancelled = false;
25104
- const load = async () => {
25105
- const { data } = await supabase.from("deployments").select("*").eq("id", deploymentId).maybeSingle();
25106
- if (!cancelled && data) setRow(data);
25107
- };
25108
- void load();
25109
- channel = supabase.channel(`setto-dep-${deploymentId}`).on(
25110
- "postgres_changes",
25111
- {
25112
- event: "UPDATE",
25113
- schema: "public",
25114
- table: "deployments",
25115
- filter: `id=eq.${deploymentId}`
25116
- },
25117
- (payload) => {
25118
- setRow(payload.new);
25119
- }
25120
- ).subscribe();
25121
- return () => {
25122
- cancelled = true;
25123
- if (channel) supabase.removeChannel(channel);
25124
- };
25125
- }, [supabase, deploymentId]);
25126
- return { row, status: row?.status ?? null };
25127
- }
25128
- function deploymentStepIndex(status) {
25129
- switch (status) {
25130
- case "pending":
25131
- return 0;
25132
- case "building":
25133
- return 1;
25134
- case "ready":
25135
- return 2;
25136
- default:
25137
- return -1;
25138
- }
25139
- }
25140
- function isDeploymentStepReached(status, i) {
25141
- return deploymentStepIndex(status) >= i;
25142
- }
25143
- const STEPS = ["Committed", "Building", "Deployed"];
25144
- const PROGRESS_STYLE_ID = "setto-publish-progress-styles";
25145
- function usePublishProgressStyles() {
25146
- useEffect(() => {
25147
- if (document.getElementById(PROGRESS_STYLE_ID)) return;
25148
- const el = document.createElement("style");
25149
- el.id = PROGRESS_STYLE_ID;
25150
- el.textContent = `
25151
- @keyframes setto-step-pulse {
25152
- 0%, 100% { opacity: 1; transform: scaleY(1); }
25153
- 50% { opacity: 0.72; transform: scaleY(0.82); }
25154
- }
25155
- @keyframes setto-step-shimmer {
25156
- 0% { background-position: 120% 0; }
25157
- 100% { background-position: -120% 0; }
25158
- }
25159
- .setto-step-bar-active {
25160
- animation: setto-step-pulse 1.5s ease-in-out infinite;
25161
- transform-origin: center bottom;
25162
- }
25163
- .setto-step-bar-shimmer {
25164
- background: linear-gradient(
25165
- 90deg,
25166
- #640aff 0%,
25167
- #8b5cf6 45%,
25168
- #640aff 90%
25169
- ) !important;
25170
- background-size: 220% 100% !important;
25171
- animation: setto-step-shimmer 2.2s ease-in-out infinite;
25172
- }
25173
- .setto-step-label-active {
25174
- animation: setto-step-pulse 1.5s ease-in-out infinite;
25175
- }
25176
- `;
25177
- document.head.appendChild(el);
25178
- }, []);
25179
- }
25180
- function PublishProgressSteps({
25181
- status,
25182
- compact,
25183
- buildingElapsed
25184
- }) {
25185
- usePublishProgressStyles();
25186
- const scopeId = useId().replace(/:/g, "");
25187
- const errored = status === "error" || status === "canceled";
25188
- const activeIndex = deploymentStepIndex(status);
25189
- return /* @__PURE__ */ jsx(
25190
- "div",
25191
- {
25192
- className: `setto-progress-${scopeId}`,
25193
- style: {
25194
- display: "flex",
25195
- gap: compact ? 3 : 8,
25196
- fontSize: compact ? 11 : 12,
25197
- minWidth: compact ? 100 : 200
25198
- },
25199
- children: STEPS.map((label, i) => {
25200
- const reached = isDeploymentStepReached(status, i);
25201
- const active = activeIndex === i && !errored;
25202
- const barClass = active ? "setto-step-bar-active setto-step-bar-shimmer" : void 0;
25203
- return /* @__PURE__ */ jsxs("div", { style: { flex: 1, textAlign: "center", minWidth: 0 }, children: [
25204
- /* @__PURE__ */ jsx(
25205
- "div",
25206
- {
25207
- className: barClass,
25208
- style: {
25209
- height: compact ? 3 : 4,
25210
- borderRadius: 2,
25211
- background: errored ? "#fca5a5" : reached ? "#640AFF" : "#e5e5e5",
25212
- marginBottom: compact ? 0 : 6,
25213
- transition: "background 0.35s ease"
25214
- }
25215
- }
25216
- ),
25217
- !compact ? /* @__PURE__ */ jsxs(
25218
- "span",
25219
- {
25220
- className: active ? "setto-step-label-active" : void 0,
25221
- style: {
25222
- color: active ? "#1a1a1a" : "#888",
25223
- fontWeight: active ? 500 : 400,
25224
- display: "inline-block"
25225
- },
25226
- children: [
25227
- label,
25228
- active && status === "building" && buildingElapsed != null ? ` (${buildingElapsed}s)` : active ? " …" : ""
25229
- ]
25230
- }
25231
- ) : null
25232
- ] }, label);
25233
- })
25234
- }
25235
- );
25236
- }
25237
- function PublishProgressBar({ status, compact, href }) {
25238
- const inner = /* @__PURE__ */ jsx(PublishProgressSteps, { status, compact });
25239
- const wrapStyle = {
25240
- fontSize: compact ? 11 : 12,
25241
- minWidth: compact ? 100 : 200
25242
- };
25243
- if (href) {
25244
- return /* @__PURE__ */ jsx(
25245
- "a",
25246
- {
25247
- href,
25248
- title: "Se publiseringsstatus",
25249
- style: {
25250
- ...wrapStyle,
25251
- display: "block",
25252
- textDecoration: "none",
25253
- color: "inherit",
25254
- cursor: "pointer",
25255
- borderRadius: 4,
25256
- padding: compact ? "4px 2px" : void 0
25257
- },
25258
- children: inner
25259
- }
25260
- );
25261
- }
25262
- return /* @__PURE__ */ jsx("div", { style: wrapStyle, children: inner });
25263
- }
25264
24845
  export {
25265
24846
  AuthGate,
25266
24847
  BLOCK_REGISTRY,
25267
24848
  BLOCK_TYPES,
25268
24849
  CHECKOUT_INTENT,
24850
+ DEFAULT_LANGUAGE,
25269
24851
  EMPTY_I18N_STORE_SNAPSHOT,
25270
24852
  FAB_INSET,
25271
24853
  FAB_SIZE,
25272
24854
  FORM_FIELD_TYPES,
25273
24855
  FORM_FIELD_TYPE_LABELS,
24856
+ FloatingPopover,
25274
24857
  GuestEditProvider,
25275
- IN_PROGRESS_STATUSES,
25276
- PublishProgressBar,
25277
- PublishProgressSteps,
25278
24858
  SETTO_BASE,
25279
24859
  SETTO_MODULES,
25280
- STALE_BUILDING_MS,
25281
- STALE_PENDING_MS,
25282
24860
  SettoAdminApp,
25283
24861
  SettoAnimation,
25284
24862
  SettoBlock,
@@ -25288,8 +24866,8 @@ export {
25288
24866
  SettoForm,
25289
24867
  SettoIcon,
25290
24868
  SettoImage,
24869
+ SettoLanguages,
25291
24870
  SettoMark,
25292
- SettoPreviewBar,
25293
24871
  SettoProvider,
25294
24872
  SettoRepeater,
25295
24873
  SettoSection,
@@ -25300,30 +24878,28 @@ export {
25300
24878
  checkoutIntentUrl,
25301
24879
  currentAdminView,
25302
24880
  extractContentKeys,
25303
- fetchInProgressDeploymentId,
24881
+ fieldTypeHasOptions,
24882
+ fieldTypeHasPlaceholder,
25304
24883
  formFieldsPrefix,
25305
24884
  isAdminDeploymentView,
25306
24885
  isAdminPanelView,
25307
24886
  isAdminRoute,
25308
- isDeploymentInProgress,
25309
- isDeploymentStale,
25310
24887
  isFormFieldType,
25311
24888
  isLinkContext,
25312
- isPreviewHost,
24889
+ languageName,
25313
24890
  nodeChildren,
25314
24891
  parseBlockTree,
25315
- previewBarState,
24892
+ pickInitialLanguage,
25316
24893
  readLocaleFields,
25317
24894
  settoSubmissionUrl,
25318
24895
  siteHomeUrl,
24896
+ siteLanguages,
25319
24897
  useCoarsePointer,
25320
- useDeploymentStatus,
25321
24898
  useGuestEdit,
25322
24899
  useSectionTheme,
25323
24900
  useSetto,
25324
24901
  useSettoChromeSlot,
25325
24902
  validateBlockTree,
25326
- waitForDeploymentByCommit,
25327
24903
  wantsCheckout,
25328
24904
  withoutCheckoutIntent
25329
24905
  };