@sevenfold/setto-client 0.9.0 → 0.10.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.
@@ -0,0 +1,36 @@
1
+ import * as React from 'react';
2
+ import * as ReactDOM from 'react-dom';
3
+ /** What the chrome is handed. Additive changes only; nothing may be removed. */
4
+ export interface SettoChromeHandshake {
5
+ contract: number;
6
+ /** The host's React, so the chrome bundle ships no framework of its own. */
7
+ react: typeof React;
8
+ reactDom: typeof ReactDOM;
9
+ config: {
10
+ siteId: string;
11
+ apiUrl: string;
12
+ displayName?: string;
13
+ };
14
+ /** Read the current state. Live: call it, never cache what it returned. */
15
+ getState: () => {
16
+ hasSession: boolean;
17
+ accessToken: string | null;
18
+ currentSite: {
19
+ id: string;
20
+ display_name?: string;
21
+ status?: string;
22
+ } | null;
23
+ sitesLoaded: boolean;
24
+ editMode: boolean;
25
+ authLoading: boolean;
26
+ };
27
+ /** Told whenever that state changes. Returns an unsubscribe. */
28
+ subscribe: (listener: () => void) => () => void;
29
+ }
30
+ declare global {
31
+ interface Window {
32
+ __setto__?: SettoChromeHandshake;
33
+ __settoChromeLoaded__?: boolean;
34
+ }
35
+ }
36
+ export declare function SettoChrome(): null;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { SettoProvider, useSetto } from './provider';
2
2
  export { GuestEditProvider, useGuestEdit } from './guest-edit';
3
- export { SettoPreviewBar, isPreviewHost } from './preview-bar';
3
+ export { SettoPreviewBar, isPreviewHost, previewBarState } from './preview-bar';
4
+ export { SettoChrome } from './chrome';
5
+ export type { SettoChromeHandshake } from './chrome';
4
6
  export { T } from './T';
5
7
  export { SettoSection } from './SettoSection';
6
8
  export { SettoBlock } from './SettoBlock';
@@ -1,6 +1,8 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import * as React from "react";
2
3
  import { useState, useRef, useEffect, useSyncExternalStore, useLayoutEffect, createContext, useContext, useCallback, useMemo, useId, useInsertionEffect, forwardRef, Fragment as Fragment$1 } from "react";
3
4
  import { useTranslation } from "react-i18next";
5
+ import * as ReactDOM from "react-dom";
4
6
  import { createPortal } from "react-dom";
5
7
  function __rest(s, e) {
6
8
  var t = {};
@@ -21358,6 +21360,71 @@ function useSiteAccess(supabase, session, siteId) {
21358
21360
  }, [supabase, session, siteId]);
21359
21361
  return state;
21360
21362
  }
21363
+ const CHROME_CONTRACT = 1;
21364
+ function chromeUrl(apiUrl) {
21365
+ return `${apiUrl.replace(/\/$/, "")}/chrome/v${CHROME_CONTRACT}.js`;
21366
+ }
21367
+ function SettoChrome() {
21368
+ const setto = useSetto();
21369
+ const listeners = useRef(/* @__PURE__ */ new Set());
21370
+ const stateRef = useRef(() => ({
21371
+ hasSession: false,
21372
+ accessToken: null,
21373
+ currentSite: null,
21374
+ sitesLoaded: false,
21375
+ editMode: false,
21376
+ authLoading: true
21377
+ }));
21378
+ stateRef.current = () => ({
21379
+ hasSession: !!setto.session,
21380
+ accessToken: setto.session?.access_token ?? null,
21381
+ currentSite: setto.currentSite ? {
21382
+ id: setto.currentSite.id,
21383
+ display_name: setto.currentSite.display_name,
21384
+ status: setto.currentSite.status
21385
+ } : null,
21386
+ sitesLoaded: setto.sites !== null,
21387
+ editMode: setto.editMode,
21388
+ authLoading: setto.authLoading
21389
+ });
21390
+ useEffect(() => {
21391
+ for (const listener of listeners.current) {
21392
+ try {
21393
+ listener();
21394
+ } catch {
21395
+ }
21396
+ }
21397
+ });
21398
+ useEffect(() => {
21399
+ if (typeof window === "undefined" || typeof document === "undefined") return;
21400
+ if (window.__settoChromeLoaded__) return;
21401
+ try {
21402
+ window.__setto__ = {
21403
+ contract: CHROME_CONTRACT,
21404
+ react: React,
21405
+ reactDom: ReactDOM,
21406
+ config: {
21407
+ siteId: setto.config.siteId,
21408
+ apiUrl: setto.config.apiUrl,
21409
+ displayName: setto.config.displayName
21410
+ },
21411
+ getState: () => stateRef.current(),
21412
+ subscribe: (listener) => {
21413
+ listeners.current.add(listener);
21414
+ return () => listeners.current.delete(listener);
21415
+ }
21416
+ };
21417
+ const script = document.createElement("script");
21418
+ script.src = chromeUrl(setto.config.apiUrl);
21419
+ script.async = true;
21420
+ script.onerror = () => void 0;
21421
+ document.head.appendChild(script);
21422
+ window.__settoChromeLoaded__ = true;
21423
+ } catch {
21424
+ }
21425
+ }, []);
21426
+ return null;
21427
+ }
21361
21428
  const SETTO_BASE = "/setto";
