@sevenfold/setto-client 0.2.8 → 0.3.3

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.
@@ -20748,15 +20748,37 @@ function createApi(args) {
20748
20748
  if (!res.ok) throw await asError(res, "getContent failed");
20749
20749
  return parseJson(res);
20750
20750
  },
20751
- async publish(siteId, files, message) {
20751
+ async publish(siteId, files, options) {
20752
20752
  const token = await bearer(supabase);
20753
+ const assets = options?.assets ?? [];
20754
+ if (assets.length === 0) {
20755
+ const res2 = await fetch(`${apiUrl}/sites/${encodeURIComponent(siteId)}/publish`, {
20756
+ method: "POST",
20757
+ headers: {
20758
+ "Content-Type": "application/json",
20759
+ Authorization: `Bearer ${token}`
20760
+ },
20761
+ body: JSON.stringify({ files, message: options?.message })
20762
+ });
20763
+ if (!res2.ok) throw await asError(res2, "publish failed");
20764
+ return parseJson(res2);
20765
+ }
20766
+ const form = new FormData();
20767
+ form.append(
20768
+ "manifest",
20769
+ JSON.stringify({
20770
+ files,
20771
+ assets: assets.map((a) => ({ path: a.repoPath })),
20772
+ message: options?.message
20773
+ })
20774
+ );
20775
+ assets.forEach((asset, index) => {
20776
+ form.append(`asset_${index}`, asset.file, asset.file.name);
20777
+ });
20753
20778
  const res = await fetch(`${apiUrl}/sites/${encodeURIComponent(siteId)}/publish`, {
20754
20779
  method: "POST",
20755
- headers: {
20756
- "Content-Type": "application/json",
20757
- Authorization: `Bearer ${token}`
20758
- },
20759
- body: JSON.stringify({ files, message })
20780
+ headers: { Authorization: `Bearer ${token}` },
20781
+ body: form
20760
20782
  });
20761
20783
  if (!res.ok) throw await asError(res, "publish failed");
20762
20784
  return parseJson(res);
@@ -20786,6 +20808,38 @@ function createApi(args) {
20786
20808
  }
20787
20809
  };
20788
20810
  }
