@sevenfold/setto-client 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
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
+ /**
10
+ * Hand back a component and the host renders it.
11
+ *
12
+ * Inverted deliberately. The first cut had the chrome create its own React
13
+ * root, which cannot work: `createRoot` lives in `react-dom/client`, not
14
+ * `react-dom`, and React 19 removed `render` altogether — so the bundle
15
+ * loaded, found neither, and mounted nothing at all.
16
+ *
17
+ * Rendering it from here is better than passing `createRoot` across would
18
+ * have been. The chrome ends up inside the host's own tree, so its hooks are
19
+ * the host's hooks with no second root to keep in step, and the error
20
+ * boundary that keeps a mistake off the customer's page lives on this side
21
+ * where it cannot be forgotten.
22
+ */
23
+ mount: (Component: React.ComponentType) => void;
24
+ config: {
25
+ siteId: string;
26
+ apiUrl: string;
27
+ displayName?: string;
28
+ };
29
+ /** Read the current state. Live: call it, never cache what it returned. */
30
+ getState: () => {
31
+ hasSession: boolean;
32
+ accessToken: string | null;
33
+ currentSite: {
34
+ id: string;
35
+ display_name?: string;
36
+ status?: string;
37
+ } | null;
38
+ sitesLoaded: boolean;
39
+ editMode: boolean;
40
+ authLoading: boolean;
41
+ };
42
+ /** Told whenever that state changes. Returns an unsubscribe. */
43
+ subscribe: (listener: () => void) => () => void;
44
+ }
45
+ declare global {
46
+ interface Window {
47
+ __setto__?: SettoChromeHandshake;
48
+ __settoChromeLoaded__?: boolean;
49
+ }
50
+ }
51
+ export declare function SettoChrome(): import("react/jsx-runtime").JSX.Element | 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,88 @@ 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
+ class ChromeBoundary extends React.Component {
21368
+ state = { dead: false };
21369
+ static getDerivedStateFromError() {
21370
+ return { dead: true };
21371
+ }
21372
+ componentDidCatch(error) {
21373
+ console.warn("[setto] chrome failed, continuing without it", error);
21374
+ }
21375
+ render() {
21376
+ return this.state.dead ? null : this.props.children;
21377
+ }
21378
+ }
21379
+ function SettoChrome() {
21380
+ const setto = useSetto();
21381
+ const [Chrome, setChrome] = useState(null);
21382
+ const listeners = useRef(/* @__PURE__ */ new Set());
21383
+ const stateRef = useRef(() => ({
21384
+ hasSession: false,
21385
+ accessToken: null,
21386
+ currentSite: null,
21387
+ sitesLoaded: false,
21388
+ editMode: false,
21389
+ authLoading: true
21390
+ }));
21391
+ stateRef.current = () => ({
21392
+ hasSession: !!setto.session,
21393
+ accessToken: setto.session?.access_token ?? null,
21394
+ currentSite: setto.currentSite ? {
21395
+ id: setto.currentSite.id,
21396
+ display_name: setto.currentSite.display_name,
21397
+ status: setto.currentSite.status
21398
+ } : null,
21399
+ sitesLoaded: setto.sites !== null,
21400
+ editMode: setto.editMode,
21401
+ authLoading: setto.authLoading
21402
+ });
21403
+ useEffect(() => {
21404
+ for (const listener of listeners.current) {
21405
+ try {
21406
+ listener();
21407
+ } catch {
21408
+ }
21409
+ }
21410
+ });
21411
+ useEffect(() => {
21412
+ if (typeof window === "undefined" || typeof document === "undefined") return;
21413
+ if (window.__settoChromeLoaded__) return;
21414
+ try {
21415
+ window.__setto__ = {
21416
+ contract: CHROME_CONTRACT,
21417
+ react: React,
21418
+ reactDom: ReactDOM,
21419
+ config: {
21420
+ siteId: setto.config.siteId,
21421
+ apiUrl: setto.config.apiUrl,
21422
+ displayName: setto.config.displayName
21423
+ },
21424
+ getState: () => stateRef.current(),
21425
+ subscribe: (listener) => {
21426
+ listeners.current.add(listener);
21427
+ return () => listeners.current.delete(listener);
21428
+ },
21429
+ // `setChrome(() => C)`: a component is a function, and the updater form
21430
+ // is the only way to put one into state without React calling it.
21431
+ mount: (Component) => setChrome(() => Component)
21432
+ };
21433
+ const script = document.createElement("script");
21434
+ script.src = chromeUrl(setto.config.apiUrl);
21435
+ script.async = true;
21436
+ script.onerror = () => void 0;
21437
+ document.head.appendChild(script);
21438
+ window.__settoChromeLoaded__ = true;
21439
+ } catch {
21440
+ }
21441
+ }, []);
21442
+ if (!Chrome) return null;
21443
+ return /* @__PURE__ */ jsx(ChromeBoundary, { children: /* @__PURE__ */ jsx(Chrome, {}) });
21444
+ }
21361
21445
  const SETTO_BASE = "/setto";
