@sevenfold/setto-client 0.1.0 → 0.2.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,5 +1,5 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
- import { useEffect, useState, useRef, useLayoutEffect, createContext, useContext, useCallback, useMemo, useSyncExternalStore, forwardRef } from "react";
2
+ import { useEffect, useState, useRef, useLayoutEffect, createContext, useContext, useCallback, useMemo, useId, useSyncExternalStore, forwardRef } from "react";
3
3
  import { useTranslation } from "react-i18next";
4
4
  import { createPortal } from "react-dom";
5
5
  function __rest(s, e) {
@@ -20760,6 +20760,29 @@ function createApi(args) {
20760
20760
  });
20761
20761
  if (!res.ok) throw await asError(res, "publish failed");
20762
20762
  return parseJson(res);
20763
+ },
20764
+ async expireStaleDeployments(siteId) {
20765
+ const token = await bearer(supabase);
20766
+ const res = await fetch(
20767
+ `${apiUrl}/sites/${encodeURIComponent(siteId)}/deployments/expire-stale`,
20768
+ {
20769
+ method: "POST",
20770
+ headers: { Authorization: `Bearer ${token}` }
20771
+ }
20772
+ );
20773
+ if (!res.ok) throw await asError(res, "expireStaleDeployments failed");
20774
+ return parseJson(res);
20775
+ },
20776
+ async cancelDeployment(siteId, deploymentId) {
20777
+ const token = await bearer(supabase);
20778
+ const res = await fetch(
20779
+ `${apiUrl}/sites/${encodeURIComponent(siteId)}/deployments/${encodeURIComponent(deploymentId)}/cancel`,
20780
+ {
20781
+ method: "POST",
20782
+ headers: { Authorization: `Bearer ${token}` }
20783
+ }
20784
+ );
20785
+ if (!res.ok) throw await asError(res, "cancelDeployment failed");
20763
20786
  }
20764
20787
  };
20765
20788
  }
@@ -21003,6 +21026,12 @@ function useSettoDocumentLayout(active) {
21003
21026
  html.setto-edit-mode [data-setto-section]:hover {
21004
21027
  box-shadow: inset 0 0 0 1px rgba(100, 10, 255, 0.25);
21005
21028
  }
21029
+
21030
+ @media (max-width: 480px) {
21031
+ html.setto-edit-mode [data-setto-draft-count] {
21032
+ display: none;
21033
+ }
21034
+ }
21006
21035
  `;
21007
21036
  document.head.appendChild(styleEl);
21008
21037
  }
@@ -21482,14 +21511,144 @@ function SectionToolbar() {
21482
21511
  }
21483
21512
  );
21484
21513
  }
21485
- const STEPS = [
21486
- { key: "pending", label: "Committed" },
21487
- { key: "building", label: "Building" },
21488
- { key: "ready", label: "Deployed" }
21489
- ];
21490
- function BuildStatus({ supabase, deploymentId, onDone }) {
21514
+ function getCookie(name) {
21515
+ const match = document.cookie.match(new RegExp(`(?:^|; )${escapeRegExp(name)}=([^;]*)`));
21516
+ return match ? decodeURIComponent(match[1]) : null;
21517
+ }
21518
+ function setCookie(name, value, maxAgeSeconds = 365 * 24 * 60 * 60) {
21519
+ document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAgeSeconds}; SameSite=Lax`;
21520
+ }
21521
+ function escapeRegExp(s) {
21522
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
21523
+ }
21524
+ function EditOverlay({ children, onClose }) {
21525
+ useEffect(() => {
21526
+ const prev = document.body.style.overflow;
21527
+ document.body.style.overflow = "hidden";
21528
+ return () => {
21529
+ document.body.style.overflow = prev;
21530
+ };
21531
+ }, []);
21532
+ return createPortal(
21533
+ /* @__PURE__ */ jsx(
21534
+ "div",
21535
+ {
21536
+ role: "presentation",
21537
+ onClick: onClose,
21538
+ style: {
21539
+ position: "fixed",
21540
+ inset: 0,
21541
+ zIndex: 2147483647,
21542
+ display: "flex",
21543
+ alignItems: "center",
21544
+ justifyContent: "center",
21545
+ padding: 16,
21546
+ background: "rgba(0, 0, 0, 0.35)",
21547
+ backdropFilter: "blur(6px)",
21548
+ WebkitBackdropFilter: "blur(6px)"
21549
+ },
21550
+ children: /* @__PURE__ */ jsx(
21551
+ "div",
21552
+ {
21553
+ role: "dialog",
21554
+ "aria-modal": "true",
21555
+ onClick: (e) => e.stopPropagation(),
21556
+ style: {
21557
+ background: "#fff",
21558
+ borderRadius: 12,
21559
+ padding: "24px 28px",
21560
+ maxWidth: 420,
21561
+ width: "100%",
21562
+ boxShadow: "0 16px 48px rgba(0, 0, 0, 0.18)",
21563
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
21564
+ color: "#1a1a1a"
21565
+ },
21566
+ children
21567
+ }
21568
+ )
21569
+ }
21570
+ ),
21571
+ document.body
21572
+ );
21573
+ }
21574
+ const COOKIE_NAME = "setto_edit_hint_dismissed";
21575
+ function EditOnboardingDialog() {
21576
+ const [open, setOpen] = useState(() => getCookie(COOKIE_NAME) !== "1");
21577
+ const [dontShowAgain, setDontShowAgain] = useState(false);
21578
+ if (!open) return null;
21579
+ const close = () => {
21580
+ if (dontShowAgain) setCookie(COOKIE_NAME, "1");
21581
+ setOpen(false);
21582
+ };
21583
+ return /* @__PURE__ */ jsxs(EditOverlay, { onClose: close, children: [
21584
+ /* @__PURE__ */ jsx("h2", { style: { margin: "0 0 12px", fontSize: 18, fontWeight: 600 }, children: "Velkommen til Setto" }),
21585
+ /* @__PURE__ */ jsx("p", { style: { margin: "0 0 20px", fontSize: 14, lineHeight: 1.5, color: "#444" }, children: "Klikk på tekstene for å redigere. Bruk CTRL+Klikk for å åpne lenker. Klikk på seksjoner for å endre farger mm." }),
21586
+ /* @__PURE__ */ jsxs(
21587
+ "label",
21588
+ {
21589
+ style: {
21590
+ display: "flex",
21591
+ alignItems: "center",
21592
+ gap: 8,
21593
+ marginBottom: 20,
21594
+ fontSize: 13,
21595
+ color: "#666",
21596
+ cursor: "pointer"
21597
+ },
21598
+ children: [
21599
+ /* @__PURE__ */ jsx(
21600
+ "input",
21601
+ {
21602
+ type: "checkbox",
21603
+ checked: dontShowAgain,
21604
+ onChange: (e) => setDontShowAgain(e.target.checked)
21605
+ }
21606
+ ),
21607
+ "Ikke vis denne igjen"
21608
+ ]
21609
+ }
21610
+ ),
21611
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: close, style: primaryBtnStyle$1, children: "Forstått" })
21612
+ ] });
21613
+ }
21614
+ const primaryBtnStyle$1 = {
21615
+ background: "#640AFF",
21616
+ color: "#fff",
21617
+ border: "none",
21618
+ borderRadius: 6,
21619
+ padding: "8px 20px",
21620
+ fontSize: 14,
21621
+ fontWeight: 500,
21622
+ cursor: "pointer",
21623
+ width: "100%"
21624
+ };
21625
+ const IN_PROGRESS_STATUSES = ["pending", "building"];
21626
+ function isDeploymentInProgress(status) {
21627
+ return IN_PROGRESS_STATUSES.includes(status);
21628
+ }
21629
+ const STALE_PENDING_MS = 20 * 60 * 1e3;
21630
+ const STALE_BUILDING_MS = 45 * 60 * 1e3;
21631
+ function isDeploymentStale(row) {
21632
+ if (row.status !== "pending" && row.status !== "building") return false;
21633
+ const now = Date.now();
21634
+ if (row.status === "pending") {
21635
+ return now - new Date(row.started_at).getTime() > STALE_PENDING_MS;
21636
+ }
21637
+ return now - new Date(row.updated_at).getTime() > STALE_BUILDING_MS;
21638
+ }
21639
+ async function fetchInProgressDeploymentId(supabase, siteId) {
21640
+ 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);
21641
+ const rows = data ?? [];
21642
+ const fresh = rows.find((r) => !isDeploymentStale(r));
21643
+ return fresh?.id ?? null;
21644
+ }
21645
+ function useDeploymentStatus(supabase, deploymentId) {
21491
21646
  const [row, setRow] = useState(null);
21492
21647
  useEffect(() => {
21648
+ if (!deploymentId) {
21649
+ setRow(null);
21650
+ return;
21651
+ }
21493
21652
  let channel = null;
21494
21653
  let cancelled = false;
21495
21654
  const load = async () => {
@@ -21506,59 +21665,17 @@ function BuildStatus({ supabase, deploymentId, onDone }) {
21506
21665
  filter: `id=eq.${deploymentId}`
21507
21666
  },
21508
21667
  (payload) => {
21509
- const next = payload.new;
21510
- setRow(next);
21511
- if (next.status === "ready" || next.status === "error" || next.status === "canceled") {
21512
- onDone?.(next.status);
21513
- }
21668
+ setRow(payload.new);
21514
21669
  }
21515
21670
  ).subscribe();
21516
21671
  return () => {
21517
21672
  cancelled = true;
21518
21673
  if (channel) supabase.removeChannel(channel);
21519
21674
  };
21520
- }, [supabase, deploymentId, onDone]);
21521
- const status = row?.status ?? "pending";
21522
- const errored = status === "error" || status === "canceled";
21523
- return /* @__PURE__ */ jsxs("div", { style: { fontSize: 12 }, children: [
21524
- /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "space-between", gap: 4, marginBottom: 8 }, children: STEPS.map((step, i) => {
21525
- const reached = isStepReached(status, i);
21526
- const active = currentStepIndex(status) === i && !errored;
21527
- return /* @__PURE__ */ jsxs("div", { style: { flex: 1, textAlign: "center" }, children: [
21528
- /* @__PURE__ */ jsx(
21529
- "div",
21530
- {
21531
- style: {
21532
- height: 4,
21533
- borderRadius: 2,
21534
- background: errored ? "#fca5a5" : reached ? "#640AFF" : "#e5e5e5",
21535
- marginBottom: 4
21536
- }
21537
- }
21538
- ),
21539
- /* @__PURE__ */ jsxs("span", { style: { color: active ? "#1a1a1a" : "#888" }, children: [
21540
- step.label,
21541
- active ? " …" : ""
21542
- ] })
21543
- ] }, step.key);
21544
- }) }),
21545
- errored ? /* @__PURE__ */ jsxs("div", { style: { color: "#dc2626" }, children: [
21546
- status === "error" ? "Bygg feilet." : "Avbrutt.",
21547
- row?.error_message ? ` ${row.error_message}` : ""
21548
- ] }) : null,
21549
- status === "ready" && row?.url ? /* @__PURE__ */ jsx(
21550
- "a",
21551
- {
21552
- href: row.url,
21553
- target: "_blank",
21554
- rel: "noreferrer",
21555
- style: { color: "#640AFF", fontSize: 12 },
21556
- children: "Åpne deploy →"
21557
- }
21558
- ) : null
21559
- ] });
21675
+ }, [supabase, deploymentId]);
21676
+ return { row, status: row?.status ?? null };
21560
21677
  }