21362
21429
  function siteHomeUrl(pathname = "/") {
21363
21430
  const u = new URL(window.location.href);
@@ -21392,202 +21459,6 @@ function withoutCheckoutIntent() {
21392
21459
  u.searchParams.delete(CHECKOUT_INTENT);
21393
21460
  return u.pathname + (u.search || "") + u.hash;
21394
21461
  }
21395
- const PREVIEW_HOST = /(^|\.)site\.setto\.no$/i;
21396
- function isPreviewHost(hostname) {
21397
- return PREVIEW_HOST.test(hostname);
21398
- }
21399
- function previewBarState(input2) {
21400
- if (!input2.onPreview || input2.authLoading) return "hidden";
21401
- if (input2.hasSession && !input2.sitesLoaded) return "hidden";
21402
- if (!input2.hasSession && input2.soldPublic === null) return "hidden";
21403
- const bought = input2.paidMarker || input2.currentSiteIsLive || input2.soldPublic === true;
21404
- if (bought) return "hidden";
21405
- return input2.hasSession && input2.hasCurrentSite ? "owner" : "visitor";
21406
- }
21407
- function SettoPreviewBar() {
21408
- const { supabase, api, config, session, authLoading, currentSite, sites, editMode } = useSetto();
21409
- const [onPreview, setOnPreview] = useState(false);
21410
- const [busy, setBusy] = useState(false);
21411
- const [error, setError] = useState(null);
21412
- const [paid, setPaid] = useState(false);
21413
- const [soldPublic, setSoldPublic] = useState(null);
21414
- useEffect(() => {
21415
- setOnPreview(isPreviewHost(window.location.hostname));
21416
- setPaid(new URLSearchParams(window.location.search).get("betaling") === "ok");
21417
- }, []);
21418
- useEffect(() => {
21419
- if (!onPreview || session) return;
21420
- let cancelled = false;
21421
- fetch(`${config.apiUrl}/onboarding/sites/${config.siteId}/sold`, {
21422
- headers: { Accept: "application/json" }
21423
- }).then((r) => r.ok ? r.json() : null).then((d) => {
21424
- if (!cancelled && d) setSoldPublic(d.sold === true);
21425
- }).catch(() => {
21426
- });
21427
- return () => {
21428
- cancelled = true;
21429
- };
21430
- }, [onPreview, session, config.apiUrl, config.siteId]);
21431
- const signIn = () => {
21432
- window.location.href = checkoutIntentUrl();
21433
- };
21434
- const next = () => {
21435
- if (window.parent !== window) {
21436
- window.parent.postMessage({ type: "setto:next", siteId: config.siteId }, "*");
21437
- return;
21438
- }
21439
- signIn();
21440
- };
21441
- const publish = async () => {
21442
- setBusy(true);
21443
- setError(null);
21444
- try {
21445
- const token = (await supabase.auth.getSession()).data.session?.access_token;
21446
- if (!token) {
21447
- signIn();
21448
- return;
21449
- }
21450
- const data = await api.request(
21451
- `/onboarding/sites/${config.siteId}/checkout`,
21452
- {
21453
- method: "POST",
21454
- // Back to the page they were looking at, not to a chat window.
21455
- body: { returnTo: window.location.origin + window.location.pathname }
21456
- },
21457
- "checkout"
21458
- );
21459
- if (!data.url) throw new Error("checkout_failed");
21460
- window.location.href = data.url;
21461
- } catch (err) {
21462
- const detail = err instanceof Error ? err.message : "";
21463
- setError(
21464
- 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."
21465
- );
21466
- setBusy(false);
21467
- }
21468
- };
21469
- const resumed = useRef(false);
21470
- useEffect(() => {
21471
- if (resumed.current || !onPreview || !session || !currentSite) return;
21472
- if (!wantsCheckout()) return;
21473
- if (paid || currentSite.status === "live") return;
21474
- resumed.current = true;
21475
- window.history.replaceState(null, "", withoutCheckoutIntent());
21476
- void publish();
21477
- }, [onPreview, session, currentSite, paid]);
21478
- const state = previewBarState({
21479
- onPreview,
21480
- authLoading,
21481
- hasSession: !!session,
21482
- sitesLoaded: sites !== null,
21483
- hasCurrentSite: !!currentSite,
21484
- currentSiteIsLive: currentSite?.status === "live",
21485
- soldPublic,
21486
- paidMarker: paid
21487
- });
21488
- if (state === "hidden") return null;
21489
- const owner = state === "owner";
21490
- const name = currentSite?.display_name?.trim() || config.displayName?.trim() || config.siteId;
21491
- return createPortal(
21492
- /*
21493
- * Lifted above the editor's floating button when edit mode is on, which
21494
- * sits in the same corner band. Sideways would collide again the moment
21495
- * its toolbar opens, since that expands rightward.
21496
- */
21497
- /* @__PURE__ */ jsx(
21498
- "div",
21499
- {
21500
- style: editMode ? { ...barStyle, bottom: "calc(max(16px, env(safe-area-inset-bottom, 0px)) + 68px)" } : barStyle,
21501
- "data-setto-preview-bar": "",
21502
- children: owner ? /* @__PURE__ */ jsxs(Fragment, { children: [
21503
- /* @__PURE__ */ jsx("span", { style: dotStyle, "aria-hidden": true }),
21504
- /* @__PURE__ */ jsxs("span", { style: labelStyle, children: [
21505
- "Du forhåndsviser nå ",
21506
- /* @__PURE__ */ jsx("strong", { style: { fontWeight: 600 }, children: name })
21507
- ] }),
21508
- error && /* @__PURE__ */ jsx("span", { style: errorStyle$2, children: error }),
21509
- /* @__PURE__ */ jsx("button", { type: "button", style: primaryStyle, onClick: publish, disabled: busy, children: busy ? "Åpner betaling …" : "Publiser" })
21510
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
21511
- /* @__PURE__ */ jsx("span", { style: dotStyle, "aria-hidden": true }),
21512
- /* @__PURE__ */ jsx("span", { style: labelStyle, children: "Forhåndsvisning" }),
21513
- /* @__PURE__ */ jsx("button", { type: "button", style: primaryStyle, onClick: next, children: "Neste" })
21514
- ] })
21515
- }
21516
- ),
21517
- document.body
21518
- );
21519
- }
21520
- const barStyle = {
21521
- position: "fixed",
21522
- left: "50%",
21523
- bottom: "max(16px, env(safe-area-inset-bottom, 0px))",
21524
- transform: "translateX(-50%)",
21525
- zIndex: 2147483e3,
21526
- display: "flex",
21527
- alignItems: "center",
21528
- gap: 14,
21529
- maxWidth: "calc(100vw - 24px)",
21530
- padding: "10px 10px 10px 18px",
21531
- borderRadius: 999,
21532
- background: "#211711",
21533
- color: "#fffdf8",
21534
- fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
21535
- fontSize: 14,
21536
- lineHeight: 1.3,
21537
- boxShadow: "0 18px 40px -18px rgba(0, 0, 0, 0.65)",
21538
- /*
21539
- * A frosted rim, so the pill reads as an object on any page.
21540
- *
21541
- * The bar sits on sites Setto did not choose the colours for: on a dark hero
21542
- * a dark pill disappears, and on a pale section its shadow is all that holds
21543
- * it. A translucent white border with `padding-box` clipping lets the page
21544
- * show through the rim itself, which separates the pill from either ground
21545
- * without picking a side.
21546
- */
21547
- border: "5px solid rgba(255, 255, 255, 0.22)",
21548
- backgroundClip: "padding-box",
21549
- backdropFilter: "blur(10px)",
21550
- WebkitBackdropFilter: "blur(10px)"
21551
- };
21552
- const dotStyle = {
21553
- width: 7,
21554
- height: 7,
21555
- borderRadius: "50%",
21556
- background: "#c4502a",
21557
- flex: "none"
21558
- };
21559
- const labelStyle = {
21560
- whiteSpace: "nowrap",
21561
- overflow: "hidden",
21562
- textOverflow: "ellipsis"
21563
- };
21564
- const primaryStyle = {
21565
- flex: "none",
21566
- padding: "9px 18px",
21567
- border: 0,
21568
- borderRadius: 999,
21569
- background: "#c4502a",
21570
- color: "#fffdf8",
21571
- font: "inherit",
21572
- fontWeight: 600,
21573
- cursor: "pointer"
21574
- };
21575
- const errorStyle$2 = {
21576
- color: "#ffb4a2",
21577
- fontSize: 13,
21578
- /*
21579
- * Shrinkable, unlike the label beside it.
21580
- *
21581
- * `nowrap` on a flex child whose `min-width` is `auto` cannot shrink below
21582
- * its own text, so on a 320px phone the error shouldered «Publiser» 19px
21583
- * past the right edge of the screen and the bar clipped it — the button
21584
- * that takes the money, unreachable, exactly when something had gone wrong.
21585
- */
21586
- minWidth: 0,
21587
- overflow: "hidden",
21588
- textOverflow: "ellipsis",
21589
- whiteSpace: "nowrap"
21590
- };
21591
21462
  const FAB_SIZE = 52;
21592
21463
  const FAB_INSET = 16;
21593
21464
  const STYLE_ID$3 = "setto-edit-layout";
@@ -22708,7 +22579,7 @@ function DomainView({ siteId }) {
22708
22579
  ),
22709
22580
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void attach(), disabled: busy || !host.trim(), style: primary, children: busy ? "Legger til …" : "Legg til domene" })
22710
22581
  ] }),
