@sevenfold/setto-client 0.2.9 → 0.3.4

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
  }
@@ -22479,6 +22697,30 @@ function useSetto() {
22479
22697
  }
22480
22698
  return ctx;
22481
22699
  }
22700
+ const GuestEditContext = createContext(null);
22701
+ function GuestEditProvider({ children }) {
22702
+ const { editMode } = useSetto();
22703
+ const [active, setActive] = useState(false);
22704
+ useEffect(() => {
22705
+ if (editMode && active) setActive(false);
22706
+ }, [editMode, active]);
22707
+ useEffect(() => {
22708
+ document.documentElement.classList.toggle("setto-guest-editing", active);
22709
+ return () => document.documentElement.classList.remove("setto-guest-editing");
22710
+ }, [active]);
22711
+ const start = useCallback(() => setActive(true), []);
22712
+ const stop = useCallback(() => setActive(false), []);
22713
+ return /* @__PURE__ */ jsx(GuestEditContext.Provider, { value: { active, start, stop }, children });
22714
+ }
22715
+ function useGuestEdit() {
22716
+ const ctx = useContext(GuestEditContext);
22717
+ if (!ctx) {
22718
+ return { active: false, start: () => {
22719
+ }, stop: () => {
22720
+ } };
22721
+ }
22722
+ return ctx;
22723
+ }
22482
22724
  const MARGIN = 10;
22483
22725
  const OFFSET = 8;
22484
22726
  const MIN_TOUCH = 44;
