@sevenfold/setto-client 0.6.2 → 0.7.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 { useState, useRef, useEffect, useSyncExternalStore, useLayoutEffect, createContext, useContext, useCallback, useMemo, useId, forwardRef, Fragment as Fragment$1 } from "react";
2
+ import { useState, useRef, useEffect, useSyncExternalStore, useLayoutEffect, createContext, useContext, useCallback, useMemo, useId, useInsertionEffect, forwardRef, Fragment as Fragment$1 } from "react";
3
3
  import { useTranslation } from "react-i18next";
4
4
  import { createPortal } from "react-dom";
5
5
  function __rest(s, e) {
@@ -20709,6 +20709,28 @@ function getSupabase(config) {
20709
20709
  cache.set(key, client);
20710
20710
  return client;
20711
20711
  }
20712
+ function createFormsApi(request) {
20713
+ const base = (siteId) => `/sites/${encodeURIComponent(siteId)}/forms`;
20714
+ return {
20715
+ getSettings(siteId) {
20716
+ return request(`${base(siteId)}/settings`, {}, "getFormSettings");
20717
+ },
20718
+ saveSettings(siteId, recipients) {
20719
+ return request(
20720
+ `${base(siteId)}/settings`,
20721
+ { method: "PUT", body: { recipients } },
20722
+ "saveFormSettings"
20723
+ );
20724
+ },
20725
+ deleteSubmission(siteId, id) {
20726
+ return request(
20727
+ `${base(siteId)}/submissions/${encodeURIComponent(id)}`,
20728
+ { method: "DELETE" },
20729
+ "deleteSubmission"
20730
+ );
20731
+ }
20732
+ };
20733
+ }
20712
20734
  async function bearer(sb) {
20713
20735
  const { data } = await sb.auth.getSession();
20714
20736
  const token = data.session?.access_token;
@@ -20739,7 +20761,23 @@ async function asError(res, fallback) {
20739
20761
  }
20740
20762
  function createApi(args) {
20741
20763
  const { apiUrl, supabase } = args;
20764
+ const request = async (path, init = {}, label = `${init.method ?? "GET"} ${path}`) => {
20765
+ const token = await bearer(supabase);
20766
+ const res = await fetch(`${apiUrl}${path}`, {
20767
+ method: init.method ?? "GET",
20768
+ headers: {
20769
+ Authorization: `Bearer ${token}`,
20770
+ ...init.body !== void 0 ? { "Content-Type": "application/json" } : {}
20771
+ },
20772
+ body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
20773
+ });
20774
+ if (!res.ok) throw await asError(res, `${label} failed`);
20775
+ if (res.status === 204) return void 0;
20776
+ return parseJson(res);
20777
+ };
20742
20778
  return {
20779
+ request,
20780
+ forms: createFormsApi(request),
20743
20781
  async getContent(siteId) {
20744
20782
  const token = await bearer(supabase);
20745
20783
  const res = await fetch(`${apiUrl}/sites/${encodeURIComponent(siteId)}/content`, {
@@ -20832,14 +20870,20 @@ function getNested(obj, key) {
20832
20870
  }
20833
20871
  function deleteNested(obj, key) {
20834
20872
  const parts = key.split(".");
20873
+ const chain = [obj];
20835
20874
  let cur = obj;
20836
20875
  for (let i = 0; i < parts.length - 1; i++) {
20837
20876
  const part = parts[i];
20838
20877
  const next = cur[part];
20839
20878
  if (!next || typeof next !== "object") return;
20840
20879
  cur = next;
20880
+ chain.push(cur);
20841
20881
  }
20842
20882
  delete cur[parts[parts.length - 1]];
20883
+ for (let i = chain.length - 1; i > 0; i--) {
20884
+ if (Object.keys(chain[i]).length > 0) break;
20885
+ delete chain[i - 1][parts[i - 1]];
20886
+ }
20843
20887
  }
20844
20888
  function collectKeysUnder(obj, prefix) {
20845
20889
  const base = getNested(obj, prefix);
@@ -20863,6 +20907,8 @@ class I18nStore {
20863
20907
  /** Stable namespace used by react-i18next defaults. */
20864
20908
  ns;
20865
20909
  i18n;
20910
+ /** >0 while a batch is running; consumers are notified once at the end. */
20911
+ batchDepth = 0;
20866
20912
  /** Cached reference — useSyncExternalStore requires referential stability. */
20867
20913
  cachedSnapshot = {
20868
20914
  drafts: this.drafts,
@@ -20903,34 +20949,49 @@ class I18nStore {
20903
20949
  set(key, lng, value) {
20904
20950
  const id = `${lng}::${key}`;
20905
20951
  const existing = this.drafts.get(id);
20906
- const original = existing?.original ?? String(this.i18n.getResource(lng, this.ns, key) ?? "");
20907
- if (value === original) {
20952
+ const resource = this.i18n.getResource(lng, this.ns, key);
20953
+ const original = existing?.original ?? String(resource ?? "");
20954
+ const created = existing ? existing.created === true : resource === void 0;
20955
+ if (value === original && !created) {
20908
20956
  this.drafts.delete(id);
20909
20957
  } else {
20910
- this.drafts.set(id, { key, lng, original, current: value });
20958
+ this.drafts.set(id, { key, lng, original, current: value, ...created ? { created } : {} });
20911
20959
  }
20912
20960
  this.i18n.addResource(lng, this.ns, key, value);
20913
- this.i18n.emit("languageChanged", this.i18n.language);
20914
- this.bump();
20961
+ this.notify();
20962
+ }
20963
+ /**
20964
+ * Deletes a key from the bundle. A key that was created this session simply
20965
+ * disappears; one that was on disk is kept as a removal draft so the
20966
+ * deletion counts as a change and reverts cleanly.
20967
+ */
20968
+ delete(key, lng) {
20969
+ const id = `${lng}::${key}`;
20970
+ const existing = this.drafts.get(id);
20971
+ const resource = this.i18n.getResource(lng, this.ns, key);
20972
+ if (existing?.created) {
20973
+ this.drafts.delete(id);
20974
+ } else if (existing || resource !== void 0) {
20975
+ const original = existing?.original ?? String(resource ?? "");
20976
+ this.drafts.set(id, { key, lng, original, current: "", removed: true });
20977
+ }
20978
+ this.deleteFromBundle(lng, key);
20979
+ this.notify();
20915
20980
  }
20916
20981
  /** Resets a single draft back to its original value. */
20917
20982
  revert(key, lng) {
20918
20983
  const id = `${lng}::${key}`;
20919
20984
  const draft = this.drafts.get(id);
20920
20985
  if (!draft) return;
20921
- this.i18n.addResource(lng, this.ns, key, draft.original);
20986
+ this.restore(draft);
20922
20987
  this.drafts.delete(id);
20923
- this.i18n.emit("languageChanged", this.i18n.language);
20924
- this.bump();
20988
+ this.notify();
20925
20989
  }
20926
20990
  /** Resets every draft. */
20927
20991
  revertAll() {
20928
- for (const draft of this.drafts.values()) {
20929
- this.i18n.addResource(draft.lng, this.ns, draft.key, draft.original);
20930
- }
20992
+ for (const draft of this.drafts.values()) this.restore(draft);
20931
20993
  this.drafts.clear();
20932
- this.i18n.emit("languageChanged", this.i18n.language);
20933
- this.bump();
20994
+ this.notify();
20934
20995
  }
20935
20996
  /** Marks the current state as the new baseline (after a successful publish). */
20936
20997
  commit() {
@@ -20951,34 +21012,58 @@ class I18nStore {
20951
21012
  * Adds the same key across all loaded languages.
20952
21013
  */
20953
21014
  addListItem(prefix, _lng, template) {
20954
- const langs = this.i18n.languages?.length ? this.i18n.languages : [_lng];
21015
+ const langs = this.languages(_lng);
20955
21016
  const allKeys = langs.flatMap((l) => this.getListKeys(prefix, l));
20956
21017
  const numeric = allKeys.map(Number).filter((n) => !Number.isNaN(n));
20957
21018
  const nextKey = numeric.length > 0 ? String(Math.max(...numeric) + 1) : "1";
20958
- for (const lang of langs) {
20959
- for (const [field, value] of Object.entries(template)) {
20960
- this.set(`${prefix}.${nextKey}.${field}`, lang, value);
21019
+ this.batch(() => {
21020
+ for (const lang of langs) {
21021
+ for (const [field, value] of Object.entries(template)) {
21022
+ this.set(`${prefix}.${nextKey}.${field}`, lang, value);
21023
+ }
20961
21024
  }
20962
- }
21025
+ });
20963
21026
  return nextKey;
20964
21027
  }
20965
21028
  /** Removes a list item and all nested keys under `prefix.itemKey` in every language. */
20966
21029
  removeListItem(prefix, itemKey, _lng) {
20967
- const langs = this.i18n.languages?.length ? this.i18n.languages : [_lng];
20968
- for (const lang of langs) {
20969
- const bundle = this.i18n.getResourceBundle(lang, this.ns) ?? {};
20970
- const item = getNested(bundle, `${prefix}.${itemKey}`);
20971
- if (!item || typeof item !== "object") continue;
20972
- for (const field of Object.keys(item)) {
20973
- const fullKey = `${prefix}.${itemKey}.${field}`;
20974
- const id = `${lang}::${fullKey}`;
20975
- this.drafts.delete(id);
20976
- }
20977
- deleteNested(bundle, `${prefix}.${itemKey}`);
20978
- this.i18n.addResourceBundle(lang, this.ns, bundle, true, true);
20979
- }
20980
- this.i18n.emit("languageChanged", this.i18n.language);
20981
- this.bump();
21030
+ this.batch(() => {
21031
+ for (const lang of this.languages(_lng)) {
21032
+ for (const leaf of this.leavesUnder(`${prefix}.${itemKey}`, lang)) {
21033
+ this.delete(leaf, lang);
21034
+ }
21035
+ }
21036
+ });
21037
+ }
21038
+ /**
21039
+ * Moves a list item one step up or down by swapping its values with its
21040
+ * neighbour's, in every language. Items keep their numeric keys and the keys
21041
+ * keep their order; only the contents trade places. That is what makes the
21042
+ * move a set of ordinary drafts — publish, revert and the change count all
21043
+ * work without knowing a move happened.
21044
+ */
21045
+ moveListItem(prefix, itemKey, direction, _lng) {
21046
+ const keys = this.getListKeys(prefix, _lng).sort(compareListKeys);
21047
+ const index = keys.indexOf(itemKey);
21048
+ const targetIndex = index + direction;
21049
+ if (index < 0 || targetIndex < 0 || targetIndex >= keys.length) return false;
21050
+ const other = keys[targetIndex];
21051
+ this.batch(() => {
21052
+ for (const lang of this.languages(_lng)) {
21053
+ const a = `${prefix}.${itemKey}`;
21054
+ const b = `${prefix}.${other}`;
21055
+ const fieldsA = this.leavesUnder(a, lang).map((k) => k.slice(a.length + 1));
21056
+ const fieldsB = this.leavesUnder(b, lang).map((k) => k.slice(b.length + 1));
21057
+ const fields = [.../* @__PURE__ */ new Set([...fieldsA, ...fieldsB])];
21058
+ const valuesA = new Map(fields.map((f) => [f, this.valueOrUndefined(`${a}.${f}`, lang)]));
21059
+ const valuesB = new Map(fields.map((f) => [f, this.valueOrUndefined(`${b}.${f}`, lang)]));
21060
+ for (const f of fields) {
21061
+ this.setOrDelete(`${a}.${f}`, lang, valuesB.get(f));
21062
+ this.setOrDelete(`${b}.${f}`, lang, valuesA.get(f));
21063
+ }
21064
+ }
21065
+ });
21066
+ return true;
20982
21067
  }
20983
21068
  /** Re-sorts list keys after removal (optional compaction — not used by default). */
20984
21069
  sortListKeys(prefix, lng) {
@@ -20999,6 +21084,64 @@ class I18nStore {
20999
21084
  }
21000
21085
  return out;
21001
21086
  }
21087
+ /** Runs `fn` and notifies consumers once, however many edits it makes. */
21088
+ batch(fn) {
21089
+ this.batchDepth += 1;
21090
+ try {
21091
+ fn();
21092
+ } finally {
21093
+ this.batchDepth -= 1;
21094
+ }
21095
+ if (this.batchDepth === 0) this.notify();
21096
+ }
21097
+ languages(fallback) {
21098
+ return this.i18n.languages?.length ? [...this.i18n.languages] : [fallback];
21099
+ }
21100
+ valueOrUndefined(key, lng) {
21101
+ const resource = this.i18n.getResource(lng, this.ns, key);
21102
+ return resource === void 0 ? void 0 : String(resource);
21103
+ }
21104
+ setOrDelete(key, lng, value) {
21105
+ if (value === void 0) {
21106
+ if (this.i18n.getResource(lng, this.ns, key) !== void 0) this.delete(key, lng);
21107
+ } else {
21108
+ this.set(key, lng, value);
21109
+ }
21110
+ }
21111
+ /** Every string key under `prefix`, nested to any depth. */
21112
+ leavesUnder(prefix, lng) {
21113
+ const bundle = this.i18n.getResourceBundle(lng, this.ns) ?? {};
21114
+ const node = getNested(bundle, prefix);
21115
+ const out = [];
21116
+ const walk2 = (value, path) => {
21117
+ if (value && typeof value === "object") {
21118
+ for (const [k, v] of Object.entries(value)) {
21119
+ walk2(v, `${path}.${k}`);
21120
+ }
21121
+ } else if (value !== void 0) {
21122
+ out.push(path);
21123
+ }
21124
+ };
21125
+ walk2(node, prefix);
21126
+ return out;
21127
+ }
21128
+ deleteFromBundle(lng, key) {
21129
+ const bundle = this.i18n.getResourceBundle(lng, this.ns) ?? {};
21130
+ deleteNested(bundle, key);
21131
+ this.i18n.addResourceBundle(lng, this.ns, bundle, true, true);
21132
+ }
21133
+ restore(draft) {
21134
+ if (draft.created) {
21135
+ this.deleteFromBundle(draft.lng, draft.key);
21136
+ } else {
21137
+ this.i18n.addResource(draft.lng, this.ns, draft.key, draft.original);
21138
+ }
21139
+ }
21140
+ notify() {
21141
+ if (this.batchDepth > 0) return;
21142
+ this.i18n.emit("languageChanged", this.i18n.language);
21143
+ this.bump();
21144
+ }
21002
21145
  bump() {
21003
21146
  this.version += 1;
21004
21147
  this.cachedSnapshot = { drafts: this.drafts, version: this.version };
@@ -21228,9 +21371,18 @@ function isAdminRoute() {
21228
21371
  function isAdminDeploymentView() {
21229
21372
  return isAdminRoute() && new URLSearchParams(window.location.search).has("deployment");
21230
21373
  }
21374
+ function adminViewUrl(view) {
21375
+ return `${SETTO_BASE}?view=${encodeURIComponent(view)}`;
21376
+ }
21377
+ function currentAdminView() {
21378
+ return new URLSearchParams(window.location.search).get("view");
21379
+ }
21380
+ function isAdminPanelView() {
21381
+ return isAdminDeploymentView() || isAdminRoute() && currentAdminView() !== null;
21382
+ }
21231
21383
  const FAB_SIZE = 52;
21232
21384
  const FAB_INSET = 16;
21233
- const STYLE_ID$1 = "setto-edit-layout";
21385
+ const STYLE_ID$3 = "setto-edit-layout";
21234
21386
  const HTML_CLASS = "setto-edit-mode";
21235
21387
  function useSettoDocumentLayout(active) {
21236
21388
  useEffect(() => {
@@ -21239,10 +21391,10 @@ function useSettoDocumentLayout(active) {
21239
21391
  html.classList.add(HTML_CLASS);
21240
21392
  html.style.setProperty("--setto-fab-size", `${FAB_SIZE}px`);
21241
21393
  html.style.setProperty("--setto-fab-inset", `${FAB_INSET}px`);
21242
- let styleEl = document.getElementById(STYLE_ID$1);
21394
+ let styleEl = document.getElementById(STYLE_ID$3);
21243
21395
  if (!styleEl) {
21244
21396
  styleEl = document.createElement("style");
21245
- styleEl.id = STYLE_ID$1;
21397
+ styleEl.id = STYLE_ID$3;
21246
21398
  styleEl.textContent = `
21247
21399
  html.setto-edit-mode {
21248
21400
  width: 100%;
@@ -21888,10 +22040,10 @@ function EditOnboardingDialog() {
21888
22040
  ]
21889
22041
  }
21890
22042
  ),
21891
- /* @__PURE__ */ jsx("button", { type: "button", onClick: close, style: primaryBtnStyle$1, children: "Forstått" })
22043
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: close, style: primaryBtnStyle$2, children: "Forstått" })
21892
22044
  ] });
21893
22045
  }
21894
- const primaryBtnStyle$1 = {
22046
+ const primaryBtnStyle$2 = {
21895
22047
  background: "#640AFF",
21896
22048
  color: "#fff",
21897
22049
  border: "none",
@@ -22130,14 +22282,14 @@ function PublishDialog({
22130
22282
  return /* @__PURE__ */ jsx(EditOverlay, { onClose: void 0, children: isReady ? /* @__PURE__ */ jsxs(Fragment, { children: [
22131
22283
  /* @__PURE__ */ jsx("h2", { style: { margin: "0 0 12px", fontSize: 18, fontWeight: 600 }, children: "Publisert" }),
22132
22284
  /* @__PURE__ */ jsx("p", { style: { margin: "0 0 20px", fontSize: 14, lineHeight: 1.5, color: "#444" }, children: "Endringene dine er live." }),
22133
- /* @__PURE__ */ jsx("button", { type: "button", onClick: onComplete, style: primaryBtnStyle, children: "OK" })
22285
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onComplete, style: primaryBtnStyle$1, children: "OK" })
22134
22286
  ] }) : errored ? /* @__PURE__ */ jsxs(Fragment, { children: [
22135
22287
  /* @__PURE__ */ jsx("h2", { style: { margin: "0 0 12px", fontSize: 18, fontWeight: 600, color: "#dc2626" }, children: "Publisering feilet" }),
22136
22288
  /* @__PURE__ */ jsxs("p", { style: { margin: "0 0 20px", fontSize: 14, lineHeight: 1.5, color: "#444" }, children: [
22137
22289
  progressStatus === "error" ? "Bygg feilet." : "Avbrutt.",
22138
22290
  row?.error_message ? ` ${row.error_message}` : ""
22139
22291
  ] }),
22140
- /* @__PURE__ */ jsx("button", { type: "button", onClick: onComplete, style: secondaryBtnStyle, children: "OK" })
22292
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onComplete, style: secondaryBtnStyle$1, children: "OK" })
22141
22293
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
22142
22294
  /* @__PURE__ */ jsx("h2", { style: { margin: "0 0 16px", fontSize: 16, fontWeight: 600 }, children: "Publiserer endringer" }),
22143
22295
  /* @__PURE__ */ jsx("div", { style: { marginBottom: 20 }, children: /* @__PURE__ */ jsx(
@@ -22147,10 +22299,10 @@ function PublishDialog({
22147
22299
  buildingElapsed: isBuilding ? elapsed : void 0
22148
22300
  }
22149
22301
  ) }),
22150
- /* @__PURE__ */ jsx("button", { type: "button", onClick: onClose, style: secondaryBtnStyle, children: "Lukk" })
22302
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onClose, style: secondaryBtnStyle$1, children: "Lukk" })
22151
22303
  ] }) });
22152
22304
  }
22153
- const primaryBtnStyle = {
22305
+ const primaryBtnStyle$1 = {
22154
22306
  background: "#640AFF",
22155
22307
  color: "#fff",
22156
22308
  border: "none",
@@ -22161,8 +22313,8 @@ const primaryBtnStyle = {
22161
22313
  cursor: "pointer",
22162
22314
  width: "100%"
22163
22315
  };
22164
- const secondaryBtnStyle = {
22165
- ...primaryBtnStyle,
22316
+ const secondaryBtnStyle$1 = {
22317
+ ...primaryBtnStyle$1,
22166
22318
  background: "transparent",
22167
22319
  color: "#666",
22168
22320
  border: "1px solid #ddd"
@@ -22216,709 +22368,404 @@ function useEditBaseline() {
22216
22368
  };
22217
22369
  }, [session, store, config.siteId]);
22218
22370
  }
22219
- function SettoMark({
22220
- size = 24,
22221
- letterColor = "#F2EBDF",
22222
- dotColor = "#C4502A",
22223
- style
22224
- }) {
22225
- return /* @__PURE__ */ jsxs(
22226
- "svg",
22227
- {
22228
- xmlns: "http://www.w3.org/2000/svg",
22229
- viewBox: "0 0 64 64",
22230
- width: size,
22231
- height: size,
22232
- "aria-hidden": true,
22233
- style: { display: "block", ...style },
22234
- children: [
22235
- /* @__PURE__ */ jsx(
22236
- "text",
22237
- {
22238
- x: "32",
22239
- y: "46",
22240
- textAnchor: "middle",
22241
- fontFamily: "Georgia, serif",
22242
- fontStyle: "italic",
22243
- fontSize: "42",
22244
- fill: letterColor,
22245
- children: "S"
22246
- }
22247
- ),
22248
- /* @__PURE__ */ jsx("circle", { cx: "49", cy: "17", r: "5", fill: dotColor })
22249
- ]
22250
- }
22251
- );
22371
+ const shellStyle$1 = {
22372
+ minHeight: "100dvh",
22373
+ background: "#0a0a0d",
22374
+ color: "#fff",
22375
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
22376
+ };
22377
+ const headerStyle = {
22378
+ display: "flex",
22379
+ alignItems: "center",
22380
+ justifyContent: "space-between",
22381
+ padding: "16px 24px",
22382
+ borderBottom: "1px solid #1c1c22"
22383
+ };
22384
+ const mainStyle = {
22385
+ maxWidth: 720,
22386
+ margin: "0 auto",
22387
+ padding: 24,
22388
+ display: "flex",
22389
+ flexDirection: "column",
22390
+ gap: 16
22391
+ };
22392
+ const cardStyle = {
22393
+ background: "#15151a",
22394
+ borderRadius: 12,
22395
+ padding: 20,
22396
+ display: "flex",
22397
+ flexDirection: "column",
22398
+ gap: 12
22399
+ };
22400
+ const panelStyle = {
22401
+ background: "#1a1a20",
22402
+ borderRadius: 8,
22403
+ padding: 14,
22404
+ display: "flex",
22405
+ flexDirection: "column",
22406
+ gap: 10
22407
+ };
22408
+ const rowStyle = {
22409
+ display: "flex",
22410
+ alignItems: "center",
22411
+ gap: 10,
22412
+ background: "#1a1a20",
22413
+ padding: "6px 10px",
22414
+ borderRadius: 6
22415
+ };
22416
+ const mutedStyle = {
22417
+ color: "#888",
22418
+ fontSize: 13,
22419
+ margin: 0
22420
+ };
22421
+ const errStyle = {
22422
+ color: "#ff7a7a",
22423
+ fontSize: 13,
22424
+ margin: 0
22425
+ };
22426
+ const subheadingStyle = {
22427
+ margin: 0,
22428
+ fontSize: 14,
22429
+ color: "#aaa"
22430
+ };
22431
+ const primaryBtnStyle = {
22432
+ display: "inline-block",
22433
+ background: "#640AFF",
22434
+ color: "#fff",
22435
+ border: "none",
22436
+ borderRadius: 6,
22437
+ padding: "10px 14px",
22438
+ fontSize: 14,
22439
+ fontWeight: 500,
22440
+ textDecoration: "none",
22441
+ cursor: "pointer"
22442
+ };
22443
+ const secondaryBtnStyle = {
22444
+ background: "transparent",
22445
+ color: "#bda6ff",
22446
+ border: "1px solid #2a2a32",
22447
+ borderRadius: 6,
22448
+ padding: "6px 10px",
22449
+ fontSize: 12,
22450
+ cursor: "pointer"
22451
+ };
22452
+ const dangerBtnStyle = {
22453
+ alignSelf: "flex-start",
22454
+ background: "transparent",
22455
+ color: "#ff7a7a",
22456
+ border: "1px solid #4a3030",
22457
+ borderRadius: 6,
22458
+ padding: "8px 14px",
22459
+ fontSize: 13,
22460
+ cursor: "pointer"
22461
+ };
22462
+ const inputStyle$2 = {
22463
+ width: "100%",
22464
+ boxSizing: "border-box",
22465
+ background: "#0f0f13",
22466
+ color: "#fff",
22467
+ border: "1px solid #2a2a32",
22468
+ borderRadius: 6,
22469
+ padding: "8px 10px",
22470
+ fontSize: 13,
22471
+ fontFamily: "inherit"
22472
+ };
22473
+ const tabBarStyle = {
22474
+ display: "flex",
22475
+ gap: 4,
22476
+ borderBottom: "1px solid #1c1c22",
22477
+ paddingBottom: 0
22478
+ };
22479
+ function tabStyle(active) {
22480
+ return {
22481
+ background: "transparent",
22482
+ border: "none",
22483
+ borderBottom: active ? "2px solid #640AFF" : "2px solid transparent",
22484
+ color: active ? "#fff" : "#888",
22485
+ padding: "8px 12px",
22486
+ fontSize: 13,
22487
+ fontWeight: 500,
22488
+ cursor: "pointer",
22489
+ marginBottom: -1
22490
+ };
22252
22491
  }
22253
- const BLEKK = "#211711";
22254
- const CREMA = "#F2EBDF";
22255
- const TERRACOTTA = "#C4502A";
22256
- function EditToolbar() {
22257
- const { store, themeStore, assetStore, api, config, supabase } = useSetto();
22258
- const { i18n } = useTranslation();
22259
- useEditBaseline();
22260
- const snap = useSyncExternalStore(
22261
- (cb) => store?.subscribe(() => cb()) ?? (() => {
22262
- }),
22263
- () => store?.snapshot() ?? EMPTY_I18N_STORE_SNAPSHOT,
22264
- () => EMPTY_I18N_STORE_SNAPSHOT
22265
- );
22266
- useSyncExternalStore(
22267
- (cb) => themeStore?.subscribe(() => cb()) ?? (() => {
22268
- }),
22269
- () => themeStore?.snapshot().version ?? 0,
22270
- () => 0
22271
- );
22272
- useSyncExternalStore(
22273
- (cb) => assetStore?.subscribe(() => cb()) ?? (() => {
22274
- }),
22275
- () => assetStore?.snapshot().version ?? 0,
22276
- () => 0
22277
- );
22278
- const [publishing, setPublishing] = useState(false);
22279
- const [activeDeployment, setActiveDeployment] = useState(null);
22280
- const [publishDialogOpen, setPublishDialogOpen] = useState(false);
22492
+ function FormsView({ siteId }) {
22493
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
22494
+ /* @__PURE__ */ jsx(RecipientsPanel, { siteId }),
22495
+ /* @__PURE__ */ jsx(SubmissionList, { siteId })
22496
+ ] });
22497
+ }
22498
+ function RecipientsPanel({ siteId }) {
22499
+ const { api } = useSetto();
22500
+ const [settings, setSettings] = useState(null);
22501
+ const [editing, setEditing] = useState(false);
22502
+ const [draft, setDraft] = useState("");
22503
+ const [saving, setSaving] = useState(false);
22281
22504
  const [error, setError] = useState(null);
22282
- const [publishedNotice, setPublishedNotice] = useState(false);
22283
- const [menuOpen, setMenuOpen] = useState(false);
22284
- const [expanded, setExpanded] = useState(false);
22285
- const menuRef = useRef(null);
22286
- const publishedNoticeTimer = useRef(null);
22287
- const { row: deploymentRow, status: deploymentStatus } = useDeploymentStatus(
22288
- supabase,
22289
- activeDeployment
22290
- );
22291
22505
  useEffect(() => {
22292
22506
  let cancelled = false;
22293
- const resume = async () => {
22294
- try {
22295
- await api.expireStaleDeployments(config.siteId);
22296
- } catch {
22297
- }
22298
- const id = await fetchInProgressDeploymentId(supabase, config.siteId);
22299
- if (!cancelled && id) {
22300
- setActiveDeployment((current) => current ?? id);
22301
- }
22302
- };
22303
- void resume();
22507
+ api.forms.getSettings(siteId).then((s) => {
22508
+ if (!cancelled) setSettings(s);
22509
+ }).catch((err) => {
22510
+ if (!cancelled) setError(err instanceof Error ? err.message : "Kunne ikke hente innstillinger");
22511
+ });
22304
22512
  return () => {
22305
22513
  cancelled = true;
22306
22514
  };
22307
- }, [api, supabase, config.siteId]);
22308
- const textDraftCount = snap.drafts.size;
22309
- const themeDraftCount = themeStore?.size() ?? 0;
22310
- const assetDraftCount = assetStore?.size() ?? 0;
22311
- const draftCount = textDraftCount + themeDraftCount + assetDraftCount;
22312
- const hasDrafts = draftCount > 0;
22313
- const showToolbarProgress = activeDeployment !== null && !publishDialogOpen && deploymentRow !== null && isDeploymentInProgress(deploymentRow.status) && !isDeploymentStale(deploymentRow);
22314
- const showPublishedNotice = publishedNotice && !publishDialogOpen;
22315
- useEffect(() => {
22316
- if (error || showToolbarProgress || showPublishedNotice) {
22317
- setExpanded(true);
22515
+ }, [api, siteId]);
22516
+ const startEditing = () => {
22517
+ setDraft((settings?.recipients ?? []).join("\n"));
22518
+ setEditing(true);
22519
+ setError(null);
22520
+ };
22521
+ const save = async (recipients) => {
22522
+ setSaving(true);
22523
+ setError(null);
22524
+ try {
22525
+ const next = await api.forms.saveSettings(siteId, recipients);
22526
+ setSettings(next);
22527
+ setEditing(false);
22528
+ } catch (err) {
22529
+ setError(err instanceof Error ? err.message : "Kunne ikke lagre");
22530
+ } finally {
22531
+ setSaving(false);
22318
22532
  }
22319
- }, [error, showToolbarProgress, showPublishedNotice]);
22320
- const toggleExpanded = () => {
22321
- setExpanded((prev) => {
22322
- const next = !prev;
22323
- if (!next) setMenuOpen(false);
22324
- return next;
22325
- });
22326
22533
  };
22534
+ const parseDraft = () => draft.split(/[\n,;]+/).map((s) => s.trim()).filter(Boolean);
22535
+ return /* @__PURE__ */ jsxs("section", { style: panelStyle, children: [
22536
+ /* @__PURE__ */ jsx("h3", { style: { margin: 0, fontSize: 15 }, children: "Hvem får e-post?" }),
22537
+ error ? /* @__PURE__ */ jsx("p", { style: errStyle, children: error }) : null,
22538
+ settings === null && !error ? /* @__PURE__ */ jsx("p", { style: mutedStyle, children: "Laster …" }) : null,
22539
+ settings && !editing ? /* @__PURE__ */ jsxs(Fragment, { children: [
22540
+ /* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: 14 }, children: settings.effectiveRecipients.length > 0 ? settings.effectiveRecipients.join(", ") : /* @__PURE__ */ jsx("span", { style: { color: "#ff7a7a" }, children: "Ingen mottakere — innsendinger lagres, men ingen får beskjed." }) }),
22541
+ /* @__PURE__ */ jsx("p", { style: mutedStyle, children: settings.recipients.length === 0 ? "Standard: eierne av siden. Hver innsending lagres også her i historikken." : `Standard er ${settings.defaultRecipients.join(", ") || "ingen"}.` }),
22542
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
22543
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: startEditing, style: secondaryBtnStyle, children: "Endre mottakere" }),
22544
+ settings.recipients.length > 0 ? /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void save([]), disabled: saving, style: secondaryBtnStyle, children: "Bruk standard" }) : null
22545
+ ] })
22546
+ ] }) : null,
22547
+ settings && editing ? /* @__PURE__ */ jsxs(Fragment, { children: [
22548
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 6 }, children: [
22549
+ /* @__PURE__ */ jsx("span", { style: mutedStyle, children: "E-postadresser, én per linje. Tomt betyr standard (eierne)." }),
22550
+ /* @__PURE__ */ jsx(
22551
+ "textarea",
22552
+ {
22553
+ value: draft,
22554
+ rows: 3,
22555
+ onChange: (e) => setDraft(e.target.value),
22556
+ style: { ...inputStyle$2, resize: "vertical" },
22557
+ placeholder: settings.defaultRecipients.join("\n"),
22558
+ autoFocus: true
22559
+ }
22560
+ )
22561
+ ] }),
22562
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
22563
+ /* @__PURE__ */ jsx(
22564
+ "button",
22565
+ {
22566
+ type: "button",
22567
+ onClick: () => void save(parseDraft()),
22568
+ disabled: saving,
22569
+ style: { ...primaryBtnStyle, padding: "8px 12px", fontSize: 13 },
22570
+ children: saving ? "Lagrer …" : "Lagre"
22571
+ }
22572
+ ),
22573
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setEditing(false), disabled: saving, style: secondaryBtnStyle, children: "Avbryt" })
22574
+ ] })
22575
+ ] }) : null
22576
+ ] });
22577
+ }
22578
+ const PAGE = 50;
22579
+ function SubmissionList({ siteId }) {
22580
+ const { supabase, api } = useSetto();
22581
+ const [rows, setRows] = useState(null);
22582
+ const [error, setError] = useState(null);
22583
+ const [openId, setOpenId] = useState(null);
22584
+ const [hasMore, setHasMore] = useState(false);
22585
+ const [loadingMore, setLoadingMore] = useState(false);
22327
22586
  useEffect(() => {
22587
+ let cancelled = false;
22588
+ supabase.from("form_submissions").select("*").eq("site_id", siteId).order("created_at", { ascending: false }).limit(PAGE).then(({ data, error: err }) => {
22589
+ if (cancelled) return;
22590
+ if (err) setError(err.message);
22591
+ const list = data ?? [];
22592
+ setRows(list);
22593
+ setHasMore(list.length === PAGE);
22594
+ });
22595
+ const channel = supabase.channel(`setto-site-forms-${siteId}`).on(
22596
+ "postgres_changes",
22597
+ { event: "*", schema: "public", table: "form_submissions", filter: `site_id=eq.${siteId}` },
22598
+ (payload) => {
22599
+ const next = payload.new;
22600
+ if (!next || !next.id) return;
22601
+ setRows((prev) => {
22602
+ if (!prev) return [next];
22603
+ const without = prev.filter((r) => r.id !== next.id);
22604
+ return [next, ...without].sort((a, b) => b.created_at.localeCompare(a.created_at));
22605
+ });
22606
+ }
22607
+ ).subscribe();
22328
22608
  return () => {
22329
- if (publishedNoticeTimer.current) clearTimeout(publishedNoticeTimer.current);
22609
+ cancelled = true;
22610
+ supabase.removeChannel(channel);
22330
22611
  };
22331
- }, []);
22332
- useEffect(() => {
22333
- if (!activeDeployment || !deploymentRow) return;
22334
- const { status } = deploymentRow;
22335
- if (status === "ready") {
22336
- if (publishDialogOpen) return;
22337
- setPublishedNotice(true);
22338
- setActiveDeployment(null);
22339
- if (publishedNoticeTimer.current) clearTimeout(publishedNoticeTimer.current);
22340
- publishedNoticeTimer.current = setTimeout(() => setPublishedNotice(false), 6e3);
22341
- return;
22342
- }
22343
- if (publishDialogOpen) return;
22344
- if (status === "error" || status === "canceled") {
22345
- setError(
22346
- status === "canceled" ? "Publisering avbrutt." : deploymentRow.error_message?.includes("webhook") ? "Publisering utløpt (ingen webhook)." : "Publisering feilet."
22347
- );
22348
- setActiveDeployment(null);
22612
+ }, [supabase, siteId]);
22613
+ const loadMore = async () => {
22614
+ if (!rows || rows.length === 0) return;
22615
+ setLoadingMore(true);
22616
+ const oldest = rows[rows.length - 1].created_at;
22617
+ const { data, error: err } = await supabase.from("form_submissions").select("*").eq("site_id", siteId).lt("created_at", oldest).order("created_at", { ascending: false }).limit(PAGE);
22618
+ setLoadingMore(false);
22619
+ if (err) {
22620
+ setError(err.message);
22349
22621
  return;
22350
22622
  }
22351
- if (isDeploymentStale(deploymentRow)) {
22352
- setError("Publisering utløpt (ingen webhook).");
22353
- setActiveDeployment(null);
22354
- }
22355
- }, [activeDeployment, deploymentRow, publishDialogOpen]);
22356
- useEffect(() => {
22357
- if (!activeDeployment) return;
22358
- const syncActiveDeployment = async () => {
22359
- const { data } = await supabase.from("deployments").select("status").eq("id", activeDeployment).maybeSingle();
22360
- if (!data) {
22361
- setActiveDeployment(null);
22362
- return;
22363
- }
22364
- const status = data.status;
22365
- if (status === "ready") {
22366
- if (!publishDialogOpen) {
22367
- setPublishedNotice(true);
22368
- if (publishedNoticeTimer.current) clearTimeout(publishedNoticeTimer.current);
22369
- publishedNoticeTimer.current = setTimeout(() => setPublishedNotice(false), 6e3);
22370
- }
22371
- setActiveDeployment(null);
22372
- return;
22373
- }
22374
- if (!isDeploymentInProgress(status)) {
22375
- if (status === "canceled") setError("Publisering avbrutt.");
22376
- else if (status === "error") setError("Publisering feilet.");
22377
- setActiveDeployment(null);
22378
- }
22379
- };
22380
- const onVisible = () => {
22381
- if (document.visibilityState === "visible") void syncActiveDeployment();
22382
- };
22383
- window.addEventListener("focus", syncActiveDeployment);
22384
- document.addEventListener("visibilitychange", onVisible);
22385
- return () => {
22386
- window.removeEventListener("focus", syncActiveDeployment);
22387
- document.removeEventListener("visibilitychange", onVisible);
22388
- };
22389
- }, [activeDeployment, publishDialogOpen, supabase]);
22390
- const languages = i18n.languages?.length ? Array.from(new Set(i18n.languages)) : ["no", "en"];
22391
- useEffect(() => {
22392
- if (!menuOpen) return;
22393
- const close = (e) => {
22394
- if (menuRef.current && !menuRef.current.contains(e.target)) {
22395
- setMenuOpen(false);
22396
- }
22397
- };
22398
- document.addEventListener("mousedown", close);
22399
- return () => document.removeEventListener("mousedown", close);
22400
- }, [menuOpen]);
22401
- const publish = async () => {
22402
- if (!store) return;
22403
- setPublishing(true);
22404
- setPublishDialogOpen(true);
22405
- setError(null);
22406
- setPublishedNotice(false);
22623
+ const more = data ?? [];
22624
+ setRows((prev) => [...prev ?? [], ...more]);
22625
+ setHasMore(more.length === PAGE);
22626
+ };
22627
+ const remove2 = async (id) => {
22628
+ if (!window.confirm("Slette denne henvendelsen? Dette kan ikke angres.")) return;
22407
22629
  try {
22408
- const bundles = store.serialiseBundles();
22409
- const files = Object.entries(bundles).map(([lng, data]) => ({
22410
- path: pathForLang(config.siteId, lng, languages),
22411
- content: JSON.stringify(data, null, 2) + "\n"
22412
- }));
22413
- if (themeStore && themeDraftCount > 0) {
22414
- files.push({
22415
- path: pathForTheme(config.siteId, config.themePath),
22416
- content: JSON.stringify(themeStore.serialise(), null, 2) + "\n"
22417
- });
22418
- }
22419
- if (assetStore && assetDraftCount > 0) {
22420
- const result2 = await api.publish(config.siteId, files, {
22421
- assets: assetStore.pendingAssets()
22422
- });
22423
- store.commit();
22424
- themeStore?.commit();
22425
- assetStore.commit();
22426
- let deploymentId2 = result2.deploymentId;
22427
- if (!deploymentId2 && result2.trackingDeferred) {
22428
- deploymentId2 = await waitForDeploymentByCommit(
22429
- supabase,
22430
- config.siteId,
22431
- result2.commitSha
22432
- );
22433
- }
22434
- if (deploymentId2) {
22435
- setActiveDeployment(deploymentId2);
22436
- } else {
22437
- setPublishDialogOpen(false);
22438
- setPublishedNotice(true);
22439
- }
22440
- return;
22441
- }
22442
- const result = await api.publish(config.siteId, files);
22443
- store.commit();
22444
- themeStore?.commit();
22445
- assetStore?.commit();
22446
- let deploymentId = result.deploymentId;
22447
- if (!deploymentId && result.trackingDeferred) {
22448
- deploymentId = await waitForDeploymentByCommit(
22449
- supabase,
22450
- config.siteId,
22451
- result.commitSha
22452
- );
22453
- }
22454
- if (deploymentId) {
22455
- setActiveDeployment(deploymentId);
22456
- } else {
22457
- setPublishDialogOpen(false);
22458
- setPublishedNotice(true);
22459
- }
22630
+ await api.forms.deleteSubmission(siteId, id);
22631
+ setRows((prev) => (prev ?? []).filter((r) => r.id !== id));
22632
+ if (openId === id) setOpenId(null);
22460
22633
  } catch (err) {
22461
- setError(err instanceof Error ? err.message : "unknown error");
22462
- setPublishDialogOpen(false);
22463
- } finally {
22464
- setPublishing(false);
22634
+ setError(err instanceof Error ? err.message : "Kunne ikke slette");
22465
22635
  }
22466
22636
  };
22467
- const completePublish = () => {
22468
- setPublishDialogOpen(false);
22469
- setActiveDeployment(null);
22470
- setPublishedNotice(false);
22471
- if (publishedNoticeTimer.current) clearTimeout(publishedNoticeTimer.current);
22472
- };
22473
- const dismissPublishedNotice = () => {
22474
- setPublishedNotice(false);
22475
- if (publishedNoticeTimer.current) clearTimeout(publishedNoticeTimer.current);
22476
- };
22477
- const cancelDrafts = () => {
22478
- setMenuOpen(false);
22479
- store?.revertAll();
22480
- themeStore?.revertAll();
22481
- assetStore?.revertAll();
22482
- setError(null);
22483
- setExpanded(false);
22484
- };
22485
- const signOut = () => {
22486
- setMenuOpen(false);
22487
- void supabase.auth.signOut();
22488
- };
22489
- const goToHistory = () => {
22490
- setMenuOpen(false);
22491
- window.location.href = SETTO_BASE;
22492
- };
22493
- return /* @__PURE__ */ jsxs(Fragment, { children: [
22637
+ if (!rows) return /* @__PURE__ */ jsx("p", { style: mutedStyle, children: "Laster henvendelser …" });
22638
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
22639
+ /* @__PURE__ */ jsx("h3", { style: subheadingStyle, children: "Henvendelser" }),
22640
+ error ? /* @__PURE__ */ jsx("p", { style: errStyle, children: error }) : null,
22641
+ rows.length === 0 ? /* @__PURE__ */ jsx("p", { style: mutedStyle, children: "Ingen henvendelser ennå. Når noen sender inn et skjema på siden, dukker det opp her og i e-posten." }) : /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }, children: rows.map((row) => /* @__PURE__ */ jsx(
22642
+ SubmissionItem,
22643
+ {
22644
+ row,
22645
+ open: openId === row.id,
22646
+ onToggle: () => setOpenId(openId === row.id ? null : row.id),
22647
+ onDelete: () => void remove2(row.id)
22648
+ },
22649
+ row.id
22650
+ )) }),
22651
+ hasMore ? /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void loadMore(), disabled: loadingMore, style: secondaryBtnStyle, children: loadingMore ? "Laster …" : "Vis eldre" }) : null
22652
+ ] });
22653
+ }
22654
+ function SubmissionItem({
22655
+ row,
22656
+ open,
22657
+ onToggle,
22658
+ onDelete
22659
+ }) {
22660
+ const fields = submissionFields(row);
22661
+ const sender = senderOf(fields);
22662
+ const title = row.meta.title ?? row.form_id;
22663
+ return /* @__PURE__ */ jsxs("li", { children: [
22494
22664
  /* @__PURE__ */ jsxs(
22495
- "div",
22665
+ "button",
22496
22666
  {
22497
- role: "toolbar",
22498
- "aria-label": "Setto editor",
22499
- style: {
22500
- ...containerStyleFor(hasDrafts),
22501
- ...expanded ? expandedContainerStyle : {}
22502
- },
22667
+ type: "button",
22668
+ onClick: onToggle,
22669
+ "aria-expanded": open,
22670
+ style: { ...rowStyle, width: "100%", textAlign: "left", color: "inherit", border: "none", cursor: "pointer", font: "inherit" },
22503
22671
  children: [
22504
- /* @__PURE__ */ jsx(
22505
- "button",
22506
- {
22507
- type: "button",
22508
- "aria-label": expanded ? "Lukk Setto-meny" : "Åpne Setto-meny",
22509
- "aria-expanded": expanded,
22510
- title: "Setto",
22511
- style: markButtonStyle,
22512
- onClick: toggleExpanded,
22513
- children: /* @__PURE__ */ jsx(SettoMark, { size: 28 })
22514
- }
22515
- ),
22516
- expanded ? /* @__PURE__ */ jsxs("div", { style: contentStyle, children: [
22517
- error ? /* @__PURE__ */ jsx("span", { style: errorStyle, children: error }) : null,
22518
- hasDrafts && !publishing && activeDeployment === null ? /* @__PURE__ */ jsx("button", { type: "button", onClick: publish, style: publishBtnStyle, children: "Publiser" }) : null,
22519
- showPublishedNotice ? /* @__PURE__ */ jsxs(
22520
- "button",
22521
- {
22522
- type: "button",
22523
- onClick: dismissPublishedNotice,
22524
- style: publishedNoticeStyle,
22525
- title: "Lukk",
22526
- children: [
22527
- /* @__PURE__ */ jsx(PublishedCheckIcon, {}),
22528
- "Publisert"
22529
- ]
22530
- }
22531
- ) : null,
22532
- showToolbarProgress && deploymentRow ? /* @__PURE__ */ jsx(
22533
- PublishProgressBar,
22534
- {
22535
- status: deploymentRow.status,
22536
- compact: true,
22537
- href: `${SETTO_BASE}?deployment=${encodeURIComponent(activeDeployment)}`
22538
- }
22539
- ) : null,
22540
- !hasDrafts && !error && !showPublishedNotice && !showToolbarProgress && !publishing ? /* @__PURE__ */ jsx("span", { style: emptyStateStyle, children: "Ingen endringer" }) : null,
22541
- /* @__PURE__ */ jsxs("div", { ref: menuRef, style: { position: "relative" }, children: [
22542
- /* @__PURE__ */ jsx(
22543
- "button",
22544
- {
22545
- type: "button",
22546
- onClick: () => setMenuOpen((o) => !o),
22547
- "aria-label": "Meny",
22548
- "aria-expanded": menuOpen,
22549
- style: {
22550
- ...menuBtnStyle,
22551
- background: menuOpen ? "rgba(242, 235, 223, 0.16)" : "transparent"
22552
- },
22553
- onMouseEnter: (e) => {
22554
- e.currentTarget.style.background = "rgba(242, 235, 223, 0.16)";
22555
- },
22556
- onMouseLeave: (e) => {
22557
- e.currentTarget.style.background = menuOpen ? "rgba(242, 235, 223, 0.16)" : "transparent";
22558
- },
22559
- children: "⋮"
22560
- }
22561
- ),
22562
- menuOpen ? /* @__PURE__ */ jsxs("div", { style: dropdownStyle, role: "menu", children: [
22563
- /* @__PURE__ */ jsx(
22564
- MenuItem,
22565
- {
22566
- onClick: cancelDrafts,
22567
- icon: /* @__PURE__ */ jsx(UndoIcon, {}),
22568
- disabled: !hasDrafts,
22569
- children: "Avbryt"
22570
- }
22571
- ),
22572
- /* @__PURE__ */ jsx(MenuItem, { onClick: goToHistory, icon: /* @__PURE__ */ jsx(HistoryIcon, {}), children: "Historikk" }),
22573
- /* @__PURE__ */ jsx(MenuItem, { onClick: signOut, icon: /* @__PURE__ */ jsx(LogOutIcon, {}), children: "Logg ut" })
22574
- ] }) : null
22672
+ /* @__PURE__ */ jsx("span", { style: statusDotStyle(row.email_status), title: emailStatusLabel(row) }),
22673
+ /* @__PURE__ */ jsxs("span", { style: { flex: 1, fontSize: 13, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: [
22674
+ sender ?? /* @__PURE__ */ jsx("span", { style: { color: "#aaa" }, children: "Uten navn" }),
22675
+ /* @__PURE__ */ jsxs("span", { style: { color: "#888" }, children: [
22676
+ " · ",
22677
+ title
22575
22678
  ] })
22576
- ] }) : null
22679
+ ] }),
22680
+ /* @__PURE__ */ jsx("time", { style: { fontSize: 11, color: "#888", flexShrink: 0 }, children: formatWhen(row.created_at) })
22577
22681
  ]
22578
22682
  }
22579
22683
  ),
22580
- /* @__PURE__ */ jsx(EditOnboardingDialog, {}),
22581
- publishDialogOpen && (publishing || activeDeployment !== null) ? /* @__PURE__ */ jsx(
22582
- PublishDialog,
22583
- {
22584
- deploymentId: activeDeployment ?? "",
22585
- row: deploymentRow,
22586
- status: deploymentStatus ?? "pending",
22587
- publishing,
22588
- onClose: () => setPublishDialogOpen(false),
22589
- onComplete: completePublish
22590
- }
22591
- ) : null
22684
+ open ? /* @__PURE__ */ jsxs("div", { style: { ...panelStyle, marginTop: 4, gap: 12 }, children: [
22685
+ /* @__PURE__ */ jsx("dl", { style: { margin: 0, display: "grid", gap: 10 }, children: fields.map((f) => /* @__PURE__ */ jsxs("div", { children: [
22686
+ /* @__PURE__ */ jsx("dt", { style: { fontSize: 11, color: "#888", textTransform: "uppercase", letterSpacing: "0.04em" }, children: f.label }),
22687
+ /* @__PURE__ */ jsx("dd", { style: { margin: 0, fontSize: 14, whiteSpace: "pre-wrap", wordBreak: "break-word" }, children: f.value === "" ? /* @__PURE__ */ jsx("span", { style: { color: "#666" }, children: "—" }) : f.value })
22688
+ ] }, f.name)) }),
22689
+ /* @__PURE__ */ jsxs("p", { style: mutedStyle, children: [
22690
+ emailStatusLabel(row),
22691
+ row.recipients.length > 0 ? ` til ${row.recipients.join(", ")}` : "",
22692
+ row.email_error ? ` — ${row.email_error}` : "",
22693
+ row.meta.page ? /* @__PURE__ */ jsxs(Fragment, { children: [
22694
+ " · ",
22695
+ /* @__PURE__ */ jsx("a", { href: row.meta.page, style: { color: "#bda6ff" }, target: "_blank", rel: "noreferrer", children: "Sendt fra siden" })
22696
+ ] }) : null
22697
+ ] }),
22698
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onDelete, style: dangerBtnStyle, children: "Slett henvendelse" })
22699
+ ] }) : null
22592
22700
  ] });
22593
22701
  }
22594
- const EASE_OUT_EXPO = "cubic-bezier(0.16, 1, 0.3, 1)";
22595
- const FAB_SHADOW = "0 8px 24px rgba(33, 23, 17, 0.35), 0 2px 6px rgba(33, 23, 17, 0.2)";
22596
- function containerStyleFor(hasDrafts) {
22597
- return {
22598
- display: "inline-flex",
22599
- alignItems: "center",
22600
- height: FAB_SIZE,
22601
- width: FAB_SIZE,
22602
- padding: 0,
22603
- background: BLEKK,
22604
- color: CREMA,
22605
- borderRadius: FAB_SIZE / 2,
22606
- boxShadow: hasDrafts ? `0 0 0 2px ${TERRACOTTA}, ${FAB_SHADOW}` : FAB_SHADOW,
22607
- fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
22608
- fontSize: 13,
22609
- overflow: "visible",
22610
- transition: `width 500ms ${EASE_OUT_EXPO}, padding 500ms ${EASE_OUT_EXPO}, box-shadow 300ms ${EASE_OUT_EXPO}`
22611
- };
22612
- }
22613
- const expandedContainerStyle = {
22614
- width: "auto",
22615
- paddingRight: 10
22616
- };
22617
- const markButtonStyle = {
22618
- display: "inline-flex",
22619
- alignItems: "center",
22620
- justifyContent: "center",
22621
- flexShrink: 0,
22622
- width: FAB_SIZE,
22623
- height: FAB_SIZE,
22624
- background: "transparent",
22625
- border: "none",
22626
- padding: 0,
22627
- cursor: "pointer",
22628
- color: "inherit"
22629
- };
22630
- const contentStyle = {
22631
- display: "flex",
22632
- alignItems: "center",
22633
- gap: 6,
22634
- paddingLeft: 4,
22635
- whiteSpace: "nowrap"
22636
- };
22637
- const errorStyle = {
22638
- color: "#ffb4a8",
22639
- fontSize: 12,
22640
- maxWidth: 240,
22641
- overflow: "hidden",
22642
- textOverflow: "ellipsis"
22643
- };
22644
- const publishedNoticeStyle = {
22645
- display: "inline-flex",
22646
- alignItems: "center",
22647
- gap: 5,
22648
- padding: "4px 10px",
22649
- borderRadius: 999,
22650
- border: "1px solid rgba(242, 235, 223, 0.25)",
22651
- background: "rgba(242, 235, 223, 0.08)",
22652
- color: CREMA,
22653
- fontSize: 12,
22654
- fontWeight: 500,
22655
- cursor: "pointer",
22656
- whiteSpace: "nowrap"
22657
- };
22658
- const publishBtnStyle = {
22659
- background: TERRACOTTA,
22660
- color: CREMA,
22661
- border: "none",
22662
- borderRadius: 999,
22663
- padding: "6px 14px",
22664
- fontSize: 13,
22665
- fontWeight: 600,
22666
- cursor: "pointer",
22667
- whiteSpace: "nowrap"
22668
- };
22669
- const menuBtnStyle = {
22670
- background: "transparent",
22671
- color: CREMA,
22672
- border: "none",
22673
- borderRadius: 999,
22674
- width: 32,
22675
- height: 32,
22676
- display: "inline-flex",
22677
- alignItems: "center",
22678
- justifyContent: "center",
22679
- fontSize: 22,
22680
- lineHeight: 1,
22681
- cursor: "pointer",
22682
- fontWeight: 700
22683
- };
22684
- const emptyStateStyle = {
22685
- color: "rgba(242, 235, 223, 0.55)",
22686
- fontSize: 13,
22687
- whiteSpace: "nowrap",
22688
- padding: "0 4px"
22689
- };
22690
- const dropdownStyle = {
22691
- position: "absolute",
22692
- bottom: "100%",
22693
- left: 0,
22694
- marginBottom: 8,
22695
- background: "#fff",
22696
- border: "1px solid #e8e8e8",
22697
- borderRadius: 10,
22698
- boxShadow: "0 12px 32px rgba(33, 23, 17, 0.18)",
22699
- minWidth: 160,
22700
- overflow: "hidden",
22701
- zIndex: 1
22702
- };
22703
- const menuItemStyle = {
22704
- display: "flex",
22705
- alignItems: "center",
22706
- gap: 8,
22707
- width: "100%",
22708
- background: "transparent",
22709
- border: "none",
22710
- padding: "10px 14px",
22711
- fontSize: 13,
22712
- textAlign: "left",
22713
- cursor: "pointer",
22714
- color: "#1a1a1a"
22715
- };
22716
- function MenuItem({
22717
- onClick,
22718
- icon,
22719
- children,
22720
- disabled = false
22721
- }) {
22722
- return /* @__PURE__ */ jsxs(
22723
- "button",
22724
- {
22725
- type: "button",
22726
- onClick,
22727
- disabled,
22728
- style: { ...menuItemStyle, opacity: disabled ? 0.4 : 1, cursor: disabled ? "not-allowed" : "pointer" },
22729
- role: "menuitem",
22730
- onMouseEnter: (e) => {
22731
- if (!disabled) e.currentTarget.style.background = "#f5f5f5";
22732
- },
22733
- onMouseLeave: (e) => {
22734
- e.currentTarget.style.background = "transparent";
22735
- },
22736
- children: [
22737
- /* @__PURE__ */ jsx("span", { style: menuIconWrapStyle, "aria-hidden": true, children: icon }),
22738
- children
22739
- ]
22702
+ function submissionFields(row) {
22703
+ const flat = [];
22704
+ flatten(row.data, "", flat);
22705
+ const rank = new Map((row.field_order ?? []).map((n, i) => [n, i]));
22706
+ const rankOf = (name) => rank.get(name) ?? rank.get(name.split(".")[0]) ?? Number.MAX_SAFE_INTEGER;
22707
+ return flat.map((f, i) => ({ f, i })).sort((a, b) => rankOf(a.f.name) - rankOf(b.f.name) || a.i - b.i).map(({ f }) => ({
22708
+ name: f.name,
22709
+ label: row.labels?.[f.name] ?? row.labels?.[f.name.split(".")[0]] ?? prettify(f.name),
22710
+ value: f.value
22711
+ }));
22712
+ }
22713
+ function flatten(value, path, out) {
22714
+ if (value === null || value === void 0) {
22715
+ out.push({ name: path, value: "" });
22716
+ } else if (typeof value === "boolean") {
22717
+ out.push({ name: path, value: value ? "Ja" : "Nei" });
22718
+ } else if (typeof value === "string" || typeof value === "number") {
22719
+ out.push({ name: path, value: String(value) });
22720
+ } else if (Array.isArray(value)) {
22721
+ if (value.every((v) => typeof v !== "object" || v === null)) {
22722
+ out.push({ name: path, value: value.map((v) => v == null ? "" : String(v)).filter(Boolean).join(", ") });
22723
+ } else {
22724
+ value.forEach((item, i) => flatten(item, `${path}.${i + 1}`, out));
22740
22725
  }
22741
- );
22742
- }
22743
- const menuIconWrapStyle = {
22744
- display: "flex",
22745
- alignItems: "center",
22746
- justifyContent: "center",
22747
- flexShrink: 0,
22748
- color: "#666"
22749
- };
22750
- function PublishedCheckIcon() {
22751
- return /* @__PURE__ */ jsx(
22752
- "svg",
22753
- {
22754
- width: "14",
22755
- height: "14",
22756
- viewBox: "0 0 24 24",
22757
- fill: "none",
22758
- stroke: "currentColor",
22759
- strokeWidth: "2.5",
22760
- strokeLinecap: "round",
22761
- strokeLinejoin: "round",
22762
- "aria-hidden": true,
22763
- children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" })
22726
+ } else if (typeof value === "object") {
22727
+ for (const [k, v] of Object.entries(value)) {
22728
+ flatten(v, path ? `${path}.${k}` : k, out);
22764
22729
  }
22765
- );
22730
+ }
22766
22731
  }
22767
- function HistoryIcon() {
22768
- return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
22769
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
22770
- /* @__PURE__ */ jsx("polyline", { points: "12 6 12 12 16 14" })
22771
- ] });
22732
+ function prettify(name) {
22733
+ const leaf = name.split(".").pop() ?? name;
22734
+ const words = leaf.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_\-.]+/g, " ").trim().toLowerCase();
22735
+ return words ? words[0].toUpperCase() + words.slice(1) : name;
22772
22736
  }
22773
- function LogOutIcon() {
22774
- return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
22775
- /* @__PURE__ */ jsx("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
22776
- /* @__PURE__ */ jsx("polyline", { points: "16 17 21 12 16 7" }),
22777
- /* @__PURE__ */ jsx("line", { x1: "21", y1: "12", x2: "9", y2: "12" })
22778
- ] });
22737
+ const EMAIL_RE$1 = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
22738
+ function senderOf(fields) {
22739
+ const named = fields.find(
22740
+ (f) => /(^|[._-])(navn|name|fullname|full_name|fornavn|firstname|first_name)$/i.test(f.name) && f.value && !EMAIL_RE$1.test(f.value)
22741
+ );
22742
+ if (named) return named.value.slice(0, 80);
22743
+ const email = fields.find((f) => EMAIL_RE$1.test(f.value));
22744
+ if (email) return email.value;
22745
+ const first = fields.find((f) => f.value !== "");
22746
+ return first ? first.value.slice(0, 80) : null;
22747
+ }
22748
+ function emailStatusLabel(row) {
22749
+ switch (row.email_status) {
22750
+ case "sent":
22751
+ return "E-post sendt";
22752
+ case "failed":
22753
+ return "E-post feilet";
22754
+ case "skipped":
22755
+ return "Ingen mottakere";
22756
+ case "pending":
22757
+ return "Sender e-post";
22758
+ }
22779
22759
  }
22780
- function UndoIcon() {
22781
- return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
22782
- /* @__PURE__ */ jsx("path", { d: "M3 7v6h6" }),
22783
- /* @__PURE__ */ jsx("path", { d: "M21 17a9 9 0 0 0-15-6.7L3 13" })
22784
- ] });
22760
+ function formatWhen(iso) {
22761
+ const d = new Date(iso);
22762
+ const now = /* @__PURE__ */ new Date();
22763
+ const sameDay = d.toDateString() === now.toDateString();
22764
+ return sameDay ? d.toLocaleTimeString("nb-NO", { hour: "2-digit", minute: "2-digit" }) : d.toLocaleDateString("nb-NO", { day: "numeric", month: "short" }) + " " + d.toLocaleTimeString("nb-NO", { hour: "2-digit", minute: "2-digit" });
22785
22765
  }
22786
- function EditModeShell({ children, themeStore, active }) {
22787
- useSettoDocumentLayout(active);
22788
- const [toolbarRoot, setToolbarRoot] = useState(null);
22789
- const [hintRoot, setHintRoot] = useState(null);
22790
- const [sectionToolbarRoot, setSectionToolbarRoot] = useState(null);
22791
- useEffect(() => {
22792
- if (!active) {
22793
- setToolbarRoot(null);
22794
- setHintRoot(null);
22795
- setSectionToolbarRoot(null);
22796
- return;
22797
- }
22798
- const toolbar = document.createElement("div");
22799
- toolbar.id = "setto-toolbar-root";
22800
- document.body.appendChild(toolbar);
22801
- const hint = document.createElement("div");
22802
- hint.id = "setto-hint-root";
22803
- hint.style.pointerEvents = "none";
22804
- document.body.appendChild(hint);
22805
- const sectionBar = document.createElement("div");
22806
- sectionBar.id = "setto-section-toolbar-root";
22807
- sectionBar.style.pointerEvents = "none";
22808
- document.body.appendChild(sectionBar);
22809
- setToolbarRoot(toolbar);
22810
- setHintRoot(hint);
22811
- setSectionToolbarRoot(sectionBar);
22812
- return () => {
22813
- toolbar.remove();
22814
- hint.remove();
22815
- sectionBar.remove();
22816
- };
22817
- }, [active]);
22818
- return /* @__PURE__ */ jsxs(Fragment, { children: [
22819
- children,
22820
- active && toolbarRoot ? createPortal(/* @__PURE__ */ jsx(EditToolbar, {}), toolbarRoot) : null,
22821
- active && hintRoot ? createPortal(/* @__PURE__ */ jsx(EditLinkHint, {}), hintRoot) : null,
22822
- active && sectionToolbarRoot && themeStore ? createPortal(/* @__PURE__ */ jsx(SectionToolbar, {}), sectionToolbarRoot) : null
22823
- ] });
22824
- }
22825
- const SettoContext = createContext(null);
22826
- function SettoProvider({ config, children }) {
22827
- const supabase = useMemo(() => getSupabase(config.supabase), [config.supabase]);
22828
- const api = useMemo(() => createApi({ apiUrl: config.apiUrl, supabase }), [config.apiUrl, supabase]);
22829
- const [session, setSession] = useState(null);
22830
- const [authLoading, setAuthLoading] = useState(true);
22831
- const [onAdminRoute, setOnAdminRoute] = useState(
22832
- () => typeof window === "undefined" ? false : isAdminRoute()
22833
- );
22834
- useEffect(() => {
22835
- let cancelled = false;
22836
- supabase.auth.getSession().then(({ data }) => {
22837
- if (cancelled) return;
22838
- setSession(data.session ?? null);
22839
- setAuthLoading(false);
22840
- });
22841
- const { data: sub } = supabase.auth.onAuthStateChange((_event, session2) => {
22842
- setSession(session2);
22843
- });
22844
- return () => {
22845
- cancelled = true;
22846
- sub.subscription.unsubscribe();
22847
- };
22848
- }, [supabase]);
22849
- useEffect(() => {
22850
- const compute = () => setOnAdminRoute(isAdminRoute());
22851
- compute();
22852
- window.addEventListener("popstate", compute);
22853
- return () => window.removeEventListener("popstate", compute);
22854
- }, []);
22855
- const access = useSiteAccess(supabase, session, config.siteId);
22856
- const { i18n } = useTranslation();
22857
- const storeRef = useRef(null);
22858
- const themeStoreRef = useRef(null);
22859
- const assetStoreRef = useRef(null);
22860
- const [storeReady, setStoreReady] = useState(false);
22861
- useEffect(() => {
22862
- if (!i18n) return;
22863
- storeRef.current = new I18nStore(i18n);
22864
- themeStoreRef.current = new ThemeStore(config.theme ?? {});
22865
- assetStoreRef.current = new AssetStore();
22866
- setStoreReady(true);
22867
- }, [i18n, config.theme]);
22868
- const editMode = !!session && !!access.currentSite && !onAdminRoute && !authLoading && !access.loading;
22869
- const value = {
22870
- config,
22871
- supabase,
22872
- api,
22873
- session,
22874
- authLoading,
22875
- editMode,
22876
- currentSite: access.currentSite,
22877
- sites: access.sites,
22878
- store: storeReady ? storeRef.current : null,
22879
- themeStore: storeReady ? themeStoreRef.current : null,
22880
- assetStore: storeReady ? assetStoreRef.current : null
22881
- };
22882
- const editShellActive = editMode && !!value.store;
22883
- return /* @__PURE__ */ jsx(SettoContext.Provider, { value, children: /* @__PURE__ */ jsx(SectionEditProvider, { children: /* @__PURE__ */ jsx(EditModeShell, { active: editShellActive, themeStore: value.themeStore, children }) }) });
22884
- }
22885
- function useSetto() {
22886
- const ctx = useContext(SettoContext);
22887
- if (!ctx) {
22888
- throw new Error("useSetto() must be used inside <SettoProvider>");
22889
- }
22890
- return ctx;
22891
- }
22892
- const GuestEditContext = createContext(null);
22893
- function GuestEditProvider({ children }) {
22894
- const { editMode, themeStore } = useSetto();
22895
- const { selectSection } = useSectionEdit();
22896
- const [active, setActive] = useState(false);
22897
- useEffect(() => {
22898
- if (editMode && active) setActive(false);
22899
- }, [editMode, active]);
22900
- useEffect(() => {
22901
- document.documentElement.classList.toggle("setto-guest-editing", active);
22902
- return () => document.documentElement.classList.remove("setto-guest-editing");
22903
- }, [active]);
22904
- const start = useCallback(() => setActive(true), []);
22905
- const stop = useCallback(() => {
22906
- setActive(false);
22907
- selectSection(null);
22908
- }, [selectSection]);
22909
- return /* @__PURE__ */ jsxs(GuestEditContext.Provider, { value: { active, start, stop }, children: [
22910
- children,
22911
- active && themeStore ? createPortal(/* @__PURE__ */ jsx(SectionToolbar, {}), document.body) : null
22912
- ] });
22913
- }
22914
- function useGuestEdit() {
22915
- const ctx = useContext(GuestEditContext);
22916
- if (!ctx) {
22917
- return { active: false, start: () => {
22918
- }, stop: () => {
22919
- } };
22920
- }
22921
- return ctx;
22766
+ function statusDotStyle(status) {
22767
+ const colour = status === "sent" ? "#34d399" : status === "failed" ? "#ff7a7a" : status === "skipped" ? "#fbbf24" : "#888";
22768
+ return { width: 8, height: 8, borderRadius: "50%", background: colour, flexShrink: 0 };
22922
22769
  }
22923
22770
  const MARGIN = 10;
22924
22771
  const OFFSET = 8;
@@ -23034,6 +22881,37 @@ function rangeFromPoint(doc, x, y) {
23034
22881
  range.collapse(true);
23035
22882
  return range;
23036
22883
  }
22884
+ const GuestEditContext = createContext(null);
22885
+ function GuestEditProvider({ children }) {
22886
+ const { editMode, themeStore } = useSetto();
22887
+ const { selectSection } = useSectionEdit();
22888
+ const [active, setActive] = useState(false);
22889
+ useEffect(() => {
22890
+ if (editMode && active) setActive(false);
22891
+ }, [editMode, active]);
22892
+ useEffect(() => {
22893
+ document.documentElement.classList.toggle("setto-guest-editing", active);
22894
+ return () => document.documentElement.classList.remove("setto-guest-editing");
22895
+ }, [active]);
22896
+ const start = useCallback(() => setActive(true), []);
22897
+ const stop = useCallback(() => {
22898
+ setActive(false);
22899
+ selectSection(null);
22900
+ }, [selectSection]);
22901
+ return /* @__PURE__ */ jsxs(GuestEditContext.Provider, { value: { active, start, stop }, children: [
22902
+ children,
22903
+ active && themeStore ? createPortal(/* @__PURE__ */ jsx(SectionToolbar, {}), document.body) : null
22904
+ ] });
22905
+ }
22906
+ function useGuestEdit() {
22907
+ const ctx = useContext(GuestEditContext);
22908
+ if (!ctx) {
22909
+ return { active: false, start: () => {
22910
+ }, stop: () => {
22911
+ } };
22912
+ }
22913
+ return ctx;
22914
+ }
23037
22915
  function T({ k }) {
23038
22916
  const { t, i18n } = useTranslation();
23039
22917
  const { editMode, store } = useSetto();
@@ -23114,47 +22992,1894 @@ function T({ k }) {
23114
22992
  const lang = toBcp47(i18n.language);
23115
22993
  const showLinkChip = coarsePointer && focused && linkContext;
23116
22994
  return /* @__PURE__ */ jsxs(Fragment, { children: [
23117
- /* @__PURE__ */ jsx(
23118
- "span",
23119
- {
23120
- ref,
23121
- "data-setto-key": k,
23122
- contentEditable: true,
23123
- suppressContentEditableWarning: true,
23124
- lang,
23125
- spellCheck: false,
23126
- role: "textbox",
23127
- tabIndex: 0,
23128
- onFocus: (e) => {
23129
- setFocused(true);
23130
- setLinkContext(isLinkContext(e.currentTarget));
23131
- },
23132
- onBlur: handleBlur,
23133
- onInput: handleInput,
23134
- onPaste: handlePaste,
23135
- onMouseDown: handleMouseDown,
23136
- onMouseUp: handleMouseUp,
23137
- onClick: handleClick,
23138
- onKeyDown: handleKeyDown,
23139
- onKeyUp: handleKeyUp,
23140
- style: {
23141
- cursor: "text",
23142
- color: "inherit",
23143
- outline: focused ? editMode ? "2px solid #640AFF" : "2px solid #C4502A" : void 0,
23144
- outlineOffset: 2,
23145
- borderRadius: 2,
23146
- transition: "outline-color 120ms"
23147
- }
23148
- }
23149
- ),
23150
- showLinkChip ? /* @__PURE__ */ jsx(LinkActionChip, { anchorRef: ref, visible: focused }) : null
22995
+ /* @__PURE__ */ jsx(
22996
+ "span",
22997
+ {
22998
+ ref,
22999
+ "data-setto-key": k,
23000
+ contentEditable: true,
23001
+ suppressContentEditableWarning: true,
23002
+ lang,
23003
+ spellCheck: false,
23004
+ role: "textbox",
23005
+ tabIndex: 0,
23006
+ onFocus: (e) => {
23007
+ setFocused(true);
23008
+ setLinkContext(isLinkContext(e.currentTarget));
23009
+ },
23010
+ onBlur: handleBlur,
23011
+ onInput: handleInput,
23012
+ onPaste: handlePaste,
23013
+ onMouseDown: handleMouseDown,
23014
+ onMouseUp: handleMouseUp,
23015
+ onClick: handleClick,
23016
+ onKeyDown: handleKeyDown,
23017
+ onKeyUp: handleKeyUp,
23018
+ style: {
23019
+ cursor: "text",
23020
+ color: "inherit",
23021
+ outline: focused ? editMode ? "2px solid #640AFF" : "2px solid #C4502A" : void 0,
23022
+ outlineOffset: 2,
23023
+ borderRadius: 2,
23024
+ transition: "outline-color 120ms"
23025
+ }
23026
+ }
23027
+ ),
23028
+ showLinkChip ? /* @__PURE__ */ jsx(LinkActionChip, { anchorRef: ref, visible: focused }) : null
23029
+ ] });
23030
+ }
23031
+ function toBcp47(code) {
23032
+ const base = (code.split("-")[0] ?? code).toLowerCase();
23033
+ if (base === "no" || base === "nb" || base === "nn") return "nb-NO";
23034
+ if (base === "en") return "en";
23035
+ return code;
23036
+ }
23037
+ const FORM_FIELD_TYPES = [
23038
+ "text",
23039
+ "email",
23040
+ "tel",
23041
+ "number",
23042
+ "url",
23043
+ "date",
23044
+ "textarea",
23045
+ "select",
23046
+ "radio",
23047
+ "checkbox",
23048
+ "checkboxes"
23049
+ ];
23050
+ const FORM_FIELD_TYPE_LABELS = {
23051
+ text: "Kort tekst",
23052
+ email: "E-post",
23053
+ tel: "Telefon",
23054
+ number: "Tall",
23055
+ url: "Nettadresse",
23056
+ date: "Dato",
23057
+ textarea: "Lang tekst",
23058
+ select: "Nedtrekksliste",
23059
+ radio: "Ett valg",
23060
+ checkbox: "Avkrysning",
23061
+ checkboxes: "Flere valg"
23062
+ };
23063
+ function isFormFieldType(value) {
23064
+ return typeof value === "string" && FORM_FIELD_TYPES.includes(value);
23065
+ }
23066
+ function fieldTypeHasOptions(type) {
23067
+ return type === "select" || type === "radio" || type === "checkboxes";
23068
+ }
23069
+ function fieldTypeHasPlaceholder(type) {
23070
+ return !fieldTypeHasOptions(type) && type !== "checkbox" && type !== "date";
23071
+ }
23072
+ function formFieldsPrefix(formId) {
23073
+ return `form.${formId}.fields`;
23074
+ }
23075
+ function parseOptions(value) {
23076
+ if (!value) return [];
23077
+ return value.split("\n").map((o) => o.trim()).filter((o) => o !== "");
23078
+ }
23079
+ function readString(item, prop) {
23080
+ const value = item[prop];
23081
+ return typeof value === "string" ? value : "";
23082
+ }
23083
+ function readLocaleFields(i18n, lng, formId, ns = "translation") {
23084
+ const prefix = formFieldsPrefix(formId);
23085
+ const candidates = [lng, ...i18n.languages ?? []].filter((l, i, all) => all.indexOf(l) === i);
23086
+ let bundle;
23087
+ let list;
23088
+ for (const candidate of candidates) {
23089
+ const b = i18n.getResourceBundle(candidate, ns);
23090
+ const l = getNested(b, prefix);
23091
+ if (l && typeof l === "object" && !Array.isArray(l)) {
23092
+ bundle = b;
23093
+ list = l;
23094
+ break;
23095
+ }
23096
+ }
23097
+ if (!list) return null;
23098
+ const fields = [];
23099
+ for (const itemKey of collectKeysUnder(bundle, prefix)) {
23100
+ const item = list[itemKey];
23101
+ if (!item || typeof item !== "object") continue;
23102
+ const record = item;
23103
+ const name = readString(record, "name").trim();
23104
+ if (!name) continue;
23105
+ const type = isFormFieldType(record.type) ? record.type : "text";
23106
+ fields.push({
23107
+ itemKey,
23108
+ name,
23109
+ type,
23110
+ labelKey: `${prefix}.${itemKey}.label`,
23111
+ label: readString(record, "label"),
23112
+ placeholder: readString(record, "placeholder"),
23113
+ required: readString(record, "required") === "true",
23114
+ options: parseOptions(readString(record, "options"))
23115
+ });
23116
+ }
23117
+ return fields;
23118
+ }
23119
+ function resolvePropFields(fields, t) {
23120
+ return fields.map((field) => ({
23121
+ name: field.name,
23122
+ type: isFormFieldType(field.type) ? field.type : "text",
23123
+ labelKey: field.labelKey,
23124
+ label: t(field.labelKey, { defaultValue: "" }),
23125
+ placeholder: field.placeholderKey ? t(field.placeholderKey, { defaultValue: "" }) : "",
23126
+ required: field.required === true,
23127
+ options: field.optionsKey ? parseOptions(t(field.optionsKey, { defaultValue: "" })) : []
23128
+ }));
23129
+ }
23130
+ function slugifyFieldName(label) {
23131
+ const slug = label.toLowerCase().replace(/æ/g, "ae").replace(/ø/g, "o").replace(/å/g, "a").normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9_]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
23132
+ return slug || "felt";
23133
+ }
23134
+ function uniqueFieldName(base, taken) {
23135
+ const used = new Set(taken);
23136
+ if (!used.has(base)) return base;
23137
+ let n = 2;
23138
+ while (used.has(`${base}-${n}`)) n += 1;
23139
+ return `${base}-${n}`;
23140
+ }
23141
+ function emptyValue(type) {
23142
+ if (type === "checkbox") return false;
23143
+ if (type === "checkboxes") return [];
23144
+ return "";
23145
+ }
23146
+ function isBlank(value) {
23147
+ if (value === void 0) return true;
23148
+ if (typeof value === "string") return value.trim() === "";
23149
+ if (typeof value === "boolean") return !value;
23150
+ return value.length === 0;
23151
+ }
23152
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
23153
+ const URL_RE = /^(https?:\/\/)?[^\s/$.?#].[^\s]*$/i;
23154
+ const FIELD_ERROR_DEFAULTS = {
23155
+ required: "Må fylles ut",
23156
+ email: "Skriv inn en gyldig e-postadresse",
23157
+ url: "Skriv inn en gyldig nettadresse",
23158
+ number: "Skriv inn et tall"
23159
+ };
23160
+ function validateValues(fields, values) {
23161
+ const errors = {};
23162
+ for (const field of fields) {
23163
+ const value = values[field.name];
23164
+ if (field.required && isBlank(value)) {
23165
+ errors[field.name] = "required";
23166
+ continue;
23167
+ }
23168
+ if (typeof value !== "string" || value.trim() === "") continue;
23169
+ if (field.type === "email" && !EMAIL_RE.test(value.trim())) errors[field.name] = "email";
23170
+ if (field.type === "url" && !URL_RE.test(value.trim())) errors[field.name] = "url";
23171
+ if (field.type === "number" && Number.isNaN(Number(value.trim()))) errors[field.name] = "number";
23172
+ }
23173
+ return errors;
23174
+ }
23175
+ function buildEnvelope(fields, values, extra) {
23176
+ const data = {};
23177
+ const labels = {};
23178
+ for (const field of fields) {
23179
+ const value = values[field.name] ?? emptyValue(field.type);
23180
+ data[field.name] = typeof value === "string" ? value.trim() : value;
23181
+ if (field.label.trim()) labels[field.name] = field.label.trim();
23182
+ }
23183
+ return {
23184
+ data,
23185
+ labels,
23186
+ order: fields.map((f) => f.name),
23187
+ ...extra.title ? { title: extra.title } : {},
23188
+ ...extra.page ? { page: extra.page } : {},
23189
+ hp: extra.hp,
23190
+ elapsed: Math.max(0, Math.round(extra.elapsed))
23191
+ };
23192
+ }
23193
+ function settoSubmissionUrl(apiUrl, siteId, formId) {
23194
+ return `${apiUrl.replace(/\/+$/, "")}/sites/${encodeURIComponent(siteId)}/forms/${encodeURIComponent(formId)}/submissions`;
23195
+ }
23196
+ function FloatingPopover({
23197
+ open,
23198
+ anchorRef,
23199
+ onClose,
23200
+ ariaLabel,
23201
+ children,
23202
+ style,
23203
+ minWidth = 280
23204
+ }) {
23205
+ const popoverRef = useRef(null);
23206
+ const [pos, setPos] = useState(null);
23207
+ useLayoutEffect(() => {
23208
+ if (!open || !anchorRef.current) {
23209
+ setPos(null);
23210
+ return;
23211
+ }
23212
+ const update = () => {
23213
+ const anchor = anchorRef.current;
23214
+ const pop = popoverRef.current;
23215
+ if (!anchor) return;
23216
+ const rect = anchor.getBoundingClientRect();
23217
+ const popoverH = pop?.offsetHeight ?? 200;
23218
+ const popoverW = pop?.offsetWidth ?? minWidth;
23219
+ let top = rect.bottom + 6;
23220
+ let left = rect.left;
23221
+ if (left + popoverW > window.innerWidth - 8) {
23222
+ left = window.innerWidth - popoverW - 8;
23223
+ }
23224
+ if (left < 8) left = 8;
23225
+ if (top + popoverH > window.innerHeight - 8) {
23226
+ top = rect.top - popoverH - 6;
23227
+ }
23228
+ if (top < 8) top = rect.bottom + 6;
23229
+ setPos({ top, left });
23230
+ };
23231
+ update();
23232
+ requestAnimationFrame(update);
23233
+ window.addEventListener("scroll", update, true);
23234
+ window.addEventListener("resize", update);
23235
+ return () => {
23236
+ window.removeEventListener("scroll", update, true);
23237
+ window.removeEventListener("resize", update);
23238
+ };
23239
+ }, [open, anchorRef, minWidth]);
23240
+ useEffect(() => {
23241
+ if (!open) return;
23242
+ const onKey = (e) => {
23243
+ if (e.key === "Escape") onClose();
23244
+ };
23245
+ window.addEventListener("keydown", onKey);
23246
+ return () => window.removeEventListener("keydown", onKey);
23247
+ }, [open, onClose]);
23248
+ useEffect(() => {
23249
+ if (!open) return;
23250
+ const close = (e) => {
23251
+ const target = e.target;
23252
+ if (anchorRef.current?.contains(target)) return;
23253
+ if (popoverRef.current?.contains(target)) return;
23254
+ onClose();
23255
+ };
23256
+ document.addEventListener("mousedown", close);
23257
+ return () => document.removeEventListener("mousedown", close);
23258
+ }, [open, onClose, anchorRef]);
23259
+ if (!open) return null;
23260
+ return createPortal(
23261
+ /* @__PURE__ */ jsx(
23262
+ "div",
23263
+ {
23264
+ ref: popoverRef,
23265
+ role: "dialog",
23266
+ "aria-label": ariaLabel,
23267
+ "data-setto-ui": true,
23268
+ style: {
23269
+ position: "fixed",
23270
+ top: pos?.top ?? -9999,
23271
+ left: pos?.left ?? 0,
23272
+ visibility: pos ? "visible" : "hidden",
23273
+ zIndex: 2147483647,
23274
+ background: "#fff",
23275
+ border: "1px solid #e0e0e0",
23276
+ borderRadius: 8,
23277
+ boxShadow: "0 8px 24px rgba(0,0,0,0.15)",
23278
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
23279
+ ...style
23280
+ },
23281
+ onMouseDown: (e) => e.stopPropagation(),
23282
+ onClick: (e) => e.stopPropagation(),
23283
+ children
23284
+ }
23285
+ ),
23286
+ document.body
23287
+ );
23288
+ }
23289
+ const STYLE_ID$2 = "setto-form-edit-styles";
23290
+ function useFormEditStyles() {
23291
+ useEffect(() => {
23292
+ if (document.getElementById(STYLE_ID$2)) return;
23293
+ const el = document.createElement("style");
23294
+ el.id = STYLE_ID$2;
23295
+ el.textContent = `
23296
+ .setto-form-field-tools {
23297
+ opacity: 0;
23298
+ transition: opacity 120ms ease;
23299
+ }
23300
+ .setto-form__field:hover .setto-form-field-tools,
23301
+ .setto-form__field:focus-within .setto-form-field-tools,
23302
+ .setto-form-field-tools[data-open] {
23303
+ opacity: 1;
23304
+ }
23305
+ @media (hover: none) {
23306
+ .setto-form-field-tools { opacity: 1; }
23307
+ }
23308
+ `;
23309
+ document.head.appendChild(el);
23310
+ }, []);
23311
+ }
23312
+ function FormFieldTools({
23313
+ field,
23314
+ index,
23315
+ count,
23316
+ autoOpen,
23317
+ onMove,
23318
+ onRemove,
23319
+ onChange
23320
+ }) {
23321
+ useFormEditStyles();
23322
+ const [open, setOpen] = useState(autoOpen === true);
23323
+ const gearRef = useRef(null);
23324
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
23325
+ /* @__PURE__ */ jsxs(
23326
+ "div",
23327
+ {
23328
+ "data-setto-ui": true,
23329
+ className: "setto-form-field-tools",
23330
+ style: toolsStyle,
23331
+ ...open ? { "data-open": "" } : {},
23332
+ onMouseDown: (e) => e.stopPropagation(),
23333
+ onClick: (e) => e.stopPropagation(),
23334
+ children: [
23335
+ /* @__PURE__ */ jsx(ToolButton, { label: "Flytt opp", disabled: index === 0, onClick: () => onMove(-1), children: /* @__PURE__ */ jsx(ArrowIcon, { direction: "up" }) }),
23336
+ /* @__PURE__ */ jsx(
23337
+ ToolButton,
23338
+ {
23339
+ label: "Flytt ned",
23340
+ disabled: index === count - 1,
23341
+ onClick: () => onMove(1),
23342
+ children: /* @__PURE__ */ jsx(ArrowIcon, { direction: "down" })
23343
+ }
23344
+ ),
23345
+ /* @__PURE__ */ jsx(
23346
+ ToolButton,
23347
+ {
23348
+ ref: gearRef,
23349
+ label: "Feltinnstillinger",
23350
+ active: open,
23351
+ onClick: () => setOpen((o) => !o),
23352
+ children: /* @__PURE__ */ jsx(GearIcon$1, {})
23353
+ }
23354
+ ),
23355
+ /* @__PURE__ */ jsx(ToolButton, { label: "Fjern felt", disabled: count <= 1, onClick: onRemove, danger: true, children: "×" })
23356
+ ]
23357
+ }
23358
+ ),
23359
+ /* @__PURE__ */ jsx(
23360
+ FloatingPopover,
23361
+ {
23362
+ open,
23363
+ anchorRef: gearRef,
23364
+ onClose: () => setOpen(false),
23365
+ ariaLabel: "Feltinnstillinger",
23366
+ minWidth: 300,
23367
+ style: { width: 300, padding: 14 },
23368
+ children: /* @__PURE__ */ jsx(FieldSettings, { field, onChange })
23369
+ }
23370
+ )
23371
+ ] });
23372
+ }
23373
+ function FieldSettings({ field, onChange }) {
23374
+ const [label, setLabel] = useState(field.label);
23375
+ const [placeholder, setPlaceholder] = useState(field.placeholder);
23376
+ const [options, setOptions] = useState(field.options.join("\n"));
23377
+ const [name, setName] = useState(field.name);
23378
+ useEffect(() => setLabel(field.label), [field.label]);
23379
+ useEffect(() => setPlaceholder(field.placeholder), [field.placeholder]);
23380
+ useEffect(() => setName(field.name), [field.name]);
23381
+ return /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 10, fontSize: 13, color: "#222" }, children: [
23382
+ /* @__PURE__ */ jsx(Row, { label: "Type", children: /* @__PURE__ */ jsx(
23383
+ "select",
23384
+ {
23385
+ value: field.type,
23386
+ onChange: (e) => onChange({ type: e.target.value }),
23387
+ style: inputStyle$1,
23388
+ children: FORM_FIELD_TYPES.map((type) => /* @__PURE__ */ jsx("option", { value: type, children: FORM_FIELD_TYPE_LABELS[type] }, type))
23389
+ }
23390
+ ) }),
23391
+ /* @__PURE__ */ jsx(Row, { label: "Etikett", children: /* @__PURE__ */ jsx(
23392
+ "input",
23393
+ {
23394
+ type: "text",
23395
+ value: label,
23396
+ onChange: (e) => setLabel(e.target.value),
23397
+ onBlur: () => label !== field.label && onChange({ label }),
23398
+ style: inputStyle$1
23399
+ }
23400
+ ) }),
23401
+ fieldTypeHasPlaceholder(field.type) ? /* @__PURE__ */ jsx(Row, { label: "Plassholder", children: /* @__PURE__ */ jsx(
23402
+ "input",
23403
+ {
23404
+ type: "text",
23405
+ value: placeholder,
23406
+ onChange: (e) => setPlaceholder(e.target.value),
23407
+ onBlur: () => placeholder !== field.placeholder && onChange({ placeholder }),
23408
+ style: inputStyle$1
23409
+ }
23410
+ ) }) : null,
23411
+ fieldTypeHasOptions(field.type) ? /* @__PURE__ */ jsx(Row, { label: "Alternativer", hint: "Ett per linje", children: /* @__PURE__ */ jsx(
23412
+ "textarea",
23413
+ {
23414
+ value: options,
23415
+ rows: 4,
23416
+ onChange: (e) => setOptions(e.target.value),
23417
+ onBlur: () => options !== field.options.join("\n") && onChange({ options }),
23418
+ style: { ...inputStyle$1, resize: "vertical", fontFamily: "inherit" }
23419
+ }
23420
+ ) }) : null,
23421
+ /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }, children: [
23422
+ /* @__PURE__ */ jsx(
23423
+ "input",
23424
+ {
23425
+ type: "checkbox",
23426
+ checked: field.required,
23427
+ onChange: (e) => onChange({ required: e.target.checked })
23428
+ }
23429
+ ),
23430
+ "Må fylles ut"
23431
+ ] }),
23432
+ /* @__PURE__ */ jsx(Row, { label: "Teknisk navn", hint: "Brukes i e-posten og i historikken", children: /* @__PURE__ */ jsx(
23433
+ "input",
23434
+ {
23435
+ type: "text",
23436
+ value: name,
23437
+ onChange: (e) => setName(e.target.value),
23438
+ onBlur: () => {
23439
+ const next = name.trim();
23440
+ if (next && next !== field.name) onChange({ name: next });
23441
+ else setName(field.name);
23442
+ },
23443
+ style: { ...inputStyle$1, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" }
23444
+ }
23445
+ ) })
23446
+ ] });
23447
+ }
23448
+ function Row({ label, hint, children }) {
23449
+ return /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 4 }, children: [
23450
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 12, color: "#666" }, children: [
23451
+ label,
23452
+ hint ? /* @__PURE__ */ jsxs("span", { style: { color: "#999" }, children: [
23453
+ " · ",
23454
+ hint
23455
+ ] }) : null
23456
+ ] }),
23457
+ children
23458
+ ] });
23459
+ }
23460
+ function ToolButton({ label, onClick, disabled, active, danger, children, ref }) {
23461
+ return /* @__PURE__ */ jsx(
23462
+ "button",
23463
+ {
23464
+ ref,
23465
+ type: "button",
23466
+ "data-setto-ui": true,
23467
+ "aria-label": label,
23468
+ title: label,
23469
+ disabled,
23470
+ onClick,
23471
+ style: {
23472
+ ...toolBtnStyle,
23473
+ ...active ? { background: "#640AFF", color: "#fff", borderColor: "#640AFF" } : {},
23474
+ ...danger ? { fontSize: 16 } : {},
23475
+ ...disabled ? { opacity: 0.35, cursor: "default" } : {}
23476
+ },
23477
+ children
23478
+ }
23479
+ );
23480
+ }
23481
+ function ArrowIcon({ direction }) {
23482
+ return /* @__PURE__ */ jsx(
23483
+ "svg",
23484
+ {
23485
+ width: "12",
23486
+ height: "12",
23487
+ viewBox: "0 0 24 24",
23488
+ fill: "none",
23489
+ stroke: "currentColor",
23490
+ strokeWidth: "2.5",
23491
+ strokeLinecap: "round",
23492
+ strokeLinejoin: "round",
23493
+ "aria-hidden": "true",
23494
+ style: direction === "down" ? { transform: "rotate(180deg)" } : void 0,
23495
+ children: /* @__PURE__ */ jsx("path", { d: "M12 19V5M5 12l7-7 7 7" })
23496
+ }
23497
+ );
23498
+ }
23499
+ function GearIcon$1() {
23500
+ return /* @__PURE__ */ jsxs(
23501
+ "svg",
23502
+ {
23503
+ width: "13",
23504
+ height: "13",
23505
+ viewBox: "0 0 24 24",
23506
+ fill: "none",
23507
+ stroke: "currentColor",
23508
+ strokeWidth: "2",
23509
+ strokeLinecap: "round",
23510
+ strokeLinejoin: "round",
23511
+ "aria-hidden": "true",
23512
+ children: [
23513
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
23514
+ /* @__PURE__ */ jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" })
23515
+ ]
23516
+ }
23517
+ );
23518
+ }
23519
+ const toolsStyle = {
23520
+ position: "absolute",
23521
+ top: -12,
23522
+ right: 0,
23523
+ zIndex: 2,
23524
+ display: "flex",
23525
+ gap: 4,
23526
+ padding: 3,
23527
+ background: "#ffffff",
23528
+ border: "1px solid #e0e0e0",
23529
+ borderRadius: 999,
23530
+ boxShadow: "0 1px 4px rgba(0,0,0,0.18)",
23531
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
23532
+ };
23533
+ const toolBtnStyle = {
23534
+ display: "flex",
23535
+ alignItems: "center",
23536
+ justifyContent: "center",
23537
+ width: 24,
23538
+ height: 24,
23539
+ padding: 0,
23540
+ background: "transparent",
23541
+ border: "1px solid transparent",
23542
+ borderRadius: "50%",
23543
+ fontSize: 13,
23544
+ lineHeight: 1,
23545
+ color: "#444",
23546
+ cursor: "pointer"
23547
+ };
23548
+ const inputStyle$1 = {
23549
+ width: "100%",
23550
+ boxSizing: "border-box",
23551
+ padding: "6px 8px",
23552
+ border: "1px solid #d8d8d8",
23553
+ borderRadius: 6,
23554
+ fontSize: 13,
23555
+ color: "#222",
23556
+ background: "#fff",
23557
+ fontFamily: "inherit"
23558
+ };
23559
+ const HONEYPOT_NAME = "website";
23560
+ const warnedEndpointKeys = /* @__PURE__ */ new Set();
23561
+ function cx(...parts) {
23562
+ return parts.filter(Boolean).join(" ");
23563
+ }
23564
+ function SettoForm({
23565
+ formId,
23566
+ fields: propFields,
23567
+ endpoint,
23568
+ endpointKey,
23569
+ className,
23570
+ classNames = {}
23571
+ }) {
23572
+ const { t, i18n } = useTranslation();
23573
+ const { config, editMode, store } = useSetto();
23574
+ const guestEdit = useGuestEdit();
23575
+ const editable = editMode || guestEdit.active;
23576
+ const lng = i18n.language;
23577
+ const uid = useId();
23578
+ useFormStyles();
23579
+ const [, force] = useState(0);
23580
+ useEffect(() => {
23581
+ if (!store) return;
23582
+ return store.subscribe(() => force((x) => x + 1));
23583
+ }, [store]);
23584
+ useEffect(() => {
23585
+ if (endpointKey && !warnedEndpointKeys.has(formId)) {
23586
+ warnedEndpointKeys.add(formId);
23587
+ console.warn(
23588
+ `[setto] <SettoForm formId="${formId}"> passes endpointKey, which is no longer used. Forms submit to Setto; pass \`endpoint\` for a custom backend.`
23589
+ );
23590
+ }
23591
+ }, [endpointKey, formId]);
23592
+ const localeFields = readLocaleFields(i18n, lng, formId);
23593
+ const fields = localeFields ?? resolvePropFields(propFields ?? [], t);
23594
+ const fromLocale = localeFields !== null;
23595
+ const prefix = formFieldsPrefix(formId);
23596
+ const [values, setValues] = useState({});
23597
+ const [errors, setErrors] = useState({});
23598
+ const [status, setStatus] = useState("idle");
23599
+ const [justAdded, setJustAdded] = useState(null);
23600
+ const formRef = useRef(null);
23601
+ const successRef = useRef(null);
23602
+ const honeypotRef = useRef(null);
23603
+ const startedAt = useRef(null);
23604
+ useEffect(() => {
23605
+ if (status === "success") successRef.current?.focus({ preventScroll: false });
23606
+ }, [status]);
23607
+ const valueOf = (field) => values[field.name] ?? emptyValue(field.type);
23608
+ const setValue = (name, value) => {
23609
+ setValues((prev) => ({ ...prev, [name]: value }));
23610
+ setErrors((prev) => {
23611
+ if (!(name in prev)) return prev;
23612
+ const next = { ...prev };
23613
+ delete next[name];
23614
+ return next;
23615
+ });
23616
+ };
23617
+ const markStarted = () => {
23618
+ if (startedAt.current === null) startedAt.current = performance.now();
23619
+ };
23620
+ const submitKey = `form.${formId}.submit`;
23621
+ const successKey = `form.${formId}.success`;
23622
+ const errorKey = `form.${formId}.error`;
23623
+ const title = t(`form.${formId}.title`, { defaultValue: "" }) || void 0;
23624
+ const onSubmit = async (e) => {
23625
+ e.preventDefault();
23626
+ if (status === "submitting") return;
23627
+ const nextErrors = validateValues(fields, values);
23628
+ setErrors(nextErrors);
23629
+ const firstInvalid = fields.find((f) => nextErrors[f.name]);
23630
+ if (firstInvalid) {
23631
+ const el = formRef.current?.querySelector(
23632
+ `[data-setto-field="${cssEscape(firstInvalid.name)}"] [name]`
23633
+ );
23634
+ el?.focus();
23635
+ return;
23636
+ }
23637
+ setStatus("submitting");
23638
+ const elapsed = startedAt.current === null ? 0 : performance.now() - startedAt.current;
23639
+ const envelope = buildEnvelope(fields, values, {
23640
+ title,
23641
+ page: typeof window !== "undefined" ? window.location.href : void 0,
23642
+ hp: honeypotRef.current?.value ?? "",
23643
+ elapsed
23644
+ });
23645
+ try {
23646
+ const url = endpoint || settoSubmissionUrl(config.apiUrl, config.siteId, formId);
23647
+ const res = await fetch(url, {
23648
+ method: "POST",
23649
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
23650
+ body: JSON.stringify(endpoint ? envelope.data : envelope)
23651
+ });
23652
+ if (!res.ok) throw new Error(`Form submission failed: ${res.status}`);
23653
+ setStatus("success");
23654
+ setValues({});
23655
+ startedAt.current = null;
23656
+ } catch (err) {
23657
+ console.error("[setto] form submission failed", err);
23658
+ setStatus("error");
23659
+ }
23660
+ };
23661
+ const ensureLocaleFields = () => {
23662
+ const keys = /* @__PURE__ */ new Map();
23663
+ if (!store) return keys;
23664
+ if (fromLocale) {
23665
+ for (const f of fields) if (f.itemKey) keys.set(f.name, f.itemKey);
23666
+ return keys;
23667
+ }
23668
+ const langs = i18n.languages?.length ? i18n.languages : [lng];
23669
+ for (const prop of propFields ?? []) {
23670
+ const itemKey = store.addListItem(prefix, lng, {
23671
+ name: prop.name,
23672
+ type: prop.type,
23673
+ ...prop.required ? { required: "true" } : {}
23674
+ });
23675
+ for (const lang of langs) {
23676
+ const ft = i18n.getFixedT(lang);
23677
+ const label = ft(prop.labelKey, { defaultValue: "" });
23678
+ if (label) store.set(`${prefix}.${itemKey}.label`, lang, label);
23679
+ const placeholder = prop.placeholderKey ? ft(prop.placeholderKey, { defaultValue: "" }) : "";
23680
+ if (placeholder) store.set(`${prefix}.${itemKey}.placeholder`, lang, placeholder);
23681
+ const options = prop.optionsKey ? ft(prop.optionsKey, { defaultValue: "" }) : "";
23682
+ if (options) store.set(`${prefix}.${itemKey}.options`, lang, options);
23683
+ }
23684
+ keys.set(prop.name, itemKey);
23685
+ }
23686
+ return keys;
23687
+ };
23688
+ const itemKeyFor = (field) => field.itemKey ?? ensureLocaleFields().get(field.name);
23689
+ const allLanguages = () => i18n.languages?.length ? [...i18n.languages] : [lng];
23690
+ const setEverywhere = (key, value) => {
23691
+ for (const lang of allLanguages()) {
23692
+ if (value === void 0 || value === "") store.delete(key, lang);
23693
+ else store.set(key, lang, value);
23694
+ }
23695
+ };
23696
+ const addField = () => {
23697
+ if (!store) return;
23698
+ store.batch(() => {
23699
+ ensureLocaleFields();
23700
+ const taken = fields.map((f) => f.name);
23701
+ const itemKey = store.addListItem(prefix, lng, {
23702
+ name: uniqueFieldName("felt", taken),
23703
+ type: "text"
23704
+ });
23705
+ for (const lang of allLanguages()) store.set(`${prefix}.${itemKey}.label`, lang, "Nytt felt");
23706
+ setJustAdded(itemKey);
23707
+ });
23708
+ };
23709
+ const moveField = (field, direction) => {
23710
+ if (!store) return;
23711
+ store.batch(() => {
23712
+ const itemKey = itemKeyFor(field);
23713
+ if (itemKey) store.moveListItem(prefix, itemKey, direction, lng);
23714
+ });
23715
+ };
23716
+ const removeField = (field) => {
23717
+ if (!store || fields.length <= 1) return;
23718
+ store.batch(() => {
23719
+ const itemKey = itemKeyFor(field);
23720
+ if (itemKey) store.removeListItem(prefix, itemKey, lng);
23721
+ });
23722
+ };
23723
+ const updateField = (field, patch) => {
23724
+ if (!store) return;
23725
+ store.batch(() => {
23726
+ const itemKey = itemKeyFor(field);
23727
+ if (!itemKey) return;
23728
+ const item = `${prefix}.${itemKey}`;
23729
+ const others = fields.filter((f) => f !== field).map((f) => f.name);
23730
+ if (patch.type !== void 0) setEverywhere(`${item}.type`, patch.type);
23731
+ if (patch.required !== void 0) {
23732
+ setEverywhere(`${item}.required`, patch.required ? "true" : void 0);
23733
+ }
23734
+ if (patch.name !== void 0) {
23735
+ setEverywhere(`${item}.name`, uniqueFieldName(slugifyFieldName(patch.name), others));
23736
+ }
23737
+ if (patch.label !== void 0) {
23738
+ store.set(`${item}.label`, lng, patch.label);
23739
+ if (patch.name === void 0 && /^felt(-\d+)?$/.test(field.name) && patch.label.trim()) {
23740
+ setEverywhere(`${item}.name`, uniqueFieldName(slugifyFieldName(patch.label), others));
23741
+ }
23742
+ }
23743
+ if (patch.placeholder !== void 0) {
23744
+ if (patch.placeholder) store.set(`${item}.placeholder`, lng, patch.placeholder);
23745
+ else store.delete(`${item}.placeholder`, lng);
23746
+ }
23747
+ if (patch.options !== void 0) {
23748
+ if (patch.options.trim()) store.set(`${item}.options`, lng, patch.options);
23749
+ else store.delete(`${item}.options`, lng);
23750
+ }
23751
+ });
23752
+ };
23753
+ if (status === "success") {
23754
+ return /* @__PURE__ */ jsxs(
23755
+ "div",
23756
+ {
23757
+ ref: successRef,
23758
+ tabIndex: -1,
23759
+ role: "status",
23760
+ "aria-live": "polite",
23761
+ "data-setto-form": formId,
23762
+ "data-setto-form-status": "success",
23763
+ className: cx("setto-form__success", className, classNames.success),
23764
+ children: [
23765
+ /* @__PURE__ */ jsx(CheckIcon, {}),
23766
+ /* @__PURE__ */ jsx("p", { className: "setto-form__success-text", children: /* @__PURE__ */ jsx(T, { k: successKey }) }),
23767
+ /* @__PURE__ */ jsx(
23768
+ "button",
23769
+ {
23770
+ type: "button",
23771
+ className: "setto-form__again",
23772
+ onClick: () => {
23773
+ setStatus("idle");
23774
+ setErrors({});
23775
+ },
23776
+ children: t("form.again", { defaultValue: "Send en ny melding" })
23777
+ }
23778
+ )
23779
+ ]
23780
+ }
23781
+ );
23782
+ }
23783
+ const submitting = status === "submitting";
23784
+ return /* @__PURE__ */ jsxs(
23785
+ "form",
23786
+ {
23787
+ ref: formRef,
23788
+ "data-setto-form": formId,
23789
+ className: cx("setto-form", className, classNames.form),
23790
+ onSubmit,
23791
+ onFocusCapture: markStarted,
23792
+ onPointerDownCapture: markStarted,
23793
+ onKeyDownCapture: markStarted,
23794
+ noValidate: true,
23795
+ children: [
23796
+ fields.map((field, index) => {
23797
+ const id = `${uid}-${field.name}`;
23798
+ const error = errors[field.name];
23799
+ const errorId = error ? `${id}-error` : void 0;
23800
+ return /* @__PURE__ */ jsxs(
23801
+ "div",
23802
+ {
23803
+ "data-setto-field": field.name,
23804
+ className: cx("setto-form__field", `setto-form__field--${field.type}`, classNames.field),
23805
+ children: [
23806
+ /* @__PURE__ */ jsx(
23807
+ FieldControl,
23808
+ {
23809
+ field,
23810
+ id,
23811
+ value: valueOf(field),
23812
+ editable,
23813
+ invalid: error !== void 0,
23814
+ describedBy: errorId,
23815
+ classNames,
23816
+ placeholderFallback: t("form.choose", { defaultValue: "Velg …" }),
23817
+ onChange: (value) => setValue(field.name, value)
23818
+ }
23819
+ ),
23820
+ error ? /* @__PURE__ */ jsx("p", { id: errorId, className: cx("setto-form__field-error", classNames.fieldError), children: t(`form.validation.${error}`, { defaultValue: FIELD_ERROR_DEFAULTS[error] }) }) : null,
23821
+ editable && store ? /* @__PURE__ */ jsx(
23822
+ FormFieldTools,
23823
+ {
23824
+ field,
23825
+ index,
23826
+ count: fields.length,
23827
+ autoOpen: justAdded !== null && justAdded === field.itemKey,
23828
+ onMove: (direction) => moveField(field, direction),
23829
+ onRemove: () => removeField(field),
23830
+ onChange: (patch) => updateField(field, patch)
23831
+ }
23832
+ ) : null
23833
+ ]
23834
+ },
23835
+ field.itemKey ?? field.name
23836
+ );
23837
+ }),
23838
+ editable && store ? /* @__PURE__ */ jsx(
23839
+ "button",
23840
+ {
23841
+ type: "button",
23842
+ "data-setto-ui": true,
23843
+ className: "setto-form__add-field",
23844
+ style: addFieldStyle,
23845
+ onClick: addField,
23846
+ children: "+ Legg til felt"
23847
+ }
23848
+ ) : null,
23849
+ /* @__PURE__ */ jsxs("div", { className: "setto-form__hp", "aria-hidden": "true", children: [
23850
+ /* @__PURE__ */ jsx("label", { htmlFor: `${uid}-${HONEYPOT_NAME}`, children: "Nettside" }),
23851
+ /* @__PURE__ */ jsx(
23852
+ "input",
23853
+ {
23854
+ ref: honeypotRef,
23855
+ id: `${uid}-${HONEYPOT_NAME}`,
23856
+ type: "text",
23857
+ name: HONEYPOT_NAME,
23858
+ tabIndex: -1,
23859
+ autoComplete: "off",
23860
+ defaultValue: ""
23861
+ }
23862
+ )
23863
+ ] }),
23864
+ /* @__PURE__ */ jsxs("div", { className: cx("setto-form__actions", classNames.actions), children: [
23865
+ /* @__PURE__ */ jsx(
23866
+ "button",
23867
+ {
23868
+ type: "submit",
23869
+ className: cx("setto-form__button", classNames.button),
23870
+ disabled: submitting,
23871
+ "aria-busy": submitting || void 0,
23872
+ children: submitting ? t("form.sending", { defaultValue: "Sender …" }) : /* @__PURE__ */ jsx(T, { k: submitKey })
23873
+ }
23874
+ ),
23875
+ status === "error" ? /* @__PURE__ */ jsx("p", { role: "alert", "data-setto-form-status": "error", className: cx("setto-form__error", classNames.error), children: /* @__PURE__ */ jsx(T, { k: errorKey }) }) : null
23876
+ ] }),
23877
+ editable ? /* @__PURE__ */ jsxs("div", { className: "setto-form__edit-previews", style: previewStyle, children: [
23878
+ /* @__PURE__ */ jsxs("div", { children: [
23879
+ /* @__PURE__ */ jsx("span", { style: previewHintStyle, children: "Etter innsending vises:" }),
23880
+ " ",
23881
+ /* @__PURE__ */ jsx(T, { k: successKey })
23882
+ ] }),
23883
+ status !== "error" ? /* @__PURE__ */ jsxs("div", { children: [
23884
+ /* @__PURE__ */ jsx("span", { style: previewHintStyle, children: "Ved feil vises:" }),
23885
+ " ",
23886
+ /* @__PURE__ */ jsx(T, { k: errorKey })
23887
+ ] }) : null
23888
+ ] }) : null
23889
+ ]
23890
+ }
23891
+ );
23892
+ }
23893
+ function FieldControl({
23894
+ field,
23895
+ id,
23896
+ value,
23897
+ editable,
23898
+ invalid,
23899
+ describedBy,
23900
+ classNames,
23901
+ placeholderFallback,
23902
+ onChange
23903
+ }) {
23904
+ const labelId = `${id}-label`;
23905
+ const aria = {
23906
+ "aria-invalid": invalid || void 0,
23907
+ "aria-describedby": describedBy,
23908
+ required: field.required || void 0
23909
+ };
23910
+ const labelText = /* @__PURE__ */ jsxs(Fragment, { children: [
23911
+ /* @__PURE__ */ jsx(T, { k: field.labelKey }),
23912
+ field.required ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "setto-form__required", children: "*" }) : null
23913
+ ] });
23914
+ if (field.type === "checkbox") {
23915
+ const checked = value === true;
23916
+ return /* @__PURE__ */ jsxs(
23917
+ FieldLabel,
23918
+ {
23919
+ editable,
23920
+ htmlFor: id,
23921
+ id: labelId,
23922
+ className: cx("setto-form__check", classNames.checkbox),
23923
+ children: [
23924
+ /* @__PURE__ */ jsx(
23925
+ "input",
23926
+ {
23927
+ id,
23928
+ type: "checkbox",
23929
+ name: field.name,
23930
+ checked,
23931
+ onChange: (e) => onChange(e.target.checked),
23932
+ ...editable ? { "aria-labelledby": labelId } : {},
23933
+ ...aria
23934
+ }
23935
+ ),
23936
+ /* @__PURE__ */ jsx("span", { children: labelText })
23937
+ ]
23938
+ }
23939
+ );
23940
+ }
23941
+ if (field.type === "radio" || field.type === "checkboxes") {
23942
+ const multiple = field.type === "checkboxes";
23943
+ const selected = multiple ? Array.isArray(value) ? value : [] : value;
23944
+ const toggle = (option, on) => {
23945
+ if (!multiple) {
23946
+ onChange(option);
23947
+ return;
23948
+ }
23949
+ const set = new Set(Array.isArray(selected) ? selected : []);
23950
+ if (on) set.add(option);
23951
+ else set.delete(option);
23952
+ onChange(field.options.filter((o) => set.has(o)));
23953
+ };
23954
+ return /* @__PURE__ */ jsxs(
23955
+ "fieldset",
23956
+ {
23957
+ className: "setto-form__fieldset",
23958
+ role: multiple ? void 0 : "radiogroup",
23959
+ "aria-invalid": invalid || void 0,
23960
+ "aria-describedby": describedBy,
23961
+ "aria-required": !multiple && field.required ? true : void 0,
23962
+ children: [
23963
+ /* @__PURE__ */ jsx("legend", { className: cx("setto-form__legend", classNames.label), children: labelText }),
23964
+ /* @__PURE__ */ jsx("div", { className: cx("setto-form__options", classNames.options), children: field.options.map((option, i) => {
23965
+ const optionId = `${id}-${i}`;
23966
+ const on = multiple ? selected.includes(option) : selected === option;
23967
+ return /* @__PURE__ */ jsxs("label", { htmlFor: optionId, className: cx("setto-form__option", classNames.option), children: [
23968
+ /* @__PURE__ */ jsx(
23969
+ "input",
23970
+ {
23971
+ id: optionId,
23972
+ type: multiple ? "checkbox" : "radio",
23973
+ name: field.name,
23974
+ value: option,
23975
+ checked: on,
23976
+ required: !multiple && field.required ? true : void 0,
23977
+ onChange: (e) => toggle(option, e.target.checked)
23978
+ }
23979
+ ),
23980
+ /* @__PURE__ */ jsx("span", { children: option })
23981
+ ] }, option);
23982
+ }) })
23983
+ ]
23984
+ }
23985
+ );
23986
+ }
23987
+ const text = typeof value === "string" ? value : "";
23988
+ const labelled = editable ? { "aria-labelledby": labelId } : {};
23989
+ let control;
23990
+ if (field.type === "textarea") {
23991
+ control = /* @__PURE__ */ jsx(
23992
+ "textarea",
23993
+ {
23994
+ id,
23995
+ name: field.name,
23996
+ value: text,
23997
+ rows: 5,
23998
+ placeholder: field.placeholder || void 0,
23999
+ onChange: (e) => onChange(e.target.value),
24000
+ className: cx("setto-form__textarea", classNames.textarea),
24001
+ ...labelled,
24002
+ ...aria
24003
+ }
24004
+ );
24005
+ } else if (field.type === "select") {
24006
+ control = /* @__PURE__ */ jsxs(
24007
+ "select",
24008
+ {
24009
+ id,
24010
+ name: field.name,
24011
+ value: text,
24012
+ onChange: (e) => onChange(e.target.value),
24013
+ className: cx("setto-form__select", classNames.select),
24014
+ ...labelled,
24015
+ ...aria,
24016
+ children: [
24017
+ /* @__PURE__ */ jsx("option", { value: "", disabled: field.required, children: field.placeholder || placeholderFallback }),
24018
+ field.options.map((option) => /* @__PURE__ */ jsx("option", { value: option, children: option }, option))
24019
+ ]
24020
+ }
24021
+ );
24022
+ } else {
24023
+ control = /* @__PURE__ */ jsx(
24024
+ "input",
24025
+ {
24026
+ id,
24027
+ type: inputType(field),
24028
+ name: field.name,
24029
+ value: text,
24030
+ placeholder: field.placeholder || void 0,
24031
+ inputMode: inputMode(field),
24032
+ autoComplete: autoCompleteFor(field),
24033
+ onChange: (e) => onChange(e.target.value),
24034
+ className: cx("setto-form__input", classNames.input),
24035
+ ...labelled,
24036
+ ...aria
24037
+ }
24038
+ );
24039
+ }
24040
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
24041
+ /* @__PURE__ */ jsx(
24042
+ FieldLabel,
24043
+ {
24044
+ editable,
24045
+ htmlFor: id,
24046
+ id: labelId,
24047
+ className: cx("setto-form__label", classNames.label),
24048
+ children: labelText
24049
+ }
24050
+ ),
24051
+ control
24052
+ ] });
24053
+ }
24054
+ function FieldLabel({
24055
+ editable,
24056
+ htmlFor,
24057
+ id,
24058
+ className,
24059
+ children
24060
+ }) {
24061
+ if (editable) {
24062
+ return /* @__PURE__ */ jsx("span", { id, className, children });
24063
+ }
24064
+ return /* @__PURE__ */ jsx("label", { htmlFor, className, children });
24065
+ }
24066
+ function inputType(field) {
24067
+ switch (field.type) {
24068
+ case "email":
24069
+ case "tel":
24070
+ case "url":
24071
+ case "date":
24072
+ return field.type;
24073
+ case "number":
24074
+ return "text";
24075
+ default:
24076
+ return "text";
24077
+ }
24078
+ }
24079
+ function inputMode(field) {
24080
+ switch (field.type) {
24081
+ case "email":
24082
+ return "email";
24083
+ case "tel":
24084
+ return "tel";
24085
+ case "url":
24086
+ return "url";
24087
+ case "number":
24088
+ return "decimal";
24089
+ default:
24090
+ return void 0;
24091
+ }
24092
+ }
24093
+ function autoCompleteFor(field) {
24094
+ if (field.type === "email") return "email";
24095
+ if (field.type === "tel") return "tel";
24096
+ if (field.type === "url") return "url";
24097
+ if (/^(navn|name|fullt?[-_]?navn|full[-_]?name)$/i.test(field.name)) return "name";
24098
+ if (/^(firma|bedrift|company|organisasjon|organization)$/i.test(field.name)) return "organization";
24099
+ return void 0;
24100
+ }
24101
+ function cssEscape(value) {
24102
+ return typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(value) : value.replace(/["\\]/g, "\\$&");
24103
+ }
24104
+ function CheckIcon() {
24105
+ return /* @__PURE__ */ jsxs(
24106
+ "svg",
24107
+ {
24108
+ className: "setto-form__success-icon",
24109
+ viewBox: "0 0 40 40",
24110
+ fill: "none",
24111
+ "aria-hidden": "true",
24112
+ focusable: "false",
24113
+ children: [
24114
+ /* @__PURE__ */ jsx("circle", { cx: "20", cy: "20", r: "19", stroke: "currentColor", strokeWidth: "2" }),
24115
+ /* @__PURE__ */ jsx(
24116
+ "path",
24117
+ {
24118
+ d: "M12 20.5l5.5 5.5L28 15",
24119
+ stroke: "currentColor",
24120
+ strokeWidth: "2.5",
24121
+ strokeLinecap: "round",
24122
+ strokeLinejoin: "round"
24123
+ }
24124
+ )
24125
+ ]
24126
+ }
24127
+ );
24128
+ }
24129
+ const STYLE_ID$1 = "setto-form-styles";
24130
+ const BASE_CSS = `
24131
+ :where(.setto-form){display:grid;gap:1.1rem;position:relative}
24132
+ :where(.setto-form__field){display:grid;gap:.4rem;position:relative}
24133
+ :where(.setto-form__label,.setto-form__legend){font-weight:500;line-height:1.3;padding:0}
24134
+ :where(.setto-form__required){opacity:.6;margin-left:.15em}
24135
+ :where(.setto-form__input,.setto-form__textarea,.setto-form__select){width:100%;box-sizing:border-box;padding:.7rem .85rem;border:1px solid color-mix(in srgb,currentColor 28%,transparent);border-radius:.6rem;background:color-mix(in srgb,currentColor 4%,transparent);color:inherit;font:inherit;line-height:1.4}
24136
+ :where(.setto-form__input,.setto-form__textarea,.setto-form__select)::placeholder{color:inherit;opacity:.5}
24137
+ :where(.setto-form__input,.setto-form__textarea,.setto-form__select):focus-visible{outline:2px solid currentColor;outline-offset:1px}
24138
+ :where(.setto-form__textarea){min-height:7.5rem;resize:vertical}
24139
+ :where(.setto-form__fieldset){border:0;padding:0;margin:0;min-width:0;display:grid;gap:.5rem}
24140
+ :where(.setto-form__options){display:grid;gap:.5rem}
24141
+ :where(.setto-form__option,.setto-form__check){display:flex;gap:.6rem;align-items:flex-start;line-height:1.4;cursor:pointer;font-weight:400}
24142
+ :where(.setto-form__option input,.setto-form__check input){margin:.25em 0 0;flex:none;accent-color:currentColor}
24143
+ :where(.setto-form__field-error,.setto-form__error){font-size:.875rem;color:#b42318;margin:0}
24144
+ :where(.setto-form [aria-invalid="true"]){border-color:#b42318}
24145
+ :where(.setto-form__actions){display:flex;flex-wrap:wrap;gap:.75rem 1rem;align-items:center}
24146
+ :where(.setto-form__button){cursor:pointer;font:inherit;font-weight:500;padding:.75rem 1.4rem;border-radius:999px;border:1px solid currentColor;background:transparent;color:inherit}
24147
+ :where(.setto-form__button:disabled){opacity:.6;cursor:progress}
24148
+ :where(.setto-form__success){display:grid;gap:.75rem;justify-items:start;outline:none}
24149
+ :where(.setto-form__success-icon){width:2.5rem;height:2.5rem;color:#0f7b4f}
24150
+ :where(.setto-form__success-text){margin:0;font-size:1.05em}
24151
+ :where(.setto-form__again){background:none;border:0;padding:0;color:inherit;text-decoration:underline;cursor:pointer;font:inherit;opacity:.8;width:fit-content;max-width:100%;text-align:inherit}
24152
+ :where(.setto-form__hp){position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none}
24153
+ `;
24154
+ function useFormStyles() {
24155
+ useInsertionEffect(() => {
24156
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID$1)) return;
24157
+ const el = document.createElement("style");
24158
+ el.id = STYLE_ID$1;
24159
+ el.textContent = BASE_CSS;
24160
+ document.head.appendChild(el);
24161
+ }, []);
24162
+ }
24163
+ const addFieldStyle = {
24164
+ justifySelf: "start",
24165
+ padding: "6px 12px",
24166
+ border: "1px dashed #999",
24167
+ borderRadius: 999,
24168
+ background: "rgba(255,255,255,0.85)",
24169
+ color: "#333",
24170
+ fontSize: 13,
24171
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
24172
+ cursor: "pointer"
24173
+ };
24174
+ const previewStyle = {
24175
+ display: "grid",
24176
+ gap: 6,
24177
+ padding: "10px 12px",
24178
+ border: "1px dashed rgba(100,10,255,0.35)",
24179
+ borderRadius: 8,
24180
+ fontSize: "0.9em",
24181
+ lineHeight: 1.4
24182
+ };
24183
+ const previewHintStyle = {
24184
+ color: "#640AFF",
24185
+ fontSize: 11,
24186
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
24187
+ textTransform: "uppercase",
24188
+ letterSpacing: "0.04em",
24189
+ marginRight: 4
24190
+ };
24191
+ const formsModule = {
24192
+ id: "forms",
24193
+ label: "Skjemaer",
24194
+ AdminView: FormsView
24195
+ };
24196
+ const SETTO_MODULES = [formsModule];
24197
+ function findModule(id) {
24198
+ return id ? SETTO_MODULES.find((m) => m.id === id) : void 0;
24199
+ }
24200
+ function SettoMark({
24201
+ size = 24,
24202
+ letterColor = "#F2EBDF",
24203
+ dotColor = "#C4502A",
24204
+ style
24205
+ }) {
24206
+ return /* @__PURE__ */ jsxs(
24207
+ "svg",
24208
+ {
24209
+ xmlns: "http://www.w3.org/2000/svg",
24210
+ viewBox: "0 0 64 64",
24211
+ width: size,
24212
+ height: size,
24213
+ "aria-hidden": true,
24214
+ style: { display: "block", ...style },
24215
+ children: [
24216
+ /* @__PURE__ */ jsx(
24217
+ "text",
24218
+ {
24219
+ x: "32",
24220
+ y: "46",
24221
+ textAnchor: "middle",
24222
+ fontFamily: "Georgia, serif",
24223
+ fontStyle: "italic",
24224
+ fontSize: "42",
24225
+ fill: letterColor,
24226
+ children: "S"
24227
+ }
24228
+ ),
24229
+ /* @__PURE__ */ jsx("circle", { cx: "49", cy: "17", r: "5", fill: dotColor })
24230
+ ]
24231
+ }
24232
+ );
24233
+ }
24234
+ const BLEKK = "#211711";
24235
+ const CREMA = "#F2EBDF";
24236
+ const TERRACOTTA = "#C4502A";
24237
+ function EditToolbar() {
24238
+ const { store, themeStore, assetStore, api, config, supabase } = useSetto();
24239
+ const { i18n } = useTranslation();
24240
+ useEditBaseline();
24241
+ const snap = useSyncExternalStore(
24242
+ (cb) => store?.subscribe(() => cb()) ?? (() => {
24243
+ }),
24244
+ () => store?.snapshot() ?? EMPTY_I18N_STORE_SNAPSHOT,
24245
+ () => EMPTY_I18N_STORE_SNAPSHOT
24246
+ );
24247
+ useSyncExternalStore(
24248
+ (cb) => themeStore?.subscribe(() => cb()) ?? (() => {
24249
+ }),
24250
+ () => themeStore?.snapshot().version ?? 0,
24251
+ () => 0
24252
+ );
24253
+ useSyncExternalStore(
24254
+ (cb) => assetStore?.subscribe(() => cb()) ?? (() => {
24255
+ }),
24256
+ () => assetStore?.snapshot().version ?? 0,
24257
+ () => 0
24258
+ );
24259
+ const [publishing, setPublishing] = useState(false);
24260
+ const [activeDeployment, setActiveDeployment] = useState(null);
24261
+ const [publishDialogOpen, setPublishDialogOpen] = useState(false);
24262
+ const [error, setError] = useState(null);
24263
+ const [publishedNotice, setPublishedNotice] = useState(false);
24264
+ const [menuOpen, setMenuOpen] = useState(false);
24265
+ const [expanded, setExpanded] = useState(false);
24266
+ const menuRef = useRef(null);
24267
+ const publishedNoticeTimer = useRef(null);
24268
+ const { row: deploymentRow, status: deploymentStatus } = useDeploymentStatus(
24269
+ supabase,
24270
+ activeDeployment
24271
+ );
24272
+ useEffect(() => {
24273
+ let cancelled = false;
24274
+ const resume = async () => {
24275
+ try {
24276
+ await api.expireStaleDeployments(config.siteId);
24277
+ } catch {
24278
+ }
24279
+ const id = await fetchInProgressDeploymentId(supabase, config.siteId);
24280
+ if (!cancelled && id) {
24281
+ setActiveDeployment((current) => current ?? id);
24282
+ }
24283
+ };
24284
+ void resume();
24285
+ return () => {
24286
+ cancelled = true;
24287
+ };
24288
+ }, [api, supabase, config.siteId]);
24289
+ const textDraftCount = snap.drafts.size;
24290
+ const themeDraftCount = themeStore?.size() ?? 0;
24291
+ const assetDraftCount = assetStore?.size() ?? 0;
24292
+ const draftCount = textDraftCount + themeDraftCount + assetDraftCount;
24293
+ const hasDrafts = draftCount > 0;
24294
+ const showToolbarProgress = activeDeployment !== null && !publishDialogOpen && deploymentRow !== null && isDeploymentInProgress(deploymentRow.status) && !isDeploymentStale(deploymentRow);
24295
+ const showPublishedNotice = publishedNotice && !publishDialogOpen;
24296
+ useEffect(() => {
24297
+ if (error || showToolbarProgress || showPublishedNotice) {
24298
+ setExpanded(true);
24299
+ }
24300
+ }, [error, showToolbarProgress, showPublishedNotice]);
24301
+ const toggleExpanded = () => {
24302
+ setExpanded((prev) => {
24303
+ const next = !prev;
24304
+ if (!next) setMenuOpen(false);
24305
+ return next;
24306
+ });
24307
+ };
24308
+ useEffect(() => {
24309
+ return () => {
24310
+ if (publishedNoticeTimer.current) clearTimeout(publishedNoticeTimer.current);
24311
+ };
24312
+ }, []);
24313
+ useEffect(() => {
24314
+ if (!activeDeployment || !deploymentRow) return;
24315
+ const { status } = deploymentRow;
24316
+ if (status === "ready") {
24317
+ if (publishDialogOpen) return;
24318
+ setPublishedNotice(true);
24319
+ setActiveDeployment(null);
24320
+ if (publishedNoticeTimer.current) clearTimeout(publishedNoticeTimer.current);
24321
+ publishedNoticeTimer.current = setTimeout(() => setPublishedNotice(false), 6e3);
24322
+ return;
24323
+ }
24324
+ if (publishDialogOpen) return;
24325
+ if (status === "error" || status === "canceled") {
24326
+ setError(
24327
+ status === "canceled" ? "Publisering avbrutt." : deploymentRow.error_message?.includes("webhook") ? "Publisering utløpt (ingen webhook)." : "Publisering feilet."
24328
+ );
24329
+ setActiveDeployment(null);
24330
+ return;
24331
+ }
24332
+ if (isDeploymentStale(deploymentRow)) {
24333
+ setError("Publisering utløpt (ingen webhook).");
24334
+ setActiveDeployment(null);
24335
+ }
24336
+ }, [activeDeployment, deploymentRow, publishDialogOpen]);
24337
+ useEffect(() => {
24338
+ if (!activeDeployment) return;
24339
+ const syncActiveDeployment = async () => {
24340
+ const { data } = await supabase.from("deployments").select("status").eq("id", activeDeployment).maybeSingle();
24341
+ if (!data) {
24342
+ setActiveDeployment(null);
24343
+ return;
24344
+ }
24345
+ const status = data.status;
24346
+ if (status === "ready") {
24347
+ if (!publishDialogOpen) {
24348
+ setPublishedNotice(true);
24349
+ if (publishedNoticeTimer.current) clearTimeout(publishedNoticeTimer.current);
24350
+ publishedNoticeTimer.current = setTimeout(() => setPublishedNotice(false), 6e3);
24351
+ }
24352
+ setActiveDeployment(null);
24353
+ return;
24354
+ }
24355
+ if (!isDeploymentInProgress(status)) {
24356
+ if (status === "canceled") setError("Publisering avbrutt.");
24357
+ else if (status === "error") setError("Publisering feilet.");
24358
+ setActiveDeployment(null);
24359
+ }
24360
+ };
24361
+ const onVisible = () => {
24362
+ if (document.visibilityState === "visible") void syncActiveDeployment();
24363
+ };
24364
+ window.addEventListener("focus", syncActiveDeployment);
24365
+ document.addEventListener("visibilitychange", onVisible);
24366
+ return () => {
24367
+ window.removeEventListener("focus", syncActiveDeployment);
24368
+ document.removeEventListener("visibilitychange", onVisible);
24369
+ };
24370
+ }, [activeDeployment, publishDialogOpen, supabase]);
24371
+ const languages = i18n.languages?.length ? Array.from(new Set(i18n.languages)) : ["no", "en"];
24372
+ useEffect(() => {
24373
+ if (!menuOpen) return;
24374
+ const close = (e) => {
24375
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
24376
+ setMenuOpen(false);
24377
+ }
24378
+ };
24379
+ document.addEventListener("mousedown", close);
24380
+ return () => document.removeEventListener("mousedown", close);
24381
+ }, [menuOpen]);
24382
+ const publish = async () => {
24383
+ if (!store) return;
24384
+ setPublishing(true);
24385
+ setPublishDialogOpen(true);
24386
+ setError(null);
24387
+ setPublishedNotice(false);
24388
+ try {
24389
+ const bundles = store.serialiseBundles();
24390
+ const files = Object.entries(bundles).map(([lng, data]) => ({
24391
+ path: pathForLang(config.siteId, lng, languages),
24392
+ content: JSON.stringify(data, null, 2) + "\n"
24393
+ }));
24394
+ if (themeStore && themeDraftCount > 0) {
24395
+ files.push({
24396
+ path: pathForTheme(config.siteId, config.themePath),
24397
+ content: JSON.stringify(themeStore.serialise(), null, 2) + "\n"
24398
+ });
24399
+ }
24400
+ if (assetStore && assetDraftCount > 0) {
24401
+ const result2 = await api.publish(config.siteId, files, {
24402
+ assets: assetStore.pendingAssets()
24403
+ });
24404
+ store.commit();
24405
+ themeStore?.commit();
24406
+ assetStore.commit();
24407
+ let deploymentId2 = result2.deploymentId;
24408
+ if (!deploymentId2 && result2.trackingDeferred) {
24409
+ deploymentId2 = await waitForDeploymentByCommit(
24410
+ supabase,
24411
+ config.siteId,
24412
+ result2.commitSha
24413
+ );
24414
+ }
24415
+ if (deploymentId2) {
24416
+ setActiveDeployment(deploymentId2);
24417
+ } else {
24418
+ setPublishDialogOpen(false);
24419
+ setPublishedNotice(true);
24420
+ }
24421
+ return;
24422
+ }
24423
+ const result = await api.publish(config.siteId, files);
24424
+ store.commit();
24425
+ themeStore?.commit();
24426
+ assetStore?.commit();
24427
+ let deploymentId = result.deploymentId;
24428
+ if (!deploymentId && result.trackingDeferred) {
24429
+ deploymentId = await waitForDeploymentByCommit(
24430
+ supabase,
24431
+ config.siteId,
24432
+ result.commitSha
24433
+ );
24434
+ }
24435
+ if (deploymentId) {
24436
+ setActiveDeployment(deploymentId);
24437
+ } else {
24438
+ setPublishDialogOpen(false);
24439
+ setPublishedNotice(true);
24440
+ }
24441
+ } catch (err) {
24442
+ setError(err instanceof Error ? err.message : "unknown error");
24443
+ setPublishDialogOpen(false);
24444
+ } finally {
24445
+ setPublishing(false);
24446
+ }
24447
+ };
24448
+ const completePublish = () => {
24449
+ setPublishDialogOpen(false);
24450
+ setActiveDeployment(null);
24451
+ setPublishedNotice(false);
24452
+ if (publishedNoticeTimer.current) clearTimeout(publishedNoticeTimer.current);
24453
+ };
24454
+ const dismissPublishedNotice = () => {
24455
+ setPublishedNotice(false);
24456
+ if (publishedNoticeTimer.current) clearTimeout(publishedNoticeTimer.current);
24457
+ };
24458
+ const cancelDrafts = () => {
24459
+ setMenuOpen(false);
24460
+ store?.revertAll();
24461
+ themeStore?.revertAll();
24462
+ assetStore?.revertAll();
24463
+ setError(null);
24464
+ setExpanded(false);
24465
+ };
24466
+ const signOut = () => {
24467
+ setMenuOpen(false);
24468
+ void supabase.auth.signOut();
24469
+ };
24470
+ const goToHistory = () => {
24471
+ setMenuOpen(false);
24472
+ window.location.href = SETTO_BASE;
24473
+ };
24474
+ const goToSettings = () => {
24475
+ setMenuOpen(false);
24476
+ window.location.href = adminViewUrl(SETTO_MODULES[0]?.id ?? "forms");
24477
+ };
24478
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
24479
+ /* @__PURE__ */ jsxs(
24480
+ "div",
24481
+ {
24482
+ role: "toolbar",
24483
+ "aria-label": "Setto editor",
24484
+ style: {
24485
+ ...containerStyleFor(hasDrafts),
24486
+ ...expanded ? expandedContainerStyle : {}
24487
+ },
24488
+ children: [
24489
+ /* @__PURE__ */ jsx(
24490
+ "button",
24491
+ {
24492
+ type: "button",
24493
+ "aria-label": expanded ? "Lukk Setto-meny" : "Åpne Setto-meny",
24494
+ "aria-expanded": expanded,
24495
+ title: "Setto",
24496
+ style: markButtonStyle,
24497
+ onClick: toggleExpanded,
24498
+ children: /* @__PURE__ */ jsx(SettoMark, { size: 28 })
24499
+ }
24500
+ ),
24501
+ expanded ? /* @__PURE__ */ jsxs("div", { style: contentStyle, children: [
24502
+ error ? /* @__PURE__ */ jsx("span", { style: errorStyle, children: error }) : null,
24503
+ hasDrafts && !publishing && activeDeployment === null ? /* @__PURE__ */ jsx("button", { type: "button", onClick: publish, style: publishBtnStyle, children: "Publiser" }) : null,
24504
+ showPublishedNotice ? /* @__PURE__ */ jsxs(
24505
+ "button",
24506
+ {
24507
+ type: "button",
24508
+ onClick: dismissPublishedNotice,
24509
+ style: publishedNoticeStyle,
24510
+ title: "Lukk",
24511
+ children: [
24512
+ /* @__PURE__ */ jsx(PublishedCheckIcon, {}),
24513
+ "Publisert"
24514
+ ]
24515
+ }
24516
+ ) : null,
24517
+ showToolbarProgress && deploymentRow ? /* @__PURE__ */ jsx(
24518
+ PublishProgressBar,
24519
+ {
24520
+ status: deploymentRow.status,
24521
+ compact: true,
24522
+ href: `${SETTO_BASE}?deployment=${encodeURIComponent(activeDeployment)}`
24523
+ }
24524
+ ) : null,
24525
+ !hasDrafts && !error && !showPublishedNotice && !showToolbarProgress && !publishing ? /* @__PURE__ */ jsx("span", { style: emptyStateStyle, children: "Ingen endringer" }) : null,
24526
+ /* @__PURE__ */ jsxs("div", { ref: menuRef, style: { position: "relative" }, children: [
24527
+ /* @__PURE__ */ jsx(
24528
+ "button",
24529
+ {
24530
+ type: "button",
24531
+ onClick: () => setMenuOpen((o) => !o),
24532
+ "aria-label": "Meny",
24533
+ "aria-expanded": menuOpen,
24534
+ style: {
24535
+ ...menuBtnStyle,
24536
+ background: menuOpen ? "rgba(242, 235, 223, 0.16)" : "transparent"
24537
+ },
24538
+ onMouseEnter: (e) => {
24539
+ e.currentTarget.style.background = "rgba(242, 235, 223, 0.16)";
24540
+ },
24541
+ onMouseLeave: (e) => {
24542
+ e.currentTarget.style.background = menuOpen ? "rgba(242, 235, 223, 0.16)" : "transparent";
24543
+ },
24544
+ children: "⋮"
24545
+ }
24546
+ ),
24547
+ menuOpen ? /* @__PURE__ */ jsxs("div", { style: dropdownStyle, role: "menu", children: [
24548
+ /* @__PURE__ */ jsx(
24549
+ MenuItem,
24550
+ {
24551
+ onClick: cancelDrafts,
24552
+ icon: /* @__PURE__ */ jsx(UndoIcon, {}),
24553
+ disabled: !hasDrafts,
24554
+ children: "Avbryt"
24555
+ }
24556
+ ),
24557
+ /* @__PURE__ */ jsx(MenuItem, { onClick: goToHistory, icon: /* @__PURE__ */ jsx(HistoryIcon, {}), children: "Historikk" }),
24558
+ /* @__PURE__ */ jsx(MenuItem, { onClick: goToSettings, icon: /* @__PURE__ */ jsx(GearIcon, {}), children: "Innstillinger" }),
24559
+ /* @__PURE__ */ jsx(MenuItem, { onClick: signOut, icon: /* @__PURE__ */ jsx(LogOutIcon, {}), children: "Logg ut" })
24560
+ ] }) : null
24561
+ ] })
24562
+ ] }) : null
24563
+ ]
24564
+ }
24565
+ ),
24566
+ /* @__PURE__ */ jsx(EditOnboardingDialog, {}),
24567
+ publishDialogOpen && (publishing || activeDeployment !== null) ? /* @__PURE__ */ jsx(
24568
+ PublishDialog,
24569
+ {
24570
+ deploymentId: activeDeployment ?? "",
24571
+ row: deploymentRow,
24572
+ status: deploymentStatus ?? "pending",
24573
+ publishing,
24574
+ onClose: () => setPublishDialogOpen(false),
24575
+ onComplete: completePublish
24576
+ }
24577
+ ) : null
24578
+ ] });
24579
+ }
24580
+ const EASE_OUT_EXPO = "cubic-bezier(0.16, 1, 0.3, 1)";
24581
+ const FAB_SHADOW = "0 8px 24px rgba(33, 23, 17, 0.35), 0 2px 6px rgba(33, 23, 17, 0.2)";
24582
+ function containerStyleFor(hasDrafts) {
24583
+ return {
24584
+ display: "inline-flex",
24585
+ alignItems: "center",
24586
+ height: FAB_SIZE,
24587
+ width: FAB_SIZE,
24588
+ padding: 0,
24589
+ background: BLEKK,
24590
+ color: CREMA,
24591
+ borderRadius: FAB_SIZE / 2,
24592
+ boxShadow: hasDrafts ? `0 0 0 2px ${TERRACOTTA}, ${FAB_SHADOW}` : FAB_SHADOW,
24593
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
24594
+ fontSize: 13,
24595
+ overflow: "visible",
24596
+ transition: `width 500ms ${EASE_OUT_EXPO}, padding 500ms ${EASE_OUT_EXPO}, box-shadow 300ms ${EASE_OUT_EXPO}`
24597
+ };
24598
+ }
24599
+ const expandedContainerStyle = {
24600
+ width: "auto",
24601
+ paddingRight: 10
24602
+ };
24603
+ const markButtonStyle = {
24604
+ display: "inline-flex",
24605
+ alignItems: "center",
24606
+ justifyContent: "center",
24607
+ flexShrink: 0,
24608
+ width: FAB_SIZE,
24609
+ height: FAB_SIZE,
24610
+ background: "transparent",
24611
+ border: "none",
24612
+ padding: 0,
24613
+ cursor: "pointer",
24614
+ color: "inherit"
24615
+ };
24616
+ const contentStyle = {
24617
+ display: "flex",
24618
+ alignItems: "center",
24619
+ gap: 6,
24620
+ paddingLeft: 4,
24621
+ whiteSpace: "nowrap"
24622
+ };
24623
+ const errorStyle = {
24624
+ color: "#ffb4a8",
24625
+ fontSize: 12,
24626
+ maxWidth: 240,
24627
+ overflow: "hidden",
24628
+ textOverflow: "ellipsis"
24629
+ };
24630
+ const publishedNoticeStyle = {
24631
+ display: "inline-flex",
24632
+ alignItems: "center",
24633
+ gap: 5,
24634
+ padding: "4px 10px",
24635
+ borderRadius: 999,
24636
+ border: "1px solid rgba(242, 235, 223, 0.25)",
24637
+ background: "rgba(242, 235, 223, 0.08)",
24638
+ color: CREMA,
24639
+ fontSize: 12,
24640
+ fontWeight: 500,
24641
+ cursor: "pointer",
24642
+ whiteSpace: "nowrap"
24643
+ };
24644
+ const publishBtnStyle = {
24645
+ background: TERRACOTTA,
24646
+ color: CREMA,
24647
+ border: "none",
24648
+ borderRadius: 999,
24649
+ padding: "6px 14px",
24650
+ fontSize: 13,
24651
+ fontWeight: 600,
24652
+ cursor: "pointer",
24653
+ whiteSpace: "nowrap"
24654
+ };
24655
+ const menuBtnStyle = {
24656
+ background: "transparent",
24657
+ color: CREMA,
24658
+ border: "none",
24659
+ borderRadius: 999,
24660
+ width: 32,
24661
+ height: 32,
24662
+ display: "inline-flex",
24663
+ alignItems: "center",
24664
+ justifyContent: "center",
24665
+ fontSize: 22,
24666
+ lineHeight: 1,
24667
+ cursor: "pointer",
24668
+ fontWeight: 700
24669
+ };
24670
+ const emptyStateStyle = {
24671
+ color: "rgba(242, 235, 223, 0.55)",
24672
+ fontSize: 13,
24673
+ whiteSpace: "nowrap",
24674
+ padding: "0 4px"
24675
+ };
24676
+ const dropdownStyle = {
24677
+ position: "absolute",
24678
+ bottom: "100%",
24679
+ left: 0,
24680
+ marginBottom: 8,
24681
+ background: "#fff",
24682
+ border: "1px solid #e8e8e8",
24683
+ borderRadius: 10,
24684
+ boxShadow: "0 12px 32px rgba(33, 23, 17, 0.18)",
24685
+ minWidth: 160,
24686
+ overflow: "hidden",
24687
+ zIndex: 1
24688
+ };
24689
+ const menuItemStyle = {
24690
+ display: "flex",
24691
+ alignItems: "center",
24692
+ gap: 8,
24693
+ width: "100%",
24694
+ background: "transparent",
24695
+ border: "none",
24696
+ padding: "10px 14px",
24697
+ fontSize: 13,
24698
+ textAlign: "left",
24699
+ cursor: "pointer",
24700
+ color: "#1a1a1a"
24701
+ };
24702
+ function MenuItem({
24703
+ onClick,
24704
+ icon,
24705
+ children,
24706
+ disabled = false
24707
+ }) {
24708
+ return /* @__PURE__ */ jsxs(
24709
+ "button",
24710
+ {
24711
+ type: "button",
24712
+ onClick,
24713
+ disabled,
24714
+ style: { ...menuItemStyle, opacity: disabled ? 0.4 : 1, cursor: disabled ? "not-allowed" : "pointer" },
24715
+ role: "menuitem",
24716
+ onMouseEnter: (e) => {
24717
+ if (!disabled) e.currentTarget.style.background = "#f5f5f5";
24718
+ },
24719
+ onMouseLeave: (e) => {
24720
+ e.currentTarget.style.background = "transparent";
24721
+ },
24722
+ children: [
24723
+ /* @__PURE__ */ jsx("span", { style: menuIconWrapStyle, "aria-hidden": true, children: icon }),
24724
+ children
24725
+ ]
24726
+ }
24727
+ );
24728
+ }
24729
+ const menuIconWrapStyle = {
24730
+ display: "flex",
24731
+ alignItems: "center",
24732
+ justifyContent: "center",
24733
+ flexShrink: 0,
24734
+ color: "#666"
24735
+ };
24736
+ function PublishedCheckIcon() {
24737
+ return /* @__PURE__ */ jsx(
24738
+ "svg",
24739
+ {
24740
+ width: "14",
24741
+ height: "14",
24742
+ viewBox: "0 0 24 24",
24743
+ fill: "none",
24744
+ stroke: "currentColor",
24745
+ strokeWidth: "2.5",
24746
+ strokeLinecap: "round",
24747
+ strokeLinejoin: "round",
24748
+ "aria-hidden": true,
24749
+ children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" })
24750
+ }
24751
+ );
24752
+ }
24753
+ function HistoryIcon() {
24754
+ return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
24755
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
24756
+ /* @__PURE__ */ jsx("polyline", { points: "12 6 12 12 16 14" })
24757
+ ] });
24758
+ }
24759
+ function GearIcon() {
24760
+ return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
24761
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
24762
+ /* @__PURE__ */ jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" })
24763
+ ] });
24764
+ }
24765
+ function LogOutIcon() {
24766
+ return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
24767
+ /* @__PURE__ */ jsx("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
24768
+ /* @__PURE__ */ jsx("polyline", { points: "16 17 21 12 16 7" }),
24769
+ /* @__PURE__ */ jsx("line", { x1: "21", y1: "12", x2: "9", y2: "12" })
24770
+ ] });
24771
+ }
24772
+ function UndoIcon() {
24773
+ return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
24774
+ /* @__PURE__ */ jsx("path", { d: "M3 7v6h6" }),
24775
+ /* @__PURE__ */ jsx("path", { d: "M21 17a9 9 0 0 0-15-6.7L3 13" })
24776
+ ] });
24777
+ }
24778
+ function EditModeShell({ children, themeStore, active }) {
24779
+ useSettoDocumentLayout(active);
24780
+ const [toolbarRoot, setToolbarRoot] = useState(null);
24781
+ const [hintRoot, setHintRoot] = useState(null);
24782
+ const [sectionToolbarRoot, setSectionToolbarRoot] = useState(null);
24783
+ useEffect(() => {
24784
+ if (!active) {
24785
+ setToolbarRoot(null);
24786
+ setHintRoot(null);
24787
+ setSectionToolbarRoot(null);
24788
+ return;
24789
+ }
24790
+ const toolbar = document.createElement("div");
24791
+ toolbar.id = "setto-toolbar-root";
24792
+ document.body.appendChild(toolbar);
24793
+ const hint = document.createElement("div");
24794
+ hint.id = "setto-hint-root";
24795
+ hint.style.pointerEvents = "none";
24796
+ document.body.appendChild(hint);
24797
+ const sectionBar = document.createElement("div");
24798
+ sectionBar.id = "setto-section-toolbar-root";
24799
+ sectionBar.style.pointerEvents = "none";
24800
+ document.body.appendChild(sectionBar);
24801
+ setToolbarRoot(toolbar);
24802
+ setHintRoot(hint);
24803
+ setSectionToolbarRoot(sectionBar);
24804
+ return () => {
24805
+ toolbar.remove();
24806
+ hint.remove();
24807
+ sectionBar.remove();
24808
+ };
24809
+ }, [active]);
24810
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
24811
+ children,
24812
+ active && toolbarRoot ? createPortal(/* @__PURE__ */ jsx(EditToolbar, {}), toolbarRoot) : null,
24813
+ active && hintRoot ? createPortal(/* @__PURE__ */ jsx(EditLinkHint, {}), hintRoot) : null,
24814
+ active && sectionToolbarRoot && themeStore ? createPortal(/* @__PURE__ */ jsx(SectionToolbar, {}), sectionToolbarRoot) : null
23151
24815
  ] });