22711
- error && /* @__PURE__ */ jsx("p", { style: errorStyle$1, children: error })
22582
+ error && /* @__PURE__ */ jsx("p", { style: errorStyle$2, children: error })
22712
22583
  ] });
22713
22584
  }
22714
22585
  return /* @__PURE__ */ jsxs("div", { style: wrap, children: [
@@ -22728,7 +22599,7 @@ function DomainView({ siteId }) {
22728
22599
  /* @__PURE__ */ jsx("p", { style: { ...muted, margin: "0.35rem 0 0" }, children: rec.why })
22729
22600
  ] }, `${rec.type}-${rec.name}`)) })
22730
22601
  ] }),
22731
- error && /* @__PURE__ */ jsx("p", { style: errorStyle$1, children: error }),
22602
+ error && /* @__PURE__ */ jsx("p", { style: errorStyle$2, children: error }),
22732
22603
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "0.6rem", flexWrap: "wrap" }, children: [
22733
22604
  !state.verified && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void recheck(), disabled: busy, style: primary, children: busy ? "Sjekker …" : "Jeg har lagt inn radene" }),
22734
22605
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void detach(), disabled: busy, style: quiet, children: "Fjern domenet" })
@@ -22776,7 +22647,7 @@ const quiet = {
22776
22647
  font: "inherit",
22777
22648
  cursor: "pointer"
22778
22649
  };