20811
+ function getNested(obj, key) {
20812
+ if (!obj || typeof obj !== "object") return void 0;
20813
+ const parts = key.split(".");
20814
+ let cur = obj;
20815
+ for (const part of parts) {
20816
+ if (!cur || typeof cur !== "object") return void 0;
20817
+ cur = cur[part];
20818
+ }
20819
+ return cur;
20820
+ }
20821
+ function deleteNested(obj, key) {
20822
+ const parts = key.split(".");
20823
+ let cur = obj;
20824
+ for (let i = 0; i < parts.length - 1; i++) {
20825
+ const part = parts[i];
20826
+ const next = cur[part];
20827
+ if (!next || typeof next !== "object") return;
20828
+ cur = next;
20829
+ }
20830
+ delete cur[parts[parts.length - 1]];
20831
+ }
20832
+ function collectKeysUnder(obj, prefix) {
20833
+ const base = getNested(obj, prefix);
20834
+ if (!base || typeof base !== "object" || Array.isArray(base)) return [];
20835
+ return Object.keys(base).sort(compareListKeys);
20836
+ }
20837
+ function compareListKeys(a, b) {
20838
+ const na = Number(a);
20839
+ const nb = Number(b);
20840
+ if (!Number.isNaN(na) && !Number.isNaN(nb)) return na - nb;
20841
+ return a.localeCompare(b);
20842
+ }
20789
20843
  const EMPTY_I18N_STORE_SNAPSHOT = {
20790
20844
  drafts: /* @__PURE__ */ new Map(),
20791
20845
  version: 0
@@ -20875,6 +20929,49 @@ class I18nStore {
20875
20929
  size() {
20876
20930
  return this.drafts.size;
20877
20931
  }
20932
+ /** Returns sorted item keys under a list prefix, e.g. `faq.items` → ['1','2',…]. */
20933
+ getListKeys(prefix, lng) {
20934
+ const bundle = this.i18n.getResourceBundle(lng, this.ns);
20935
+ return collectKeysUnder(bundle, prefix);
20936
+ }
20937
+ /**
20938
+ * Adds a new list item under `prefix` with the given field defaults.
20939
+ * Adds the same key across all loaded languages.
20940
+ */
20941
+ addListItem(prefix, _lng, template) {
20942
+ const langs = this.i18n.languages?.length ? this.i18n.languages : [_lng];
20943
+ const allKeys = langs.flatMap((l) => this.getListKeys(prefix, l));
20944
+ const numeric = allKeys.map(Number).filter((n) => !Number.isNaN(n));
20945
+ const nextKey = numeric.length > 0 ? String(Math.max(...numeric) + 1) : "1";
20946
+ for (const lang of langs) {
20947
+ for (const [field, value] of Object.entries(template)) {
20948
+ this.set(`${prefix}.${nextKey}.${field}`, lang, value);
20949
+ }
20950
+ }
20951
+ return nextKey;
20952
+ }
20953
+ /** Removes a list item and all nested keys under `prefix.itemKey` in every language. */
20954
+ removeListItem(prefix, itemKey, _lng) {
20955
+ const langs = this.i18n.languages?.length ? this.i18n.languages : [_lng];
20956
+ for (const lang of langs) {
20957
+ const bundle = this.i18n.getResourceBundle(lang, this.ns) ?? {};
20958
+ const item = getNested(bundle, `${prefix}.${itemKey}`);
20959
+ if (!item || typeof item !== "object") continue;
20960
+ for (const field of Object.keys(item)) {
20961
+ const fullKey = `${prefix}.${itemKey}.${field}`;
20962
+ const id = `${lang}::${fullKey}`;
20963
+ this.drafts.delete(id);
20964
+ }
20965
+ deleteNested(bundle, `${prefix}.${itemKey}`);
20966
+ this.i18n.addResourceBundle(lang, this.ns, bundle, true, true);
20967
+ }
20968
+ this.i18n.emit("languageChanged", this.i18n.language);
20969
+ this.bump();
20970
+ }
20971
+ /** Re-sorts list keys after removal (optional compaction — not used by default). */
20972
+ sortListKeys(prefix, lng) {
20973
+ return this.getListKeys(prefix, lng).sort(compareListKeys);
20974
+ }
20878
20975
  /**
20879
20976
  * Serialises the full resource bundle per language as it currently looks in
20880
20977
  * i18next (with drafts applied). Used as the payload for /publish.
@@ -20896,6 +20993,93 @@ class I18nStore {
20896
20993
  for (const fn of this.listeners) fn(this.cachedSnapshot);
20897
20994
  }
20898
20995
  }
20996
+ const SETTO_IMAGE_REPO_DIR = "public/images/setto";
20997
+ const SETTO_IMAGE_URL_PREFIX = "/images/setto";
20998
+ const MIME_EXT = {
20999
+ "image/png": "png",
21000
+ "image/jpeg": "jpg",
21001
+ "image/webp": "webp",
21002
+ "image/svg+xml": "svg"
21003
+ };
21004
+ function extensionFromMime(mime) {
21005
+ return MIME_EXT[mime] ?? "bin";
21006
+ }
21007
+ function generateSettoImagePaths(srcKey, file) {
21008
+ const ext = extensionFromMime(file.type);
21009
+ const slug = srcKey.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "") || "image";
21010
+ const name = `${slug}-${Date.now()}.${ext}`;
21011
+ return {
21012
+ repoPath: `${SETTO_IMAGE_REPO_DIR}/${name}`,
21013
+ publicPath: `${SETTO_IMAGE_URL_PREFIX}/${name}`
21014
+ };
21015
+ }
21016
+ class AssetStore {
21017
+ drafts = /* @__PURE__ */ new Map();
21018
+ version = 0;
21019
+ listeners = /* @__PURE__ */ new Set();
21020
+ cachedSnapshot = {
21021
+ drafts: this.drafts,
21022
+ version: 0
21023
+ };
21024
+ snapshot() {
21025
+ return this.cachedSnapshot;
21026
+ }
21027
+ subscribe(listener) {
21028
+ this.listeners.add(listener);
21029
+ return () => this.listeners.delete(listener);
21030
+ }
21031
+ /** Returns a preview URL if a draft exists for this src key. */
21032
+ getPreviewUrl(srcKey) {
21033
+ return this.drafts.get(srcKey)?.previewUrl ?? null;
21034
+ }
21035
+ async set(args) {
21036
+ const existing = this.drafts.get(args.srcKey);
21037
+ if (existing) URL.revokeObjectURL(existing.previewUrl);
21038
+ const previewUrl = URL.createObjectURL(args.file);
21039
+ this.drafts.set(args.srcKey, {
21040
+ srcKey: args.srcKey,
21041
+ repoPath: args.repoPath,
21042
+ publicPath: args.publicPath,
21043
+ originalPublicPath: args.originalPublicPath,
21044
+ file: args.file,
21045
+ previewUrl
21046
+ });
21047
+ this.bump();
21048
+ }
21049
+ revert(srcKey) {
21050
+ const draft = this.drafts.get(srcKey);
21051
+ if (!draft) return null;
21052
+ URL.revokeObjectURL(draft.previewUrl);
21053
+ const original = draft.originalPublicPath;
21054
+ this.drafts.delete(srcKey);
21055
+ this.bump();
21056
+ return original;
21057
+ }
21058
+ revertAll() {
21059
+ for (const draft of this.drafts.values()) {
21060
+ URL.revokeObjectURL(draft.previewUrl);
21061
+ }
21062
+ this.drafts.clear();
21063
+ this.bump();
21064
+ }
21065
+ commit() {
21066
+ this.revertAll();
21067
+ }
21068
+ size() {
21069
+ return this.drafts.size;
21070
+ }
21071
+ pendingAssets() {
21072
+ return Array.from(this.drafts.values());
21073
+ }
21074
+ static isValidRepoPath(path) {
21075
+ return path.startsWith(`${SETTO_IMAGE_REPO_DIR}/`);
21076
+ }
21077
+ bump() {
21078
+ this.version += 1;
21079
+ this.cachedSnapshot = { drafts: this.drafts, version: this.version };
21080
+ for (const fn of this.listeners) fn(this.cachedSnapshot);
21081
+ }
21082
+ }
20899
21083
  const EMPTY_THEME_STORE_SNAPSHOT = {
20900
21084
  version: 0
20901
21085
  };
@@ -21880,16 +22064,6 @@ function PublishDialog({
21880
22064
  return /* @__PURE__ */ jsx(EditOverlay, { onClose: void 0, children: isReady ? /* @__PURE__ */ jsxs(Fragment, { children: [
21881
22065
  /* @__PURE__ */ jsx("h2", { style: { margin: "0 0 12px", fontSize: 18, fontWeight: 600 }, children: "Publisert" }),
21882
22066
  /* @__PURE__ */ jsx("p", { style: { margin: "0 0 20px", fontSize: 14, lineHeight: 1.5, color: "#444" }, children: "Endringene dine er live." }),
21883
- row?.url ? /* @__PURE__ */ jsx(
21884
- "a",
21885
- {
21886
- href: row.url,
21887
- target: "_blank",
21888
- rel: "noreferrer",
21889
- style: { ...linkStyle$1, display: "block", marginBottom: 16 },
21890
- children: "Åpne publisert side →"
21891
- }
21892
- ) : null,
21893
22067
  /* @__PURE__ */ jsx("button", { type: "button", onClick: onComplete, style: primaryBtnStyle, children: "OK" })
21894
22068
  ] }) : errored ? /* @__PURE__ */ jsxs(Fragment, { children: [
21895
22069
  /* @__PURE__ */ jsx("h2", { style: { margin: "0 0 12px", fontSize: 18, fontWeight: 600, color: "#dc2626" }, children: "Publisering feilet" }),
@@ -21927,11 +22101,6 @@ const secondaryBtnStyle = {
21927
22101
  color: "#666",
21928
22102
  border: "1px solid #ddd"
21929
22103
  };
21930
- const linkStyle$1 = {
21931
- color: "#640AFF",
21932
- fontSize: 14,
21933
- textDecoration: "none"
21934
- };
21935
22104
  function guessLangFromPath(path, languages) {
21936
22105
  for (const lng of languages) {
21937
22106
  if (path.includes(`/${lng}.`) || path.includes(`/${lng}/`)) return lng;
@@ -21982,7 +22151,7 @@ function useEditBaseline() {
21982
22151
  }, [session, store, config.siteId]);
21983
22152
  }
21984
22153
  function EditToolbar() {
21985
- const { store, themeStore, api, config, supabase } = useSetto();
22154
+ const { store, themeStore, assetStore, api, config, supabase } = useSetto();
21986
22155
  const { i18n } = useTranslation();
21987
22156
  useEditBaseline();
21988
22157
  const snap = useSyncExternalStore(
@@ -21997,6 +22166,12 @@ function EditToolbar() {
21997
22166
  () => themeStore?.snapshot().version ?? 0,
21998
22167
  () => 0
21999
22168
  );
22169
+ useSyncExternalStore(
22170
+ (cb) => assetStore?.subscribe(() => cb()) ?? (() => {
22171
+ }),
22172
+ () => assetStore?.snapshot().version ?? 0,
22173
+ () => 0
22174
+ );
22000
22175
  const [publishing, setPublishing] = useState(false);
22001
22176
  const [activeDeployment, setActiveDeployment] = useState(null);
22002
22177
  const [publishDialogOpen, setPublishDialogOpen] = useState(false);
@@ -22028,7 +22203,8 @@ function EditToolbar() {
22028
22203
  }, [api, supabase, config.siteId]);
22029
22204
  const textDraftCount = snap.drafts.size;
22030
22205
  const themeDraftCount = themeStore?.size() ?? 0;
22031
- const draftCount = textDraftCount + themeDraftCount;
22206
+ const assetDraftCount = assetStore?.size() ?? 0;
22207
+ const draftCount = textDraftCount + themeDraftCount + assetDraftCount;
22032
22208
  const hasDrafts = draftCount > 0;
22033
22209
  const showToolbarProgress = activeDeployment !== null && !publishDialogOpen && deploymentRow !== null && isDeploymentInProgress(deploymentRow.status) && !isDeploymentStale(deploymentRow);
22034
22210
  const showPublishedNotice = publishedNotice && !publishDialogOpen;
@@ -22124,9 +22300,33 @@ function EditToolbar() {
22124
22300
  content: JSON.stringify(themeStore.serialise(), null, 2) + "\n"
22125
22301
  });
22126
22302
  }
22303
+ if (assetStore && assetDraftCount > 0) {
22304
+ const result2 = await api.publish(config.siteId, files, {
22305
+ assets: assetStore.pendingAssets()
22306
+ });
22307
+ store.commit();
22308
+ themeStore?.commit();
22309
+ assetStore.commit();
22310
+ let deploymentId2 = result2.deploymentId;
22311
+ if (!deploymentId2 && result2.trackingDeferred) {
22312
+ deploymentId2 = await waitForDeploymentByCommit(
22313
+ supabase,
22314
+ config.siteId,
22315
+ result2.commitSha
22316
+ );
22317
+ }
22318
+ if (deploymentId2) {
22319
+ setActiveDeployment(deploymentId2);
22320
+ } else {
22321
+ setPublishDialogOpen(false);
22322
+ setPublishedNotice(true);
22323
+ }
22324
+ return;
22325
+ }
22127
22326
  const result = await api.publish(config.siteId, files);
22128
22327
  store.commit();
22129
22328
  themeStore?.commit();
22329
+ assetStore?.commit();
22130
22330
  let deploymentId = result.deploymentId;
22131
22331
  if (!deploymentId && result.trackingDeferred) {
22132
22332
  deploymentId = await waitForDeploymentByCommit(
@@ -22467,11 +22667,13 @@ function SettoProvider({ config, children }) {
22467
22667
  const { i18n } = useTranslation();
22468
22668
  const storeRef = useRef(null);
22469
22669
  const themeStoreRef = useRef(null);
22670
+ const assetStoreRef = useRef(null);
22470
22671
  const [storeReady, setStoreReady] = useState(false);
22471
22672
  useEffect(() => {
22472
22673
  if (!i18n) return;
22473
22674
  storeRef.current = new I18nStore(i18n);
22474
22675
  themeStoreRef.current = new ThemeStore(config.theme ?? {});
22676
+ assetStoreRef.current = new AssetStore();
22475
22677
  setStoreReady(true);
22476
22678
  }, [i18n, config.theme]);
22477
22679
  const editMode = !!session && editParam;
@@ -22483,7 +22685,8 @@ function SettoProvider({ config, children }) {
22483
22685
  authLoading,
22484
22686
  editMode,
22485
22687
  store: storeReady ? storeRef.current : null,
22486
- themeStore: storeReady ? themeStoreRef.current : null
22688
+ themeStore: storeReady ? themeStoreRef.current : null,
22689
+ assetStore: storeReady ? assetStoreRef.current : null
22487
22690
  };
22488
22691
  return /* @__PURE__ */ jsx(SettoContext.Provider, { value, children: /* @__PURE__ */ jsx(SectionEditProvider, { children: editMode && value.store ? /* @__PURE__ */ jsx(EditModeShell, { themeStore: value.themeStore, children }) : children }) });
22489
22692
  }
@@ -22674,6 +22877,15 @@ function T({ k }) {
22674
22877
  const handleInput = (e) => {
22675
22878
  store?.set(k, i18n.language, e.currentTarget.textContent ?? "");
22676
22879
  };
22880
+ const handlePaste = (e) => {
22881
+ e.preventDefault();
22882
+ const text = e.clipboardData.getData("text/plain");
22883
+ if (!text) return;
22884
+ const el = e.currentTarget;
22885
+ el.focus();
22886
+ document.execCommand("insertText", false, text);
22887
+ store?.set(k, i18n.language, el.textContent ?? "");
22888
+ };
22677
22889
  const lang = toBcp47(i18n.language);
22678
22890
  const showLinkChip = coarsePointer && focused && linkContext;
22679
22891
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -22694,6 +22906,7 @@ function T({ k }) {
22694
22906
  },
22695
22907
  onBlur: handleBlur,
22696
22908
  onInput: handleInput,
22909
+ onPaste: handlePaste,
22697
22910
  onMouseDown: handleMouseDown,
22698
22911
  onMouseUp: handleMouseUp,
22699
22912
  onClick: handleClick,
@@ -22721,6 +22934,9 @@ function toBcp47(code) {
22721
22934
  function isTextEditClick(target) {
22722
22935
  if (target.closest("[data-setto-key]")) return true;
22723
22936
  if (target.closest("[data-setto-ui]")) return true;
22937
+ if (target.closest("[data-setto-icon]")) return true;
22938
+ if (target.closest("[data-setto-image]")) return true;
22939
+ if (target.closest("[data-setto-repeater]")) return true;
22724
22940
  let node = target;
22725
22941
  while (node) {
22726
22942
  if (node.matches("h1,h2,h3,h4,h5,h6,p,li,figcaption") && node.querySelector("[data-setto-key]")) {
@@ -22836,6 +23052,502 @@ const SettoBlock = forwardRef(
22836
23052
  );
22837
23053
  }
22838
23054
  );
23055
+ function FloatingPopover({
23056
+ open,
23057
+ anchorRef,
23058
+ onClose,
23059
+ ariaLabel,
23060
+ children,
23061
+ style,
23062
+ minWidth = 280
23063
+ }) {
23064
+ const popoverRef = useRef(null);
23065
+ const [pos, setPos] = useState(null);
23066
+ useLayoutEffect(() => {
23067
+ if (!open || !anchorRef.current) {
23068
+ setPos(null);
23069
+ return;
23070
+ }
23071
+ const update = () => {
23072
+ const anchor = anchorRef.current;
23073
+ const pop = popoverRef.current;
23074
+ if (!anchor) return;
23075
+ const rect = anchor.getBoundingClientRect();
23076
+ const popoverH = pop?.offsetHeight ?? 200;
23077
+ const popoverW = pop?.offsetWidth ?? minWidth;
23078
+ let top = rect.bottom + 6;
23079
+ let left = rect.left;
23080
+ if (left + popoverW > window.innerWidth - 8) {
23081
+ left = window.innerWidth - popoverW - 8;
23082
+ }
23083
+ if (left < 8) left = 8;
23084
+ if (top + popoverH > window.innerHeight - 8) {
23085
+ top = rect.top - popoverH - 6;
23086
+ }
23087
+ if (top < TOOLBAR_HEIGHT + 4) top = rect.bottom + 6;
23088
+ setPos({ top, left });
23089
+ };
23090
+ update();
23091
+ requestAnimationFrame(update);
23092
+ window.addEventListener("scroll", update, true);
23093
+ window.addEventListener("resize", update);
23094
+ return () => {
23095
+ window.removeEventListener("scroll", update, true);
23096
+ window.removeEventListener("resize", update);
23097
+ };
23098
+ }, [open, anchorRef, minWidth]);
23099
+ useEffect(() => {
23100
+ if (!open) return;
23101
+ const onKey = (e) => {
23102
+ if (e.key === "Escape") onClose();
23103
+ };
23104
+ window.addEventListener("keydown", onKey);
23105
+ return () => window.removeEventListener("keydown", onKey);
23106
+ }, [open, onClose]);
23107
+ useEffect(() => {
23108
+ if (!open) return;
23109
+ const close = (e) => {
23110
+ const target = e.target;
23111
+ if (anchorRef.current?.contains(target)) return;
23112
+ if (popoverRef.current?.contains(target)) return;
23113
+ onClose();
23114
+ };
23115
+ document.addEventListener("mousedown", close);
23116
+ return () => document.removeEventListener("mousedown", close);
23117
+ }, [open, onClose, anchorRef]);
23118
+ if (!open) return null;
23119
+ return createPortal(
23120
+ /* @__PURE__ */ jsx(
23121
+ "div",
23122
+ {
23123
+ ref: popoverRef,
23124
+ role: "dialog",
23125
+ "aria-label": ariaLabel,
23126
+ "data-setto-ui": true,
23127
+ style: {
23128
+ position: "fixed",
23129
+ top: pos?.top ?? -9999,
23130
+ left: pos?.left ?? 0,
23131
+ visibility: pos ? "visible" : "hidden",
23132
+ zIndex: 2147483647,
23133
+ background: "#fff",
23134
+ border: "1px solid #e0e0e0",
23135
+ borderRadius: 8,
23136
+ boxShadow: "0 8px 24px rgba(0,0,0,0.15)",
23137
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
23138
+ ...style
23139
+ },
23140
+ onMouseDown: (e) => e.stopPropagation(),
23141
+ onClick: (e) => e.stopPropagation(),
23142
+ children
23143
+ }
23144
+ ),
23145
+ document.body
23146
+ );
23147
+ }
23148
+ function SettoIcon({ k, icons, size = 24, className, style }) {
23149
+ const { t, i18n } = useTranslation();
23150
+ const { editMode, store } = useSetto();
23151
+ const anchorRef = useRef(null);
23152
+ const [open, setOpen] = useState(false);
23153
+ const [search, setSearch] = useState("");
23154
+ const [, force] = useState(0);
23155
+ useEffect(() => {
23156
+ if (!store) return;
23157
+ return store.subscribe(() => force((x) => x + 1));
23158
+ }, [store]);
23159
+ const iconName = store ? store.get(k, i18n.language) : t(k);
23160
+ const Icon = icons[iconName] ?? icons[Object.keys(icons)[0] ?? ""] ?? null;
23161
+ if (!Icon) return null;
23162
+ if (!editMode) {
23163
+ return /* @__PURE__ */ jsx(Icon, { size, className, style });
23164
+ }
23165
+ const handleClick = (e) => {
23166
+ e.stopPropagation();
23167
+ e.preventDefault();
23168
+ setOpen((v) => !v);
23169
+ };
23170
+ const filtered = Object.entries(icons).filter(
23171
+ ([name]) => name.toLowerCase().includes(search.toLowerCase())
23172
+ );
23173
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
23174
+ /* @__PURE__ */ jsx(
23175
+ "span",
23176
+ {
23177
+ ref: anchorRef,
23178
+ "data-setto-icon": true,
23179
+ "data-setto-key": k,
23180
+ role: "button",
23181
+ tabIndex: 0,
23182
+ "aria-label": "Velg ikon",
23183
+ onClick: handleClick,
23184
+ onKeyDown: (e) => {
23185
+ if (e.key === "Enter" || e.key === " ") {
23186
+ e.preventDefault();
23187
+ setOpen((v) => !v);
23188
+ }
23189
+ },
23190
+ style: {
23191
+ display: "inline-flex",
23192
+ cursor: "pointer",
23193
+ outline: open ? "2px solid #640AFF" : void 0,
23194
+ outlineOffset: 2,
23195
+ borderRadius: 4
23196
+ },
23197
+ children: /* @__PURE__ */ jsx(Icon, { size, className, style })
23198
+ }
23199
+ ),
23200
+ /* @__PURE__ */ jsxs(
23201
+ FloatingPopover,
23202
+ {
23203
+ open,
23204
+ anchorRef,
23205
+ onClose: () => {
23206
+ setOpen(false);
23207
+ setSearch("");
23208
+ },
23209
+ ariaLabel: "Ikonvelger",
23210
+ minWidth: 320,
23211
+ style: { padding: 12, maxWidth: 360 },
23212
+ children: [
23213
+ /* @__PURE__ */ jsx(
23214
+ "input",
23215
+ {
23216
+ type: "search",
23217
+ value: search,
23218
+ onChange: (e) => setSearch(e.target.value),
23219
+ placeholder: "Søk ikon…",
23220
+ "data-setto-ui": true,
23221
+ style: {
23222
+ width: "100%",
23223
+ boxSizing: "border-box",
23224
+ padding: "8px 10px",
23225
+ marginBottom: 10,
23226
+ border: "1px solid #e0e0e0",
23227
+ borderRadius: 6,
23228
+ fontSize: 13
23229
+ }
23230
+ }
23231
+ ),
23232
+ /* @__PURE__ */ jsx(
23233
+ "div",
23234
+ {
23235
+ style: {
23236
+ display: "grid",
23237
+ gridTemplateColumns: "repeat(6, 1fr)",
23238
+ gap: 4,
23239
+ maxHeight: 240,
23240
+ overflowY: "auto"
23241
+ },
23242
+ children: filtered.map(([name, Item]) => {
23243
+ const selected = name === iconName;
23244
+ return /* @__PURE__ */ jsx(
23245
+ "button",
23246
+ {
23247
+ type: "button",
23248
+ title: name,
23249
+ "data-setto-ui": true,
23250
+ onClick: () => {
23251
+ store?.set(k, i18n.language, name);
23252
+ setOpen(false);
23253
+ setSearch("");
23254
+ },
23255
+ style: {
23256
+ display: "flex",
23257
+ alignItems: "center",
23258
+ justifyContent: "center",
23259
+ padding: 8,
23260
+ background: selected ? "#f5f3ff" : "transparent",
23261
+ border: selected ? "1px solid #640AFF" : "1px solid transparent",
23262
+ borderRadius: 6,
23263
+ cursor: "pointer",
23264
+ color: style?.color ?? "currentColor"
23265
+ },
23266
+ children: /* @__PURE__ */ jsx(Item, { size: 20 })
23267
+ },
23268
+ name
23269
+ );
23270
+ })
23271
+ }
23272
+ )
23273
+ ]
23274
+ }
23275
+ )
23276
+ ] });
23277
+ }
23278
+ const ACCEPT = "image/png,image/jpeg,image/webp,image/svg+xml";
23279
+ function setSrcAllLanguages(store, i18n, srcKey, value) {
23280
+ const langs = i18n.languages?.length ? i18n.languages : [i18n.language];
23281
+ for (const lng of langs) {
23282
+ store.set(srcKey, lng, value);
23283
+ }
23284
+ }
23285
+ function SettoImage({ srcKey, alt, className, style, ...rest }) {
23286
+ const { t, i18n } = useTranslation();
23287
+ const { editMode, store, assetStore } = useSetto();
23288
+ const fileRef = useRef(null);
23289
+ const [hovered, setHovered] = useState(false);
23290
+ const [uploading, setUploading] = useState(false);
23291
+ const [, force] = useState(0);
23292
+ useEffect(() => {
23293
+ if (!store) return;
23294
+ return store.subscribe(() => force((x) => x + 1));
23295
+ }, [store]);
23296
+ const [, forceAssets] = useState(0);
23297
+ useEffect(() => {
23298
+ if (!assetStore) return;
23299
+ return assetStore.subscribe(() => forceAssets((x) => x + 1));
23300
+ }, [assetStore]);
23301
+ const srcPath = store ? store.get(srcKey, i18n.language) : t(srcKey);
23302
+ const previewUrl = assetStore?.getPreviewUrl(srcKey);
23303
+ const src = previewUrl ?? srcPath;
23304
+ if (!editMode) {
23305
+ return /* @__PURE__ */ jsx("img", { src: srcPath, alt, className, style, ...rest });
23306
+ }
23307
+ const isFullWidth = className?.includes("w-full");
23308
+ const isFullHeight = className?.includes("h-full");
23309
+ const openFilePicker = (e) => {
23310
+ e.stopPropagation();
23311
+ e.preventDefault();
23312
+ if (!uploading) fileRef.current?.click();
23313
+ };
23314
+ const handleFile = async (e) => {
23315
+ const file = e.target.files?.[0];
23316
+ e.target.value = "";
23317
+ if (!file || !assetStore || !store) return;
23318
+ setUploading(true);
23319
+ try {
23320
+ const originalPublicPath = store.original(srcKey, i18n.language);
23321
+ const { repoPath, publicPath } = generateSettoImagePaths(srcKey, file);
23322
+ await assetStore.set({
23323
+ srcKey,
23324
+ originalPublicPath,
23325
+ repoPath,
23326
+ publicPath,
23327
+ file
23328
+ });
23329
+ setSrcAllLanguages(store, i18n, srcKey, publicPath);
23330
+ } finally {
23331
+ setUploading(false);
23332
+ }
23333
+ };
23334
+ const handleRevert = (e) => {
23335
+ e.stopPropagation();
23336
+ e.preventDefault();
23337
+ if (!assetStore || !store) return;
23338
+ const original = assetStore.revert(srcKey);
23339
+ if (original !== null) {
23340
+ setSrcAllLanguages(store, i18n, srcKey, original);
23341
+ }
23342
+ };
23343
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
23344
+ /* @__PURE__ */ jsxs(
23345
+ "span",
23346
+ {
23347
+ "data-setto-image": true,
23348
+ "data-setto-ui": true,
23349
+ onMouseEnter: () => setHovered(true),
23350
+ onMouseLeave: () => setHovered(false),
23351
+ style: {
23352
+ display: isFullWidth || isFullHeight ? "block" : "inline-block",
23353
+ width: isFullWidth ? "100%" : void 0,
23354
+ height: isFullHeight ? "100%" : void 0,
23355
+ position: "relative",
23356
+ outline: hovered ? "2px solid #640AFF" : void 0,
23357
+ outlineOffset: 2,
23358
+ borderRadius: 4
23359
+ },
23360
+ children: [
23361
+ /* @__PURE__ */ jsx("img", { src, alt, className, style, ...rest }),
23362
+ previewUrl && !hovered ? /* @__PURE__ */ jsx(
23363
+ "span",
23364
+ {
23365
+ "aria-hidden": true,
23366
+ "data-setto-ui": true,
23367
+ style: {
23368
+ position: "absolute",
23369
+ top: 4,
23370
+ right: 4,
23371
+ background: "#640AFF",
23372
+ color: "#fff",
23373
+ fontSize: 10,
23374
+ padding: "2px 6px",
23375
+ borderRadius: 4,
23376
+ fontFamily: "system-ui, sans-serif",
23377
+ pointerEvents: "none"
23378
+ },
23379
+ children: "Nytt bilde"
23380
+ }
23381
+ ) : null,
23382
+ hovered ? /* @__PURE__ */ jsxs(
23383
+ "span",
23384
+ {
23385
+ "data-setto-ui": true,
23386
+ style: {
23387
+ position: "absolute",
23388
+ inset: 0,
23389
+ display: "flex",
23390
+ flexDirection: "column",
23391
+ alignItems: "center",
23392
+ justifyContent: "center",
23393
+ gap: 8,
23394
+ background: "rgba(0, 0, 0, 0.45)",
23395
+ borderRadius: 4
23396
+ },
23397
+ children: [
23398
+ /* @__PURE__ */ jsx(
23399
+ "button",
23400
+ {
23401
+ type: "button",
23402
+ "data-setto-ui": true,
23403
+ disabled: uploading,
23404
+ onClick: openFilePicker,
23405
+ onMouseDown: (e) => e.stopPropagation(),
23406
+ style: {
23407
+ padding: "10px 18px",
23408
+ background: "#fff",
23409
+ color: "#222",
23410
+ border: "none",
23411
+ borderRadius: 6,
23412
+ fontSize: 13,
23413
+ fontWeight: 500,
23414
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
23415
+ cursor: uploading ? "wait" : "pointer",
23416
+ boxShadow: "0 2px 8px rgba(0,0,0,0.2)"
23417
+ },
23418
+ children: uploading ? "Laster opp…" : "Velg fil…"
23419
+ }
23420
+ ),
23421
+ previewUrl ? /* @__PURE__ */ jsx(
23422
+ "button",
23423
+ {
23424
+ type: "button",
23425
+ "data-setto-ui": true,
23426
+ disabled: uploading,
23427
+ onClick: handleRevert,
23428
+ onMouseDown: (e) => e.stopPropagation(),
23429
+ style: {
23430
+ padding: "6px 12px",
23431
+ background: "transparent",
23432
+ color: "#fff",
23433
+ border: "1px solid rgba(255,255,255,0.6)",
23434
+ borderRadius: 6,
23435
+ fontSize: 12,
23436
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
23437
+ cursor: uploading ? "wait" : "pointer"
23438
+ },
23439
+ children: "Angre"
23440
+ }
23441
+ ) : null
23442
+ ]
23443
+ }
23444
+ ) : null
23445
+ ]
23446
+ }
23447
+ ),
23448
+ /* @__PURE__ */ jsx(
23449
+ "input",
23450
+ {
23451
+ ref: fileRef,
23452
+ type: "file",
23453
+ accept: ACCEPT,
23454
+ hidden: true,
23455
+ onChange: handleFile
23456
+ }
23457
+ )
23458
+ ] });
23459
+ }
23460
+ function SettoRepeater({
23461
+ itemsKey,
23462
+ defaultItem,
23463
+ itemLabel = "element",
23464
+ children,
23465
+ className
23466
+ }) {
23467
+ const { i18n } = useTranslation();
23468
+ const { editMode, store } = useSetto();
23469
+ const lng = i18n.language;
23470
+ const [, force] = useState(0);
23471
+ useEffect(() => {
23472
+ if (!store) return;
23473
+ return store.subscribe(() => force((x) => x + 1));
23474
+ }, [store]);
23475
+ const keys = store ? store.getListKeys(itemsKey, lng) : [];
23476
+ const handleAdd = () => {
23477
+ store?.addListItem(itemsKey, lng, defaultItem);
23478
+ };
23479
+ const handleRemove = (itemKey) => {
23480
+ if (keys.length <= 1) return;
23481
+ store?.removeListItem(itemsKey, itemKey, lng);
23482
+ };
23483
+ return /* @__PURE__ */ jsxs("div", { className, "data-setto-repeater": true, children: [
23484
+ editMode ? /* @__PURE__ */ jsx(
23485
+ "div",
23486
+ {
23487
+ "data-setto-ui": true,
23488
+ style: {
23489
+ display: "flex",
23490
+ justifyContent: "flex-end",
23491
+ marginBottom: 8,
23492
+ gap: 8
23493
+ },
23494
+ children: /* @__PURE__ */ jsxs(
23495
+ "button",
23496
+ {
23497
+ type: "button",
23498
+ "data-setto-ui": true,
23499
+ onClick: (e) => {
23500
+ e.stopPropagation();
23501
+ handleAdd();
23502
+ },
23503
+ style: controlBtnStyle,
23504
+ children: [
23505
+ "+ Legg til ",
23506
+ itemLabel
23507
+ ]
23508
+ }
23509
+ )
23510
+ }
23511
+ ) : null,
23512
+ keys.map((itemKey, index) => /* @__PURE__ */ jsxs("div", { className: "setto-repeater-item", style: { position: "relative" }, children: [
23513
+ editMode ? /* @__PURE__ */ jsx(
23514
+ "button",
23515
+ {
23516
+ type: "button",
23517
+ "data-setto-ui": true,
23518
+ "aria-label": `Fjern ${itemLabel}`,
23519
+ disabled: keys.length <= 1,
23520
+ onClick: (e) => {
23521
+ e.stopPropagation();
23522
+ handleRemove(itemKey);
23523
+ },
23524
+ style: {
23525
+ ...controlBtnStyle,
23526
+ position: "absolute",
23527
+ top: 0,
23528
+ right: 0,
23529
+ zIndex: 2,
23530
+ padding: "4px 8px",
23531
+ fontSize: 11,
23532
+ opacity: keys.length <= 1 ? 0.4 : 1
23533
+ },
23534
+ children: "× Fjern"
23535
+ }
23536
+ ) : null,
23537
+ children(itemKey, index)
23538
+ ] }, itemKey))
23539
+ ] });
23540
+ }
23541
+ const controlBtnStyle = {
23542
+ padding: "6px 10px",
23543
+ background: "#f7f7f7",
23544
+ border: "1px solid #e0e0e0",
23545
+ borderRadius: 6,
23546
+ fontSize: 12,
23547
+ color: "#444",
23548
+ cursor: "pointer",
23549
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
23550
+ };
22839
23551
  function editModeUrl(pathname = "/") {
22840
23552
  const u = new URL(window.location.href);
22841
23553
  u.pathname = pathname;
@@ -23475,7 +24187,10 @@ export {
23475
24187
  AuthGate,
23476
24188
  SettoAdminApp,
23477
24189
  SettoBlock,
24190
+ SettoIcon,
24191
+ SettoImage,
23478
24192
  SettoProvider,
24193
+ SettoRepeater,
23479
24194
  SettoSection,
23480
24195
  T,
23481
24196
  useSectionTheme,