21561
- function currentStepIndex(status) {
21678
+ function deploymentStepIndex(status) {
21562
21679
  switch (status) {
21563
21680
  case "pending":
21564
21681
  return 0;
@@ -21570,9 +21687,212 @@ function currentStepIndex(status) {
21570
21687
  return -1;
21571
21688
  }
21572
21689
  }
21573
- function isStepReached(status, i) {
21574
- return currentStepIndex(status) >= i;
21690
+ function isDeploymentStepReached(status, i) {
21691
+ return deploymentStepIndex(status) >= i;
21692
+ }
21693
+ const STEPS = ["Committed", "Building", "Deployed"];
21694
+ const PROGRESS_STYLE_ID = "setto-publish-progress-styles";
21695
+ function usePublishProgressStyles() {
21696
+ useEffect(() => {
21697
+ if (document.getElementById(PROGRESS_STYLE_ID)) return;
21698
+ const el = document.createElement("style");
21699
+ el.id = PROGRESS_STYLE_ID;
21700
+ el.textContent = `
21701
+ @keyframes setto-step-pulse {
21702
+ 0%, 100% { opacity: 1; transform: scaleY(1); }
21703
+ 50% { opacity: 0.72; transform: scaleY(0.82); }
21704
+ }
21705
+ @keyframes setto-step-shimmer {
21706
+ 0% { background-position: 120% 0; }
21707
+ 100% { background-position: -120% 0; }
21708
+ }
21709
+ .setto-step-bar-active {
21710
+ animation: setto-step-pulse 1.5s ease-in-out infinite;
21711
+ transform-origin: center bottom;
21712
+ }
21713
+ .setto-step-bar-shimmer {
21714
+ background: linear-gradient(
21715
+ 90deg,
21716
+ #640aff 0%,
21717
+ #8b5cf6 45%,
21718
+ #640aff 90%
21719
+ ) !important;
21720
+ background-size: 220% 100% !important;
21721
+ animation: setto-step-shimmer 2.2s ease-in-out infinite;
21722
+ }
21723
+ .setto-step-label-active {
21724
+ animation: setto-step-pulse 1.5s ease-in-out infinite;
21725
+ }
21726
+ `;
21727
+ document.head.appendChild(el);
21728
+ }, []);
21729
+ }
21730
+ function PublishProgressSteps({
21731
+ status,
21732
+ compact,
21733
+ buildingElapsed
21734
+ }) {
21735
+ usePublishProgressStyles();
21736
+ const scopeId = useId().replace(/:/g, "");
21737
+ const errored = status === "error" || status === "canceled";
21738
+ const activeIndex = deploymentStepIndex(status);
21739
+ return /* @__PURE__ */ jsx(
21740
+ "div",
21741
+ {
21742
+ className: `setto-progress-${scopeId}`,
21743
+ style: {
21744
+ display: "flex",
21745
+ gap: compact ? 3 : 8,
21746
+ fontSize: compact ? 11 : 12,
21747
+ minWidth: compact ? 100 : 200
21748
+ },
21749
+ children: STEPS.map((label, i) => {
21750
+ const reached = isDeploymentStepReached(status, i);
21751
+ const active = activeIndex === i && !errored;
21752
+ const barClass = active ? "setto-step-bar-active setto-step-bar-shimmer" : void 0;
21753
+ return /* @__PURE__ */ jsxs("div", { style: { flex: 1, textAlign: "center", minWidth: 0 }, children: [
21754
+ /* @__PURE__ */ jsx(
21755
+ "div",
21756
+ {
21757
+ className: barClass,
21758
+ style: {
21759
+ height: compact ? 3 : 4,
21760
+ borderRadius: 2,
21761
+ background: errored ? "#fca5a5" : reached ? "#640AFF" : "#e5e5e5",
21762
+ marginBottom: compact ? 0 : 6,
21763
+ transition: "background 0.35s ease"
21764
+ }
21765
+ }
21766
+ ),
21767
+ !compact ? /* @__PURE__ */ jsxs(
21768
+ "span",
21769
+ {
21770
+ className: active ? "setto-step-label-active" : void 0,
21771
+ style: {
21772
+ color: active ? "#1a1a1a" : "#888",
21773
+ fontWeight: active ? 500 : 400,
21774
+ display: "inline-block"
21775
+ },
21776
+ children: [
21777
+ label,
21778
+ active && status === "building" && buildingElapsed != null ? ` (${buildingElapsed}s)` : active ? " …" : ""
21779
+ ]
21780
+ }
21781
+ ) : null
21782
+ ] }, label);
21783
+ })
21784
+ }
21785
+ );
21786
+ }
21787
+ function PublishProgressBar({ status, compact, href }) {
21788
+ const inner = /* @__PURE__ */ jsx(PublishProgressSteps, { status, compact });
21789
+ const wrapStyle = {
21790
+ fontSize: compact ? 11 : 12,
21791
+ minWidth: compact ? 100 : 200
21792
+ };
21793
+ if (href) {
21794
+ return /* @__PURE__ */ jsx(
21795
+ "a",
21796
+ {
21797
+ href,
21798
+ title: "Se publiseringsstatus",
21799
+ style: {
21800
+ ...wrapStyle,
21801
+ display: "block",
21802
+ textDecoration: "none",
21803
+ color: "inherit",
21804
+ cursor: "pointer",
21805
+ borderRadius: 4,
21806
+ padding: compact ? "4px 2px" : void 0
21807
+ },
21808
+ children: inner
21809
+ }
21810
+ );
21811
+ }
21812
+ return /* @__PURE__ */ jsx("div", { style: wrapStyle, children: inner });
21813
+ }
21814
+ function PublishDialog({
21815
+ row,
21816
+ status,
21817
+ onClose,
21818
+ onComplete
21819
+ }) {
21820
+ const buildingStartedAt = useRef(null);
21821
+ const [elapsed, setElapsed] = useState(0);
21822
+ const progressStatus = row?.status ?? status ?? "pending";
21823
+ const errored = progressStatus === "error" || progressStatus === "canceled";
21824
+ const isReady = progressStatus === "ready";
21825
+ const isBuilding = progressStatus === "building";
21826
+ useEffect(() => {
21827
+ if (isBuilding && buildingStartedAt.current === null) {
21828
+ buildingStartedAt.current = Date.now();
21829
+ }
21830
+ if (!isBuilding) buildingStartedAt.current = null;
21831
+ }, [isBuilding]);
21832
+ useEffect(() => {
21833
+ if (!isBuilding) return;
21834
+ const id = window.setInterval(() => {
21835
+ if (buildingStartedAt.current !== null) {
21836
+ setElapsed(Math.floor((Date.now() - buildingStartedAt.current) / 1e3));
21837
+ }
21838
+ }, 1e3);
21839
+ return () => clearInterval(id);
21840
+ }, [isBuilding]);
21841
+ return /* @__PURE__ */ jsx(EditOverlay, { onClose: void 0, children: isReady ? /* @__PURE__ */ jsxs(Fragment, { children: [
21842
+ /* @__PURE__ */ jsx("h2", { style: { margin: "0 0 12px", fontSize: 18, fontWeight: 600 }, children: "Publisert" }),
21843
+ /* @__PURE__ */ jsx("p", { style: { margin: "0 0 20px", fontSize: 14, lineHeight: 1.5, color: "#444" }, children: "Endringene dine er live." }),
21844
+ row?.url ? /* @__PURE__ */ jsx(
21845
+ "a",
21846
+ {
21847
+ href: row.url,
21848
+ target: "_blank",
21849
+ rel: "noreferrer",
21850
+ style: { ...linkStyle$1, display: "block", marginBottom: 16 },
21851
+ children: "Åpne publisert side →"
21852
+ }
21853
+ ) : null,
21854
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onComplete, style: primaryBtnStyle, children: "OK" })
21855
+ ] }) : errored ? /* @__PURE__ */ jsxs(Fragment, { children: [
21856
+ /* @__PURE__ */ jsx("h2", { style: { margin: "0 0 12px", fontSize: 18, fontWeight: 600, color: "#dc2626" }, children: "Publisering feilet" }),
21857
+ /* @__PURE__ */ jsxs("p", { style: { margin: "0 0 20px", fontSize: 14, lineHeight: 1.5, color: "#444" }, children: [
21858
+ progressStatus === "error" ? "Bygg feilet." : "Avbrutt.",
21859
+ row?.error_message ? ` ${row.error_message}` : ""
21860
+ ] }),
21861
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onComplete, style: secondaryBtnStyle, children: "OK" })
21862
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
21863
+ /* @__PURE__ */ jsx("h2", { style: { margin: "0 0 16px", fontSize: 16, fontWeight: 600 }, children: "Publiserer endringer" }),
21864
+ /* @__PURE__ */ jsx("div", { style: { marginBottom: 20 }, children: /* @__PURE__ */ jsx(
21865
+ PublishProgressSteps,
21866
+ {
21867
+ status: progressStatus,
21868
+ buildingElapsed: isBuilding ? elapsed : void 0
21869
+ }
21870
+ ) }),
21871
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onClose, style: secondaryBtnStyle, children: "Lukk" })
21872
+ ] }) });
21575
21873
  }