22779
- const errorStyle$1 = { margin: 0, color: "#b42318" };
22650
+ const errorStyle$2 = { margin: 0, color: "#b42318" };
22780
22651
  const liveBadge = {
22781
22652
  fontSize: "0.75rem",
22782
22653
  padding: "0.15rem 0.5rem",
@@ -24930,7 +24801,7 @@ function EditToolbar() {
24930
24801
  }
24931
24802
  ),
24932
24803
  expanded ? /* @__PURE__ */ jsxs("div", { style: contentStyle, children: [
24933
- error ? /* @__PURE__ */ jsx("span", { style: errorStyle, children: error }) : null,
24804
+ error ? /* @__PURE__ */ jsx("span", { style: errorStyle$1, children: error }) : null,
24934
24805
  hasDrafts && !publishing && activeDeployment === null ? /* @__PURE__ */ jsx("button", { type: "button", onClick: publish, style: publishBtnStyle, children: "Publiser" }) : null,
24935
24806
  showPublishedNotice ? /* @__PURE__ */ jsxs(
24936
24807
  "button",
@@ -25051,7 +24922,7 @@ const contentStyle = {
25051
24922
  paddingLeft: 4,
25052
24923
  whiteSpace: "nowrap"
25053
24924
  };
25054
- const errorStyle = {
24925
+ const errorStyle$1 = {
25055
24926
  color: "#ffb4a8",
25056
24927
  fontSize: 12,
25057
24928
  maxWidth: 240,
@@ -25305,7 +25176,7 @@ function SettoProvider({ config, children }) {
25305
25176
  const editShellActive = editMode && !!value.store;
25306
25177
  return /* @__PURE__ */ jsx(SettoContext.Provider, { value, children: /* @__PURE__ */ jsxs(SectionEditProvider, { children: [
25307
25178
  /* @__PURE__ */ jsx(EditModeShell, { active: editShellActive, themeStore: value.themeStore, children }),
25308
- !onAdminRoute && /* @__PURE__ */ jsx(SettoPreviewBar, {})
25179
+ !onAdminRoute && /* @__PURE__ */ jsx(SettoChrome, {})
25309
25180
  ] }) });
25310
25181
  }
25311
25182
  function useSetto() {
@@ -25315,6 +25186,202 @@ function useSetto() {
25315
25186
  }
25316
25187
  return ctx;
25317
25188
  }
25189
+ const PREVIEW_HOST = /(^|\.)site\.setto\.no$/i;
25190
+ function isPreviewHost(hostname) {
25191
+ return PREVIEW_HOST.test(hostname);
25192
+ }
25193
+ function previewBarState(input2) {
25194
+ if (!input2.onPreview || input2.authLoading) return "hidden";
25195
+ if (input2.hasSession && !input2.sitesLoaded) return "hidden";
25196
+ if (!input2.hasSession && input2.soldPublic === null) return "hidden";
25197
+ const bought = input2.paidMarker || input2.currentSiteIsLive || input2.soldPublic === true;
25198
+ if (bought) return "hidden";
25199
+ return input2.hasSession && input2.hasCurrentSite ? "owner" : "visitor";
25200
+ }
25201
+ function SettoPreviewBar() {
25202
+ const { supabase, api, config, session, authLoading, currentSite, sites, editMode } = useSetto();
25203
+ const [onPreview, setOnPreview] = useState(false);
25204
+ const [busy, setBusy] = useState(false);
25205
+ const [error, setError] = useState(null);
25206
+ const [paid, setPaid] = useState(false);
25207
+ const [soldPublic, setSoldPublic] = useState(null);
25208
+ useEffect(() => {
25209
+ setOnPreview(isPreviewHost(window.location.hostname));
25210
+ setPaid(new URLSearchParams(window.location.search).get("betaling") === "ok");
25211
+ }, []);
25212
+ useEffect(() => {
25213
+ if (!onPreview || session) return;
25214
+ let cancelled = false;
25215
+ fetch(`${config.apiUrl}/onboarding/sites/${config.siteId}/sold`, {
25216
+ headers: { Accept: "application/json" }
25217
+ }).then((r) => r.ok ? r.json() : null).then((d) => {
25218
+ if (!cancelled && d) setSoldPublic(d.sold === true);
25219
+ }).catch(() => {
25220
+ });
25221
+ return () => {
25222
+ cancelled = true;
25223
+ };
25224
+ }, [onPreview, session, config.apiUrl, config.siteId]);
25225
+ const signIn = () => {
25226
+ window.location.href = checkoutIntentUrl();
25227
+ };
25228
+ const next = () => {
25229
+ if (window.parent !== window) {
25230
+ window.parent.postMessage({ type: "setto:next", siteId: config.siteId }, "*");
25231
+ return;
25232
+ }
25233
+ signIn();
25234
+ };
25235
+ const publish = async () => {
25236
+ setBusy(true);
25237
+ setError(null);
25238
+ try {
25239
+ const token = (await supabase.auth.getSession()).data.session?.access_token;
25240
+ if (!token) {
25241
+ signIn();
25242
+ return;
25243
+ }
25244
+ const data = await api.request(
25245
+ `/onboarding/sites/${config.siteId}/checkout`,
25246
+ {
25247
+ method: "POST",
25248
+ // Back to the page they were looking at, not to a chat window.
25249
+ body: { returnTo: window.location.origin + window.location.pathname }
25250
+ },
25251
+ "checkout"
25252
+ );
25253
+ if (!data.url) throw new Error("checkout_failed");
25254
+ window.location.href = data.url;
25255
+ } catch (err) {
25256
+ const detail = err instanceof Error ? err.message : "";
25257
+ setError(
25258
+ 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."
25259
+ );
25260
+ setBusy(false);
25261
+ }
25262
+ };
25263
+ const resumed = useRef(false);
25264
+ useEffect(() => {
25265
+ if (resumed.current || !onPreview || !session || !currentSite) return;
25266
+ if (!wantsCheckout()) return;
25267
+ if (paid || currentSite.status === "live") return;
25268
+ resumed.current = true;
25269
+ window.history.replaceState(null, "", withoutCheckoutIntent());
25270
+ void publish();
25271
+ }, [onPreview, session, currentSite, paid]);
25272
+ const state = previewBarState({
25273
+ onPreview,
25274
+ authLoading,
25275
+ hasSession: !!session,
25276
+ sitesLoaded: sites !== null,
25277
+ hasCurrentSite: !!currentSite,
25278
+ currentSiteIsLive: currentSite?.status === "live",
25279
+ soldPublic,
25280
+ paidMarker: paid
25281
+ });
25282
+ if (state === "hidden") return null;
25283
+ const owner = state === "owner";
25284
+ const name = currentSite?.display_name?.trim() || config.displayName?.trim() || config.siteId;
25285
+ return createPortal(
25286
+ /*
25287
+ * Lifted above the editor's floating button when edit mode is on, which
25288
+ * sits in the same corner band. Sideways would collide again the moment
25289
+ * its toolbar opens, since that expands rightward.
25290
+ */
25291
+ /* @__PURE__ */ jsx(
25292
+ "div",
25293
+ {
25294
+ style: editMode ? { ...barStyle, bottom: "calc(max(16px, env(safe-area-inset-bottom, 0px)) + 68px)" } : barStyle,
25295
+ "data-setto-preview-bar": "",
25296
+ children: owner ? /* @__PURE__ */ jsxs(Fragment, { children: [
25297
+ /* @__PURE__ */ jsx("span", { style: dotStyle, "aria-hidden": true }),
25298
+ /* @__PURE__ */ jsxs("span", { style: labelStyle, children: [
25299
+ "Du forhåndsviser nå ",
25300
+ /* @__PURE__ */ jsx("strong", { style: { fontWeight: 600 }, children: name })
25301
+ ] }),
25302
+ error && /* @__PURE__ */ jsx("span", { style: errorStyle, children: error }),
25303
+ /* @__PURE__ */ jsx("button", { type: "button", style: primaryStyle, onClick: publish, disabled: busy, children: busy ? "Åpner betaling …" : "Publiser" })
25304
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
25305
+ /* @__PURE__ */ jsx("span", { style: dotStyle, "aria-hidden": true }),
25306
+ /* @__PURE__ */ jsx("span", { style: labelStyle, children: "Forhåndsvisning" }),
25307
+ /* @__PURE__ */ jsx("button", { type: "button", style: primaryStyle, onClick: next, children: "Neste" })
25308
+ ] })
25309
+ }
25310
+ ),
25311
+ document.body
25312
+ );
25313
+ }
25314
+ const barStyle = {
25315
+ position: "fixed",
25316
+ left: "50%",
25317
+ bottom: "max(16px, env(safe-area-inset-bottom, 0px))",
25318
+ transform: "translateX(-50%)",
25319
+ zIndex: 2147483e3,
25320
+ display: "flex",
25321
+ alignItems: "center",
25322
+ gap: 14,
25323
+ maxWidth: "calc(100vw - 24px)",
25324
+ padding: "10px 10px 10px 18px",
25325
+ borderRadius: 999,
25326
+ background: "#211711",
25327
+ color: "#fffdf8",
25328
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
25329
+ fontSize: 14,
25330
+ lineHeight: 1.3,
25331
+ boxShadow: "0 18px 40px -18px rgba(0, 0, 0, 0.65)",
25332
+ /*
25333
+ * A frosted rim, so the pill reads as an object on any page.
25334
+ *
25335
+ * The bar sits on sites Setto did not choose the colours for: on a dark hero
25336
+ * a dark pill disappears, and on a pale section its shadow is all that holds
25337
+ * it. A translucent white border with `padding-box` clipping lets the page
25338
+ * show through the rim itself, which separates the pill from either ground
25339
+ * without picking a side.
25340
+ */
25341
+ border: "5px solid rgba(255, 255, 255, 0.22)",
25342
+ backgroundClip: "padding-box",
25343
+ backdropFilter: "blur(10px)",
25344
+ WebkitBackdropFilter: "blur(10px)"
25345
+ };
25346
+ const dotStyle = {
25347
+ width: 7,
25348
+ height: 7,
25349
+ borderRadius: "50%",
25350
+ background: "#c4502a",
25351
+ flex: "none"
25352
+ };
25353
+ const labelStyle = {
25354
+ whiteSpace: "nowrap",
25355
+ overflow: "hidden",
25356
+ textOverflow: "ellipsis"
25357
+ };
25358
+ const primaryStyle = {
25359
+ flex: "none",
25360
+ padding: "9px 18px",
25361
+ border: 0,
25362
+ borderRadius: 999,
25363
+ background: "#c4502a",
25364
+ color: "#fffdf8",
25365
+ font: "inherit",
25366
+ fontWeight: 600,
25367
+ cursor: "pointer"
25368
+ };
25369
+ const errorStyle = {
25370
+ color: "#ffb4a2",
25371
+ fontSize: 13,
25372
+ /*
25373
+ * Shrinkable, unlike the label beside it.
25374
+ *
25375
+ * `nowrap` on a flex child whose `min-width` is `auto` cannot shrink below
25376
+ * its own text, so on a 320px phone the error shouldered «Publiser» 19px
25377
+ * past the right edge of the screen and the bar clipped it — the button
25378
+ * that takes the money, unreachable, exactly when something had gone wrong.
25379
+ */
25380
+ minWidth: 0,
25381
+ overflow: "hidden",
25382
+ textOverflow: "ellipsis",
25383
+ whiteSpace: "nowrap"
25384
+ };
25318
25385
  function isTextEditClick(target) {
25319
25386
  if (target.closest("[data-setto-key]")) return true;
25320
25387
  if (target.closest("[data-setto-ui]")) return true;
@@ -27076,6 +27143,7 @@ export {
27076
27143
  SettoAnimation,
27077
27144
  SettoBlock,
27078
27145
  SettoBlockTree,
27146
+ SettoChrome,
27079
27147
  SettoForm,
27080
27148
  SettoIcon,
27081
27149
  SettoImage,
@@ -27092,6 +27160,7 @@ export {
27092
27160
  isPreviewHost,
27093
27161
  nodeChildren,
27094
27162
  parseBlockTree,
27163
+ previewBarState,
27095
27164
  readLocaleFields,
27096
27165
  settoSubmissionUrl,
27097
27166
  useGuestEdit,