21362
21446
  function siteHomeUrl(pathname = "/") {
21363
21447
  const u = new URL(window.location.href);
@@ -21392,202 +21476,6 @@ function withoutCheckoutIntent() {
21392
21476
  u.searchParams.delete(CHECKOUT_INTENT);
21393
21477
  return u.pathname + (u.search || "") + u.hash;
21394
21478
  }
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
21479
  const FAB_SIZE = 52;
21592
21480
  const FAB_INSET = 16;
21593
21481
  const STYLE_ID$3 = "setto-edit-layout";
@@ -22708,7 +22596,7 @@ function DomainView({ siteId }) {
22708
22596
  ),
22709
22597
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void attach(), disabled: busy || !host.trim(), style: primary, children: busy ? "Legger til …" : "Legg til domene" })
22710
22598
  ] }),
22711
- error && /* @__PURE__ */ jsx("p", { style: errorStyle$1, children: error })
22599
+ error && /* @__PURE__ */ jsx("p", { style: errorStyle$2, children: error })
22712
22600
  ] });
22713
22601
  }
22714
22602
  return /* @__PURE__ */ jsxs("div", { style: wrap, children: [
@@ -22728,7 +22616,7 @@ function DomainView({ siteId }) {
22728
22616
  /* @__PURE__ */ jsx("p", { style: { ...muted, margin: "0.35rem 0 0" }, children: rec.why })
22729
22617
  ] }, `${rec.type}-${rec.name}`)) })
22730
22618
  ] }),
22731
- error && /* @__PURE__ */ jsx("p", { style: errorStyle$1, children: error }),
22619
+ error && /* @__PURE__ */ jsx("p", { style: errorStyle$2, children: error }),
22732
22620
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "0.6rem", flexWrap: "wrap" }, children: [
22733
22621
  !state.verified && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void recheck(), disabled: busy, style: primary, children: busy ? "Sjekker …" : "Jeg har lagt inn radene" }),
22734
22622
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void detach(), disabled: busy, style: quiet, children: "Fjern domenet" })
@@ -22776,7 +22664,7 @@ const quiet = {
22776
22664
  font: "inherit",
22777
22665
  cursor: "pointer"
22778
22666
  };
22779
- const errorStyle$1 = { margin: 0, color: "#b42318" };
22667
+ const errorStyle$2 = { margin: 0, color: "#b42318" };
22780
22668
  const liveBadge = {
22781
22669
  fontSize: "0.75rem",
22782
22670
  padding: "0.15rem 0.5rem",
@@ -24930,7 +24818,7 @@ function EditToolbar() {
24930
24818
  }
24931
24819
  ),