21874
+ const primaryBtnStyle = {
21875
+ background: "#640AFF",
21876
+ color: "#fff",
21877
+ border: "none",
21878
+ borderRadius: 6,
21879
+ padding: "8px 20px",
21880
+ fontSize: 14,
21881
+ fontWeight: 500,
21882
+ cursor: "pointer",
21883
+ width: "100%"
21884
+ };
21885
+ const secondaryBtnStyle = {
21886
+ ...primaryBtnStyle,
21887
+ background: "transparent",
21888
+ color: "#666",
21889
+ border: "1px solid #ddd"
21890
+ };
21891
+ const linkStyle$1 = {
21892
+ color: "#640AFF",
21893
+ fontSize: 14,
21894
+ textDecoration: "none"
21895
+ };
21576
21896
  function guessLangFromPath(path, languages) {
21577
21897
  for (const lng of languages) {
21578
21898
  if (path.includes(`/${lng}.`) || path.includes(`/${lng}/`)) return lng;
@@ -21640,15 +21960,119 @@ function EditToolbar() {
21640
21960
  );
21641
21961
  const [publishing, setPublishing] = useState(false);
21642
21962
  const [activeDeployment, setActiveDeployment] = useState(null);
21963
+ const [publishDialogOpen, setPublishDialogOpen] = useState(false);
21643
21964
  const [error, setError] = useState(null);
21965
+ const [publishedNotice, setPublishedNotice] = useState(false);
21966
+ const [menuOpen, setMenuOpen] = useState(false);
21967
+ const menuRef = useRef(null);
21968
+ const publishedNoticeTimer = useRef(null);
21969
+ const { row: deploymentRow, status: deploymentStatus } = useDeploymentStatus(
21970
+ supabase,
21971
+ activeDeployment
21972
+ );
21973
+ useEffect(() => {
21974
+ let cancelled = false;
21975
+ const resume = async () => {
21976
+ try {
21977
+ await api.expireStaleDeployments(config.siteId);
21978
+ } catch {
21979
+ }
21980
+ const id = await fetchInProgressDeploymentId(supabase, config.siteId);
21981
+ if (!cancelled && id) {
21982
+ setActiveDeployment((current) => current ?? id);
21983
+ }
21984
+ };
21985
+ void resume();
21986
+ return () => {
21987
+ cancelled = true;
21988
+ };
21989
+ }, [api, supabase, config.siteId]);
21644
21990
  const textDraftCount = snap.drafts.size;
21645
21991
  const themeDraftCount = themeStore?.size() ?? 0;
21646
21992
  const draftCount = textDraftCount + themeDraftCount;
21993
+ const hasDrafts = draftCount > 0;
21994
+ const showToolbarProgress = activeDeployment !== null && !publishDialogOpen && deploymentRow !== null && isDeploymentInProgress(deploymentRow.status) && !isDeploymentStale(deploymentRow);
21995
+ const showPublishedNotice = publishedNotice && !publishDialogOpen;
21996
+ useEffect(() => {
21997
+ return () => {
21998
+ if (publishedNoticeTimer.current) clearTimeout(publishedNoticeTimer.current);
21999
+ };
22000
+ }, []);
22001
+ useEffect(() => {
22002
+ if (!activeDeployment || !deploymentRow) return;
22003
+ const { status } = deploymentRow;
22004
+ if (status === "ready") {
22005
+ if (publishDialogOpen) return;
22006
+ setPublishedNotice(true);
22007
+ setActiveDeployment(null);
22008
+ if (publishedNoticeTimer.current) clearTimeout(publishedNoticeTimer.current);
22009
+ publishedNoticeTimer.current = setTimeout(() => setPublishedNotice(false), 6e3);
22010
+ return;
22011
+ }
22012
+ if (publishDialogOpen) return;
22013
+ if (status === "error" || status === "canceled") {
22014
+ setError(
22015
+ status === "canceled" ? "Publisering avbrutt." : deploymentRow.error_message?.includes("webhook") ? "Publisering utløpt (ingen webhook)." : "Publisering feilet."
22016
+ );
22017
+ setActiveDeployment(null);
22018
+ return;
22019
+ }
22020
+ if (isDeploymentStale(deploymentRow)) {
22021
+ setError("Publisering utløpt (ingen webhook).");
22022
+ setActiveDeployment(null);
22023
+ }
22024
+ }, [activeDeployment, deploymentRow, publishDialogOpen]);
22025
+ useEffect(() => {
22026
+ if (!activeDeployment) return;
22027
+ const syncActiveDeployment = async () => {
22028
+ const { data } = await supabase.from("deployments").select("status").eq("id", activeDeployment).maybeSingle();
22029
+ if (!data) {
22030
+ setActiveDeployment(null);
22031
+ return;
22032
+ }
22033
+ const status = data.status;
22034
+ if (status === "ready") {
22035
+ if (!publishDialogOpen) {
22036
+ setPublishedNotice(true);
22037
+ if (publishedNoticeTimer.current) clearTimeout(publishedNoticeTimer.current);
22038
+ publishedNoticeTimer.current = setTimeout(() => setPublishedNotice(false), 6e3);
22039
+ }
22040
+ setActiveDeployment(null);
22041
+ return;
22042
+ }
22043
+ if (!isDeploymentInProgress(status)) {
22044
+ if (status === "canceled") setError("Publisering avbrutt.");
22045
+ else if (status === "error") setError("Publisering feilet.");
22046
+ setActiveDeployment(null);
22047
+ }
22048
+ };
22049
+ const onVisible = () => {
22050
+ if (document.visibilityState === "visible") void syncActiveDeployment();
22051
+ };
22052
+ window.addEventListener("focus", syncActiveDeployment);
22053
+ document.addEventListener("visibilitychange", onVisible);
22054
+ return () => {
22055
+ window.removeEventListener("focus", syncActiveDeployment);
22056
+ document.removeEventListener("visibilitychange", onVisible);
22057
+ };
22058
+ }, [activeDeployment, publishDialogOpen, supabase]);
21647
22059
  const languages = i18n.languages?.length ? Array.from(new Set(i18n.languages)) : ["no", "en"];
22060
+ useEffect(() => {
22061
+ if (!menuOpen) return;
22062
+ const close = (e) => {
22063
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
22064
+ setMenuOpen(false);
22065
+ }
22066
+ };
22067
+ document.addEventListener("mousedown", close);
22068
+ return () => document.removeEventListener("mousedown", close);
22069
+ }, [menuOpen]);
21648
22070
  const publish = async () => {
21649
22071
  if (!store) return;
21650
22072
  setPublishing(true);
22073
+ setPublishDialogOpen(true);
21651
22074
  setError(null);
22075
+ setPublishedNotice(false);
21652
22076
  try {
21653
22077
  const bundles = store.serialiseBundles();
21654
22078
  const files = Object.entries(bundles).map(([lng, data]) => ({
@@ -21667,63 +22091,112 @@ function EditToolbar() {
21667
22091
  setActiveDeployment(result.deploymentId);
21668
22092
  } catch (err) {
21669
22093
  setError(err instanceof Error ? err.message : "unknown error");
22094
+ setPublishDialogOpen(false);
21670
22095
  } finally {
21671
22096
  setPublishing(false);
21672
22097
  }
21673
22098
  };
21674
- return /* @__PURE__ */ jsxs("header", { style: barStyle, role: "toolbar", "aria-label": "Setto editor", children: [
21675
- /* @__PURE__ */ jsxs("div", { style: leftStyle, children: [
21676
- /* @__PURE__ */ jsx("strong", { style: { fontSize: 13 }, children: "Setto" }),
21677
- /* @__PURE__ */ jsx("span", { style: sepStyle, children: "·" }),
21678
- /* @__PURE__ */ jsx("span", { style: mutedStyle$1, children: config.siteId }),
21679
- /* @__PURE__ */ jsx("span", { style: hintStyle, children: "Klikk tekst for å redigere · klikk seksjon for farger" })
21680
- ] }),
21681
- /* @__PURE__ */ jsxs("div", { style: rightStyle, children: [
21682
- error ? /* @__PURE__ */ jsx("span", { style: errorStyle, children: error }) : null,
21683
- activeDeployment ? /* @__PURE__ */ jsx("div", { style: { minWidth: 200 }, children: /* @__PURE__ */ jsx(
21684
- BuildStatus,
21685
- {
21686
- supabase,
21687
- deploymentId: activeDeployment,
21688
- onDone: () => setActiveDeployment(null)
21689
- }
21690
- ) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
21691
- draftCount > 0 ? /* @__PURE__ */ jsxs("span", { style: draftStyle, children: [
21692
- draftCount,
21693
- " ",
21694
- draftCount === 1 ? "endring" : "endringer"
22099
+ const completePublish = () => {
22100
+ setPublishDialogOpen(false);
22101
+ setActiveDeployment(null);
22102
+ setPublishedNotice(false);
22103
+ if (publishedNoticeTimer.current) clearTimeout(publishedNoticeTimer.current);
22104
+ };
22105
+ const dismissPublishedNotice = () => {
22106
+ setPublishedNotice(false);
22107
+ if (publishedNoticeTimer.current) clearTimeout(publishedNoticeTimer.current);
22108
+ };
22109
+ const signOut = () => {
22110
+ setMenuOpen(false);
22111
+ void supabase.auth.signOut();
22112
+ };
22113
+ const goToHistory = () => {
22114
+ setMenuOpen(false);
22115
+ window.location.href = "/admin";
22116
+ };
22117
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
22118
+ /* @__PURE__ */ jsxs("header", { style: barStyle, role: "toolbar", "aria-label": "Setto editor", children: [
22119
+ /* @__PURE__ */ jsx("div", { style: leftStyle, children: /* @__PURE__ */ jsx("strong", { style: { fontSize: 13 }, children: "Setto" }) }),
22120
+ /* @__PURE__ */ jsxs("div", { style: rightStyle, children: [
22121
+ error ? /* @__PURE__ */ jsx("span", { style: errorStyle, children: error }) : null,
22122
+ hasDrafts && !publishing && activeDeployment === null ? /* @__PURE__ */ jsxs(Fragment, { children: [
22123
+ /* @__PURE__ */ jsxs("span", { style: draftStyle, "data-setto-draft-count": true, children: [
22124
+ draftCount,
22125
+ " ",
22126
+ draftCount === 1 ? "endring" : "endringer"
22127
+ ] }),
22128
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: publish, style: publishBtnStyle, children: "Publiser" })
21695
22129
  ] }) : null,
21696
- /* @__PURE__ */ jsx(
22130
+ showPublishedNotice ? /* @__PURE__ */ jsxs(
21697
22131
  "button",
21698
22132
  {
21699
22133
  type: "button",
21700
- onClick: publish,
21701
- disabled: draftCount === 0 || publishing,
21702
- style: {
21703
- ...publishBtnStyle,
21704
- opacity: draftCount === 0 || publishing ? 0.5 : 1
21705
- },
21706
- children: publishing ? "Publiserer …" : "Publiser"
22134
+ onClick: dismissPublishedNotice,
22135
+ style: publishedNoticeStyle,
22136
+ title: "Lukk",
22137
+ children: [
22138
+ /* @__PURE__ */ jsx(PublishedCheckIcon, {}),
22139
+ "Publisert"
22140
+ ]
21707
22141
  }
21708
- )
21709
- ] }),
21710
- /* @__PURE__ */ jsx("button", { type: "button", onClick: exitEditMode, style: exitBtnStyle, children: "Avslutt" })
21711
- ] })
22142
+ ) : null,
22143
+ showToolbarProgress && deploymentRow ? /* @__PURE__ */ jsx(
22144
+ PublishProgressBar,
22145
+ {
22146
+ status: deploymentRow.status,
22147
+ compact: true,
22148
+ href: `/admin?deployment=${encodeURIComponent(activeDeployment)}`
22149
+ }
22150
+ ) : null,
22151
+ /* @__PURE__ */ jsxs("div", { ref: menuRef, style: { position: "relative" }, children: [
22152
+ /* @__PURE__ */ jsx(
22153
+ "button",
22154
+ {
22155
+ type: "button",
22156
+ onClick: () => setMenuOpen((o) => !o),
22157
+ "aria-label": "Meny",
22158
+ "aria-expanded": menuOpen,
22159
+ style: {
22160
+ ...menuBtnStyle,
22161
+ background: menuOpen ? "#f0f0f0" : void 0
22162
+ },
22163
+ onMouseEnter: (e) => {
22164
+ e.currentTarget.style.background = "#f0f0f0";
22165
+ },
22166
+ onMouseLeave: (e) => {
22167
+ e.currentTarget.style.background = menuOpen ? "#f0f0f0" : "transparent";
22168
+ },
22169
+ children: "⋮"
22170
+ }
22171
+ ),
22172
+ menuOpen ? /* @__PURE__ */ jsxs("div", { style: dropdownStyle, role: "menu", children: [
22173
+ /* @__PURE__ */ jsx(MenuItem, { onClick: goToHistory, icon: /* @__PURE__ */ jsx(HistoryIcon, {}), children: "Historikk" }),
22174
+ /* @__PURE__ */ jsx(MenuItem, { onClick: signOut, icon: /* @__PURE__ */ jsx(LogOutIcon, {}), children: "Logg ut" })
22175
+ ] }) : null
22176
+ ] })
22177
+ ] })
22178
+ ] }),
22179
+ /* @__PURE__ */ jsx(EditOnboardingDialog, {}),
22180
+ publishDialogOpen && (publishing || activeDeployment !== null) ? /* @__PURE__ */ jsx(
22181
+ PublishDialog,
22182
+ {
22183
+ deploymentId: activeDeployment ?? "",
22184
+ row: deploymentRow,
22185
+ status: deploymentStatus ?? "pending",
22186
+ publishing,
22187
+ onClose: () => setPublishDialogOpen(false),
22188
+ onComplete: completePublish
22189
+ }
22190
+ ) : null
21712
22191
  ] });
21713
22192
  }
21714
- function exitEditMode() {
21715
- const u = new URL(window.location.href);
21716
- u.searchParams.delete("setto");
21717
- window.history.replaceState({}, "", u.toString());
21718
- window.dispatchEvent(new PopStateEvent("popstate"));
21719
- }
21720
22193
  const barStyle = {
21721
22194
  height: "100%",
21722
22195
  display: "flex",
21723
22196
  alignItems: "center",
21724
22197
  justifyContent: "space-between",
21725
- gap: 16,
21726
- padding: "0 16px",
22198
+ gap: 12,
22199
+ padding: "0 12px",
21727
22200
  background: "#ffffff",
21728
22201
  color: "#1a1a1a",
21729
22202
  fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
@@ -21734,24 +22207,34 @@ const barStyle = {
21734
22207
  const leftStyle = {
21735
22208
  display: "flex",
21736
22209
  alignItems: "center",
21737
- gap: 6,
21738
22210
  minWidth: 0
21739
22211
  };
21740
22212
  const rightStyle = {
21741
22213
  display: "flex",
21742
22214
  alignItems: "center",
21743
- gap: 10,
22215
+ gap: 8,
21744
22216
  flexShrink: 0
21745
22217
  };
21746
- const sepStyle = { color: "#ccc" };
21747
- const mutedStyle$1 = { color: "#666", fontSize: 12 };
21748
- const hintStyle = {
21749
- color: "#999",
22218
+ const draftStyle = {
22219
+ color: "#666",
21750
22220
  fontSize: 12,
21751
- marginLeft: 8
22221
+ whiteSpace: "nowrap"
21752
22222
  };
21753
- const draftStyle = { color: "#666", fontSize: 12 };
21754
22223
  const errorStyle = { color: "#dc2626", fontSize: 12 };
22224
+ const publishedNoticeStyle = {
22225
+ display: "inline-flex",
22226
+ alignItems: "center",
22227
+ gap: 5,
22228
+ padding: "4px 10px",
22229
+ borderRadius: 6,
22230
+ border: "1px solid #bbf7d0",
22231
+ background: "#f0fdf4",
22232
+ color: "#15803d",
22233
+ fontSize: 12,
22234
+ fontWeight: 500,
22235
+ cursor: "pointer",
22236
+ whiteSpace: "nowrap"
22237
+ };
21755
22238
  const publishBtnStyle = {
21756
22239
  background: "#640AFF",
21757
22240
  color: "#fff",
@@ -21760,17 +22243,107 @@ const publishBtnStyle = {
21760
22243
  padding: "6px 14px",
21761
22244
  fontSize: 13,
21762
22245
  fontWeight: 500,
21763
- cursor: "pointer"
22246
+ cursor: "pointer",
22247
+ whiteSpace: "nowrap"
21764
22248
  };
21765
- const exitBtnStyle = {
22249
+ const menuBtnStyle = {
21766
22250
  background: "transparent",
21767
22251
  color: "#666",
21768
- border: "1px solid #ddd",
22252
+ border: "none",
21769
22253
  borderRadius: 6,
21770
- padding: "6px 10px",
21771
- fontSize: 12,
22254
+ padding: "4px 8px",
22255
+ fontSize: 20,
22256
+ lineHeight: 1,
21772
22257
  cursor: "pointer"
21773
22258
  };
22259
+ const dropdownStyle = {
22260
+ position: "absolute",
22261
+ top: "100%",
22262
+ right: 0,
22263
+ marginTop: 4,
22264
+ background: "#fff",
22265
+ border: "1px solid #e8e8e8",
22266
+ borderRadius: 8,
22267
+ boxShadow: "0 4px 16px rgba(0, 0, 0, 0.12)",
22268
+ minWidth: 140,
22269
+ overflow: "hidden",
22270
+ zIndex: 1
22271
+ };
22272
+ const menuItemStyle = {
22273
+ display: "flex",
22274
+ alignItems: "center",
22275
+ gap: 8,
22276
+ width: "100%",
22277
+ background: "transparent",
22278
+ border: "none",
22279
+ padding: "10px 14px",
22280
+ fontSize: 13,
22281
+ textAlign: "left",
22282
+ cursor: "pointer",
22283
+ color: "#1a1a1a"
22284
+ };
22285
+ function MenuItem({
22286
+ onClick,
22287
+ icon,
22288
+ children
22289
+ }) {
22290
+ return /* @__PURE__ */ jsxs(
22291
+ "button",
22292
+ {
22293
+ type: "button",
22294
+ onClick,
22295
+ style: menuItemStyle,
22296
+ role: "menuitem",
22297
+ onMouseEnter: (e) => {
22298
+ e.currentTarget.style.background = "#f5f5f5";
22299
+ },
22300
+ onMouseLeave: (e) => {
22301
+ e.currentTarget.style.background = "transparent";
22302
+ },
22303
+ children: [
22304
+ /* @__PURE__ */ jsx("span", { style: menuIconWrapStyle, "aria-hidden": true, children: icon }),
22305
+ children
22306
+ ]
22307
+ }
22308
+ );
22309
+ }
22310
+ const menuIconWrapStyle = {
22311
+ display: "flex",
22312
+ alignItems: "center",
22313
+ justifyContent: "center",
22314
+ flexShrink: 0,
22315
+ color: "#666"
22316
+ };
22317
+ function PublishedCheckIcon() {
22318
+ return /* @__PURE__ */ jsx(
22319
+ "svg",
22320
+ {
22321
+ width: "14",
22322
+ height: "14",
22323
+ viewBox: "0 0 24 24",
22324
+ fill: "none",
22325
+ stroke: "currentColor",
22326
+ strokeWidth: "2.5",
22327
+ strokeLinecap: "round",
22328
+ strokeLinejoin: "round",
22329
+ "aria-hidden": true,
22330
+ children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" })
22331
+ }
22332
+ );
22333
+ }
22334
+ function HistoryIcon() {
22335
+ return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
22336
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
22337
+ /* @__PURE__ */ jsx("polyline", { points: "12 6 12 12 16 14" })
22338
+ ] });
22339
+ }
22340
+ function LogOutIcon() {
22341
+ return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
22342
+ /* @__PURE__ */ jsx("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
22343
+ /* @__PURE__ */ jsx("polyline", { points: "16 17 21 12 16 7" }),
22344
+ /* @__PURE__ */ jsx("line", { x1: "21", y1: "12", x2: "9", y2: "12" })
22345
+ ] });
22346
+ }
21774
22347
  function EditModeShell({ children, themeStore }) {
21775
22348
  useSettoDocumentLayout(true);
21776
22349
  const [toolbarRoot, setToolbarRoot] = useState(null);
@@ -21919,6 +22492,7 @@ function T({ k }) {
21919
22492
  const handleInput = (e) => {
21920
22493
  store?.set(k, i18n.language, e.currentTarget.textContent ?? "");
21921
22494
  };
22495
+ const lang = toBcp47(i18n.language);
21922
22496
  return /* @__PURE__ */ jsx(
21923
22497
  "span",
21924
22498
  {
@@ -21926,6 +22500,8 @@ function T({ k }) {
21926
22500
  "data-setto-key": k,
21927
22501
  contentEditable: true,
21928
22502
  suppressContentEditableWarning: true,
22503
+ lang,
22504
+ spellCheck: true,
21929
22505
  role: "textbox",
21930
22506
  tabIndex: 0,
21931
22507
  onFocus: () => setFocused(true),
@@ -21943,6 +22519,12 @@ function T({ k }) {
21943
22519
  }
21944
22520
  );
21945
22521
  }
22522
+ function toBcp47(code) {
22523
+ const base = (code.split("-")[0] ?? code).toLowerCase();
22524
+ if (base === "no" || base === "nb" || base === "nn") return "nb-NO";
22525
+ if (base === "en") return "en";
22526
+ return code;
22527
+ }
21946
22528
  function isTextEditClick(target) {
21947
22529
  if (target.closest("[data-setto-key]")) return true;
21948
22530
  if (target.closest("[data-setto-ui]")) return true;
@@ -22061,23 +22643,47 @@ const SettoBlock = forwardRef(
22061
22643
  );
22062
22644
  }
22063
22645
  );
22646
+ function adminRedirectUrl() {
22647
+ return `${window.location.origin}/admin`;
22648
+ }
22649
+ function authCallbackType() {
22650
+ const hash = window.location.hash.replace(/^#/, "");
22651
+ if (!hash) return null;
22652
+ const type = new URLSearchParams(hash).get("type");
22653
+ if (type === "invite" || type === "recovery") return type;
22654
+ return null;
22655
+ }
22656
+ function clearAuthHashFromUrl() {
22657
+ const { pathname, search } = window.location;
22658
+ window.history.replaceState(null, "", pathname + search);
22659
+ }
22064
22660
  function AuthGate({ children }) {
22065
22661
  const { supabase, session, authLoading } = useSetto();
22066
22662
  const [mode, setMode] = useState("signin");
22067
22663
  const [email, setEmail] = useState("");
22068
22664
  const [password, setPassword] = useState("");
22665
+ const [confirmPassword, setConfirmPassword] = useState("");
22069
22666
  const [error, setError] = useState(null);
22667
+ const [info, setInfo] = useState(null);
22070
22668
  const [busy, setBusy] = useState(false);
22669
+ useEffect(() => {
22670
+ const callback = authCallbackType();
22671
+ if (callback) setMode("set_password");
22672
+ }, []);
22071
22673
  if (authLoading) {
22072
22674
  return /* @__PURE__ */ jsx("div", { style: loadingStyle, children: "Laster …" });
22073
22675
  }
22074
- if (session) return /* @__PURE__ */ jsx(Fragment, { children });
22075
- const submit = async (e) => {
22676
+ if (session && mode !== "set_password") return /* @__PURE__ */ jsx(Fragment, { children });
22677
+ if (mode === "set_password" && !session) {
22678
+ return /* @__PURE__ */ jsx("div", { style: loadingStyle, children: authCallbackType() ? "Aktiverer invitasjon …" : "Åpne lenken fra e-posten for å sette passord." });
22679
+ }
22680
+ const submitSignIn = async (e) => {
22076
22681
  e.preventDefault();
22077
22682
  setBusy(true);
22078
22683
  setError(null);
22684
+ setInfo(null);
22079
22685
  try {
22080
- const result = mode === "signin" ? await supabase.auth.signInWithPassword({ email, password }) : await supabase.auth.signUp({ email, password });
22686
+ const result = await supabase.auth.signInWithPassword({ email, password });
22081
22687
  if (result.error) throw result.error;
22082
22688
  if (result.data.session) {
22083
22689
  await supabase.auth.setSession({
@@ -22091,9 +22697,122 @@ function AuthGate({ children }) {
22091
22697
  setBusy(false);
22092
22698
  }
22093
22699
  };
22094
- return /* @__PURE__ */ jsx("div", { style: shellStyle$1, children: /* @__PURE__ */ jsxs("form", { onSubmit: submit, style: formStyle, children: [
22700
+ const submitForgot = async (e) => {
22701
+ e.preventDefault();
22702
+ setBusy(true);
22703
+ setError(null);
22704
+ setInfo(null);
22705
+ try {
22706
+ const { error: resetError } = await supabase.auth.resetPasswordForEmail(email, {
22707
+ redirectTo: adminRedirectUrl()
22708
+ });
22709
+ if (resetError) throw resetError;
22710
+ setInfo("Vi har sendt en lenke for å nullstille passordet. Sjekk innboksen.");
22711
+ } catch (err) {
22712
+ setError(err instanceof Error ? err.message : "Ukjent feil");
22713
+ } finally {
22714
+ setBusy(false);
22715
+ }
22716
+ };
22717
+ const submitSetPassword = async (e) => {
22718
+ e.preventDefault();
22719
+ if (password !== confirmPassword) {
22720
+ setError("Passordene er ikke like.");
22721
+ return;
22722
+ }
22723
+ if (password.length < 8) {
22724
+ setError("Passordet må være minst 8 tegn.");
22725
+ return;
22726
+ }
22727
+ setBusy(true);
22728
+ setError(null);
22729
+ setInfo(null);
22730
+ try {
22731
+ const { error: updateError } = await supabase.auth.updateUser({ password });
22732
+ if (updateError) throw updateError;
22733
+ clearAuthHashFromUrl();
22734
+ setMode("signin");
22735
+ setPassword("");
22736
+ setConfirmPassword("");
22737
+ } catch (err) {
22738
+ setError(err instanceof Error ? err.message : "Ukjent feil");
22739
+ } finally {
22740
+ setBusy(false);
22741
+ }
22742
+ };
22743
+ if (mode === "set_password") {
22744
+ const callback = authCallbackType();
22745
+ return /* @__PURE__ */ jsx("div", { style: shellStyle$1, children: /* @__PURE__ */ jsxs("form", { onSubmit: submitSetPassword, style: formStyle, children: [
22746
+ /* @__PURE__ */ jsx("h1", { style: { margin: 0, fontSize: 22 }, children: "Setto" }),
22747
+ /* @__PURE__ */ jsx("p", { style: { margin: 0, color: "#888", fontSize: 13 }, children: callback === "invite" ? "Velg et passord for å fullføre invitasjonen." : "Velg et nytt passord." }),
22748
+ /* @__PURE__ */ jsx(
22749
+ "input",
22750
+ {
22751
+ type: "password",
22752
+ value: password,
22753
+ onChange: (e) => setPassword(e.target.value),
22754
+ placeholder: "Nytt passord",
22755
+ autoComplete: "new-password",
22756
+ required: true,
22757
+ minLength: 8,
22758
+ style: inputStyle
22759
+ }
22760
+ ),
22761
+ /* @__PURE__ */ jsx(
22762
+ "input",
22763
+ {
22764
+ type: "password",
22765
+ value: confirmPassword,
22766
+ onChange: (e) => setConfirmPassword(e.target.value),
22767
+ placeholder: "Gjenta passord",
22768
+ autoComplete: "new-password",
22769
+ required: true,
22770
+ minLength: 8,
22771
+ style: inputStyle
22772
+ }
22773
+ ),
22774
+ error ? /* @__PURE__ */ jsx("div", { style: { color: "#ff7a7a", fontSize: 12 }, children: error }) : null,
22775
+ info ? /* @__PURE__ */ jsx("div", { style: { color: "#9fd89f", fontSize: 12 }, children: info }) : null,
22776
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: busy, style: btnStyle, children: busy ? "..." : "Lagre passord" })
22777
+ ] }) });
22778
+ }
22779
+ if (mode === "forgot") {
22780
+ return /* @__PURE__ */ jsx("div", { style: shellStyle$1, children: /* @__PURE__ */ jsxs("form", { onSubmit: submitForgot, style: formStyle, children: [
22781
+ /* @__PURE__ */ jsx("h1", { style: { margin: 0, fontSize: 22 }, children: "Setto" }),
22782
+ /* @__PURE__ */ jsx("p", { style: { margin: 0, color: "#888", fontSize: 13 }, children: "Skriv inn e-posten din, så sender vi en lenke for å nullstille passordet." }),
22783
+ /* @__PURE__ */ jsx(
22784
+ "input",
22785
+ {
22786
+ type: "email",
22787
+ value: email,
22788
+ onChange: (e) => setEmail(e.target.value),
22789
+ placeholder: "E-post",
22790
+ autoComplete: "email",
22791
+ required: true,
22792
+ style: inputStyle
22793
+ }
22794
+ ),
22795
+ error ? /* @__PURE__ */ jsx("div", { style: { color: "#ff7a7a", fontSize: 12 }, children: error }) : null,
22796
+ info ? /* @__PURE__ */ jsx("div", { style: { color: "#9fd89f", fontSize: 12 }, children: info }) : null,
22797
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: busy, style: btnStyle, children: busy ? "..." : "Send lenke" }),
22798
+ /* @__PURE__ */ jsx(
22799
+ "button",
22800
+ {
22801
+ type: "button",
22802
+ onClick: () => {
22803
+ setMode("signin");
22804
+ setError(null);
22805
+ setInfo(null);
22806
+ },
22807
+ style: linkStyle,
22808
+ children: "Tilbake til innlogging"
22809
+ }
22810
+ )
22811
+ ] }) });
22812
+ }
22813
+ return /* @__PURE__ */ jsx("div", { style: shellStyle$1, children: /* @__PURE__ */ jsxs("form", { onSubmit: submitSignIn, style: formStyle, children: [
22095
22814
  /* @__PURE__ */ jsx("h1", { style: { margin: 0, fontSize: 22 }, children: "Setto" }),
22096
- /* @__PURE__ */ jsx("p", { style: { margin: 0, color: "#888", fontSize: 13 }, children: mode === "signin" ? "Logg inn for å redigere innholdet." : "Opprett konto." }),
22815
+ /* @__PURE__ */ jsx("p", { style: { margin: 0, color: "#888", fontSize: 13 }, children: "Logg inn for å redigere innholdet. Har du fått invitasjon, bruk lenken i e-posten første gang." }),
22097
22816
  /* @__PURE__ */ jsx(
22098
22817
  "input",
22099
22818
  {
@@ -22113,20 +22832,25 @@ function AuthGate({ children }) {
22113
22832
  value: password,
22114
22833
  onChange: (e) => setPassword(e.target.value),
22115
22834
  placeholder: "Passord",
22116
- autoComplete: mode === "signin" ? "current-password" : "new-password",
22835
+ autoComplete: "current-password",
22117
22836
  required: true,
22118
22837
  style: inputStyle
22119
22838
  }
22120
22839
  ),
22121
22840
  error ? /* @__PURE__ */ jsx("div", { style: { color: "#ff7a7a", fontSize: 12 }, children: error }) : null,
22122
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: busy, style: btnStyle, children: busy ? "..." : mode === "signin" ? "Logg inn" : "Opprett konto" }),
22841
+ info ? /* @__PURE__ */ jsx("div", { style: { color: "#9fd89f", fontSize: 12 }, children: info }) : null,
22842
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: busy, style: btnStyle, children: busy ? "..." : "Logg inn" }),
22123
22843
  /* @__PURE__ */ jsx(
22124
22844
  "button",
22125
22845
  {
22126
22846
  type: "button",
22127
- onClick: () => setMode((m) => m === "signin" ? "signup" : "signin"),
22847
+ onClick: () => {
22848
+ setMode("forgot");
22849
+ setError(null);
22850
+ setInfo(null);
22851
+ },
22128
22852
  style: linkStyle,
22129
- children: mode === "signin" ? "Ny her? Opprett konto" : "Har konto? Logg inn"
22853
+ children: "Glemt passord?"
22130
22854
  }
22131
22855
  )
22132
22856
  ] }) });
@@ -22180,6 +22904,16 @@ const loadingStyle = {
22180
22904
  fontSize: 14,
22181
22905
  color: "#888"
22182
22906
  };
22907
+ function deploymentAdminUrl(deploymentId) {
22908
+ const u = new URL(window.location.href);
22909
+ u.pathname = "/admin";
22910
+ u.search = "";
22911
+ u.searchParams.set("deployment", deploymentId);
22912
+ return u.pathname + u.search;
22913
+ }
22914
+ function focusedDeploymentId() {
22915
+ return new URLSearchParams(window.location.search).get("deployment");
22916
+ }
22183
22917
  function SettoAdminApp() {
22184
22918
  return /* @__PURE__ */ jsx(AuthGate, { children: /* @__PURE__ */ jsx(SiteList, {}) });
22185
22919
  }
@@ -22244,8 +22978,35 @@ function SiteList() {
22244
22978
  ] });
22245
22979
  }
22246
22980
  function DeploymentList({ siteId }) {
22247
- const { supabase } = useSetto();
22981
+ const { supabase, api, config } = useSetto();
22248
22982
  const [rows, setRows] = useState(null);
22983
+ const [focusId, setFocusId] = useState(focusedDeploymentId);
22984
+ const [canceling, setCanceling] = useState(false);
22985
+ const [cancelError, setCancelError] = useState(null);
22986
+ const { row: focusRow } = useDeploymentStatus(supabase, focusId);
22987
+ useEffect(() => {
22988
+ const onPopState = () => setFocusId(focusedDeploymentId());
22989
+ window.addEventListener("popstate", onPopState);
22990
+ return () => window.removeEventListener("popstate", onPopState);
22991
+ }, []);
22992
+ const cancelDeployment = async () => {
22993
+ if (!focusId) return;
22994
+ setCanceling(true);
22995
+ setCancelError(null);
22996
+ try {
22997
+ await api.cancelDeployment(config.siteId, focusId);
22998
+ setFocusId(null);
22999
+ window.history.replaceState(null, "", "/admin");
23000
+ } catch (err) {
23001
+ setCancelError(err instanceof Error ? err.message : "Kunne ikke avbryte");
23002
+ } finally {
23003
+ setCanceling(false);
23004
+ }
23005
+ };
23006
+ useEffect(() => {
23007
+ void api.expireStaleDeployments(config.siteId).catch(() => {
23008
+ });
23009
+ }, [api, config.siteId]);
22249
23010
  useEffect(() => {
22250
23011
  let cancelled = false;
22251
23012
  supabase.from("deployments").select("*").eq("site_id", siteId).order("started_at", { ascending: false }).limit(10).then(({ data }) => {
@@ -22269,14 +23030,85 @@ function DeploymentList({ siteId }) {
22269
23030
  supabase.removeChannel(channel);
22270
23031
  };
22271
23032
  }, [supabase, siteId]);
23033
+ const showFocusPanel = focusId !== null && (focusRow === null || isDeploymentInProgress(focusRow.status));
22272
23034
  if (!rows) return /* @__PURE__ */ jsx("p", { style: mutedStyle, children: "Laster deploys…" });
22273
- if (rows.length === 0) return /* @__PURE__ */ jsx("p", { style: mutedStyle, children: "Ingen deploys ennå." });
22274
- return /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }, children: rows.map((r) => /* @__PURE__ */ jsxs("li", { style: depRowStyle, children: [
22275
- /* @__PURE__ */ jsx("span", { style: depDotStyle(r.status) }),
22276
- /* @__PURE__ */ jsx("code", { style: { fontSize: 12, color: "#aaa" }, children: r.commit_sha.slice(0, 7) }),
22277
- /* @__PURE__ */ jsx("span", { style: { flex: 1, fontSize: 12 }, children: r.status }),
22278
- /* @__PURE__ */ jsx("time", { style: { fontSize: 11, color: "#888" }, children: new Date(r.started_at).toLocaleString() })
22279
- ] }, r.id)) });
23035
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
23036
+ showFocusPanel ? /* @__PURE__ */ jsxs("section", { style: activeDepStyle, children: [
23037
+ /* @__PURE__ */ jsx("h3", { style: { margin: 0, fontSize: 15 }, children: "Publisering pågår" }),
23038
+ focusRow ? /* @__PURE__ */ jsxs(Fragment, { children: [
23039
+ /* @__PURE__ */ jsx(PublishProgressBar, { status: focusRow.status }),
23040
+ /* @__PURE__ */ jsxs("p", { style: mutedStyle, children: [
23041
+ "Commit ",
23042
+ /* @__PURE__ */ jsx("code", { style: { color: "#bda6ff" }, children: focusRow.commit_sha.slice(0, 7) }),
23043
+ focusRow.vercel_deployment_id ? /* @__PURE__ */ jsxs(Fragment, { children: [
23044
+ " ",
23045
+ "· Vercel",
23046
+ " ",
23047
+ /* @__PURE__ */ jsxs("code", { style: { color: "#888", fontSize: 11 }, children: [
23048
+ focusRow.vercel_deployment_id.slice(0, 12),
23049
+ "…"
23050
+ ] })
23051
+ ] }) : null
23052
+ ] }),
23053
+ cancelError ? /* @__PURE__ */ jsx("p", { style: errStyle, children: cancelError }) : null,
23054
+ /* @__PURE__ */ jsx(
23055
+ "button",
23056
+ {
23057
+ type: "button",
23058
+ onClick: () => void cancelDeployment(),
23059
+ disabled: canceling,
23060
+ style: cancelBtnStyle,
23061
+ children: canceling ? "Avbryter …" : "Avbryt publisering"
23062
+ }
23063
+ )
23064
+ ] }) : /* @__PURE__ */ jsx("p", { style: mutedStyle, children: "Laster status …" })
23065
+ ] }) : null,
23066
+ rows.length === 0 ? /* @__PURE__ */ jsx("p", { style: mutedStyle, children: "Ingen deploys ennå." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
23067
+ /* @__PURE__ */ jsx("h3", { style: { margin: 0, fontSize: 14, color: "#aaa" }, children: "Historikk" }),
23068
+ /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }, children: rows.map((r) => {
23069
+ const focused = r.id === focusId;
23070
+ const inProgress = isDeploymentInProgress(r.status);
23071
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
23072
+ "a",
23073
+ {
23074
+ href: inProgress ? deploymentAdminUrl(r.id) : void 0,
23075
+ style: {
23076
+ ...depRowStyle,
23077
+ textDecoration: "none",
23078
+ color: "inherit",
23079
+ outline: focused ? "1px solid #640AFF" : void 0,
23080
+ cursor: inProgress ? "pointer" : "default"
23081
+ },
23082
+ onClick: inProgress ? (e) => {
23083
+ e.preventDefault();
23084
+ setFocusId(r.id);
23085
+ window.history.pushState(null, "", deploymentAdminUrl(r.id));
23086
+ } : void 0,
23087
+ children: [
23088
+ /* @__PURE__ */ jsx("span", { style: depDotStyle(r.status) }),
23089
+ /* @__PURE__ */ jsx("code", { style: { fontSize: 12, color: "#aaa" }, children: r.commit_sha.slice(0, 7) }),
23090
+ /* @__PURE__ */ jsx("span", { style: { flex: 1, fontSize: 12 }, children: statusLabel(r) }),
23091
+ /* @__PURE__ */ jsx("time", { style: { fontSize: 11, color: "#888" }, children: new Date(r.started_at).toLocaleString() })
23092
+ ]
23093
+ }
23094
+ ) }, r.id);
23095
+ }) })
23096
+ ] })
23097
+ ] });
23098
+ }
23099
+ function statusLabel(row) {
23100
+ switch (row.status) {
23101
+ case "pending":
23102
+ return "Venter";
23103
+ case "building":
23104
+ return "Bygger";
23105
+ case "ready":
23106
+ return "Publisert";
23107
+ case "error":
23108
+ return row.error_message?.includes("webhook") ? "Utløpt" : "Feilet";
23109
+ case "canceled":
23110
+ return "Avbrutt";
23111
+ }
22280
23112
  }