@@ -22596,6 +22838,8 @@ function rangeFromPoint(doc, x, y) {
22596
22838
  function T({ k }) {
22597
22839
  const { t, i18n } = useTranslation();
22598
22840
  const { editMode, store } = useSetto();
22841
+ const guestEdit = useGuestEdit();
22842
+ const editable = editMode || guestEdit.active;
22599
22843
  const ref = useRef(null);
22600
22844
  const [focused, setFocused] = useState(false);
22601
22845
  const [linkContext, setLinkContext] = useState(false);
@@ -22611,7 +22855,7 @@ function T({ k }) {
22611
22855
  if (!el || focused || !editMode) return;
22612
22856
  if (el.textContent !== value) el.textContent = value;
22613
22857
  }, [value, focused, editMode]);
22614
- if (!editMode) return /* @__PURE__ */ jsx(Fragment, { children: value });
22858
+ if (!editable) return /* @__PURE__ */ jsx(Fragment, { children: value });
22615
22859
  const handleMouseDown = (e) => {
22616
22860
  e.stopPropagation();
22617
22861
  const el = e.currentTarget;
@@ -22657,8 +22901,18 @@ function T({ k }) {
22657
22901
  commit(e.currentTarget);
22658
22902
  };
22659
22903
  const handleInput = (e) => {
22904
+ if (!editMode) return;
22660
22905
  store?.set(k, i18n.language, e.currentTarget.textContent ?? "");
22661
22906
  };
22907
+ const handlePaste = (e) => {
22908
+ e.preventDefault();
22909
+ const text = e.clipboardData.getData("text/plain");
22910
+ if (!text) return;
22911
+ const el = e.currentTarget;
22912
+ el.focus();
22913
+ document.execCommand("insertText", false, text);
22914
+ store?.set(k, i18n.language, el.textContent ?? "");
22915
+ };
22662
22916
  const lang = toBcp47(i18n.language);
22663
22917
  const showLinkChip = coarsePointer && focused && linkContext;
22664
22918
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -22679,6 +22933,7 @@ function T({ k }) {
22679
22933
  },
22680
22934
  onBlur: handleBlur,
22681
22935
  onInput: handleInput,
22936
+ onPaste: handlePaste,
22682
22937
  onMouseDown: handleMouseDown,
22683
22938
  onMouseUp: handleMouseUp,
22684
22939
  onClick: handleClick,
@@ -22687,7 +22942,7 @@ function T({ k }) {
22687
22942
  style: {
22688
22943
  cursor: "text",
22689
22944
  color: "inherit",
22690
- outline: focused ? "2px solid #640AFF" : void 0,
22945
+ outline: focused ? editMode ? "2px solid #640AFF" : "2px solid #C4502A" : void 0,
22691
22946
  outlineOffset: 2,
22692
22947
  borderRadius: 2,
22693
22948
  transition: "outline-color 120ms"
@@ -22706,6 +22961,9 @@ function toBcp47(code) {
22706
22961
  function isTextEditClick(target) {
22707
22962
  if (target.closest("[data-setto-key]")) return true;
22708
22963
  if (target.closest("[data-setto-ui]")) return true;
22964
+ if (target.closest("[data-setto-icon]")) return true;
22965
+ if (target.closest("[data-setto-image]")) return true;
22966
+ if (target.closest("[data-setto-repeater]")) return true;
22709
22967
  let node = target;
22710
22968
  while (node) {
22711
22969
  if (node.matches("h1,h2,h3,h4,h5,h6,p,li,figcaption") && node.querySelector("[data-setto-key]")) {
@@ -22821,6 +23079,502 @@ const SettoBlock = forwardRef(
22821
23079
  );
22822
23080
  }
22823
23081
  );
23082
+ function FloatingPopover({
23083
+ open,
23084
+ anchorRef,
23085
+ onClose,
23086
+ ariaLabel,
23087
+ children,
23088
+ style,
23089
+ minWidth = 280
23090
+ }) {
23091
+ const popoverRef = useRef(null);
23092
+ const [pos, setPos] = useState(null);
23093
+ useLayoutEffect(() => {
23094
+ if (!open || !anchorRef.current) {
23095
+ setPos(null);
23096
+ return;
23097
+ }
23098
+ const update = () => {
23099
+ const anchor = anchorRef.current;
23100
+ const pop = popoverRef.current;
23101
+ if (!anchor) return;
23102
+ const rect = anchor.getBoundingClientRect();
23103
+ const popoverH = pop?.offsetHeight ?? 200;
23104
+ const popoverW = pop?.offsetWidth ?? minWidth;
23105
+ let top = rect.bottom + 6;
23106
+ let left = rect.left;
23107
+ if (left + popoverW > window.innerWidth - 8) {
23108
+ left = window.innerWidth - popoverW - 8;
23109
+ }
23110
+ if (left < 8) left = 8;
23111
+ if (top + popoverH > window.innerHeight - 8) {
23112
+ top = rect.top - popoverH - 6;
23113
+ }
23114
+ if (top < TOOLBAR_HEIGHT + 4) top = rect.bottom + 6;
23115
+ setPos({ top, left });
23116
+ };
23117
+ update();
23118
+ requestAnimationFrame(update);
23119
+ window.addEventListener("scroll", update, true);
23120
+ window.addEventListener("resize", update);
23121
+ return () => {
23122
+ window.removeEventListener("scroll", update, true);
23123
+ window.removeEventListener("resize", update);
23124
+ };
23125
+ }, [open, anchorRef, minWidth]);
23126
+ useEffect(() => {
23127
+ if (!open) return;
23128
+ const onKey = (e) => {
23129
+ if (e.key === "Escape") onClose();
23130
+ };
23131
+ window.addEventListener("keydown", onKey);
23132
+ return () => window.removeEventListener("keydown", onKey);
23133
+ }, [open, onClose]);
23134
+ useEffect(() => {
23135
+ if (!open) return;
23136
+ const close = (e) => {
23137
+ const target = e.target;
23138
+ if (anchorRef.current?.contains(target)) return;
23139
+ if (popoverRef.current?.contains(target)) return;
23140
+ onClose();
23141
+ };
23142
+ document.addEventListener("mousedown", close);
23143
+ return () => document.removeEventListener("mousedown", close);
23144
+ }, [open, onClose, anchorRef]);
23145
+ if (!open) return null;
23146
+ return createPortal(
23147
+ /* @__PURE__ */ jsx(
23148
+ "div",
23149
+ {
23150
+ ref: popoverRef,
23151
+ role: "dialog",
23152
+ "aria-label": ariaLabel,
23153
+ "data-setto-ui": true,
23154
+ style: {
23155
+ position: "fixed",
23156
+ top: pos?.top ?? -9999,
23157
+ left: pos?.left ?? 0,
23158
+ visibility: pos ? "visible" : "hidden",
23159
+ zIndex: 2147483647,
23160
+ background: "#fff",
23161
+ border: "1px solid #e0e0e0",
23162
+ borderRadius: 8,
23163
+ boxShadow: "0 8px 24px rgba(0,0,0,0.15)",
23164
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
23165
+ ...style
23166
+ },
23167
+ onMouseDown: (e) => e.stopPropagation(),
23168
+ onClick: (e) => e.stopPropagation(),
23169
+ children
23170
+ }
23171
+ ),
23172
+ document.body
23173
+ );
23174
+ }
23175
+ function SettoIcon({ k, icons, size = 24, className, style }) {
23176
+ const { t, i18n } = useTranslation();
23177
+ const { editMode, store } = useSetto();
23178
+ const anchorRef = useRef(null);
23179
+ const [open, setOpen] = useState(false);
23180
+ const [search, setSearch] = useState("");
23181
+ const [, force] = useState(0);
23182
+ useEffect(() => {
23183
+ if (!store) return;
23184
+ return store.subscribe(() => force((x) => x + 1));
23185
+ }, [store]);
23186
+ const iconName = store ? store.get(k, i18n.language) : t(k);
23187
+ const Icon = icons[iconName] ?? icons[Object.keys(icons)[0] ?? ""] ?? null;
23188
+ if (!Icon) return null;
23189
+ if (!editMode) {
23190
+ return /* @__PURE__ */ jsx(Icon, { size, className, style });
23191
+ }
23192
+ const handleClick = (e) => {
23193
+ e.stopPropagation();
23194
+ e.preventDefault();
23195
+ setOpen((v) => !v);
23196
+ };
23197
+ const filtered = Object.entries(icons).filter(
23198
+ ([name]) => name.toLowerCase().includes(search.toLowerCase())
23199
+ );
23200
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
23201
+ /* @__PURE__ */ jsx(
23202
+ "span",
23203
+ {
23204
+ ref: anchorRef,
23205
+ "data-setto-icon": true,
23206
+ "data-setto-key": k,
23207
+ role: "button",
23208
+ tabIndex: 0,
23209
+ "aria-label": "Velg ikon",
23210
+ onClick: handleClick,
23211
+ onKeyDown: (e) => {
23212
+ if (e.key === "Enter" || e.key === " ") {
23213
+ e.preventDefault();
23214
+ setOpen((v) => !v);
23215
+ }
23216
+ },
23217
+ style: {
23218
+ display: "inline-flex",
23219
+ cursor: "pointer",
23220
+ outline: open ? "2px solid #640AFF" : void 0,
23221
+ outlineOffset: 2,
23222
+ borderRadius: 4
23223
+ },
23224
+ children: /* @__PURE__ */ jsx(Icon, { size, className, style })
23225
+ }
23226
+ ),
23227
+ /* @__PURE__ */ jsxs(
23228
+ FloatingPopover,
23229
+ {
23230
+ open,
23231
+ anchorRef,
23232
+ onClose: () => {
23233
+ setOpen(false);
23234
+ setSearch("");
23235
+ },
23236
+ ariaLabel: "Ikonvelger",
23237
+ minWidth: 320,
23238
+ style: { padding: 12, maxWidth: 360 },
23239
+ children: [
23240
+ /* @__PURE__ */ jsx(
23241
+ "input",
23242
+ {
23243
+ type: "search",
23244
+ value: search,
23245
+ onChange: (e) => setSearch(e.target.value),
23246
+ placeholder: "Søk ikon…",
23247
+ "data-setto-ui": true,
23248
+ style: {
23249
+ width: "100%",
23250
+ boxSizing: "border-box",
23251
+ padding: "8px 10px",
23252
+ marginBottom: 10,
23253
+ border: "1px solid #e0e0e0",
23254
+ borderRadius: 6,
23255
+ fontSize: 13
23256
+ }
23257
+ }
23258
+ ),
23259
+ /* @__PURE__ */ jsx(
23260
+ "div",
23261
+ {
23262
+ style: {
23263
+ display: "grid",
23264
+ gridTemplateColumns: "repeat(6, 1fr)",
23265
+ gap: 4,
23266
+ maxHeight: 240,
23267
+ overflowY: "auto"
23268
+ },
23269
+ children: filtered.map(([name, Item]) => {
23270
+ const selected = name === iconName;
23271
+ return /* @__PURE__ */ jsx(
23272
+ "button",
23273
+ {
23274
+ type: "button",
23275
+ title: name,
23276
+ "data-setto-ui": true,
23277
+ onClick: () => {
23278
+ store?.set(k, i18n.language, name);
23279
+ setOpen(false);
23280
+ setSearch("");
23281
+ },
23282
+ style: {
23283
+ display: "flex",
23284
+ alignItems: "center",
23285
+ justifyContent: "center",
23286
+ padding: 8,
23287
+ background: selected ? "#f5f3ff" : "transparent",
23288
+ border: selected ? "1px solid #640AFF" : "1px solid transparent",
23289
+ borderRadius: 6,
23290
+ cursor: "pointer",
23291
+ color: style?.color ?? "currentColor"
23292
+ },
23293
+ children: /* @__PURE__ */ jsx(Item, { size: 20 })
23294
+ },
23295
+ name
23296
+ );
23297
+ })
23298
+ }
23299
+ )
23300
+ ]
23301
+ }
23302
+ )
23303
+ ] });
23304
+ }
23305
+ const ACCEPT = "image/png,image/jpeg,image/webp,image/svg+xml";
23306
+ function setSrcAllLanguages(store, i18n, srcKey, value) {
23307
+ const langs = i18n.languages?.length ? i18n.languages : [i18n.language];
23308
+ for (const lng of langs) {
23309
+ store.set(srcKey, lng, value);
23310
+ }
23311
+ }
23312
+ function SettoImage({ srcKey, alt, className, style, ...rest }) {
23313
+ const { t, i18n } = useTranslation();
23314
+ const { editMode, store, assetStore } = useSetto();
23315
+ const fileRef = useRef(null);
23316
+ const [hovered, setHovered] = useState(false);
23317
+ const [uploading, setUploading] = useState(false);
23318
+ const [, force] = useState(0);
23319
+ useEffect(() => {
23320
+ if (!store) return;
23321
+ return store.subscribe(() => force((x) => x + 1));
23322
+ }, [store]);
23323
+ const [, forceAssets] = useState(0);
23324
+ useEffect(() => {
23325
+ if (!assetStore) return;
23326
+ return assetStore.subscribe(() => forceAssets((x) => x + 1));
23327
+ }, [assetStore]);
23328
+ const srcPath = store ? store.get(srcKey, i18n.language) : t(srcKey);
23329
+ const previewUrl = assetStore?.getPreviewUrl(srcKey);
23330
+ const src = previewUrl ?? srcPath;
23331
+ if (!editMode) {
23332
+ return /* @__PURE__ */ jsx("img", { src: srcPath, alt, className, style, ...rest });
23333
+ }
23334
+ const isFullWidth = className?.includes("w-full");
23335
+ const isFullHeight = className?.includes("h-full");
23336
+ const openFilePicker = (e) => {
23337
+ e.stopPropagation();
23338
+ e.preventDefault();
23339
+ if (!uploading) fileRef.current?.click();
23340
+ };
23341
+ const handleFile = async (e) => {
23342
+ const file = e.target.files?.[0];
23343
+ e.target.value = "";
23344
+ if (!file || !assetStore || !store) return;
23345
+ setUploading(true);
23346
+ try {
23347
+ const originalPublicPath = store.original(srcKey, i18n.language);
23348
+ const { repoPath, publicPath } = generateSettoImagePaths(srcKey, file);
23349
+ await assetStore.set({
23350
+ srcKey,
23351
+ originalPublicPath,
23352
+ repoPath,
23353
+ publicPath,
23354
+ file
23355
+ });
23356
+ setSrcAllLanguages(store, i18n, srcKey, publicPath);
23357
+ } finally {
23358
+ setUploading(false);
23359
+ }
23360
+ };
23361
+ const handleRevert = (e) => {
23362
+ e.stopPropagation();
23363
+ e.preventDefault();
23364
+ if (!assetStore || !store) return;
23365
+ const original = assetStore.revert(srcKey);
23366
+ if (original !== null) {
23367
+ setSrcAllLanguages(store, i18n, srcKey, original);
23368
+ }
23369
+ };
23370
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
23371
+ /* @__PURE__ */ jsxs(
23372
+ "span",
23373
+ {
23374
+ "data-setto-image": true,
23375
+ "data-setto-ui": true,
23376
+ onMouseEnter: () => setHovered(true),
23377
+ onMouseLeave: () => setHovered(false),
23378
+ style: {
23379
+ display: isFullWidth || isFullHeight ? "block" : "inline-block",
23380
+ width: isFullWidth ? "100%" : void 0,
23381
+ height: isFullHeight ? "100%" : void 0,
23382
+ position: "relative",
23383
+ outline: hovered ? "2px solid #640AFF" : void 0,
23384
+ outlineOffset: 2,
23385
+ borderRadius: 4
23386
+ },
23387
+ children: [
23388
+ /* @__PURE__ */ jsx("img", { src, alt, className, style, ...rest }),
23389
+ previewUrl && !hovered ? /* @__PURE__ */ jsx(
23390
+ "span",
23391
+ {
23392
+ "aria-hidden": true,
23393
+ "data-setto-ui": true,
23394
+ style: {
23395
+ position: "absolute",
23396
+ top: 4,
23397
+ right: 4,
23398
+ background: "#640AFF",
23399
+ color: "#fff",
23400
+ fontSize: 10,
23401
+ padding: "2px 6px",
23402
+ borderRadius: 4,
23403
+ fontFamily: "system-ui, sans-serif",
23404
+ pointerEvents: "none"
23405
+ },
23406
+ children: "Nytt bilde"
23407
+ }
23408
+ ) : null,
23409
+ hovered ? /* @__PURE__ */ jsxs(
23410
+ "span",
23411
+ {
23412
+ "data-setto-ui": true,
23413
+ style: {
23414
+ position: "absolute",
23415
+ inset: 0,
23416
+ display: "flex",
23417
+ flexDirection: "column",
23418
+ alignItems: "center",
23419
+ justifyContent: "center",
23420
+ gap: 8,
23421
+ background: "rgba(0, 0, 0, 0.45)",
23422
+ borderRadius: 4
23423
+ },
23424
+ children: [
23425
+ /* @__PURE__ */ jsx(
23426
+ "button",
23427
+ {
23428
+ type: "button",
23429
+ "data-setto-ui": true,
23430
+ disabled: uploading,
23431
+ onClick: openFilePicker,
23432
+ onMouseDown: (e) => e.stopPropagation(),
23433
+ style: {
23434
+ padding: "10px 18px",
23435
+ background: "#fff",
23436
+ color: "#222",
23437
+ border: "none",
23438
+ borderRadius: 6,
23439
+ fontSize: 13,
23440
+ fontWeight: 500,
23441
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
23442
+ cursor: uploading ? "wait" : "pointer",
23443
+ boxShadow: "0 2px 8px rgba(0,0,0,0.2)"
23444
+ },
23445
+ children: uploading ? "Laster opp…" : "Velg fil…"
23446
+ }
23447
+ ),
23448
+ previewUrl ? /* @__PURE__ */ jsx(
23449
+ "button",
23450
+ {
23451
+ type: "button",
23452
+ "data-setto-ui": true,
23453
+ disabled: uploading,
23454
+ onClick: handleRevert,
23455
+ onMouseDown: (e) => e.stopPropagation(),
23456
+ style: {
23457
+ padding: "6px 12px",
23458
+ background: "transparent",
23459
+ color: "#fff",
23460
+ border: "1px solid rgba(255,255,255,0.6)",
23461
+ borderRadius: 6,
23462
+ fontSize: 12,
23463
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
23464
+ cursor: uploading ? "wait" : "pointer"
23465
+ },
23466
+ children: "Angre"
23467
+ }
23468
+ ) : null
23469
+ ]
23470
+ }
23471
+ ) : null
23472
+ ]
23473
+ }
23474
+ ),
23475
+ /* @__PURE__ */ jsx(
23476
+ "input",
23477
+ {
23478
+ ref: fileRef,
23479
+ type: "file",
23480
+ accept: ACCEPT,
23481
+ hidden: true,
23482
+ onChange: handleFile
23483
+ }
23484
+ )
23485
+ ] });
23486
+ }
23487
+ function SettoRepeater({
23488
+ itemsKey,
23489
+ defaultItem,
23490
+ itemLabel = "element",
23491
+ children,
23492
+ className
23493
+ }) {
23494
+ const { i18n } = useTranslation();
23495
+ const { editMode, store } = useSetto();
23496
+ const lng = i18n.language;
23497
+ const [, force] = useState(0);
23498
+ useEffect(() => {
23499
+ if (!store) return;
23500
+ return store.subscribe(() => force((x) => x + 1));
23501
+ }, [store]);
23502
+ const keys = store ? store.getListKeys(itemsKey, lng) : [];
23503
+ const handleAdd = () => {
23504
+ store?.addListItem(itemsKey, lng, defaultItem);
23505
+ };
23506
+ const handleRemove = (itemKey) => {
23507
+ if (keys.length <= 1) return;
23508
+ store?.removeListItem(itemsKey, itemKey, lng);
23509
+ };
23510
+ return /* @__PURE__ */ jsxs("div", { className, "data-setto-repeater": true, children: [
23511
+ editMode ? /* @__PURE__ */ jsx(
23512
+ "div",
23513
+ {
23514
+ "data-setto-ui": true,
23515
+ style: {
23516
+ display: "flex",
23517
+ justifyContent: "flex-end",
23518
+ marginBottom: 8,
23519
+ gap: 8
23520
+ },
23521
+ children: /* @__PURE__ */ jsxs(
23522
+ "button",
23523
+ {
23524
+ type: "button",
23525
+ "data-setto-ui": true,
23526
+ onClick: (e) => {
23527
+ e.stopPropagation();
23528
+ handleAdd();
23529
+ },
23530
+ style: controlBtnStyle,
23531
+ children: [
23532
+ "+ Legg til ",
23533
+ itemLabel
23534
+ ]
23535
+ }
23536
+ )
23537
+ }
23538
+ ) : null,
23539
+ keys.map((itemKey, index) => /* @__PURE__ */ jsxs("div", { className: "setto-repeater-item", style: { position: "relative" }, children: [
23540
+ editMode ? /* @__PURE__ */ jsx(
23541
+ "button",
23542
+ {
23543
+ type: "button",
23544
+ "data-setto-ui": true,
23545
+ "aria-label": `Fjern ${itemLabel}`,
23546
+ disabled: keys.length <= 1,
23547
+ onClick: (e) => {
23548
+ e.stopPropagation();
23549
+ handleRemove(itemKey);
23550
+ },
23551
+ style: {
23552
+ ...controlBtnStyle,
23553
+ position: "absolute",
23554
+ top: 0,
23555
+ right: 0,
23556
+ zIndex: 2,
23557
+ padding: "4px 8px",
23558
+ fontSize: 11,
23559
+ opacity: keys.length <= 1 ? 0.4 : 1
23560
+ },
23561
+ children: "× Fjern"
23562
+ }
23563
+ ) : null,
23564
+ children(itemKey, index)
23565
+ ] }, itemKey))
23566
+ ] });
23567
+ }
23568
+ const controlBtnStyle = {
23569
+ padding: "6px 10px",
23570
+ background: "#f7f7f7",
23571
+ border: "1px solid #e0e0e0",
23572
+ borderRadius: 6,
23573
+ fontSize: 12,
23574
+ color: "#444",
23575
+ cursor: "pointer",
23576
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
23577
+ };
22824
23578
  function editModeUrl(pathname = "/") {
22825
23579
  const u = new URL(window.location.href);
22826
23580
  u.pathname = pathname;
@@ -23458,11 +24212,16 @@ function depDotStyle(status) {
23458
24212
  }
23459
24213
  export {
23460
24214
  AuthGate,
24215
+ GuestEditProvider,
23461
24216
  SettoAdminApp,
23462
24217
  SettoBlock,
24218
+ SettoIcon,
24219
+ SettoImage,
23463
24220
  SettoProvider,
24221
+ SettoRepeater,
23464
24222
  SettoSection,
23465
24223
  T,
24224
+ useGuestEdit,
23466
24225
  useSectionTheme,
23467
24226
  useSetto
23468
24227
  };