@sevenfold/setto-client 0.28.1 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { jsxs, jsx, Fragment } from "react/jsx-runtime";
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { useState, useRef, useEffect, useMemo, useId, useInsertionEffect, Children, createContext, useContext, useCallback, useLayoutEffect, useSyncExternalStore, forwardRef, Fragment as Fragment$1 } from "react";
4
4
  import { useTranslation } from "react-i18next";
@@ -72,235 +72,6 @@ function pickInitialLanguage(available, browserLanguages = typeof navigator ===
72
72
  }
73
73
  return fallback;
74
74
  }
75
- function normalise(pathname) {
76
- const path2 = pathname.startsWith("/") ? pathname : `/${pathname}`;
77
- return path2.length > 1 && path2.endsWith("/") ? path2.slice(0, -1) : path2;
78
- }
79
- function languageFromPath(pathname, languages, primary = languages[0] ?? DEFAULT_LANGUAGE) {
80
- const path2 = normalise(pathname);
81
- const first = path2.split("/")[1] ?? "";
82
- const match = languages.find(
83
- (tag) => tag !== primary && tag.toLowerCase() === first.toLowerCase()
84
- );
85
- if (!match) return { language: primary, basename: "", rest: path2 };
86
- const rest = path2.slice(first.length + 1);
87
- return { language: match, basename: `/${match}`, rest: rest === "" ? "/" : rest };
88
- }
89
- function pathForLanguage(pathname, target, languages, primary = languages[0] ?? DEFAULT_LANGUAGE) {
90
- const { rest } = languageFromPath(pathname, languages, primary);
91
- if (target === primary) return rest;
92
- return rest === "/" ? `/${target}` : `/${target}${rest}`;
93
- }
94
- function alternateLinks(origin, pathname, languages, primary = languages[0] ?? DEFAULT_LANGUAGE) {
95
- if (languages.length < 2) return [];
96
- const links = languages.map((tag) => ({
97
- hreflang: tag,
98
- href: origin + pathForLanguage(pathname, tag, languages, primary)
99
- }));
100
- links.push({
101
- hreflang: "x-default",
102
- href: origin + pathForLanguage(pathname, primary, languages, primary)
103
- });
104
- return links;
105
- }
106
- function isReservedPath(pathname) {
107
- return pathname === "/setto" || pathname.startsWith("/setto/");
108
- }
109
- function redirectForVisitor(pathname, languages, browserLanguages, stored, primary = languages[0] ?? DEFAULT_LANGUAGE) {
110
- if (languages.length < 2) return null;
111
- if (isReservedPath(pathname)) return null;
112
- const current = languageFromPath(pathname, languages, primary);
113
- if (current.basename !== "") return null;
114
- if (stored) {
115
- return stored !== primary && languages.includes(stored) ? pathForLanguage(pathname, stored, languages, primary) : null;
116
- }
117
- for (const wanted of browserLanguages) {
118
- const base = wanted.toLowerCase().split("-")[0];
119
- if (!base) continue;
120
- if (base === primary.toLowerCase().split("-")[0]) return null;
121
- const norwegian2 = ["no", "nb", "nn"];
122
- if (norwegian2.includes(base) && norwegian2.includes(primary.toLowerCase())) return null;
123
- const hit = languages.find((tag) => tag.toLowerCase().split("-")[0] === base);
124
- if (hit && hit !== primary) return pathForLanguage(pathname, hit, languages, primary);
125
- }
126
- return null;
127
- }
128
- const INLINE_MARKER = "data-setto-languages-inline";
129
- function SettoLanguages({ languages, inline = false }) {
130
- const { i18n } = useTranslation();
131
- const [open, setOpen] = useState(false);
132
- const ref = useRef(null);
133
- useEffect(() => {
134
- if (!inline) return;
135
- const root2 = document.documentElement;
136
- const count = Number(root2.getAttribute(INLINE_MARKER) ?? "0") + 1;
137
- root2.setAttribute(INLINE_MARKER, String(count));
138
- return () => {
139
- const left = Number(root2.getAttribute(INLINE_MARKER) ?? "1") - 1;
140
- if (left > 0) root2.setAttribute(INLINE_MARKER, String(left));
141
- else root2.removeAttribute(INLINE_MARKER);
142
- };
143
- }, [inline]);
144
- useEffect(() => {
145
- if (!open) return;
146
- const onDown = (e) => {
147
- if (!ref.current?.contains(e.target)) setOpen(false);
148
- };
149
- const onKey = (e) => {
150
- if (e.key === "Escape") setOpen(false);
151
- };
152
- document.addEventListener("mousedown", onDown);
153
- document.addEventListener("keydown", onKey);
154
- return () => {
155
- document.removeEventListener("mousedown", onDown);
156
- document.removeEventListener("keydown", onKey);
157
- };
158
- }, [open]);
159
- if (languages.length < 2) return null;
160
- const current = i18n.language;
161
- const choose = (tag) => {
162
- setOpen(false);
163
- if (tag === current) return;
164
- storeLanguage(tag);
165
- const target = pathForLanguage(window.location.pathname, tag, languages);
166
- window.location.assign(target + window.location.search + window.location.hash);
167
- };
168
- return /* @__PURE__ */ jsxs("div", { ref, style: inline ? inlineWrapStyle : floatingWrapStyle, children: [
169
- /* @__PURE__ */ jsxs(
170
- "button",
171
- {
172
- type: "button",
173
- onClick: () => setOpen((o) => !o),
174
- "aria-expanded": open,
175
- "aria-haspopup": "listbox",
176
- "aria-label": `Språk: ${languageName(current)}`,
177
- style: inline ? inlineTriggerStyle : floatingTriggerStyle,
178
- children: [
179
- /* @__PURE__ */ jsx(GlobeIcon, {}),
180
- languageName(current)
181
- ]
182
- }
183
- ),
184
- open ? /* @__PURE__ */ jsx("ul", { role: "listbox", style: inline ? inlineListStyle : floatingListStyle, children: languages.map((tag) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
185
- "button",
186
- {
187
- type: "button",
188
- role: "option",
189
- "aria-selected": tag === current,
190
- onClick: () => choose(tag),
191
- style: {
192
- ...optionStyle,
193
- fontWeight: tag === current ? 600 : 400
194
- },
195
- children: languageName(tag)
196
- }
197
- ) }, tag)) }) : null
198
- ] });
199
- }
200
- function AutoLanguageSwitcher({ languages }) {
201
- const [inlineElsewhere, setInlineElsewhere] = useState(false);
202
- useEffect(() => {
203
- const check = () => setInlineElsewhere(document.documentElement.hasAttribute(INLINE_MARKER));
204
- check();
205
- const observer = new MutationObserver(check);
206
- observer.observe(document.documentElement, {
207
- attributes: true,
208
- attributeFilter: [INLINE_MARKER]
209
- });
210
- return () => observer.disconnect();
211
- }, []);
212
- if (inlineElsewhere || languages.length < 2) return null;
213
- return /* @__PURE__ */ jsx(SettoLanguages, { languages });
214
- }
215
- function GlobeIcon() {
216
- return /* @__PURE__ */ jsxs(
217
- "svg",
218
- {
219
- width: "15",
220
- height: "15",
221
- viewBox: "0 0 24 24",
222
- fill: "none",
223
- stroke: "currentColor",
224
- strokeWidth: "1.8",
225
- strokeLinecap: "round",
226
- strokeLinejoin: "round",
227
- "aria-hidden": "true",
228
- children: [
229
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
230
- /* @__PURE__ */ jsx("path", { d: "M3 12h18M12 3c2.4 2.6 3.6 5.6 3.6 9s-1.2 6.4-3.6 9c-2.4-2.6-3.6-5.6-3.6-9S9.6 5.6 12 3z" })
231
- ]
232
- }
233
- );
234
- }
235
- const floatingWrapStyle = {
236
- position: "fixed",
237
- left: "max(16px, env(safe-area-inset-left))",
238
- bottom: "max(16px, env(safe-area-inset-bottom))",
239
- zIndex: 2147482e3,
240
- fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
241
- };
242
- const inlineWrapStyle = {
243
- position: "relative",
244
- display: "inline-block",
245
- fontFamily: "inherit"
246
- };
247
- const triggerBase = {
248
- display: "inline-flex",
249
- alignItems: "center",
250
- gap: 7,
251
- minHeight: 40,
252
- fontSize: 13.5,
253
- fontWeight: 500,
254
- lineHeight: 1,
255
- cursor: "pointer"
256
- };
257
- const floatingTriggerStyle = {
258
- ...triggerBase,
259
- padding: "8px 14px",
260
- borderRadius: 999,
261
- border: "1px solid rgba(127, 127, 127, 0.32)",
262
- background: "rgba(255, 255, 255, 0.82)",
263
- backdropFilter: "blur(8px)",
264
- WebkitBackdropFilter: "blur(8px)",
265
- color: "#1a1a1a",
266
- boxShadow: "0 4px 14px rgba(0, 0, 0, 0.12)"
267
- };
268
- const inlineTriggerStyle = {
269
- ...triggerBase,
270
- padding: "8px 6px",
271
- border: 0,
272
- borderRadius: 8,
273
- background: "transparent",
274
- color: "inherit",
275
- opacity: 0.75
276
- };
277
- const listBase = {
278
- position: "absolute",
279
- margin: 0,
280
- padding: 4,
281
- listStyle: "none",
282
- minWidth: 150,
283
- borderRadius: 12,
284
- border: "1px solid rgba(127, 127, 127, 0.24)",
285
- background: "#ffffff",
286
- boxShadow: "0 12px 32px rgba(0, 0, 0, 0.18)",
287
- zIndex: 1
288
- };
289
- const floatingListStyle = { ...listBase, left: 0, bottom: "calc(100% + 8px)" };
290
- const inlineListStyle = { ...listBase, left: 0, top: "calc(100% + 8px)" };
291
- const optionStyle = {
292
- display: "block",
293
- width: "100%",
294
- minHeight: 40,
295
- padding: "9px 12px",
296
- border: 0,
297
- borderRadius: 8,
298
- background: "transparent",
299
- color: "#1a1a1a",
300
- fontSize: 13.5,
301
- textAlign: "left",
302
- cursor: "pointer"
303
- };
304
75
  function __rest(s, e) {
305
76
  var t = {};
306
77
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
@@ -20993,10 +20764,10 @@ function shouldShowDeprecationWarning() {
20993
20764
  return parseInt(versionMatch[1], 10) <= 18;
20994
20765
  }
20995
20766
  if (shouldShowDeprecationWarning()) console.warn("⚠️ Node.js 18 and below are deprecated and will no longer be supported in future versions of @supabase/supabase-js. Please upgrade to Node.js 20 or later. For more information, visit: https://github.com/orgs/supabase/discussions/37217");
20996
- const cache = /* @__PURE__ */ new WeakMap();
20767
+ const cache$1 = /* @__PURE__ */ new WeakMap();
20997
20768
  function getSupabase(config) {
20998
20769
  const key = { url: config.url, anonKey: config.anonKey };
20999
- const hit = cache.get(key);
20770
+ const hit = cache$1.get(key);
21000
20771
  if (hit) return hit;
21001
20772
  const client = createClient(config.url, config.anonKey, {
21002
20773
  auth: {
@@ -21005,7 +20776,7 @@ function getSupabase(config) {
21005
20776
  storageKey: `setto-auth-${config.url}`
21006
20777
  }
21007
20778
  });
21008
- cache.set(key, client);
20779
+ cache$1.set(key, client);
21009
20780
  return client;
21010
20781
  }
21011
20782
  function createFormsApi(request) {
@@ -21473,6 +21244,59 @@ class I18nStore {
21473
21244
  for (const fn of this.listeners) fn(this.cachedSnapshot);
21474
21245
  }
21475
21246
  }
21247
+ function normalise(pathname) {
21248
+ const path2 = pathname.startsWith("/") ? pathname : `/${pathname}`;
21249
+ return path2.length > 1 && path2.endsWith("/") ? path2.slice(0, -1) : path2;
21250
+ }
21251
+ function languageFromPath(pathname, languages, primary = languages[0] ?? DEFAULT_LANGUAGE) {
21252
+ const path2 = normalise(pathname);
21253
+ const first = path2.split("/")[1] ?? "";
21254
+ const match = languages.find(
21255
+ (tag) => tag !== primary && tag.toLowerCase() === first.toLowerCase()
21256
+ );
21257
+ if (!match) return { language: primary, basename: "", rest: path2 };
21258
+ const rest = path2.slice(first.length + 1);
21259
+ return { language: match, basename: `/${match}`, rest: rest === "" ? "/" : rest };
21260
+ }
21261
+ function pathForLanguage(pathname, target, languages, primary = languages[0] ?? DEFAULT_LANGUAGE) {
21262
+ const { rest } = languageFromPath(pathname, languages, primary);
21263
+ if (target === primary) return rest;
21264
+ return rest === "/" ? `/${target}` : `/${target}${rest}`;
21265
+ }
21266
+ function alternateLinks(origin, pathname, languages, primary = languages[0] ?? DEFAULT_LANGUAGE) {
21267
+ if (languages.length < 2) return [];
21268
+ const links = languages.map((tag) => ({
21269
+ hreflang: tag,
21270
+ href: origin + pathForLanguage(pathname, tag, languages, primary)
21271
+ }));
21272
+ links.push({
21273
+ hreflang: "x-default",
21274
+ href: origin + pathForLanguage(pathname, primary, languages, primary)
21275
+ });
21276
+ return links;
21277
+ }
21278
+ function isReservedPath(pathname) {
21279
+ return pathname === "/setto" || pathname.startsWith("/setto/");
21280
+ }
21281
+ function redirectForVisitor(pathname, languages, browserLanguages, stored, primary = languages[0] ?? DEFAULT_LANGUAGE) {
21282
+ if (languages.length < 2) return null;
21283
+ if (isReservedPath(pathname)) return null;
21284
+ const current = languageFromPath(pathname, languages, primary);
21285
+ if (current.basename !== "") return null;
21286
+ if (stored) {
21287
+ return stored !== primary && languages.includes(stored) ? pathForLanguage(pathname, stored, languages, primary) : null;
21288
+ }
21289
+ for (const wanted of browserLanguages) {
21290
+ const base = wanted.toLowerCase().split("-")[0];
21291
+ if (!base) continue;
21292
+ if (base === primary.toLowerCase().split("-")[0]) return null;
21293
+ const norwegian2 = ["no", "nb", "nn"];
21294
+ if (norwegian2.includes(base) && norwegian2.includes(primary.toLowerCase())) return null;
21295
+ const hit = languages.find((tag) => tag.toLowerCase().split("-")[0] === base);
21296
+ if (hit && hit !== primary) return pathForLanguage(pathname, hit, languages, primary);
21297
+ }
21298
+ return null;
21299
+ }
21476
21300
  const SETTO_IMAGE_REPO_DIR = "public/images/setto";
21477
21301
  const SETTO_IMAGE_URL_PREFIX = "/images/setto";
21478
21302
  const MIME_EXT = {
@@ -21635,6 +21459,452 @@ class ThemeStore {
21635
21459
  for (const fn of this.listeners) fn(this.cachedSnapshot);
21636
21460
  }
21637
21461
  }
21462
+ const PAGES_MANIFEST_VERSION = 1;
21463
+ const PAGES_PATH = "src/content/pages.json";
21464
+ const BUILTIN_SECTION_TYPES = ["text"];
21465
+ const ID = /^[a-z0-9][a-z0-9-]{0,62}$/;
21466
+ const PATH = /^\/(?:[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-]*)*)?$/;
21467
+ function isRecord$1(value) {
21468
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21469
+ }
21470
+ function parseManifest(input) {
21471
+ const issues = [];
21472
+ if (!isRecord$1(input)) return { ok: false, issues: [{ at: "$", message: "not an object" }] };
21473
+ if (input.version !== PAGES_MANIFEST_VERSION) {
21474
+ issues.push({ at: "version", message: `expected ${PAGES_MANIFEST_VERSION}` });
21475
+ }
21476
+ const pages = [];
21477
+ const pageIds = /* @__PURE__ */ new Set();
21478
+ const paths = /* @__PURE__ */ new Set();
21479
+ const sectionIds = /* @__PURE__ */ new Set();
21480
+ const rawPages = Array.isArray(input.pages) ? input.pages : null;
21481
+ if (!rawPages) issues.push({ at: "pages", message: "must be a list" });
21482
+ (rawPages ?? []).forEach((raw, i) => {
21483
+ const at = `pages[${i}]`;
21484
+ if (!isRecord$1(raw)) {
21485
+ issues.push({ at, message: "not an object" });
21486
+ return;
21487
+ }
21488
+ const id = String(raw.id ?? "");
21489
+ const path2 = String(raw.path ?? "");
21490
+ const titleKey = String(raw.titleKey ?? "");
21491
+ if (!ID.test(id)) issues.push({ at: `${at}.id`, message: `invalid id "${id}"` });
21492
+ else if (pageIds.has(id)) issues.push({ at: `${at}.id`, message: `duplicate page "${id}"` });
21493
+ pageIds.add(id);
21494
+ if (!PATH.test(path2)) issues.push({ at: `${at}.path`, message: `invalid path "${path2}"` });
21495
+ else if (path2 === "/setto" || path2.startsWith("/setto/")) {
21496
+ issues.push({ at: `${at}.path`, message: "/setto belongs to the editor" });
21497
+ } else if (paths.has(path2)) issues.push({ at: `${at}.path`, message: `duplicate path "${path2}"` });
21498
+ paths.add(path2);
21499
+ if (!titleKey) issues.push({ at: `${at}.titleKey`, message: "required" });
21500
+ const sections = [];
21501
+ const rawSections = Array.isArray(raw.sections) ? raw.sections : null;
21502
+ if (!rawSections) issues.push({ at: `${at}.sections`, message: "must be a list" });
21503
+ (rawSections ?? []).forEach((s, j) => {
21504
+ const sat = `${at}.sections[${j}]`;
21505
+ if (!isRecord$1(s)) {
21506
+ issues.push({ at: sat, message: "not an object" });
21507
+ return;
21508
+ }
21509
+ const sid = String(s.id ?? "");
21510
+ if (!ID.test(sid)) issues.push({ at: `${sat}.id`, message: `invalid id "${sid}"` });
21511
+ else if (sectionIds.has(sid)) issues.push({ at: `${sat}.id`, message: `duplicate section "${sid}"` });
21512
+ sectionIds.add(sid);
21513
+ const type = s.type;
21514
+ if (type !== void 0 && !BUILTIN_SECTION_TYPES.includes(type)) {
21515
+ issues.push({ at: `${sat}.type`, message: `unknown section type "${String(type)}"` });
21516
+ }
21517
+ sections.push({
21518
+ id: sid,
21519
+ ...type !== void 0 ? { type } : {},
21520
+ ...s.hidden === true ? { hidden: true } : {},
21521
+ ...typeof s.label === "string" && s.label.trim() ? { label: s.label.trim() } : {}
21522
+ });
21523
+ });
21524
+ pages.push({
21525
+ id,
21526
+ path: path2,
21527
+ titleKey,
21528
+ ...raw.hidden === true ? { hidden: true } : {},
21529
+ sections
21530
+ });
21531
+ });
21532
+ const menu = parseMenu(input.menu, "menu", issues);
21533
+ const footer2 = parseMenu(input.footer, "footer", issues);
21534
+ if (issues.length > 0) return { ok: false, issues };
21535
+ return { ok: true, manifest: { version: PAGES_MANIFEST_VERSION, pages, menu, footer: footer2 } };
21536
+ }
21537
+ function parseMenu(raw, at, issues) {
21538
+ if (raw === void 0) return [];
21539
+ if (!Array.isArray(raw)) {
21540
+ issues.push({ at, message: "must be a list" });
21541
+ return [];
21542
+ }
21543
+ const ids = /* @__PURE__ */ new Set();
21544
+ const items = [];
21545
+ raw.forEach((item, i) => {
21546
+ const iat = `${at}[${i}]`;
21547
+ if (!isRecord$1(item)) {
21548
+ issues.push({ at: iat, message: "not an object" });
21549
+ return;
21550
+ }
21551
+ const id = String(item.id ?? "");
21552
+ const href = String(item.href ?? "");
21553
+ const labelKey = String(item.labelKey ?? "");
21554
+ if (!ID.test(id)) issues.push({ at: `${iat}.id`, message: `invalid id "${id}"` });
21555
+ else if (ids.has(id)) issues.push({ at: `${iat}.id`, message: `duplicate item "${id}"` });
21556
+ ids.add(id);
21557
+ if (!isSafeHref(href)) issues.push({ at: `${iat}.href`, message: `invalid link "${href}"` });
21558
+ if (!labelKey) issues.push({ at: `${iat}.labelKey`, message: "required" });
21559
+ items.push({ id, href, labelKey, ...item.hidden === true ? { hidden: true } : {} });
21560
+ });
21561
+ return items;
21562
+ }
21563
+ function isSafeHref(href) {
21564
+ if (!href) return false;
21565
+ if (href.startsWith("/") && !href.startsWith("//")) return true;
21566
+ if (href.startsWith("#")) return true;
21567
+ return /^(https?:\/\/|mailto:|tel:)/i.test(href);
21568
+ }
21569
+ function normalisePath(path2) {
21570
+ const bare = (path2.split(/[?#]/)[0] ?? "").replace(/\/+$/, "");
21571
+ return bare === "" ? "/" : bare.toLowerCase();
21572
+ }
21573
+ function pageForPath(manifest, path2) {
21574
+ const wanted = normalisePath(path2);
21575
+ return manifest.pages.find((p) => p.path === wanted) ?? null;
21576
+ }
21577
+ function pageById(manifest, id) {
21578
+ return manifest.pages.find((p) => p.id === id) ?? null;
21579
+ }
21580
+ function findSection(manifest, sectionId) {
21581
+ for (const page of manifest.pages) {
21582
+ const index2 = page.sections.findIndex((s) => s.id === sectionId);
21583
+ if (index2 >= 0) return { page, index: index2, section: page.sections[index2] };
21584
+ }
21585
+ return null;
21586
+ }
21587
+ function visibleMenu(manifest, list2) {
21588
+ return manifest[list2].filter((item) => !item.hidden && !leadsNowhere(manifest, item.href));
21589
+ }
21590
+ function leadsNowhere(manifest, href) {
21591
+ if (!href.startsWith("/") && !href.startsWith("#")) return false;
21592
+ const [pathPart, fragment] = href.split("#");
21593
+ const page = pageForPath(manifest, pathPart || "/");
21594
+ if (pathPart && !page) return false;
21595
+ if (page?.hidden) return true;
21596
+ if (!fragment) return false;
21597
+ const target = findSection(manifest, fragment);
21598
+ return target ? Boolean(target.section.hidden || target.page.hidden) : false;
21599
+ }
21600
+ class ManifestError extends Error {
21601
+ constructor(code2, message) {
21602
+ super(message);
21603
+ this.code = code2;
21604
+ this.name = "ManifestError";
21605
+ }
21606
+ code;
21607
+ }
21608
+ function clone(manifest) {
21609
+ return JSON.parse(JSON.stringify(manifest));
21610
+ }
21611
+ function mustPage(manifest, pageId) {
21612
+ const page = pageById(manifest, pageId);
21613
+ if (!page) throw new ManifestError("page_not_found", `no page "${pageId}"`);
21614
+ return page;
21615
+ }
21616
+ function moveSection(manifest, sectionId, delta) {
21617
+ const next = clone(manifest);
21618
+ const found = findSection(next, sectionId);
21619
+ if (!found) throw new ManifestError("section_not_found", `no section "${sectionId}"`);
21620
+ const { page, index: index2 } = found;
21621
+ let target = index2 + delta;
21622
+ while (target >= 0 && target < page.sections.length && page.sections[target].hidden) target += delta;
21623
+ if (target < 0 || target >= page.sections.length) return manifest;
21624
+ const [moved] = page.sections.splice(index2, 1);
21625
+ page.sections.splice(target, 0, moved);
21626
+ return next;
21627
+ }
21628
+ function placeSection(manifest, sectionId, index2) {
21629
+ const next = clone(manifest);
21630
+ const found = findSection(next, sectionId);
21631
+ if (!found) throw new ManifestError("section_not_found", `no section "${sectionId}"`);
21632
+ const [moved] = found.page.sections.splice(found.index, 1);
21633
+ const at = Math.max(0, Math.min(index2, found.page.sections.length));
21634
+ found.page.sections.splice(at, 0, moved);
21635
+ return next;
21636
+ }
21637
+ function setSectionHidden(manifest, sectionId, hidden) {
21638
+ const next = clone(manifest);
21639
+ const found = findSection(next, sectionId);
21640
+ if (!found) throw new ManifestError("section_not_found", `no section "${sectionId}"`);
21641
+ if (hidden) found.section.hidden = true;
21642
+ else delete found.section.hidden;
21643
+ return next;
21644
+ }
21645
+ function addSection(manifest, pageId, section, index2) {
21646
+ if (!ID.test(section.id)) throw new ManifestError("invalid", `invalid section id "${section.id}"`);
21647
+ if (findSection(manifest, section.id)) {
21648
+ throw new ManifestError("duplicate_id", `section "${section.id}" already exists`);
21649
+ }
21650
+ const next = clone(manifest);
21651
+ const page = mustPage(next, pageId);
21652
+ const at = index2 === void 0 ? page.sections.length : Math.max(0, Math.min(index2, page.sections.length));
21653
+ page.sections.splice(at, 0, { ...section });
21654
+ return next;
21655
+ }
21656
+ function addPage(manifest, page) {
21657
+ const path2 = normalisePath(page.path);
21658
+ if (!ID.test(page.id) || !PATH.test(path2) || path2 === "/setto" || path2.startsWith("/setto/")) {
21659
+ throw new ManifestError("invalid", `invalid page "${page.id}" at "${page.path}"`);
21660
+ }
21661
+ if (pageById(manifest, page.id)) throw new ManifestError("duplicate_id", `page "${page.id}" already exists`);
21662
+ if (pageForPath(manifest, path2)) throw new ManifestError("duplicate_path", `a page already lives at ${path2}`);
21663
+ let next = clone(manifest);
21664
+ next.pages.push({ id: page.id, path: path2, titleKey: page.titleKey, sections: [] });
21665
+ for (const section of page.sections) next = addSection(next, page.id, section);
21666
+ return next;
21667
+ }
21668
+ function setPageHidden(manifest, pageId, hidden) {
21669
+ const next = clone(manifest);
21670
+ const page = mustPage(next, pageId);
21671
+ if (page.path === "/" && hidden) throw new ManifestError("invalid", "the front page cannot be hidden");
21672
+ if (hidden) page.hidden = true;
21673
+ else delete page.hidden;
21674
+ return next;
21675
+ }
21676
+ function upsertMenuItem(manifest, list2, item, index2) {
21677
+ if (!ID.test(item.id) || !isSafeHref(item.href) || !item.labelKey) {
21678
+ throw new ManifestError("invalid", `invalid link "${item.id}"`);
21679
+ }
21680
+ const next = clone(manifest);
21681
+ const items = next[list2];
21682
+ const existing = items.findIndex((i) => i.id === item.id);
21683
+ const clean = { id: item.id, href: item.href, labelKey: item.labelKey };
21684
+ if (item.hidden) clean.hidden = true;
21685
+ if (existing >= 0) items[existing] = clean;
21686
+ else items.splice(index2 === void 0 ? items.length : Math.max(0, Math.min(index2, items.length)), 0, clean);
21687
+ return next;
21688
+ }
21689
+ function moveMenuItem(manifest, list2, itemId, delta) {
21690
+ const next = clone(manifest);
21691
+ const items = next[list2];
21692
+ const index2 = items.findIndex((i) => i.id === itemId);
21693
+ if (index2 < 0) throw new ManifestError("item_not_found", `no link "${itemId}" in ${list2}`);
21694
+ const target = index2 + delta;
21695
+ if (target < 0 || target >= items.length) return manifest;
21696
+ const [moved] = items.splice(index2, 1);
21697
+ items.splice(target, 0, moved);
21698
+ return next;
21699
+ }
21700
+ function setMenuItemHidden(manifest, list2, itemId, hidden) {
21701
+ const next = clone(manifest);
21702
+ const item = next[list2].find((i) => i.id === itemId);
21703
+ if (!item) throw new ManifestError("item_not_found", `no link "${itemId}" in ${list2}`);
21704
+ if (hidden) item.hidden = true;
21705
+ else delete item.hidden;
21706
+ return next;
21707
+ }
21708
+ function applyOp(manifest, op) {
21709
+ switch (op.op) {
21710
+ case "move_section":
21711
+ return moveSection(manifest, op.sectionId, op.delta);
21712
+ case "place_section":
21713
+ return placeSection(manifest, op.sectionId, op.index);
21714
+ case "hide_section":
21715
+ return setSectionHidden(manifest, op.sectionId, true);
21716
+ case "show_section":
21717
+ return setSectionHidden(manifest, op.sectionId, false);
21718
+ case "add_section":
21719
+ return addSection(manifest, op.pageId, op.section, op.index);
21720
+ case "add_page":
21721
+ return addPage(manifest, op.page);
21722
+ case "hide_page":
21723
+ return setPageHidden(manifest, op.pageId, true);
21724
+ case "show_page":
21725
+ return setPageHidden(manifest, op.pageId, false);
21726
+ case "upsert_link":
21727
+ return upsertMenuItem(manifest, op.list, op.item, op.index);
21728
+ case "move_link":
21729
+ return moveMenuItem(manifest, op.list, op.itemId, op.delta);
21730
+ case "hide_link":
21731
+ return setMenuItemHidden(manifest, op.list, op.itemId, true);
21732
+ case "show_link":
21733
+ return setMenuItemHidden(manifest, op.list, op.itemId, false);
21734
+ }
21735
+ }
21736
+ function applyOps(manifest, ops) {
21737
+ let next = manifest;
21738
+ for (let i = 0; i < ops.length; i++) {
21739
+ try {
21740
+ next = applyOp(next, ops[i]);
21741
+ } catch (err) {
21742
+ return { ok: false, error: err instanceof Error ? err.message : String(err), at: i };
21743
+ }
21744
+ }
21745
+ return { ok: true, manifest: next };
21746
+ }
21747
+ function builtinKeys(sectionId) {
21748
+ const base = `sections.${sectionId}`;
21749
+ return {
21750
+ heading: `${base}.heading`,
21751
+ intro: `${base}.intro`,
21752
+ items: `${base}.items`,
21753
+ itemHeading: (itemKey) => `${base}.items.${itemKey}.heading`,
21754
+ itemText: (itemKey) => `${base}.items.${itemKey}.text`
21755
+ };
21756
+ }
21757
+ function pageTitleKey(pageId) {
21758
+ return `pages.${pageId}.title`;
21759
+ }
21760
+ function serialiseManifest(manifest) {
21761
+ return JSON.stringify(manifest, null, 2) + "\n";
21762
+ }
21763
+ const EMPTY_PAGE_STORE_SNAPSHOT = {
21764
+ manifest: null,
21765
+ changes: 0,
21766
+ version: 0
21767
+ };
21768
+ class PageStore {
21769
+ data;
21770
+ baseline;
21771
+ version = 0;
21772
+ listeners = /* @__PURE__ */ new Set();
21773
+ cached;
21774
+ constructor(initial) {
21775
+ this.data = initial ? structuredClone(initial) : null;
21776
+ this.baseline = initial ? structuredClone(initial) : null;
21777
+ this.cached = { manifest: this.data, changes: 0, version: 0 };
21778
+ }
21779
+ snapshot() {
21780
+ return this.cached;
21781
+ }
21782
+ subscribe(listener) {
21783
+ this.listeners.add(listener);
21784
+ return () => this.listeners.delete(listener);
21785
+ }
21786
+ get manifest() {
21787
+ return this.data;
21788
+ }
21789
+ /** True once the site has a manifest to edit. */
21790
+ has() {
21791
+ return this.data !== null;
21792
+ }
21793
+ /**
21794
+ * Apply changes as drafts. All of them or none: a list that fails halfway
21795
+ * leaves the page exactly as it was and says which one failed.
21796
+ */
21797
+ apply(ops) {
21798
+ if (!this.data) return { ok: false, error: "this site has no page manifest", at: 0 };
21799
+ const result = applyOps(this.data, ops);
21800
+ if (!result.ok) return result;
21801
+ this.data = result.manifest;
21802
+ this.bump();
21803
+ return { ok: true };
21804
+ }
21805
+ /**
21806
+ * Put the draft back to an earlier manifest, keeping the baseline.
21807
+ *
21808
+ * What «Angre» on one of Pixel's changes does: the panel remembers the
21809
+ * manifest from before the change and hands it back here. Checked like
21810
+ * anything else, because it has been through a round trip it did not have
21811
+ * to survive.
21812
+ */
21813
+ restore(manifest) {
21814
+ const parsed = parseManifest(manifest);
21815
+ if (!parsed.ok) return { ok: false, error: parsed.issues[0]?.message ?? "invalid manifest" };
21816
+ this.data = parsed.manifest;
21817
+ this.bump();
21818
+ return { ok: true };
21819
+ }
21820
+ /**
21821
+ * Replace the baseline with what the repository holds, dropping drafts.
21822
+ *
21823
+ * Checked first: the file in the repository can be newer than this bundle,
21824
+ * or hand-edited, and a baseline this store cannot render must not replace
21825
+ * the one it can. Returns whether it was taken.
21826
+ */
21827
+ loadBaseline(next) {
21828
+ const parsed = parseManifest(next);
21829
+ if (!parsed.ok) {
21830
+ console.warn("[setto] pages.json from the repository is not valid and was ignored", parsed.issues);
21831
+ return false;
21832
+ }
21833
+ this.data = parsed.manifest;
21834
+ this.baseline = structuredClone(parsed.manifest);
21835
+ this.bump();
21836
+ return true;
21837
+ }
21838
+ revertAll() {
21839
+ this.data = this.baseline ? structuredClone(this.baseline) : null;
21840
+ this.bump();
21841
+ }
21842
+ commit() {
21843
+ this.baseline = this.data ? structuredClone(this.data) : null;
21844
+ this.bump();
21845
+ }
21846
+ /** Number of changes, for the toolbar's count. Zero means nothing to publish. */
21847
+ size() {
21848
+ return this.cached.changes;
21849
+ }
21850
+ serialise() {
21851
+ return this.data ? serialiseManifest(this.data) : null;
21852
+ }
21853
+ bump() {
21854
+ this.version += 1;
21855
+ this.cached = {
21856
+ manifest: this.data,
21857
+ changes: countChanges(this.baseline, this.data),
21858
+ version: this.version
21859
+ };
21860
+ for (const fn of this.listeners) fn(this.cached);
21861
+ }
21862
+ }
21863
+ function countChanges(before, after) {
21864
+ if (!before || !after) return before === after ? 0 : 1;
21865
+ let changes = 0;
21866
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
21867
+ const beforePages = new Map(before.pages.map((p) => [p.id, p]));
21868
+ for (const page of after.pages) {
21869
+ const was = beforePages.get(page.id);
21870
+ if (!was) {
21871
+ changes += 1;
21872
+ continue;
21873
+ }
21874
+ if (was.path !== page.path || was.titleKey !== page.titleKey || Boolean(was.hidden) !== Boolean(page.hidden)) {
21875
+ changes += 1;
21876
+ }
21877
+ const wasSections = new Map(was.sections.map((s, i) => [s.id, { s, i }]));
21878
+ page.sections.forEach((s, i) => {
21879
+ const prior = wasSections.get(s.id);
21880
+ if (!prior || prior.i !== i || !same(prior.s, s)) changes += 1;
21881
+ });
21882
+ if (was.sections.length > page.sections.length) changes += was.sections.length - page.sections.length;
21883
+ }
21884
+ if (before.pages.length > after.pages.length) changes += before.pages.length - after.pages.length;
21885
+ for (const list2 of ["menu", "footer"]) {
21886
+ const wasItems = new Map(before[list2].map((item, i) => [item.id, { item, i }]));
21887
+ after[list2].forEach((item, i) => {
21888
+ const prior = wasItems.get(item.id);
21889
+ if (!prior || prior.i !== i || !same(prior.item, item)) changes += 1;
21890
+ });
21891
+ if (before[list2].length > after[list2].length) changes += before[list2].length - after[list2].length;
21892
+ }
21893
+ return changes;
21894
+ }
21895
+ const cache = /* @__PURE__ */ new WeakMap();
21896
+ function bundledManifest(raw) {
21897
+ if (!raw || typeof raw !== "object") return null;
21898
+ const hit = cache.get(raw);
21899
+ if (hit !== void 0) return hit;
21900
+ const parsed = parseManifest(raw);
21901
+ if (!parsed.ok) {
21902
+ console.warn("[setto] pages.json is not valid and was ignored", parsed.issues);
21903
+ }
21904
+ const manifest = parsed.ok ? parsed.manifest : null;
21905
+ cache.set(raw, manifest);
21906
+ return manifest;
21907
+ }
21638
21908
  const EMPTY$1 = {
21639
21909
  sites: null,
21640
21910
  currentSite: null,
@@ -30074,7 +30344,7 @@ function requireExtend() {
30074
30344
  return obj[name2];
30075
30345
  };
30076
30346
  extend$1 = function extend2() {
30077
- var options, name2, src, copy, copyIsArray, clone;
30347
+ var options, name2, src, copy, copyIsArray, clone2;
30078
30348
  var target = arguments[0];
30079
30349
  var i = 1;
30080
30350
  var length = arguments.length;
@@ -30097,11 +30367,11 @@ function requireExtend() {
30097
30367
  if (deep && copy && (isPlainObject2(copy) || (copyIsArray = isArray(copy)))) {
30098
30368
  if (copyIsArray) {
30099
30369
  copyIsArray = false;
30100
- clone = src && isArray(src) ? src : [];
30370
+ clone2 = src && isArray(src) ? src : [];
30101
30371
  } else {
30102
- clone = src && isPlainObject2(src) ? src : {};
30372
+ clone2 = src && isPlainObject2(src) ? src : {};
30103
30373
  }
30104
- setProperty(target, { name: name2, newValue: extend2(deep, clone, copy) });
30374
+ setProperty(target, { name: name2, newValue: extend2(deep, clone2, copy) });
30105
30375
  } else if (typeof copy !== "undefined") {
30106
30376
  setProperty(target, { name: name2, newValue: copy });
30107
30377
  }
@@ -36317,14 +36587,16 @@ function SettoProvider({ config, children }) {
36317
36587
  const storeRef = useRef(null);
36318
36588
  const themeStoreRef = useRef(null);
36319
36589
  const assetStoreRef = useRef(null);
36590
+ const pageStoreRef = useRef(null);
36320
36591
  const [storeReady, setStoreReady] = useState(false);
36321
36592
  useEffect(() => {
36322
36593
  if (!i18n) return;
36323
36594
  storeRef.current = new I18nStore(i18n);
36324
36595
  themeStoreRef.current = new ThemeStore(config.theme ?? {});
36325
36596
  assetStoreRef.current = new AssetStore();
36597
+ pageStoreRef.current = new PageStore(bundledManifest(config.pages));
36326
36598
  setStoreReady(true);
36327
- }, [i18n, config.theme]);
36599
+ }, [i18n, config.theme, config.pages]);
36328
36600
  const editMode = !!session && access.currentSite?.status === "live" && !onAdminRoute && !authLoading && !access.loading;
36329
36601
  const [selectedSectionId, setSelectedSectionId] = useState(null);
36330
36602
  const selectSection = useCallback((id) => setSelectedSectionId(id), []);
@@ -36348,6 +36620,7 @@ function SettoProvider({ config, children }) {
36348
36620
  store: storeReady ? storeRef.current : null,
36349
36621
  themeStore: storeReady ? themeStoreRef.current : null,
36350
36622
  assetStore: storeReady ? assetStoreRef.current : null,
36623
+ pageStore: storeReady ? pageStoreRef.current : null,
36351
36624
  selectedSectionId,
36352
36625
  selectSection,
36353
36626
  guestEdit,
@@ -36368,6 +36641,219 @@ function useSetto() {
36368
36641
  }
36369
36642
  return ctx;
36370
36643
  }
36644
+ const INLINE_MARKER = "data-setto-languages-inline";
36645
+ function SettoLanguages({ languages: given, inline = false }) {
36646
+ const { i18n } = useTranslation();
36647
+ const setto = useContext(SettoContext);
36648
+ const configured = setto?.config.languages;
36649
+ const languages = given ?? siteLanguages(configured, configured?.[0]);
36650
+ const [open, setOpen] = useState(false);
36651
+ const [hover, setHover] = useState(false);
36652
+ const [surface, setSurface] = useState(null);
36653
+ const ref = useRef(null);
36654
+ useEffect(() => {
36655
+ if (!inline) return;
36656
+ const root2 = document.documentElement;
36657
+ const count = Number(root2.getAttribute(INLINE_MARKER) ?? "0") + 1;
36658
+ root2.setAttribute(INLINE_MARKER, String(count));
36659
+ return () => {
36660
+ const left = Number(root2.getAttribute(INLINE_MARKER) ?? "1") - 1;
36661
+ if (left > 0) root2.setAttribute(INLINE_MARKER, String(left));
36662
+ else root2.removeAttribute(INLINE_MARKER);
36663
+ };
36664
+ }, [inline]);
36665
+ useEffect(() => {
36666
+ if (!open) return;
36667
+ const onDown = (e) => {
36668
+ if (!ref.current?.contains(e.target)) setOpen(false);
36669
+ };
36670
+ const onKey = (e) => {
36671
+ if (e.key === "Escape") setOpen(false);
36672
+ };
36673
+ document.addEventListener("mousedown", onDown);
36674
+ document.addEventListener("keydown", onKey);
36675
+ return () => {
36676
+ document.removeEventListener("mousedown", onDown);
36677
+ document.removeEventListener("keydown", onKey);
36678
+ };
36679
+ }, [open]);
36680
+ if (languages.length < 2) return null;
36681
+ const current = i18n.language;
36682
+ const toggle = () => {
36683
+ if (!open && inline && ref.current) setSurface(surroundings(ref.current));
36684
+ setOpen((o) => !o);
36685
+ };
36686
+ const choose = (tag) => {
36687
+ setOpen(false);
36688
+ if (tag === current) return;
36689
+ storeLanguage(tag);
36690
+ const target = pathForLanguage(window.location.pathname, tag, languages);
36691
+ window.location.assign(target + window.location.search + window.location.hash);
36692
+ };
36693
+ return /* @__PURE__ */ jsxs("div", { ref, style: inline ? inlineWrapStyle : floatingWrapStyle, children: [
36694
+ /* @__PURE__ */ jsxs(
36695
+ "button",
36696
+ {
36697
+ type: "button",
36698
+ onClick: toggle,
36699
+ onMouseEnter: () => setHover(true),
36700
+ onMouseLeave: () => setHover(false),
36701
+ "aria-expanded": open,
36702
+ "aria-haspopup": "listbox",
36703
+ "aria-label": `Språk: ${languageName(current)}`,
36704
+ title: inline ? languageName(current) : void 0,
36705
+ style: inline ? { ...inlineTriggerStyle, opacity: hover || open ? 1 : inlineTriggerStyle.opacity } : floatingTriggerStyle,
36706
+ children: [
36707
+ /* @__PURE__ */ jsx(GlobeIcon, {}),
36708
+ inline ? shortCode(current) : languageName(current)
36709
+ ]
36710
+ }
36711
+ ),
36712
+ open ? /* @__PURE__ */ jsx(
36713
+ "ul",
36714
+ {
36715
+ role: "listbox",
36716
+ style: inline ? { ...inlineListStyle, ...surface ? { background: surface.background, color: surface.color } : {} } : floatingListStyle,
36717
+ children: languages.map((tag) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
36718
+ "button",
36719
+ {
36720
+ type: "button",
36721
+ role: "option",
36722
+ "aria-selected": tag === current,
36723
+ onClick: () => choose(tag),
36724
+ style: {
36725
+ ...optionStyle,
36726
+ ...inline ? { color: "inherit" } : {},
36727
+ fontWeight: tag === current ? 600 : 400
36728
+ },
36729
+ children: languageName(tag)
36730
+ }
36731
+ ) }, tag))
36732
+ }
36733
+ ) : null
36734
+ ] });
36735
+ }
36736
+ function AutoLanguageSwitcher({ languages }) {
36737
+ const [inlineElsewhere, setInlineElsewhere] = useState(false);
36738
+ useEffect(() => {
36739
+ const check = () => setInlineElsewhere(document.documentElement.hasAttribute(INLINE_MARKER));
36740
+ check();
36741
+ const observer = new MutationObserver(check);
36742
+ observer.observe(document.documentElement, {
36743
+ attributes: true,
36744
+ attributeFilter: [INLINE_MARKER]
36745
+ });
36746
+ return () => observer.disconnect();
36747
+ }, []);
36748
+ if (inlineElsewhere || languages.length < 2) return null;
36749
+ return /* @__PURE__ */ jsx(SettoLanguages, { languages });
36750
+ }
36751
+ function shortCode(tag) {
36752
+ return tag.toUpperCase();
36753
+ }
36754
+ function surroundings(el) {
36755
+ const color2 = getComputedStyle(el).color;
36756
+ for (let node2 = el; node2; node2 = node2.parentElement) {
36757
+ const background = getComputedStyle(node2).backgroundColor;
36758
+ if (background && !/^(transparent|rgba\(.*,\s*0\))$/.test(background)) return { background, color: color2 };
36759
+ }
36760
+ return { background: "#ffffff", color: "#1a1a1a" };
36761
+ }
36762
+ function GlobeIcon() {
36763
+ return /* @__PURE__ */ jsxs(
36764
+ "svg",
36765
+ {
36766
+ width: "15",
36767
+ height: "15",
36768
+ viewBox: "0 0 24 24",
36769
+ fill: "none",
36770
+ stroke: "currentColor",
36771
+ strokeWidth: "1.8",
36772
+ strokeLinecap: "round",
36773
+ strokeLinejoin: "round",
36774
+ "aria-hidden": "true",
36775
+ children: [
36776
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
36777
+ /* @__PURE__ */ jsx("path", { d: "M3 12h18M12 3c2.4 2.6 3.6 5.6 3.6 9s-1.2 6.4-3.6 9c-2.4-2.6-3.6-5.6-3.6-9S9.6 5.6 12 3z" })
36778
+ ]
36779
+ }
36780
+ );
36781
+ }
36782
+ const floatingWrapStyle = {
36783
+ position: "fixed",
36784
+ left: "max(16px, env(safe-area-inset-left))",
36785
+ bottom: "max(16px, env(safe-area-inset-bottom))",
36786
+ zIndex: 2147482e3,
36787
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
36788
+ };
36789
+ const inlineWrapStyle = {
36790
+ position: "relative",
36791
+ display: "inline-block",
36792
+ fontFamily: "inherit"
36793
+ };
36794
+ const triggerBase = {
36795
+ display: "inline-flex",
36796
+ alignItems: "center",
36797
+ gap: 7,
36798
+ minHeight: 40,
36799
+ fontSize: 13.5,
36800
+ fontWeight: 500,
36801
+ lineHeight: 1,
36802
+ cursor: "pointer"
36803
+ };
36804
+ const floatingTriggerStyle = {
36805
+ ...triggerBase,
36806
+ padding: "8px 14px",
36807
+ borderRadius: 999,
36808
+ border: "1px solid rgba(127, 127, 127, 0.32)",
36809
+ background: "rgba(255, 255, 255, 0.82)",
36810
+ backdropFilter: "blur(8px)",
36811
+ WebkitBackdropFilter: "blur(8px)",
36812
+ color: "#1a1a1a",
36813
+ boxShadow: "0 4px 14px rgba(0, 0, 0, 0.12)"
36814
+ };
36815
+ const inlineTriggerStyle = {
36816
+ ...triggerBase,
36817
+ gap: 5,
36818
+ minHeight: 36,
36819
+ padding: "6px 6px",
36820
+ border: 0,
36821
+ borderRadius: 8,
36822
+ background: "transparent",
36823
+ color: "inherit",
36824
+ fontSize: 12,
36825
+ fontWeight: 500,
36826
+ letterSpacing: "0.06em",
36827
+ opacity: 0.7,
36828
+ transition: "opacity 160ms ease"
36829
+ };
36830
+ const listBase = {
36831
+ position: "absolute",
36832
+ margin: 0,
36833
+ padding: 4,
36834
+ listStyle: "none",
36835
+ minWidth: 150,
36836
+ borderRadius: 12,
36837
+ border: "1px solid rgba(127, 127, 127, 0.24)",
36838
+ background: "#ffffff",
36839
+ boxShadow: "0 12px 32px rgba(0, 0, 0, 0.18)",
36840
+ zIndex: 1
36841
+ };
36842
+ const floatingListStyle = { ...listBase, left: 0, bottom: "calc(100% + 8px)" };
36843
+ const inlineListStyle = { ...listBase, right: 0, top: "calc(100% + 8px)" };
36844
+ const optionStyle = {
36845
+ display: "block",
36846
+ width: "100%",
36847
+ minHeight: 40,
36848
+ padding: "9px 12px",
36849
+ border: 0,
36850
+ borderRadius: 8,
36851
+ background: "transparent",
36852
+ color: "#1a1a1a",
36853
+ fontSize: 13.5,
36854
+ textAlign: "left",
36855
+ cursor: "pointer"
36856
+ };
36371
36857
  function GuestEditProvider({ children }) {
36372
36858
  const { guestEdit, themeStore } = useSetto();
36373
36859
  const SectionTools = useSettoChromeSlot("section-toolbar");
@@ -37023,6 +37509,8 @@ function SettoImage({ srcKey, alt, className, style, ...rest }) {
37023
37509
  const fileRef = useRef(null);
37024
37510
  const [hovered, setHovered] = useState(false);
37025
37511
  const [uploading, setUploading] = useState(false);
37512
+ const imgRef = useRef(null);
37513
+ const Tools = useSettoChromeSlot("image-tools");
37026
37514
  const [, force] = useState(0);
37027
37515
  useEffect(() => {
37028
37516
  if (!store) return;
@@ -37046,10 +37534,8 @@ function SettoImage({ srcKey, alt, className, style, ...rest }) {
37046
37534
  e.preventDefault();
37047
37535
  if (!uploading) fileRef.current?.click();
37048
37536
  };
37049
- const handleFile = async (e) => {
37050
- const file = e.target.files?.[0];
37051
- e.target.value = "";
37052
- if (!file || !assetStore || !store) return;
37537
+ const applyFile = async (file) => {
37538
+ if (!assetStore || !store) return;
37053
37539
  setUploading(true);
37054
37540
  try {
37055
37541
  const originalPublicPath = store.original(srcKey, i18n.language);
@@ -37066,6 +37552,18 @@ function SettoImage({ srcKey, alt, className, style, ...rest }) {
37066
37552
  setUploading(false);
37067
37553
  }
37068
37554
  };
37555
+ const handleFile = async (e) => {
37556
+ const file = e.target.files?.[0];
37557
+ e.target.value = "";
37558
+ if (file) await applyFile(file);
37559
+ };
37560
+ const revert2 = () => {
37561
+ if (!assetStore || !store) return;
37562
+ const original = assetStore.revert(srcKey);
37563
+ if (original !== null) {
37564
+ setSrcAllLanguages(store, i18n, srcKey, original);
37565
+ }
37566
+ };
37069
37567
  const handleRevert = (e) => {
37070
37568
  e.stopPropagation();
37071
37569
  e.preventDefault();
@@ -37080,6 +37578,7 @@ function SettoImage({ srcKey, alt, className, style, ...rest }) {
37080
37578
  "span",
37081
37579
  {
37082
37580
  "data-setto-image": true,
37581
+ "data-setto-src-key": srcKey,
37083
37582
  "data-setto-ui": true,
37084
37583
  onMouseEnter: () => setHovered(true),
37085
37584
  onMouseLeave: () => setHovered(false),
@@ -37093,8 +37592,21 @@ function SettoImage({ srcKey, alt, className, style, ...rest }) {
37093
37592
  borderRadius: 4
37094
37593
  },
37095
37594
  children: [
37096
- /* @__PURE__ */ jsx("img", { src, alt, className, style, ...rest }),
37097
- previewUrl && !hovered ? /* @__PURE__ */ jsx(
37595
+ /* @__PURE__ */ jsx("img", { ref: imgRef, src, alt, className, style, ...rest }),
37596
+ Tools ? /* @__PURE__ */ jsx(
37597
+ Tools,
37598
+ {
37599
+ srcKey,
37600
+ alt,
37601
+ hovered,
37602
+ hasDraft: Boolean(previewUrl),
37603
+ busy: uploading,
37604
+ imageEl: imgRef.current,
37605
+ applyFile,
37606
+ revert: revert2
37607
+ }
37608
+ ) : null,
37609
+ previewUrl && !hovered && !Tools ? /* @__PURE__ */ jsx(
37098
37610
  "span",
37099
37611
  {
37100
37612
  "aria-hidden": true,
@@ -37114,7 +37626,7 @@ function SettoImage({ srcKey, alt, className, style, ...rest }) {
37114
37626
  children: "Nytt bilde"
37115
37627
  }
37116
37628
  ) : null,
37117
- hovered ? /* @__PURE__ */ jsxs(
37629
+ hovered && !Tools ? /* @__PURE__ */ jsxs(
37118
37630
  "span",
37119
37631
  {
37120
37632
  "data-setto-ui": true,
@@ -38325,6 +38837,193 @@ const iframeStyle = {
38325
38837
  height: "100%",
38326
38838
  border: 0
38327
38839
  };
38840
+ function usePageManifest() {
38841
+ const { pageStore, config } = useSetto();
38842
+ const snap = useSyncExternalStore(
38843
+ (cb) => pageStore?.subscribe(() => cb()) ?? (() => {
38844
+ }),
38845
+ () => pageStore?.snapshot() ?? EMPTY_PAGE_STORE_SNAPSHOT,
38846
+ () => pageStore?.snapshot() ?? EMPTY_PAGE_STORE_SNAPSHOT
38847
+ );
38848
+ return pageStore ? snap.manifest : bundledManifest(config.pages);
38849
+ }
38850
+ function useSettoMenu(list2 = "menu") {
38851
+ const manifest = usePageManifest();
38852
+ return manifest ? visibleMenu(manifest, list2) : null;
38853
+ }
38854
+ function SettoPage({ pageId, path: path2, sections = {}, builtins, fallback = null }) {
38855
+ const manifest = usePageManifest();
38856
+ const { editMode } = useSetto();
38857
+ if (!manifest) {
38858
+ if (!pageId && path2 !== void 0 && path2 !== "/" && path2 !== "") return /* @__PURE__ */ jsx(Fragment, { children: fallback });
38859
+ return /* @__PURE__ */ jsx(Fragment, { children: Object.entries(sections).map(([id, Component]) => /* @__PURE__ */ jsx(Component, {}, id)) });
38860
+ }
38861
+ const page = pageId ? pageById(manifest, pageId) : pageForPath(manifest, path2 ?? "/");
38862
+ if (!page) {
38863
+ if (pageId) {
38864
+ return /* @__PURE__ */ jsx(Fragment, { children: Object.entries(sections).map(([id, Component]) => /* @__PURE__ */ jsx(Component, {}, id)) });
38865
+ }
38866
+ return /* @__PURE__ */ jsx(Fragment, { children: fallback });
38867
+ }
38868
+ if (page.hidden && !editMode) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
38869
+ return /* @__PURE__ */ jsx(PageBody, { page, sections, builtins, editMode, manifest });
38870
+ }
38871
+ function PageBody({
38872
+ page,
38873
+ sections,
38874
+ builtins,
38875
+ editMode,
38876
+ manifest
38877
+ }) {
38878
+ usePageTitle(page);
38879
+ const HiddenPageNote = useSettoChromeSlot("hidden-page");
38880
+ const listed = new Set(manifest.pages.flatMap((p) => p.sections.map((s) => s.id)));
38881
+ const unlisted = Object.keys(sections).filter((id) => !listed.has(id));
38882
+ const firstVisible = page.sections.find((s) => !s.hidden && (s.type || sections[s.id]))?.id;
38883
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
38884
+ page.hidden && editMode ? HiddenPageNote ? /* @__PURE__ */ jsx(HiddenPageNote, { pageId: page.id }) : /* @__PURE__ */ jsx(HiddenNote, { text: "Denne siden er skjult for besøkende." }) : null,
38885
+ page.sections.map((section) => /* @__PURE__ */ jsx(Fragment$1, { children: renderSection(section, sections, builtins, editMode, section.id === firstVisible) }, section.id)),
38886
+ unlisted.map((id) => {
38887
+ const Component = sections[id];
38888
+ return /* @__PURE__ */ jsx(Component, {}, id);
38889
+ })
38890
+ ] });
38891
+ }
38892
+ function renderSection(section, sections, builtins, editMode, first) {
38893
+ if (section.hidden) {
38894
+ return editMode ? /* @__PURE__ */ jsx(HiddenSection, { section }) : null;
38895
+ }
38896
+ if (section.type) {
38897
+ const Builtin = builtins?.[section.type] ?? BUILTIN_RENDERERS[section.type];
38898
+ return /* @__PURE__ */ jsx(Builtin, { sectionId: section.id, first });
38899
+ }
38900
+ const Component = sections[section.id];
38901
+ if (!Component) {
38902
+ warnOnce(section.id);
38903
+ return null;
38904
+ }
38905
+ return /* @__PURE__ */ jsx(Component, {});
38906
+ }
38907
+ const warned = /* @__PURE__ */ new Set();
38908
+ function warnOnce(id) {
38909
+ if (warned.has(id)) return;
38910
+ warned.add(id);
38911
+ console.warn(`[setto] section "${id}" is in pages.json but the page does not provide it`);
38912
+ }
38913
+ function HiddenSection({ section }) {
38914
+ const { pageStore } = useSetto();
38915
+ const Offered = useSettoChromeSlot("hidden-section");
38916
+ const label = section.label ?? section.id;
38917
+ if (Offered) return /* @__PURE__ */ jsx(Offered, { sectionId: section.id, label });
38918
+ return /* @__PURE__ */ jsxs("div", { "data-setto-ui": true, style: hiddenBarStyle, children: [
38919
+ /* @__PURE__ */ jsxs("span", { children: [
38920
+ "Skjult: ",
38921
+ label
38922
+ ] }),
38923
+ pageStore ? /* @__PURE__ */ jsx(
38924
+ "button",
38925
+ {
38926
+ type: "button",
38927
+ "data-setto-ui": true,
38928
+ style: hiddenBtnStyle,
38929
+ onClick: () => pageStore.apply([{ op: "show_section", sectionId: section.id }]),
38930
+ children: "Vis"
38931
+ }
38932
+ ) : null
38933
+ ] });
38934
+ }
38935
+ function HiddenNote({ text: text2 }) {
38936
+ return /* @__PURE__ */ jsx("div", { "data-setto-ui": true, style: hiddenBarStyle, children: /* @__PURE__ */ jsx("span", { children: text2 }) });
38937
+ }
38938
+ function usePageTitle(page) {
38939
+ const { t } = useTranslation();
38940
+ const original = useRef(null);
38941
+ const title = page.path === "/" ? null : t(page.titleKey);
38942
+ useEffect(() => {
38943
+ if (!title || typeof document === "undefined") return;
38944
+ original.current ??= document.title;
38945
+ const base = original.current;
38946
+ document.title = base ? `${title} · ${base}` : title;
38947
+ return () => {
38948
+ document.title = base;
38949
+ };
38950
+ }, [title]);
38951
+ }
38952
+ function SettoTextSection({ sectionId, first }) {
38953
+ const keys2 = builtinKeys(sectionId);
38954
+ const { i18n } = useTranslation();
38955
+ const { store, editMode } = useSetto();
38956
+ const intro2 = store ? store.get(keys2.intro, i18n.language) : i18n.t(keys2.intro, { defaultValue: "" });
38957
+ const Heading = first ? "h1" : "h2";
38958
+ return /* @__PURE__ */ jsx(SettoSection, { sectionId, style: textSectionStyle, children: /* @__PURE__ */ jsxs("div", { style: textInnerStyle, children: [
38959
+ /* @__PURE__ */ jsx(Heading, { style: first ? textH1Style : textH2Style, children: /* @__PURE__ */ jsx(T, { k: keys2.heading }) }),
38960
+ intro2 || editMode ? /* @__PURE__ */ jsx("p", { style: textIntroStyle, children: /* @__PURE__ */ jsx(T, { k: keys2.intro }) }) : null,
38961
+ /* @__PURE__ */ jsx(
38962
+ SettoRepeater,
38963
+ {
38964
+ itemsKey: keys2.items,
38965
+ defaultItem: { heading: "Overskrift", text: "Tekst" },
38966
+ itemLabel: "avsnitt",
38967
+ children: (itemKey) => /* @__PURE__ */ jsx(TextItem, { sectionId, itemKey })
38968
+ }
38969
+ )
38970
+ ] }) });
38971
+ }
38972
+ function TextItem({ sectionId, itemKey }) {
38973
+ const keys2 = builtinKeys(sectionId);
38974
+ const { i18n } = useTranslation();
38975
+ const { store, editMode } = useSetto();
38976
+ const heading2 = store ? store.get(keys2.itemHeading(itemKey), i18n.language) : i18n.t(keys2.itemHeading(itemKey), { defaultValue: "" });
38977
+ return /* @__PURE__ */ jsxs("div", { style: textItemStyle, children: [
38978
+ heading2 || editMode ? /* @__PURE__ */ jsx("h3", { style: textH3Style, children: /* @__PURE__ */ jsx(T, { k: keys2.itemHeading(itemKey) }) }) : null,
38979
+ /* @__PURE__ */ jsx("p", { style: textParagraphStyle, children: /* @__PURE__ */ jsx(T, { k: keys2.itemText(itemKey) }) })
38980
+ ] });
38981
+ }
38982
+ const BUILTIN_RENDERERS = {
38983
+ text: SettoTextSection
38984
+ };
38985
+ const textSectionStyle = { padding: "clamp(48px, 8vw, 96px) 16px" };
38986
+ const textInnerStyle = { maxWidth: "68ch", margin: "0 auto" };
38987
+ const textH1Style = {
38988
+ fontFamily: "var(--setto-display-font, inherit)",
38989
+ fontSize: "clamp(2rem, 4vw, 2.75rem)",
38990
+ lineHeight: 1.15,
38991
+ fontWeight: 600,
38992
+ margin: "0 0 0.75em"
38993
+ };
38994
+ const textH2Style = { ...textH1Style, fontSize: "clamp(1.5rem, 3vw, 2rem)" };
38995
+ const textH3Style = {
38996
+ fontFamily: "var(--setto-display-font, inherit)",
38997
+ fontSize: "1.125rem",
38998
+ lineHeight: 1.3,
38999
+ fontWeight: 600,
39000
+ margin: "0 0 0.4em"
39001
+ };
39002
+ const textIntroStyle = { fontSize: "1.125rem", lineHeight: 1.6, opacity: 0.85, margin: "0 0 2em" };
39003
+ const textItemStyle = { margin: "0 0 1.75em" };
39004
+ const textParagraphStyle = { lineHeight: 1.7, margin: 0, whiteSpace: "pre-line" };
39005
+ const hiddenBarStyle = {
39006
+ display: "flex",
39007
+ alignItems: "center",
39008
+ justifyContent: "center",
39009
+ gap: 12,
39010
+ padding: "10px 16px",
39011
+ margin: "4px 0",
39012
+ border: "1px dashed rgba(100, 10, 255, 0.45)",
39013
+ borderRadius: 8,
39014
+ background: "rgba(100, 10, 255, 0.05)",
39015
+ color: "#3b2a66",
39016
+ font: '500 13px system-ui, -apple-system, "Segoe UI", sans-serif'
39017
+ };
39018
+ const hiddenBtnStyle = {
39019
+ border: "none",
39020
+ borderRadius: 999,
39021
+ padding: "4px 12px",
39022
+ background: "#640AFF",
39023
+ color: "#fff",
39024
+ font: '600 12px system-ui, -apple-system, "Segoe UI", sans-serif',
39025
+ cursor: "pointer"
39026
+ };
38328
39027
  const DEFAULT_DURATION = {
38329
39028
  fadeIn: 600,
38330
39029
  slideUp: 700,
@@ -39287,15 +39986,21 @@ export {
39287
39986
  AuthGate,
39288
39987
  BLOCK_REGISTRY,
39289
39988
  BLOCK_TYPES,
39989
+ BUILTIN_SECTION_TYPES,
39290
39990
  CHECKOUT_INTENT,
39291
39991
  DEFAULT_LANGUAGE,
39292
39992
  EMPTY_I18N_STORE_SNAPSHOT,
39993
+ EMPTY_PAGE_STORE_SNAPSHOT,
39293
39994
  FAB_INSET,
39294
39995
  FAB_SIZE,
39295
39996
  FORM_FIELD_TYPES,
39296
39997
  FORM_FIELD_TYPE_LABELS,
39297
39998
  FloatingPopover,
39298
39999
  GuestEditProvider,
40000
+ ManifestError,
40001
+ PAGES_MANIFEST_VERSION,
40002
+ PAGES_PATH,
40003
+ PageStore,
39299
40004
  SETTO_BASE,
39300
40005
  SETTO_MODULES,
39301
40006
  SettoAdminApp,
@@ -39311,42 +40016,59 @@ export {
39311
40016
  SettoLanguages,
39312
40017
  SettoMap,
39313
40018
  SettoMark,
40019
+ SettoPage,
39314
40020
  SettoProvider,
39315
40021
  SettoRepeater,
39316
40022
  SettoSection,
40023
+ SettoTextSection,
39317
40024
  SettoVideo,
39318
40025
  T,
39319
40026
  adminRedirectUrl,
39320
40027
  adminViewUrl,
39321
40028
  alternateLinks,
40029
+ applyOp,
40030
+ applyOps,
40031
+ builtinKeys,
39322
40032
  checkoutIntentUrl,
40033
+ countChanges,
39323
40034
  currentAdminView,
39324
40035
  extractContentKeys,
39325
40036
  fieldTypeHasOptions,
39326
40037
  fieldTypeHasPlaceholder,
40038
+ findSection,
39327
40039
  formFieldsPrefix,
39328
40040
  isAdminDeploymentView,
39329
40041
  isAdminPanelView,
39330
40042
  isAdminRoute,
39331
40043
  isFormFieldType,
39332
40044
  isLinkContext,
40045
+ isSafeHref,
39333
40046
  languageFromPath,
39334
40047
  languageName,
39335
40048
  nodeChildren,
40049
+ normalisePath,
40050
+ pageById,
40051
+ pageForPath,
40052
+ pageTitleKey,
39336
40053
  parseBlockTree,
40054
+ parseManifest,
39337
40055
  pathForLanguage,
39338
40056
  pickInitialLanguage,
39339
40057
  readLocaleFields,
39340
40058
  redirectForVisitor,
40059
+ serialiseManifest,
39341
40060
  settoSubmissionUrl,
39342
40061
  siteHomeUrl,
39343
40062
  siteLanguages,
39344
40063
  useCoarsePointer,
39345
40064
  useGuestEdit,
40065
+ usePageManifest,
39346
40066
  useSectionTheme,
39347
40067
  useSetto,
39348
40068
  useSettoChromeSlot,
40069
+ useSettoMenu,
39349
40070
  validateBlockTree,
40071
+ visibleMenu,
39350
40072
  wantsCheckout,
39351
40073
  withoutCheckoutIntent
39352
40074
  };