22281
23113
  function SignOutButton() {
22282
23114
  const { supabase } = useSetto();
@@ -22347,6 +23179,14 @@ const signOutBtnStyle = {
22347
23179
  fontSize: 12,
22348
23180
  cursor: "pointer"
22349
23181
  };
23182
+ const activeDepStyle = {
23183
+ background: "#1a1a20",
23184
+ borderRadius: 8,
23185
+ padding: 14,
23186
+ display: "flex",
23187
+ flexDirection: "column",
23188
+ gap: 10
23189
+ };
22350
23190
  const depRowStyle = {
22351
23191
  display: "flex",
22352
23192
  alignItems: "center",
@@ -22355,6 +23195,16 @@ const depRowStyle = {
22355
23195
  padding: "6px 10px",
22356
23196
  borderRadius: 6
22357
23197
  };
23198
+ const cancelBtnStyle = {
23199
+ alignSelf: "flex-start",
23200
+ background: "transparent",
23201
+ color: "#ff7a7a",
23202
+ border: "1px solid #4a3030",
23203
+ borderRadius: 6,
23204
+ padding: "8px 14px",
23205
+ fontSize: 13,
23206
+ cursor: "pointer"
23207
+ };
22358
23208
  function depDotStyle(status) {
22359
23209
  const colour = status === "ready" ? "#34d399" : status === "error" || status === "canceled" ? "#ff7a7a" : status === "building" ? "#fbbf24" : "#888";
22360
23210
  return {