@sevenfold/setto-client 0.2.9 → 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
  };
@@ -21967,7 +22151,7 @@ function useEditBaseline() {
21967
22151
  }, [session, store, config.siteId]);
21968
22152
  }
21969
22153
  function EditToolbar() {
21970
- const { store, themeStore, api, config, supabase } = useSetto();
22154
+ const { store, themeStore, assetStore, api, config, supabase } = useSetto();
21971
22155
  const { i18n } = useTranslation();
21972
22156
  useEditBaseline();
21973
22157
  const snap = useSyncExternalStore(
@@ -21982,6 +22166,12 @@ function EditToolbar() {
21982
22166
  () => themeStore?.snapshot().version ?? 0,
21983
22167
  () => 0
21984
22168
  );
22169
+ useSyncExternalStore(
22170
+ (cb) => assetStore?.subscribe(() => cb()) ?? (() => {
22171
+ }),
22172
+ () => assetStore?.snapshot().version ?? 0,
22173
+ () => 0
22174
+ );
21985
22175
  const [publishing, setPublishing] = useState(false);
21986
22176
  const [activeDeployment, setActiveDeployment] = useState(null);
21987
22177
  const [publishDialogOpen, setPublishDialogOpen] = useState(false);
@@ -22013,7 +22203,8 @@ function EditToolbar() {
22013
22203
  }, [api, supabase, config.siteId]);
22014
22204
  const textDraftCount = snap.drafts.size;
22015
22205
  const themeDraftCount = themeStore?.size() ?? 0;
22016
- const draftCount = textDraftCount + themeDraftCount;
22206
+ const assetDraftCount = assetStore?.size() ?? 0;
22207
+ const draftCount = textDraftCount + themeDraftCount + assetDraftCount;
22017
22208
  const hasDrafts = draftCount > 0;
22018
22209
  const showToolbarProgress = activeDeployment !== null && !publishDialogOpen && deploymentRow !== null && isDeploymentInProgress(deploymentRow.status) && !isDeploymentStale(deploymentRow);
22019
22210
  const showPublishedNotice = publishedNotice && !publishDialogOpen;
@@ -22109,9 +22300,33 @@ function EditToolbar() {
22109
22300
  content: JSON.stringify(themeStore.serialise(), null, 2) + "\n"
22110
22301
  });
22111
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
+ }
22112
22326
  const result = await api.publish(config.siteId, files);
22113
22327
  store.commit();
22114
22328
  themeStore?.commit();
22329
+ assetStore?.commit();
22115
22330
  let deploymentId = result.deploymentId;
22116
22331
  if (!deploymentId && result.trackingDeferred) {
22117
22332
  deploymentId = await waitForDeploymentByCommit(
@@ -22452,11 +22667,13 @@ function SettoProvider({ config, children }) {
22452
22667
  const { i18n } = useTranslation();
22453
22668
  const storeRef = useRef(null);
22454
22669
  const themeStoreRef = useRef(null);
22670
+ const assetStoreRef = useRef(null);
22455
22671
  const [storeReady, setStoreReady] = useState(false);
22456
22672
  useEffect(() => {
22457
22673
  if (!i18n) return;
22458
22674
  storeRef.current = new I18nStore(i18n);
22459
22675
  themeStoreRef.current = new ThemeStore(config.theme ?? {});
22676
+ assetStoreRef.current = new AssetStore();
22460
22677
  setStoreReady(true);
22461
22678
  }, [i18n, config.theme]);
22462
22679
  const editMode = !!session && editParam;
@@ -22468,7 +22685,8 @@ function SettoProvider({ config, children }) {
22468
22685
  authLoading,
22469
22686
  editMode,
22470
22687
  store: storeReady ? storeRef.current : null,
22471
- themeStore: storeReady ? themeStoreRef.current : null
22688
+ themeStore: storeReady ? themeStoreRef.current : null,
22689
+ assetStore: storeReady ? assetStoreRef.current : null
22472
22690
  };
22473
22691
  return /* @__PURE__ */ jsx(SettoContext.Provider, { value, children: /* @__PURE__ */ jsx(SectionEditProvider, { children: editMode && value.store ? /* @__PURE__ */ jsx(EditModeShell, { themeStore: value.themeStore, children }) : children }) });
22474
22692
  }
@@ -22659,6 +22877,15 @@ function T({ k }) {
22659
22877
  const handleInput = (e) => {
22660
22878
  store?.set(k, i18n.language, e.currentTarget.textContent ?? "");
22661
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
+ };
22662
22889
  const lang = toBcp47(i18n.language);
22663
22890
  const showLinkChip = coarsePointer && focused && linkContext;
22664
22891
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -22679,6 +22906,7 @@ function T({ k }) {
22679
22906
  },
22680
22907
  onBlur: handleBlur,
22681
22908
  onInput: handleInput,
22909
+ onPaste: handlePaste,
22682
22910
  onMouseDown: handleMouseDown,
22683
22911
  onMouseUp: handleMouseUp,
22684
22912
  onClick: handleClick,
@@ -22706,6 +22934,9 @@ function toBcp47(code) {
22706
22934
  function isTextEditClick(target) {
22707
22935
  if (target.closest("[data-setto-key]")) return true;
22708
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;
22709
22940
  let node = target;
22710
22941
  while (node) {
22711
22942
  if (node.matches("h1,h2,h3,h4,h5,h6,p,li,figcaption") && node.querySelector("[data-setto-key]")) {
@@ -22821,6 +23052,502 @@ const SettoBlock = forwardRef(
22821
23052
  );
22822
23053
  }
22823
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
+ };
22824
23551
  function editModeUrl(pathname = "/") {
22825
23552
  const u = new URL(window.location.href);
22826
23553
  u.pathname = pathname;
@@ -23460,7 +24187,10 @@ export {
23460
24187
  AuthGate,
23461
24188
  SettoAdminApp,
23462
24189
  SettoBlock,
24190
+ SettoIcon,
24191
+ SettoImage,
23463
24192
  SettoProvider,
24193
+ SettoRepeater,
23464
24194
  SettoSection,
23465
24195
  T,
23466
24196
  useSectionTheme,