23152
24816
  }
23153
- function toBcp47(code) {
23154
- const base = (code.split("-")[0] ?? code).toLowerCase();
23155
- if (base === "no" || base === "nb" || base === "nn") return "nb-NO";
23156
- if (base === "en") return "en";
23157
- return code;
24817
+ const SettoContext = createContext(null);
24818
+ function SettoProvider({ config, children }) {
24819
+ const supabase = useMemo(() => getSupabase(config.supabase), [config.supabase]);
24820
+ const api = useMemo(() => createApi({ apiUrl: config.apiUrl, supabase }), [config.apiUrl, supabase]);
24821
+ const [session, setSession] = useState(null);
24822
+ const [authLoading, setAuthLoading] = useState(true);
24823
+ const [onAdminRoute, setOnAdminRoute] = useState(
24824
+ () => typeof window === "undefined" ? false : isAdminRoute()
24825
+ );
24826
+ useEffect(() => {
24827
+ let cancelled = false;
24828
+ supabase.auth.getSession().then(({ data }) => {
24829
+ if (cancelled) return;
24830
+ setSession(data.session ?? null);
24831
+ setAuthLoading(false);
24832
+ });
24833
+ const { data: sub } = supabase.auth.onAuthStateChange((_event, session2) => {
24834
+ setSession(session2);
24835
+ });
24836
+ return () => {
24837
+ cancelled = true;
24838
+ sub.subscription.unsubscribe();
24839
+ };
24840
+ }, [supabase]);
24841
+ useEffect(() => {
24842
+ const compute = () => setOnAdminRoute(isAdminRoute());
24843
+ compute();
24844
+ window.addEventListener("popstate", compute);
24845
+ return () => window.removeEventListener("popstate", compute);
24846
+ }, []);
24847
+ const access = useSiteAccess(supabase, session, config.siteId);
24848
+ const { i18n } = useTranslation();
24849
+ const storeRef = useRef(null);
24850
+ const themeStoreRef = useRef(null);
24851
+ const assetStoreRef = useRef(null);
24852
+ const [storeReady, setStoreReady] = useState(false);
24853
+ useEffect(() => {
24854
+ if (!i18n) return;
24855
+ storeRef.current = new I18nStore(i18n);
24856
+ themeStoreRef.current = new ThemeStore(config.theme ?? {});
24857
+ assetStoreRef.current = new AssetStore();
24858
+ setStoreReady(true);
24859
+ }, [i18n, config.theme]);
24860
+ const editMode = !!session && !!access.currentSite && !onAdminRoute && !authLoading && !access.loading;
24861
+ const value = {
24862
+ config,
24863
+ supabase,
24864
+ api,
24865
+ session,
24866
+ authLoading,
24867
+ editMode,
24868
+ currentSite: access.currentSite,
24869
+ sites: access.sites,
24870
+ store: storeReady ? storeRef.current : null,
24871
+ themeStore: storeReady ? themeStoreRef.current : null,
24872
+ assetStore: storeReady ? assetStoreRef.current : null
24873
+ };
24874
+ const editShellActive = editMode && !!value.store;
24875
+ return /* @__PURE__ */ jsx(SettoContext.Provider, { value, children: /* @__PURE__ */ jsx(SectionEditProvider, { children: /* @__PURE__ */ jsx(EditModeShell, { active: editShellActive, themeStore: value.themeStore, children }) }) });
24876
+ }
24877
+ function useSetto() {
24878
+ const ctx = useContext(SettoContext);
24879
+ if (!ctx) {
24880
+ throw new Error("useSetto() must be used inside <SettoProvider>");
24881
+ }
24882
+ return ctx;
23158
24883
  }
23159
24884
  function isTextEditClick(target) {
23160
24885
  if (target.closest("[data-setto-key]")) return true;
@@ -23284,99 +25009,6 @@ const SettoBlock = forwardRef(
23284
25009
  );
23285
25010
  }
23286
25011
  );