24932
24820
  expanded ? /* @__PURE__ */ jsxs("div", { style: contentStyle, children: [
24933
- error ? /* @__PURE__ */ jsx("span", { style: errorStyle, children: error }) : null,
24821
+ error ? /* @__PURE__ */ jsx("span", { style: errorStyle$1, children: error }) : null,
24934
24822
  hasDrafts && !publishing && activeDeployment === null ? /* @__PURE__ */ jsx("button", { type: "button", onClick: publish, style: publishBtnStyle, children: "Publiser" }) : null,
24935
24823
  showPublishedNotice ? /* @__PURE__ */ jsxs(
24936
24824
  "button",
@@ -25051,7 +24939,7 @@ const contentStyle = {
25051
24939
  paddingLeft: 4,
25052
24940
  whiteSpace: "nowrap"
25053
24941
  };
25054
- const errorStyle = {
24942
+ const errorStyle$1 = {
25055
24943
  color: "#ffb4a8",
25056
24944
  fontSize: 12,
25057
24945
  maxWidth: 240,
@@ -25305,7 +25193,7 @@ function SettoProvider({ config, children }) {
25305
25193
  const editShellActive = editMode && !!value.store;
25306
25194
  return /* @__PURE__ */ jsx(SettoContext.Provider, { value, children: /* @__PURE__ */ jsxs(SectionEditProvider, { children: [
25307
25195
  /* @__PURE__ */ jsx(EditModeShell, { active: editShellActive, themeStore: value.themeStore, children }),
25308
- !onAdminRoute && /* @__PURE__ */ jsx(SettoPreviewBar, {})
25196
+ !onAdminRoute && /* @__PURE__ */ jsx(SettoChrome, {})
25309
25197
  ] }) });
25310
25198
  }
25311
25199
  function useSetto() {
@@ -25315,6 +25203,202 @@ function useSetto() {
25315
25203
  }
25316
25204
  return ctx;
25317
25205
  }
25206
+ const PREVIEW_HOST = /(^|\.)site\.setto\.no$/i;
25207
+ function isPreviewHost(hostname) {
25208
+ return PREVIEW_HOST.test(hostname);
25209
+ }
25210
+ function previewBarState(input2) {
25211
+ if (!input2.onPreview || input2.authLoading) return "hidden";
25212
+ if (input2.hasSession && !input2.sitesLoaded) return "hidden";
25213
+ if (!input2.hasSession && input2.soldPublic === null) return "hidden";
25214
+ const bought = input2.paidMarker || input2.currentSiteIsLive || input2.soldPublic === true;
25215
+ if (bought) return "hidden";
25216
+ return input2.hasSession && input2.hasCurrentSite ? "owner" : "visitor";
25217
+ }
25218
+ function SettoPreviewBar() {
25219
+ const { supabase, api, config, session, authLoading, currentSite, sites, editMode } = useSetto();
25220
+ const [onPreview, setOnPreview] = useState(false);
25221
+ const [busy, setBusy] = useState(false);
25222
+ const [error, setError] = useState(null);
25223
+ const [paid, setPaid] = useState(false);
25224
+ const [soldPublic, setSoldPublic] = useState(null);
25225
+ useEffect(() => {
25226
+ setOnPreview(isPreviewHost(window.location.hostname));
25227
+ setPaid(new URLSearchParams(window.location.search).get("betaling") === "ok");
25228
+ }, []);
25229
+ useEffect(() => {
25230
+ if (!onPreview || session) return;
25231
+ let cancelled = false;
25232
+ fetch(`${config.apiUrl}/onboarding/sites/${config.siteId}/sold`, {
25233
+ headers: { Accept: "application/json" }
25234
+ }).then((r) => r.ok ? r.json() : null).then((d) => {
25235
+ if (!cancelled && d) setSoldPublic(d.sold === true);
25236
+ }).catch(() => {
25237
+ });
25238
+ return () => {
25239
+ cancelled = true;
25240
+ };
25241
+ }, [onPreview, session, config.apiUrl, config.siteId]);
25242
+ const signIn = () => {
25243
+ window.location.href = checkoutIntentUrl();
25244
+ };
25245
+ const next = () => {
25246
+ if (window.parent !== window) {
25247
+ window.parent.postMessage({ type: "setto:next", siteId: config.siteId }, "*");
25248
+ return;
25249
+ }
25250
+ signIn();
25251
+ };
25252
+ const publish = async () => {
25253
+ setBusy(true);
25254
+ setError(null);
25255
+ try {
25256
+ const token = (await supabase.auth.getSession()).data.session?.access_token;
25257
+ if (!token) {
25258
+ signIn();
25259
+ return;
25260
+ }
25261
+ const data = await api.request(
25262
+ `/onboarding/sites/${config.siteId}/checkout`,
25263
+ {
25264
+ method: "POST",
25265
+ // Back to the page they were looking at, not to a chat window.
25266
+ body: { returnTo: window.location.origin + window.location.pathname }
25267
+ },
25268
+ "checkout"
25269
+ );
25270
+ if (!data.url) throw new Error("checkout_failed");
25271
+ window.location.href = data.url;
25272
+ } catch (err) {
25273
+ const detail = err instanceof Error ? err.message : "";
25274
+ setError(
25275
+ 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."
25276
+ );
25277
+ setBusy(false);
25278
+ }
25279
+ };
25280
+ const resumed = useRef(false);
25281
+ useEffect(() => {
25282
+ if (resumed.current || !onPreview || !session || !currentSite) return;
25283
+ if (!wantsCheckout()) return;
25284
+ if (paid || currentSite.status === "live") return;
25285
+ resumed.current = true;
25286
+ window.history.replaceState(null, "", withoutCheckoutIntent());
25287
+ void publish();
25288
+ }, [onPreview, session, currentSite, paid]);
25289
+ const state = previewBarState({
25290
+ onPreview,
25291
+ authLoading,
25292
+ hasSession: !!session,
25293
+ sitesLoaded: sites !== null,
25294
+ hasCurrentSite: !!currentSite,
25295
+ currentSiteIsLive: currentSite?.status === "live",
25296
+ soldPublic,
25297
+ paidMarker: paid
25298
+ });
25299
+ if (state === "hidden") return null;
25300
+ const owner = state === "owner";
25301
+ const name = currentSite?.display_name?.trim() || config.displayName?.trim() || config.siteId;
25302
+ return createPortal(
25303
+ /*
25304
+ * Lifted above the editor's floating button when edit mode is on, which
25305
+ * sits in the same corner band. Sideways would collide again the moment
25306
+ * its toolbar opens, since that expands rightward.
25307
+ */
25308
+ /* @__PURE__ */ jsx(
25309
+ "div",
25310
+ {
25311
+ style: editMode ? { ...barStyle, bottom: "calc(max(16px, env(safe-area-inset-bottom, 0px)) + 68px)" } : barStyle,
25312
+ "data-setto-preview-bar": "",
25313
+ children: owner ? /* @__PURE__ */ jsxs(Fragment, { children: [
25314
+ /* @__PURE__ */ jsx("span", { style: dotStyle, "aria-hidden": true }),
25315
+ /* @__PURE__ */ jsxs("span", { style: labelStyle, children: [
25316
+ "Du forhåndsviser nå ",
25317
+ /* @__PURE__ */ jsx("strong", { style: { fontWeight: 600 }, children: name })
25318
+ ] }),
25319
+ error && /* @__PURE__ */ jsx("span", { style: errorStyle, children: error }),
25320
+ /* @__PURE__ */ jsx("button", { type: "button", style: primaryStyle, onClick: publish, disabled: busy, children: busy ? "Åpner betaling …" : "Publiser" })
25321
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
25322
+ /* @__PURE__ */ jsx("span", { style: dotStyle, "aria-hidden": true }),
25323
+ /* @__PURE__ */ jsx("span", { style: labelStyle, children: "Forhåndsvisning" }),
25324
+ /* @__PURE__ */ jsx("button", { type: "button", style: primaryStyle, onClick: next, children: "Neste" })
25325
+ ] })
25326
+ }
25327
+ ),
25328
+ document.body
25329
+ );
25330
+ }
25331
+ const barStyle = {
25332
+ position: "fixed",
25333
+ left: "50%",
25334
+ bottom: "max(16px, env(safe-area-inset-bottom, 0px))",
25335
+ transform: "translateX(-50%)",
25336
+ zIndex: 2147483e3,
25337
+ display: "flex",
25338
+ alignItems: "center",
25339
+ gap: 14,
25340
+ maxWidth: "calc(100vw - 24px)",
25341
+ padding: "10px 10px 10px 18px",
25342
+ borderRadius: 999,
25343
+ background: "#211711",
25344
+ color: "#fffdf8",
25345
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
25346
+ fontSize: 14,
25347
+ lineHeight: 1.3,
25348
+ boxShadow: "0 18px 40px -18px rgba(0, 0, 0, 0.65)",
25349
+ /*
25350
+ * A frosted rim, so the pill reads as an object on any page.
25351
+ *
25352
+ * The bar sits on sites Setto did not choose the colours for: on a dark hero
25353
+ * a dark pill disappears, and on a pale section its shadow is all that holds
25354
+ * it. A translucent white border with `padding-box` clipping lets the page
25355
+ * show through the rim itself, which separates the pill from either ground
25356
+ * without picking a side.
25357
+ */
25358
+ border: "5px solid rgba(255, 255, 255, 0.22)",
25359
+ backgroundClip: "padding-box",
25360
+ backdropFilter: "blur(10px)",
25361
+ WebkitBackdropFilter: "blur(10px)"
25362
+ };
25363
+ const dotStyle = {
25364
+ width: 7,
25365
+ height: 7,
25366
+ borderRadius: "50%",
25367
+ background: "#c4502a",
25368
+ flex: "none"
25369
+ };
25370
+ const labelStyle = {
25371
+ whiteSpace: "nowrap",
25372
+ overflow: "hidden",
25373
+ textOverflow: "ellipsis"
25374
+ };
25375
+ const primaryStyle = {
25376
+ flex: "none",
25377
+ padding: "9px 18px",
25378
+ border: 0,
25379
+ borderRadius: 999,
25380
+ background: "#c4502a",
25381
+ color: "#fffdf8",
25382
+ font: "inherit",
25383
+ fontWeight: 600,
25384
+ cursor: "pointer"
25385
+ };
25386
+ const errorStyle = {
25387
+ color: "#ffb4a2",
25388
+ fontSize: 13,
25389
+ /*
25390
+ * Shrinkable, unlike the label beside it.
25391
+ *
25392
+ * `nowrap` on a flex child whose `min-width` is `auto` cannot shrink below
25393
+ * its own text, so on a 320px phone the error shouldered «Publiser» 19px
25394
+ * past the right edge of the screen and the bar clipped it — the button
25395
+ * that takes the money, unreachable, exactly when something had gone wrong.
25396
+ */
25397
+ minWidth: 0,
25398
+ overflow: "hidden",
25399
+ textOverflow: "ellipsis",
25400
+ whiteSpace: "nowrap"
25401
+ };
25318
25402
  function isTextEditClick(target) {
25319
25403
  if (target.closest("[data-setto-key]")) return true;
25320
25404
  if (target.closest("[data-setto-ui]")) return true;
@@ -27076,6 +27160,7 @@ export {
27076
27160
  SettoAnimation,
27077
27161
  SettoBlock,
27078
27162
  SettoBlockTree,
27163
+ SettoChrome,
27079
27164
  SettoForm,
27080
27165
  SettoIcon,
27081
27166
  SettoImage,
@@ -27092,6 +27177,7 @@ export {
27092
27177
  isPreviewHost,
27093
27178
  nodeChildren,
27094
27179
  parseBlockTree,
27180
+ previewBarState,
27095
27181
  readLocaleFields,
27096
27182
  settoSubmissionUrl,
27097
27183
  useGuestEdit,