23287
- function FloatingPopover({
23288
- open,
23289
- anchorRef,
23290
- onClose,
23291
- ariaLabel,
23292
- children,
23293
- style,
23294
- minWidth = 280
23295
- }) {
23296
- const popoverRef = useRef(null);
23297
- const [pos, setPos] = useState(null);
23298
- useLayoutEffect(() => {
23299
- if (!open || !anchorRef.current) {
23300
- setPos(null);
23301
- return;
23302
- }
23303
- const update = () => {
23304
- const anchor = anchorRef.current;
23305
- const pop = popoverRef.current;
23306
- if (!anchor) return;
23307
- const rect = anchor.getBoundingClientRect();
23308
- const popoverH = pop?.offsetHeight ?? 200;
23309
- const popoverW = pop?.offsetWidth ?? minWidth;
23310
- let top = rect.bottom + 6;
23311
- let left = rect.left;
23312
- if (left + popoverW > window.innerWidth - 8) {
23313
- left = window.innerWidth - popoverW - 8;
23314
- }
23315
- if (left < 8) left = 8;
23316
- if (top + popoverH > window.innerHeight - 8) {
23317
- top = rect.top - popoverH - 6;
23318
- }
23319
- if (top < 8) top = rect.bottom + 6;
23320
- setPos({ top, left });
23321
- };
23322
- update();
23323
- requestAnimationFrame(update);
23324
- window.addEventListener("scroll", update, true);
23325
- window.addEventListener("resize", update);
23326
- return () => {
23327
- window.removeEventListener("scroll", update, true);
23328
- window.removeEventListener("resize", update);
23329
- };
23330
- }, [open, anchorRef, minWidth]);
23331
- useEffect(() => {
23332
- if (!open) return;
23333
- const onKey = (e) => {
23334
- if (e.key === "Escape") onClose();
23335
- };
23336
- window.addEventListener("keydown", onKey);
23337
- return () => window.removeEventListener("keydown", onKey);
23338
- }, [open, onClose]);
23339
- useEffect(() => {
23340
- if (!open) return;
23341
- const close = (e) => {
23342
- const target = e.target;
23343
- if (anchorRef.current?.contains(target)) return;
23344
- if (popoverRef.current?.contains(target)) return;
23345
- onClose();
23346
- };
23347
- document.addEventListener("mousedown", close);
23348
- return () => document.removeEventListener("mousedown", close);
23349
- }, [open, onClose, anchorRef]);
23350
- if (!open) return null;
23351
- return createPortal(
23352
- /* @__PURE__ */ jsx(
23353
- "div",
23354
- {
23355
- ref: popoverRef,
23356
- role: "dialog",
23357
- "aria-label": ariaLabel,
23358
- "data-setto-ui": true,
23359
- style: {
23360
- position: "fixed",
23361
- top: pos?.top ?? -9999,
23362
- left: pos?.left ?? 0,
23363
- visibility: pos ? "visible" : "hidden",
23364
- zIndex: 2147483647,
23365
- background: "#fff",
23366
- border: "1px solid #e0e0e0",
23367
- borderRadius: 8,
23368
- boxShadow: "0 8px 24px rgba(0,0,0,0.15)",
23369
- fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
23370
- ...style
23371
- },
23372
- onMouseDown: (e) => e.stopPropagation(),
23373
- onClick: (e) => e.stopPropagation(),
23374
- children
23375
- }
23376
- ),
23377
- document.body
23378
- );
23379
- }
23380
25012
  function SettoIcon({ k, icons, size = 24, className, style }) {
23381
25013
  const { t, i18n } = useTranslation();
23382
25014
  const { editMode, store } = useSetto();
@@ -23812,95 +25444,17 @@ const removeBtnStyle = {
23812
25444
  justifyContent: "center",
23813
25445
  width: 24,
23814
25446
  height: 24,
23815
- padding: 0,
23816
- background: "#ffffff",
23817
- border: "1px solid #e0e0e0",
23818
- borderRadius: "50%",
23819
- fontSize: 16,
23820
- lineHeight: 1,
23821
- color: "#444",
23822
- cursor: "pointer",
23823
- boxShadow: "0 1px 4px rgba(0,0,0,0.18)",
23824
- fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
23825
- };
23826
- function SettoForm({
23827
- formId,
23828
- fields,
23829
- endpoint,
23830
- endpointKey,
23831
- className
23832
- }) {
23833
- const { t } = useTranslation();
23834
- const [status, setStatus] = useState("idle");
23835
- const [values, setValues] = useState(
23836
- () => Object.fromEntries(fields.map((f) => [f.name, ""]))
23837
- );
23838
- const setField = useCallback(
23839
- (name, value) => setValues((prev) => ({ ...prev, [name]: value })),
23840
- []
23841
- );
23842
- const resolveEndpoint = () => {
23843
- if (endpoint) return endpoint;
23844
- if (endpointKey) {
23845
- const resolved = t(endpointKey, { defaultValue: "" });
23846
- return resolved && resolved !== endpointKey ? resolved : null;
23847
- }
23848
- return null;
23849
- };
23850
- const onSubmit = async (e) => {
23851
- e.preventDefault();
23852
- const url = resolveEndpoint();
23853
- if (!url) {
23854
- setStatus("error");
23855
- return;
23856
- }
23857
- setStatus("submitting");
23858
- try {
23859
- const res = await fetch(url, {
23860
- method: "POST",
23861
- headers: { "Content-Type": "application/json" },
23862
- body: JSON.stringify(values)
23863
- });
23864
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
23865
- setStatus("success");
23866
- setValues(
23867
- (prev) => Object.fromEntries(Object.keys(prev).map((k) => [k, ""]))
23868
- );
23869
- } catch {
23870
- setStatus("error");
23871
- }
23872
- };
23873
- if (status === "success") {
23874
- return /* @__PURE__ */ jsx("div", { className, "data-setto-form-status": "success", children: /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx(T, { k: `form.${formId}.success` }) }) });
23875
- }
23876
- return /* @__PURE__ */ jsxs("form", { className, onSubmit, "data-setto-form": formId, noValidate: true, children: [
23877
- fields.map((field) => /* @__PURE__ */ jsxs("label", { "data-setto-form-field": field.name, children: [
23878
- /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(T, { k: field.labelKey }) }),
23879
- field.type === "textarea" ? /* @__PURE__ */ jsx(
23880
- "textarea",
23881
- {
23882
- name: field.name,
23883
- required: field.required,
23884
- value: values[field.name] ?? "",
23885
- placeholder: field.placeholderKey ? t(field.placeholderKey, { defaultValue: "" }) : void 0,
23886
- onChange: (e) => setField(field.name, e.target.value)
23887
- }
23888
- ) : /* @__PURE__ */ jsx(
23889
- "input",
23890
- {
23891
- type: field.type,
23892
- name: field.name,
23893
- required: field.required,
23894
- value: values[field.name] ?? "",
23895
- placeholder: field.placeholderKey ? t(field.placeholderKey, { defaultValue: "" }) : void 0,
23896
- onChange: (e) => setField(field.name, e.target.value)
23897
- }
23898
- )
23899
- ] }, field.name)),
23900
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: status === "submitting", children: /* @__PURE__ */ jsx(T, { k: `form.${formId}.submit` }) }),
23901
- status === "error" ? /* @__PURE__ */ jsx("p", { "data-setto-form-status": "error", role: "alert", children: /* @__PURE__ */ jsx(T, { k: `form.${formId}.error` }) }) : null
23902
- ] });
23903
- }
25447
+ padding: 0,
25448
+ background: "#ffffff",
25449
+ border: "1px solid #e0e0e0",
25450
+ borderRadius: "50%",
25451
+ fontSize: 16,
25452
+ lineHeight: 1,
25453
+ color: "#444",
25454
+ cursor: "pointer",
25455
+ boxShadow: "0 1px 4px rgba(0,0,0,0.18)",
25456
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
25457
+ };
23904
25458
  function SettoVideo({ srcKey, posterKey, title, className }) {
23905
25459
  const { t, i18n } = useTranslation();
23906
25460
  const { store } = useSetto();
@@ -24166,7 +25720,7 @@ function AuthGate({ children }) {
24166
25720
  };
24167
25721
  if (mode === "set_password") {
24168
25722
  const callback = authCallbackType();
24169
- return /* @__PURE__ */ jsx("div", { style: shellStyle$1, children: /* @__PURE__ */ jsxs("form", { onSubmit: submitSetPassword, style: formStyle, children: [
25723
+ return /* @__PURE__ */ jsx("div", { style: shellStyle, children: /* @__PURE__ */ jsxs("form", { onSubmit: submitSetPassword, style: formStyle, children: [
24170
25724
  /* @__PURE__ */ jsx("h1", { style: { margin: 0, fontSize: 22 }, children: "Setto" }),
24171
25725
  /* @__PURE__ */ jsx("p", { style: { margin: 0, color: "#888", fontSize: 13 }, children: callback === "invite" ? "Velg et passord for å fullføre invitasjonen." : "Velg et nytt passord." }),
24172
25726
  /* @__PURE__ */ jsx(
@@ -24201,7 +25755,7 @@ function AuthGate({ children }) {
24201
25755
  ] }) });
24202
25756
  }
24203
25757
  if (mode === "forgot") {
24204
- return /* @__PURE__ */ jsx("div", { style: shellStyle$1, children: /* @__PURE__ */ jsxs("form", { onSubmit: submitForgot, style: formStyle, children: [
25758
+ return /* @__PURE__ */ jsx("div", { style: shellStyle, children: /* @__PURE__ */ jsxs("form", { onSubmit: submitForgot, style: formStyle, children: [
24205
25759
  /* @__PURE__ */ jsx("h1", { style: { margin: 0, fontSize: 22 }, children: "Setto" }),
24206
25760
  /* @__PURE__ */ jsx("p", { style: { margin: 0, color: "#888", fontSize: 13 }, children: "Skriv inn e-posten din, så sender vi en lenke for å nullstille passordet." }),
24207
25761
  /* @__PURE__ */ jsx(
@@ -24234,7 +25788,7 @@ function AuthGate({ children }) {
24234
25788
  )
24235
25789
  ] }) });
24236
25790
  }
24237
- return /* @__PURE__ */ jsx("div", { style: shellStyle$1, children: /* @__PURE__ */ jsxs("form", { onSubmit: submitSignIn, style: formStyle, children: [
25791
+ return /* @__PURE__ */ jsx("div", { style: shellStyle, children: /* @__PURE__ */ jsxs("form", { onSubmit: submitSignIn, style: formStyle, children: [
24238
25792
  /* @__PURE__ */ jsx("h1", { style: { margin: 0, fontSize: 22 }, children: "Setto" }),
24239
25793
  /* @__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." }),
24240
25794
  /* @__PURE__ */ jsx(
@@ -24279,7 +25833,7 @@ function AuthGate({ children }) {
24279
25833
  )
24280
25834
  ] }) });
24281
25835
  }
24282
- const shellStyle$1 = {
25836
+ const shellStyle = {
24283
25837
  minHeight: "100dvh",
24284
25838
  display: "flex",
24285
25839
  alignItems: "center",
@@ -24324,7 +25878,7 @@ const linkStyle = {
24324
25878
  cursor: "pointer"
24325
25879
  };
24326
25880
  const loadingStyle = {
24327
- ...shellStyle$1,
25881
+ ...shellStyle,
24328
25882
  fontSize: 14,
24329
25883
  color: "#888"
24330
25884
  };
@@ -24338,72 +25892,7 @@ function deploymentAdminUrl(deploymentId) {
24338
25892
  function focusedDeploymentId() {
24339
25893
  return new URLSearchParams(window.location.search).get("deployment");
24340
25894
  }
24341
- function SettoAdminApp() {
24342
- return /* @__PURE__ */ jsx(AuthGate, { children: /* @__PURE__ */ jsx(SiteList, {}) });
24343
- }
24344
- function SiteList() {
24345
- const { supabase, session, sites, currentSite, config } = useSetto();
24346
- const [error, setError] = useState(null);
24347
- const redirectAfterSignIn = useRef(false);
24348
- useEffect(() => {
24349
- const { data: sub } = supabase.auth.onAuthStateChange((event) => {
24350
- if (event === "SIGNED_IN" && !isAdminDeploymentView()) {
24351
- redirectAfterSignIn.current = true;
24352
- }
24353
- });
24354
- return () => sub.subscription.unsubscribe();
24355
- }, [supabase]);
24356
- useEffect(() => {
24357
- if (!redirectAfterSignIn.current || !session || sites === null || isAdminDeploymentView()) {
24358
- return;
24359
- }
24360
- if (currentSite) {
24361
- redirectAfterSignIn.current = false;
24362
- window.location.replace(siteHomeUrl());
24363
- return;
24364
- }
24365
- redirectAfterSignIn.current = false;
24366
- }, [session, sites, currentSite]);
24367
- if (redirectAfterSignIn.current && session && (sites === null || currentSite)) {
24368
- return /* @__PURE__ */ jsx("div", { style: loadingRedirectStyle, children: "Åpner redigering …" });
24369
- }
24370
- return /* @__PURE__ */ jsxs("div", { style: shellStyle, children: [
24371
- /* @__PURE__ */ jsxs("header", { style: headerStyle, children: [
24372
- /* @__PURE__ */ jsx("h1", { style: { margin: 0, fontSize: 22 }, children: "Setto" }),
24373
- /* @__PURE__ */ jsx(SignOutButton, {})
24374
- ] }),
24375
- /* @__PURE__ */ jsxs("main", { style: mainStyle, children: [
24376
- error ? /* @__PURE__ */ jsx("p", { style: errStyle, children: error }) : null,
24377
- sites === null ? /* @__PURE__ */ jsx("p", { style: mutedStyle, children: "Laster sider…" }) : null,
24378
- currentSite ? /* @__PURE__ */ jsxs("section", { style: cardStyle, children: [
24379
- /* @__PURE__ */ jsx("h2", { style: { margin: 0, fontSize: 18 }, children: currentSite.display_name }),
24380
- /* @__PURE__ */ jsxs("p", { style: mutedStyle, children: [
24381
- currentSite.repo_owner,
24382
- "/",
24383
- currentSite.repo_name,
24384
- " · branch ",
24385
- currentSite.branch
24386
- ] }),
24387
- /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 8 }, children: /* @__PURE__ */ jsx("a", { href: siteHomeUrl(), style: primaryBtnLinkStyle, children: "Begynn å redigere" }) }),
24388
- /* @__PURE__ */ jsx(DeploymentList, { siteId: currentSite.id, onError: setError })
24389
- ] }) : sites && sites.length === 0 ? /* @__PURE__ */ jsx("p", { style: mutedStyle, children: "Du er ikke medlem av noen sider ennå. Be administratoren legge deg til." }) : null,
24390
- sites && !currentSite && sites.length > 0 ? /* @__PURE__ */ jsxs("section", { style: cardStyle, children: [
24391
- /* @__PURE__ */ jsx("h2", { style: { margin: 0, fontSize: 16 }, children: "Andre sider du har tilgang til" }),
24392
- /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", padding: 0, margin: 0 }, children: sites.map((s) => /* @__PURE__ */ jsxs("li", { style: { padding: "6px 0" }, children: [
24393
- /* @__PURE__ */ jsx("code", { style: { color: "#bda6ff" }, children: s.id }),
24394
- " — ",
24395
- s.display_name
24396
- ] }, s.id)) }),
24397
- /* @__PURE__ */ jsxs("p", { style: mutedStyle, children: [
24398
- "Du er logget inn på denne siden (",
24399
- config.siteId,
24400
- "), men er ikke medlem her. Spør administrator om tilgang."
24401
- ] })
24402
- ] }) : null
24403
- ] })
24404
- ] });
24405
- }
24406
- function DeploymentList({
25895
+ function DeploymentsView({
24407
25896
  siteId,
24408
25897
  onError
24409
25898
  }) {
@@ -24464,7 +25953,7 @@ function DeploymentList({
24464
25953
  const showFocusPanel = focusId !== null && (focusRow === null || isDeploymentInProgress(focusRow.status));
24465
25954
  if (!rows) return /* @__PURE__ */ jsx("p", { style: mutedStyle, children: "Laster deploys…" });
24466
25955
  return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
24467
- showFocusPanel ? /* @__PURE__ */ jsxs("section", { style: activeDepStyle, children: [
25956
+ showFocusPanel ? /* @__PURE__ */ jsxs("section", { style: panelStyle, children: [
24468
25957
  /* @__PURE__ */ jsx("h3", { style: { margin: 0, fontSize: 15 }, children: "Publisering pågår" }),
24469
25958
  focusRow ? /* @__PURE__ */ jsxs(Fragment, { children: [
24470
25959
  /* @__PURE__ */ jsx(PublishProgressBar, { status: focusRow.status }),
@@ -24488,14 +25977,14 @@ function DeploymentList({
24488
25977
  type: "button",
24489
25978
  onClick: () => void cancelDeployment(),
24490
25979
  disabled: canceling,
24491
- style: cancelBtnStyle,
25980
+ style: dangerBtnStyle,
24492
25981
  children: canceling ? "Avbryter …" : "Avbryt publisering"
24493
25982
  }
24494
25983
  )
24495
25984
  ] }) : /* @__PURE__ */ jsx("p", { style: mutedStyle, children: "Laster status …" })
24496
25985
  ] }) : null,
24497
25986
  rows.length === 0 ? /* @__PURE__ */ jsx("p", { style: mutedStyle, children: "Ingen deploys ennå." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
24498
- /* @__PURE__ */ jsx("h3", { style: { margin: 0, fontSize: 14, color: "#aaa" }, children: "Historikk" }),
25987
+ /* @__PURE__ */ jsx("h3", { style: subheadingStyle, children: "Historikk" }),
24499
25988
  /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }, children: rows.map((r) => {
24500
25989
  const focused = r.id === focusId;
24501
25990
  const inProgress = isDeploymentInProgress(r.status);
@@ -24504,7 +25993,7 @@ function DeploymentList({
24504
25993
  {
24505
25994
  href: inProgress ? deploymentAdminUrl(r.id) : void 0,
24506
25995
  style: {
24507
- ...depRowStyle,
25996
+ ...rowStyle,
24508
25997
  textDecoration: "none",
24509
25998
  color: "inherit",
24510
25999
  outline: focused ? "1px solid #640AFF" : void 0,
@@ -24541,9 +26030,120 @@ function statusLabel(row) {
24541
26030
  return "Avbrutt";
24542
26031
  }
24543
26032
  }
26033
+ function depDotStyle(status) {
26034
+ const colour = status === "ready" ? "#34d399" : status === "error" || status === "canceled" ? "#ff7a7a" : status === "building" ? "#fbbf24" : "#888";
26035
+ return {
26036
+ width: 8,
26037
+ height: 8,
26038
+ borderRadius: "50%",
26039
+ background: colour,
26040
+ flexShrink: 0
26041
+ };
26042
+ }
26043
+ function SettoAdminApp() {
26044
+ return /* @__PURE__ */ jsx(AuthGate, { children: /* @__PURE__ */ jsx(SiteList, {}) });
26045
+ }
26046
+ const DEPLOYMENTS_VIEW = "deployments";
26047
+ function SiteList() {
26048
+ const { supabase, session, sites, currentSite, config } = useSetto();
26049
+ const [error, setError] = useState(null);
26050
+ const [view, setView] = useState(() => currentAdminView() ?? DEPLOYMENTS_VIEW);
26051
+ const redirectAfterSignIn = useRef(false);
26052
+ useEffect(() => {
26053
+ const { data: sub } = supabase.auth.onAuthStateChange((event) => {
26054
+ if (event === "SIGNED_IN" && !isAdminPanelView()) {
26055
+ redirectAfterSignIn.current = true;
26056
+ }
26057
+ });
26058
+ return () => sub.subscription.unsubscribe();
26059
+ }, [supabase]);
26060
+ useEffect(() => {
26061
+ if (!redirectAfterSignIn.current || !session || sites === null || isAdminPanelView()) {
26062
+ return;
26063
+ }
26064
+ if (currentSite) {
26065
+ redirectAfterSignIn.current = false;
26066
+ window.location.replace(siteHomeUrl());
26067
+ return;
26068
+ }
26069
+ redirectAfterSignIn.current = false;
26070
+ }, [session, sites, currentSite]);
26071
+ useEffect(() => {
26072
+ const onPopState = () => setView(currentAdminView() ?? DEPLOYMENTS_VIEW);
26073
+ window.addEventListener("popstate", onPopState);
26074
+ return () => window.removeEventListener("popstate", onPopState);
26075
+ }, []);
26076
+ const showView = (id) => {
26077
+ setView(id);
26078
+ setError(null);
26079
+ window.history.pushState(null, "", id === DEPLOYMENTS_VIEW ? SETTO_BASE : adminViewUrl(id));
26080
+ };
26081
+ if (redirectAfterSignIn.current && session && (sites === null || currentSite)) {
26082
+ return /* @__PURE__ */ jsx("div", { style: loadingRedirectStyle, children: "Åpner redigering …" });
26083
+ }
26084
+ const activeModule = findModule(view);
26085
+ return /* @__PURE__ */ jsxs("div", { style: shellStyle$1, children: [
26086
+ /* @__PURE__ */ jsxs("header", { style: headerStyle, children: [
26087
+ /* @__PURE__ */ jsx("h1", { style: { margin: 0, fontSize: 22 }, children: "Setto" }),
26088
+ /* @__PURE__ */ jsx(SignOutButton, {})
26089
+ ] }),
26090
+ /* @__PURE__ */ jsxs("main", { style: mainStyle, children: [
26091
+ error ? /* @__PURE__ */ jsx("p", { style: errStyle, children: error }) : null,
26092
+ sites === null ? /* @__PURE__ */ jsx("p", { style: mutedStyle, children: "Laster sider…" }) : null,
26093
+ currentSite ? /* @__PURE__ */ jsxs("section", { style: cardStyle, children: [
26094
+ /* @__PURE__ */ jsx("h2", { style: { margin: 0, fontSize: 18 }, children: currentSite.display_name }),
26095
+ /* @__PURE__ */ jsxs("p", { style: mutedStyle, children: [
26096
+ currentSite.repo_owner,
26097
+ "/",
26098
+ currentSite.repo_name,
26099
+ " · branch ",
26100
+ currentSite.branch
26101
+ ] }),
26102
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 8 }, children: /* @__PURE__ */ jsx("a", { href: siteHomeUrl(), style: primaryBtnStyle, children: "Begynn å redigere" }) }),
26103
+ /* @__PURE__ */ jsxs("nav", { style: tabBarStyle, "aria-label": "Visninger", children: [
26104
+ /* @__PURE__ */ jsx(
26105
+ "button",
26106
+ {
26107
+ type: "button",
26108
+ style: tabStyle(!activeModule),
26109
+ "aria-current": !activeModule ? "page" : void 0,
26110
+ onClick: () => showView(DEPLOYMENTS_VIEW),
26111
+ children: "Publiseringer"
26112
+ }
26113
+ ),
26114
+ SETTO_MODULES.map((m) => /* @__PURE__ */ jsx(
26115
+ "button",
26116
+ {
26117
+ type: "button",
26118
+ style: tabStyle(activeModule?.id === m.id),
26119
+ "aria-current": activeModule?.id === m.id ? "page" : void 0,
26120
+ onClick: () => showView(m.id),
26121
+ children: m.label
26122
+ },
26123
+ m.id
26124
+ ))
26125
+ ] }),
26126
+ activeModule ? /* @__PURE__ */ jsx(activeModule.AdminView, { siteId: currentSite.id }) : /* @__PURE__ */ jsx(DeploymentsView, { siteId: currentSite.id, onError: setError })
26127
+ ] }) : sites && sites.length === 0 ? /* @__PURE__ */ jsx("p", { style: mutedStyle, children: "Du er ikke medlem av noen sider ennå. Be administratoren legge deg til." }) : null,
26128
+ sites && !currentSite && sites.length > 0 ? /* @__PURE__ */ jsxs("section", { style: cardStyle, children: [
26129
+ /* @__PURE__ */ jsx("h2", { style: { margin: 0, fontSize: 16 }, children: "Andre sider du har tilgang til" }),
26130
+ /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", padding: 0, margin: 0 }, children: sites.map((s) => /* @__PURE__ */ jsxs("li", { style: { padding: "6px 0" }, children: [
26131
+ /* @__PURE__ */ jsx("code", { style: { color: "#bda6ff" }, children: s.id }),
26132
+ " — ",
26133
+ s.display_name
26134
+ ] }, s.id)) }),
26135
+ /* @__PURE__ */ jsxs("p", { style: mutedStyle, children: [
26136
+ "Du er logget inn på denne siden (",
26137
+ config.siteId,
26138
+ "), men er ikke medlem her. Spør administrator om tilgang."
26139
+ ] })
26140
+ ] }) : null
26141
+ ] })
26142
+ ] });
26143
+ }
24544
26144
  function SignOutButton() {
24545
26145
  const { supabase } = useSetto();
24546
- return /* @__PURE__ */ jsx("button", { onClick: () => supabase.auth.signOut(), style: signOutBtnStyle, children: "Logg ut" });
26146
+ return /* @__PURE__ */ jsx("button", { onClick: () => supabase.auth.signOut(), style: secondaryBtnStyle, children: "Logg ut" });
24547
26147
  }
24548
26148
  const loadingRedirectStyle = {
24549
26149
  minHeight: "100dvh",
@@ -24555,101 +26155,6 @@ const loadingRedirectStyle = {
24555
26155
  fontSize: 14,
24556
26156
  fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
24557
26157
  };
24558
- const shellStyle = {
24559
- minHeight: "100dvh",
24560
- background: "#0a0a0d",
24561
- color: "#fff",
24562
- fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
24563
- };
24564
- const headerStyle = {
24565
- display: "flex",
24566
- alignItems: "center",
24567
- justifyContent: "space-between",
24568
- padding: "16px 24px",
24569
- borderBottom: "1px solid #1c1c22"
24570
- };
24571
- const mainStyle = {
24572
- maxWidth: 720,
24573
- margin: "0 auto",
24574
- padding: 24,
24575
- display: "flex",
24576
- flexDirection: "column",
24577
- gap: 16
24578
- };
24579
- const cardStyle = {
24580
- background: "#15151a",
24581
- borderRadius: 12,
24582
- padding: 20,
24583
- display: "flex",
24584
- flexDirection: "column",
24585
- gap: 12
24586
- };
24587
- const mutedStyle = {
24588
- color: "#888",
24589
- fontSize: 13,
24590
- margin: 0
24591
- };
24592
- const errStyle = {
24593
- color: "#ff7a7a",
24594
- fontSize: 13,
24595
- margin: 0
24596
- };
24597
- const primaryBtnLinkStyle = {
24598
- display: "inline-block",
24599
- background: "#640AFF",
24600
- color: "#fff",
24601
- border: "none",
24602
- borderRadius: 6,
24603
- padding: "10px 14px",
24604
- fontSize: 14,
24605
- fontWeight: 500,
24606
- textDecoration: "none"
24607
- };
24608
- const signOutBtnStyle = {
24609
- background: "transparent",
24610
- color: "#bda6ff",
24611
- border: "1px solid #2a2a32",
24612
- borderRadius: 6,
24613
- padding: "6px 10px",
24614
- fontSize: 12,
24615
- cursor: "pointer"
24616
- };
24617
- const activeDepStyle = {
24618
- background: "#1a1a20",
24619
- borderRadius: 8,
24620
- padding: 14,
24621
- display: "flex",
24622
- flexDirection: "column",
24623
- gap: 10
24624
- };
24625
- const depRowStyle = {
24626
- display: "flex",
24627
- alignItems: "center",
24628
- gap: 10,
24629
- background: "#1a1a20",
24630
- padding: "6px 10px",
24631
- borderRadius: 6
24632
- };
24633
- const cancelBtnStyle = {
24634
- alignSelf: "flex-start",
24635
- background: "transparent",
24636
- color: "#ff7a7a",
24637
- border: "1px solid #4a3030",
24638
- borderRadius: 6,
24639
- padding: "8px 14px",
24640
- fontSize: 13,
24641
- cursor: "pointer"
24642
- };
24643
- function depDotStyle(status) {
24644
- const colour = status === "ready" ? "#34d399" : status === "error" || status === "canceled" ? "#ff7a7a" : status === "building" ? "#fbbf24" : "#888";
24645
- return {
24646
- width: 8,
24647
- height: 8,
24648
- borderRadius: "50%",
24649
- background: colour,
24650
- flexShrink: 0
24651
- };
24652
- }
24653
26158
  const BLOCK_REGISTRY = {
24654
26159
  section: { allowedChildren: "any", themed: true, selectable: true },
24655
26160
  block: { allowedChildren: "any", themed: true, selectable: true },
@@ -24678,7 +26183,6 @@ function nodeChildren(node) {
24678
26183
  return [];
24679
26184
  }
24680
26185
  }
24681
- const FIELD_TYPES = ["text", "email", "tel", "textarea"];
24682
26186
  const ANIMATION_PRESETS = ["fadeIn", "slideUp", "stagger", "scrollReveal"];
24683
26187
  function validateBlockTree(input) {
24684
26188
  const issues = [];
@@ -24783,18 +26287,17 @@ function validateTypeSpecific(raw, type, path, issues) {
24783
26287
  break;
24784
26288
  case "form":
24785
26289
  requireString(raw, "formId", path, issues);
24786
- if (!Array.isArray(raw.fields) || raw.fields.length === 0) {
24787
- issues.push({ path: `${path}.fields`, message: "form must declare at least one field" });
24788
- break;
24789
- }
24790
- for (let i = 0; i < raw.fields.length; i++) {
24791
- validateFormField(raw.fields[i], `${path}.fields[${i}]`, issues);
26290
+ if (raw.fields !== void 0) {
26291
+ if (!Array.isArray(raw.fields)) {
26292
+ issues.push({ path: `${path}.fields`, message: "form.fields must be an array" });
26293
+ break;
26294
+ }
26295
+ for (let i = 0; i < raw.fields.length; i++) {
26296
+ validateFormField(raw.fields[i], `${path}.fields[${i}]`, issues);
26297
+ }
24792
26298
  }
24793
- if (raw.endpoint === void 0 && raw.endpointKey === void 0) {
24794
- issues.push({
24795
- path,
24796
- message: "form must declare either endpoint or endpointKey"
24797
- });
26299
+ if (raw.endpoint !== void 0 && typeof raw.endpoint !== "string") {
26300
+ issues.push({ path: `${path}.endpoint`, message: "form.endpoint must be a string" });
24798
26301
  }
24799
26302
  break;
24800
26303
  case "video":
@@ -24819,10 +26322,10 @@ function validateFormField(raw, path, issues) {
24819
26322
  if (typeof raw.name !== "string" || raw.name.length === 0) {
24820
26323
  issues.push({ path: `${path}.name`, message: "field.name must be a non-empty string" });
24821
26324
  }
24822
- if (typeof raw.type !== "string" || !FIELD_TYPES.includes(raw.type)) {
26325
+ if (!isFormFieldType(raw.type)) {
24823
26326
  issues.push({
24824
26327
  path: `${path}.type`,
24825
- message: `field.type must be one of ${FIELD_TYPES.join(", ")}`
26328
+ message: `field.type must be one of ${FORM_FIELD_TYPES.join(", ")}`
24826
26329
  });
24827
26330
  }
24828
26331
  requireString(raw, "labelKey", path, issues);
@@ -24915,7 +26418,6 @@ function RenderNode({ node }) {
24915
26418
  formId: node.formId,
24916
26419
  fields: node.fields,
24917
26420
  endpoint: node.endpoint,
24918
- endpointKey: node.endpointKey,
24919
26421
  className: node.className
24920
26422
  }
24921
26423
  );
@@ -25071,10 +26573,11 @@ function walk(node, out, repeaterCtx) {
25071
26573
  case "form":
25072
26574
  out.keys.add(`form.${node.formId}.submit`);
25073
26575
  out.keys.add(`form.${node.formId}.success`);
25074
- if (node.endpointKey) out.keys.add(node.endpointKey);
25075
- for (const field of node.fields) {
26576
+ out.keys.add(`form.${node.formId}.error`);
26577
+ for (const field of node.fields ?? []) {
25076
26578
  out.keys.add(field.labelKey);
25077
26579
  if (field.placeholderKey) out.keys.add(field.placeholderKey);
26580
+ if (field.optionsKey) out.keys.add(field.optionsKey);
25078
26581
  }
25079
26582
  return;
25080
26583
  case "repeater":
@@ -25128,8 +26631,11 @@ export {
25128
26631
  AuthGate,
25129
26632
  BLOCK_REGISTRY,
25130
26633
  BLOCK_TYPES,
26634
+ FORM_FIELD_TYPES,
26635
+ FORM_FIELD_TYPE_LABELS,
25131
26636
  GuestEditProvider,
25132
26637
  SETTO_BASE,
26638
+ SETTO_MODULES,
25133
26639
  SettoAdminApp,
25134
26640
  SettoAnimation,
25135
26641
  SettoBlock,
@@ -25142,9 +26648,14 @@ export {
25142
26648
  SettoSection,
25143
26649
  SettoVideo,
25144
26650
  T,
26651
+ adminViewUrl,
25145
26652
  extractContentKeys,
26653
+ formFieldsPrefix,
26654
+ isFormFieldType,
25146
26655
  nodeChildren,
25147
26656
  parseBlockTree,
26657
+ readLocaleFields,
26658
+ settoSubmissionUrl,
25148
26659
  useGuestEdit,
25149
26660
  useSectionTheme,
25150
26661
  useSetto,