@sevenfold/setto-client 0.3.3 → 0.4.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.
package/README.md CHANGED
@@ -67,7 +67,9 @@ createRoot(document.getElementById('root')!).render(
67
67
  );
68
68
  ```
69
69
 
70
- ### 2. Mount the admin route
70
+ ### 2. Mount the Setto route
71
+
72
+ Editors reach edit mode by visiting `sitenavn.no/setto`.
71
73
 
72
74
  ```tsx
73
75
  // App.tsx
@@ -75,7 +77,7 @@ import { SettoAdminApp } from '@setto/client';
75
77
 
76
78
  <Routes>
77
79
  <Route path="/" element={<Home />} />
78
- <Route path="/admin/*" element={<SettoAdminApp />} />
80
+ <Route path="/setto/*" element={<SettoAdminApp />} />
79
81
  </Routes>
80
82
  ```
81
83
 
@@ -129,7 +131,7 @@ Edit mode activates when **both** are true:
129
131
  1. Authenticated Supabase session.
130
132
  2. URL contains `?setto=edit`.
131
133
 
132
- The `/admin` dashboard does **not** activate edit mode. Use **Begynn å redigere**, which navigates to `/?setto=edit`.
134
+ The `/setto` dashboard does **not** activate edit mode. Use **Begynn å redigere**, which navigates to `/?setto=edit`.
133
135
 
134
136
  ### What editors see
135
137
 
@@ -272,25 +274,35 @@ Click a block's background to edit **that block's** colours. Click the section p
272
274
 
273
275
  ## Server setup (setto-server)
274
276
 
275
- Each site row in Supabase needs:
277
+ Site configuration is split in two:
278
+
279
+ **1. Supabase `sites` row — control plane (where + routing).** Set once when the site is registered (platform admin → **Ny side**):
276
280
 
277
281
  | Column | Example |
278
282
  |--------|---------|
279
- | `id` | `carryon-no` |
283
+ | `id` | `carryon-no` (must match `siteId` in `SettoProvider`) |
280
284
  | `repo_owner` / `repo_name` / `branch` | GitHub target |
281
- | `content_paths` | Whitelist of publishable file paths |
285
+ | `vercel_project_id` | `prj_…` (used to route Vercel webhooks) |
282
286
 
283
- Example `content_paths`:
287
+ **2. `setto.config.json` in the site repo root — content shape.** setto-server reads this from GitHub (cached, with a DB fallback):
284
288
 
285
- ```
286
- src/i18n/locales/no.json
287
- src/i18n/locales/en.json
288
- src/theme/sections.json
289
+ ```json
290
+ {
291
+ "displayName": "Carry On",
292
+ "contentPaths": [
293
+ "src/i18n/locales/no.json",
294
+ "src/i18n/locales/en.json",
295
+ "src/theme/sections.json",
296
+ "public/images/setto/"
297
+ ],
298
+ "allowedOrigins": ["https://carryon.no", "http://localhost:3000"]
299
+ }
289
300
  ```
290
301
 
291
- Only paths in this list can be committed. Add new content files here before publishing them.
302
+ - `contentPaths` is the publish whitelist — only these paths can be committed. Add new content files here before publishing them. `setto.config.json` itself is intentionally not in the list, so editors can never widen their own access.
303
+ - `allowedOrigins` is the CORS allow-list; the first entry is also used as the editor-invite activation domain.
292
304
 
293
- Register allowed origins for CORS (`allowed_origins`).
305
+ Keeping this in the repo means content shape lives with the code that defines it, and no DB change is needed when you add a content file — just commit the config. The legacy `content_paths` / `allowed_origins` / `display_name` DB columns remain as a fallback for sites without the file.
294
306
 
295
307
  ---
296
308
 
@@ -308,9 +320,9 @@ Drafts are cleared after a successful publish.
308
320
 
309
321
  ## Admin app (`SettoAdminApp`)
310
322
 
311
- Route: `/admin/*`
323
+ Route: `/setto/*`
312
324
 
313
- Provides Supabase email/password login (invite-only — no self-service sign-up), password reset, and a dashboard with a link to start editing (`/?setto=edit`). Invite links from Supabase land on `/admin` to set a password. Does not render the inline editor itself.
325
+ Provides Supabase email/password login (invite-only — no self-service sign-up), password reset, and a dashboard with a link to start editing (`/?setto=edit`). Invite links from Supabase land on `/setto` to set a password. Does not render the inline editor itself.
314
326
 
315
327
  ---
316
328
 
@@ -353,7 +365,7 @@ Produces `dist/setto-client.js` and `.d.ts` via Vite library mode. Only needed b
353
365
  | `SettoSection` | Section wrapper + edit selection |
354
366
  | `SettoBlock` | Nested card/panel with its own colour toolbar |
355
367
  | `useSectionTheme` | Read section colour tokens |
356
- | `SettoAdminApp` | `/admin` login + dashboard |
368
+ | `SettoAdminApp` | `/setto` login + dashboard |
357
369
  | `AuthGate` | Standalone login wrapper |
358
370
  | `BrandColor`, `SectionSchema`, `SettoConfig` | Types for host config |
359
371
 
@@ -10,6 +10,7 @@ export interface SettoRepeaterProps {
10
10
  className?: string;
11
11
  }
12
12
  /**
13
- * Renders a dynamic list from i18n. In edit mode, editors can add and remove items.
13
+ * Renders a dynamic list from i18n. In edit mode, editors can add items (button
14
+ * at the end of the list) and remove items (round × revealed on hover).
14
15
  */
15
16
  export declare function SettoRepeater({ itemsKey, defaultItem, itemLabel, children, className, }: SettoRepeaterProps): import("react/jsx-runtime").JSX.Element;
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Drop-in admin SPA. Mount under a route like `<Route path="/admin/*" .../>`.
2
+ * Drop-in admin SPA. Mount under a route like `<Route path="/setto/*" .../>`.
3
3
  *
4
4
  * Behaviour after login:
5
5
  * - Redirects to `/?setto=edit` on the site home (edit mode active).
6
6
  * - Shows the dashboard only when the user lacks access to this site, or on
7
- * `/admin?deployment=…` for publish progress / history.
7
+ * `/setto?deployment=…` for publish progress / history.
8
8
  *
9
9
  * Editing happens on the public site at `/?setto=edit`.
10
10
  */
@@ -0,0 +1,16 @@
1
+ import { type ReactNode } from 'react';
2
+ interface GuestEditContextValue {
3
+ active: boolean;
4
+ start: () => void;
5
+ stop: () => void;
6
+ }
7
+ /**
8
+ * Lets visitors inline-edit a marketing site (e.g. setto.no) without auth —
9
+ * text, section/block colours, icons and list items. Changes are session-only
10
+ * and never published. Real Setto edit mode always takes precedence.
11
+ */
12
+ export declare function GuestEditProvider({ children }: {
13
+ children: ReactNode;
14
+ }): import("react/jsx-runtime").JSX.Element;
15
+ export declare function useGuestEdit(): GuestEditContextValue;
16
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { SettoProvider, useSetto } from './provider';
2
+ export { GuestEditProvider, useGuestEdit } from './guest-edit';
2
3
  export { T } from './T';
3
4
  export { SettoSection } from './SettoSection';
4
5
  export { SettoBlock } from './SettoBlock';
@@ -10,6 +11,7 @@ export { SettoRepeater } from './SettoRepeater';
10
11
  export type { SettoRepeaterProps } from './SettoRepeater';
11
12
  export { useSectionTheme } from './use-section-theme';
12
13
  export { SettoAdminApp } from './admin/App';
14
+ export { SETTO_BASE } from './lib/urls';
13
15
  export { AuthGate } from './edit-mode/auth-gate';
14
16
  export type { SettoConfig, BrandColor, DeploymentRow, SiteRow, ContentFile, PublishFile, PublishResult, } from './types';
15
17
  export type { SectionSchema, SectionColorField } from './section-schema';
@@ -1,6 +1,12 @@
1
+ /**
2
+ * Base path where the Setto login + dashboard SPA is mounted on the host site.
3
+ * Editors reach edit mode by visiting `sitenavn.no/setto`. The host app must
4
+ * mount `<SettoAdminApp>` at this path (e.g. `<Route path="/setto/*" …>`).
5
+ */
6
+ export declare const SETTO_BASE = "/setto";
1
7
  /** Site home with inline edit mode active. */
2
8
  export declare function editModeUrl(pathname?: string): string;
3
9
  export declare function isAdminRoute(): boolean;
4
- /** Admin deployment progress panel (`/admin?deployment=…`). */
10
+ /** Setto deployment progress panel (`/setto?deployment=…`). */
5
11
  export declare function isAdminDeploymentView(): boolean;
6
12
  export declare function adminRedirectUrl(): string;
@@ -34,7 +34,7 @@ export interface SettoProviderProps {
34
34
  * 1. There is an authenticated Supabase session.
35
35
  * 2. The URL contains `?setto=edit`.
36
36
  *
37
- * The /admin dashboard does not activate edit mode — use "Begynn å redigere"
37
+ * The /setto dashboard does not activate edit mode — use "Begynn å redigere"
38
38
  * which navigates to `/?setto=edit`.
39
39
  */
40
40
  export declare function SettoProvider({ config, children }: SettoProviderProps): import("react/jsx-runtime").JSX.Element;
@@ -21147,17 +21147,17 @@ class ThemeStore {
21147
21147
  }
21148
21148
  }
21149
21149
  const TOOLBAR_HEIGHT = 44;
21150
- const STYLE_ID = "setto-edit-layout";
21150
+ const STYLE_ID$1 = "setto-edit-layout";
21151
21151
  const HTML_CLASS = "setto-edit-mode";
21152
21152
  function useSettoDocumentLayout(active) {
21153
21153
  useEffect(() => {
21154
21154
  const html = document.documentElement;
21155
21155
  html.classList.add(HTML_CLASS);
21156
21156
  html.style.setProperty("--setto-toolbar-height", `${TOOLBAR_HEIGHT}px`);
21157
- let styleEl = document.getElementById(STYLE_ID);
21157
+ let styleEl = document.getElementById(STYLE_ID$1);
21158
21158
  if (!styleEl) {
21159
21159
  styleEl = document.createElement("style");
21160
- styleEl.id = STYLE_ID;
21160
+ styleEl.id = STYLE_ID$1;
21161
21161
  styleEl.textContent = `
21162
21162
  html.setto-edit-mode {
21163
21163
  width: 100%;
@@ -22150,6 +22150,23 @@ function useEditBaseline() {
22150
22150
  };
22151
22151
  }, [session, store, config.siteId]);
22152
22152
  }
22153
+ const SETTO_BASE = "/setto";
22154
+ function editModeUrl(pathname = "/") {
22155
+ const u = new URL(window.location.href);
22156
+ u.pathname = pathname;
22157
+ u.search = "";
22158
+ u.searchParams.set("setto", "edit");
22159
+ return u.toString();
22160
+ }
22161
+ function isAdminRoute() {
22162
+ return window.location.pathname.startsWith(SETTO_BASE);
22163
+ }
22164
+ function isAdminDeploymentView() {
22165
+ return isAdminRoute() && new URLSearchParams(window.location.search).has("deployment");
22166
+ }
22167
+ function adminRedirectUrl() {
22168
+ return `${window.location.origin}${SETTO_BASE}`;
22169
+ }
22153
22170
  function EditToolbar() {
22154
22171
  const { store, themeStore, assetStore, api, config, supabase } = useSetto();
22155
22172
  const { i18n } = useTranslation();
@@ -22364,7 +22381,7 @@ function EditToolbar() {
22364
22381
  };
22365
22382
  const goToHistory = () => {
22366
22383
  setMenuOpen(false);
22367
- window.location.href = "/admin";
22384
+ window.location.href = SETTO_BASE;
22368
22385
  };
22369
22386
  return /* @__PURE__ */ jsxs(Fragment, { children: [
22370
22387
  /* @__PURE__ */ jsxs("header", { style: barStyle, role: "toolbar", "aria-label": "Setto editor", children: [
@@ -22397,7 +22414,7 @@ function EditToolbar() {
22397
22414
  {
22398
22415
  status: deploymentRow.status,
22399
22416
  compact: true,
22400
- href: `/admin?deployment=${encodeURIComponent(activeDeployment)}`
22417
+ href: `${SETTO_BASE}?deployment=${encodeURIComponent(activeDeployment)}`
22401
22418
  }
22402
22419
  ) : null,
22403
22420
  /* @__PURE__ */ jsxs("div", { ref: menuRef, style: { position: "relative" }, children: [
@@ -22697,6 +22714,37 @@ function useSetto() {
22697
22714
  }
22698
22715
  return ctx;
22699
22716
  }
22717
+ const GuestEditContext = createContext(null);
22718
+ function GuestEditProvider({ children }) {
22719
+ const { editMode, themeStore } = useSetto();
22720
+ const { selectSection } = useSectionEdit();
22721
+ const [active, setActive] = useState(false);
22722
+ useEffect(() => {
22723
+ if (editMode && active) setActive(false);
22724
+ }, [editMode, active]);
22725
+ useEffect(() => {
22726
+ document.documentElement.classList.toggle("setto-guest-editing", active);
22727
+ return () => document.documentElement.classList.remove("setto-guest-editing");
22728
+ }, [active]);
22729
+ const start = useCallback(() => setActive(true), []);
22730
+ const stop = useCallback(() => {
22731
+ setActive(false);
22732
+ selectSection(null);
22733
+ }, [selectSection]);
22734
+ return /* @__PURE__ */ jsxs(GuestEditContext.Provider, { value: { active, start, stop }, children: [
22735
+ children,
22736
+ active && themeStore ? createPortal(/* @__PURE__ */ jsx(SectionToolbar, {}), document.body) : null
22737
+ ] });
22738
+ }
22739
+ function useGuestEdit() {
22740
+ const ctx = useContext(GuestEditContext);
22741
+ if (!ctx) {
22742
+ return { active: false, start: () => {
22743
+ }, stop: () => {
22744
+ } };
22745
+ }
22746
+ return ctx;
22747
+ }
22700
22748
  const MARGIN = 10;
22701
22749
  const OFFSET = 8;
22702
22750
  const MIN_TOUCH = 44;
@@ -22814,6 +22862,8 @@ function rangeFromPoint(doc, x, y) {
22814
22862
  function T({ k }) {
22815
22863
  const { t, i18n } = useTranslation();
22816
22864
  const { editMode, store } = useSetto();
22865
+ const guestEdit = useGuestEdit();
22866
+ const editable = editMode || guestEdit.active;
22817
22867
  const ref = useRef(null);
22818
22868
  const [focused, setFocused] = useState(false);
22819
22869
  const [linkContext, setLinkContext] = useState(false);
@@ -22826,10 +22876,10 @@ function T({ k }) {
22826
22876
  const value = store ? store.get(k, i18n.language) : t(k);
22827
22877
  useEffect(() => {
22828
22878
  const el = ref.current;
22829
- if (!el || focused || !editMode) return;
22879
+ if (!el || focused || !editable) return;
22830
22880
  if (el.textContent !== value) el.textContent = value;
22831
- }, [value, focused, editMode]);
22832
- if (!editMode) return /* @__PURE__ */ jsx(Fragment, { children: value });
22881
+ }, [value, focused, editable]);
22882
+ if (!editable) return /* @__PURE__ */ jsx(Fragment, { children: value });
22833
22883
  const handleMouseDown = (e) => {
22834
22884
  e.stopPropagation();
22835
22885
  const el = e.currentTarget;
@@ -22897,7 +22947,7 @@ function T({ k }) {
22897
22947
  contentEditable: true,
22898
22948
  suppressContentEditableWarning: true,
22899
22949
  lang,
22900
- spellCheck: true,
22950
+ spellCheck: false,
22901
22951
  role: "textbox",
22902
22952
  tabIndex: 0,
22903
22953
  onFocus: (e) => {
@@ -22915,7 +22965,7 @@ function T({ k }) {
22915
22965
  style: {
22916
22966
  cursor: "text",
22917
22967
  color: "inherit",
22918
- outline: focused ? "2px solid #640AFF" : void 0,
22968
+ outline: focused ? editMode ? "2px solid #640AFF" : "2px solid #C4502A" : void 0,
22919
22969
  outlineOffset: 2,
22920
22970
  borderRadius: 2,
22921
22971
  transition: "outline-color 120ms"
@@ -22950,9 +23000,9 @@ function isNestedThemeTarget(target, container) {
22950
23000
  const hit = target.closest("[data-setto-section]");
22951
23001
  return !!(hit && hit !== container);
22952
23002
  }
22953
- function handleThemeTargetClick(e, editMode, themeId, selected, selectSection, onClick) {
23003
+ function handleThemeTargetClick(e, editable, themeId, selected, selectSection, onClick) {
22954
23004
  onClick?.(e);
22955
- if (!editMode || e.defaultPrevented) return;
23005
+ if (!editable || e.defaultPrevented) return;
22956
23006
  const target = e.target;
22957
23007
  if (isTextEditClick(target)) return;
22958
23008
  if (target.closest("a[href], button, input, textarea, select")) return;
@@ -22960,11 +23010,11 @@ function handleThemeTargetClick(e, editMode, themeId, selected, selectSection, o
22960
23010
  e.stopPropagation();
22961
23011
  selectSection(selected ? null : themeId);
22962
23012
  }
22963
- function themeTargetEditStyle(editMode, selected) {
22964
- if (!editMode) return {};
23013
+ function themeTargetEditStyle(editable, selected, accent = "#640AFF") {
23014
+ if (!editable) return {};
22965
23015
  return {
22966
23016
  cursor: "pointer",
22967
- outline: selected ? "2px solid #640AFF" : void 0,
23017
+ outline: selected ? `2px solid ${accent}` : void 0,
22968
23018
  outlineOffset: selected ? -2 : void 0
22969
23019
  };
22970
23020
  }
@@ -22982,23 +23032,26 @@ function useSectionTheme(sectionId) {
22982
23032
  const SettoSection = forwardRef(
22983
23033
  function SettoSection2({ sectionId, className, style, children, onClick, ...rest }, ref) {
22984
23034
  const { editMode } = useSetto();
23035
+ const guest = useGuestEdit();
23036
+ const editable = editMode || guest.active;
23037
+ const accent = editMode ? "#640AFF" : "#C4502A";
22985
23038
  const { selectedId, selectSection } = useSectionEdit();
22986
23039
  const colors = useSectionTheme(sectionId);
22987
23040
  const selected = selectedId === sectionId;
22988
23041
  const handleClick = useCallback(
22989
23042
  (e) => {
22990
- if (editMode) {
23043
+ if (editable) {
22991
23044
  const target = e.target;
22992
23045
  if (isNestedThemeTarget(target, e.currentTarget)) return;
22993
23046
  }
22994
- handleThemeTargetClick(e, editMode, sectionId, selected, selectSection, onClick);
23047
+ handleThemeTargetClick(e, editable, sectionId, selected, selectSection, onClick);
22995
23048
  },
22996
- [editMode, onClick, selectSection, selected, sectionId]
23049
+ [editable, onClick, selectSection, selected, sectionId]
22997
23050
  );
22998
23051
  const mergedStyle = {
22999
23052
  ...style,
23000
23053
  ...colors.background ? { backgroundColor: colors.background } : {},
23001
- ...themeTargetEditStyle(editMode, selected)
23054
+ ...themeTargetEditStyle(editable, selected, accent)
23002
23055
  };
23003
23056
  return /* @__PURE__ */ jsx(
23004
23057
  "section",
@@ -23017,6 +23070,9 @@ const SettoSection = forwardRef(
23017
23070
  const SettoBlock = forwardRef(
23018
23071
  function SettoBlock2({ blockId, className, style, children, onClick, ...rest }, ref) {
23019
23072
  const { editMode } = useSetto();
23073
+ const guest = useGuestEdit();
23074
+ const editable = editMode || guest.active;
23075
+ const accent = editMode ? "#640AFF" : "#C4502A";
23020
23076
  const { selectedId, selectSection } = useSectionEdit();
23021
23077
  const colors = useSectionTheme(blockId);
23022
23078
  const selected = selectedId === blockId;
@@ -23024,19 +23080,19 @@ const SettoBlock = forwardRef(
23024
23080
  (e) => {
23025
23081
  handleThemeTargetClick(
23026
23082
  e,
23027
- editMode,
23083
+ editable,
23028
23084
  blockId,
23029
23085
  selected,
23030
23086
  selectSection,
23031
23087
  onClick
23032
23088
  );
23033
23089
  },
23034
- [editMode, blockId, selected, selectSection, onClick]
23090
+ [editable, blockId, selected, selectSection, onClick]
23035
23091
  );
23036
23092
  const mergedStyle = {
23037
23093
  ...style,
23038
23094
  ...colors.background ? { backgroundColor: colors.background } : {},
23039
- ...themeTargetEditStyle(editMode, selected)
23095
+ ...themeTargetEditStyle(editable, selected, accent)
23040
23096
  };
23041
23097
  return /* @__PURE__ */ jsx(
23042
23098
  "div",
@@ -23148,6 +23204,9 @@ function FloatingPopover({
23148
23204
  function SettoIcon({ k, icons, size = 24, className, style }) {
23149
23205
  const { t, i18n } = useTranslation();
23150
23206
  const { editMode, store } = useSetto();
23207
+ const guest = useGuestEdit();
23208
+ const editable = editMode || guest.active;
23209
+ const accent = editMode ? "#640AFF" : "#C4502A";
23151
23210
  const anchorRef = useRef(null);
23152
23211
  const [open, setOpen] = useState(false);
23153
23212
  const [search, setSearch] = useState("");
@@ -23159,7 +23218,7 @@ function SettoIcon({ k, icons, size = 24, className, style }) {
23159
23218
  const iconName = store ? store.get(k, i18n.language) : t(k);
23160
23219
  const Icon = icons[iconName] ?? icons[Object.keys(icons)[0] ?? ""] ?? null;
23161
23220
  if (!Icon) return null;
23162
- if (!editMode) {
23221
+ if (!editable) {
23163
23222
  return /* @__PURE__ */ jsx(Icon, { size, className, style });
23164
23223
  }
23165
23224
  const handleClick = (e) => {
@@ -23190,7 +23249,7 @@ function SettoIcon({ k, icons, size = 24, className, style }) {
23190
23249
  style: {
23191
23250
  display: "inline-flex",
23192
23251
  cursor: "pointer",
23193
- outline: open ? "2px solid #640AFF" : void 0,
23252
+ outline: open ? `2px solid ${accent}` : void 0,
23194
23253
  outlineOffset: 2,
23195
23254
  borderRadius: 4
23196
23255
  },
@@ -23457,6 +23516,28 @@ function SettoImage({ srcKey, alt, className, style, ...rest }) {
23457
23516
  )
23458
23517
  ] });
23459
23518
  }
23519
+ const STYLE_ID = "setto-repeater-styles";
23520
+ function useRepeaterStyles() {
23521
+ useEffect(() => {
23522
+ if (document.getElementById(STYLE_ID)) return;
23523
+ const el = document.createElement("style");
23524
+ el.id = STYLE_ID;
23525
+ el.textContent = `
23526
+ .setto-repeater-remove {
23527
+ opacity: 0;
23528
+ transition: opacity 120ms ease;
23529
+ }
23530
+ .setto-repeater-item:hover .setto-repeater-remove,
23531
+ .setto-repeater-remove:focus-visible {
23532
+ opacity: 1;
23533
+ }
23534
+ @media (hover: none) {
23535
+ .setto-repeater-remove { opacity: 1; }
23536
+ }
23537
+ `;
23538
+ document.head.appendChild(el);
23539
+ }, []);
23540
+ }
23460
23541
  function SettoRepeater({
23461
23542
  itemsKey,
23462
23543
  defaultItem,
@@ -23466,7 +23547,10 @@ function SettoRepeater({
23466
23547
  }) {
23467
23548
  const { i18n } = useTranslation();
23468
23549
  const { editMode, store } = useSetto();
23550
+ const guest = useGuestEdit();
23551
+ const editable = editMode || guest.active;
23469
23552
  const lng = i18n.language;
23553
+ useRepeaterStyles();
23470
23554
  const [, force] = useState(0);
23471
23555
  useEffect(() => {
23472
23556
  if (!store) return;
@@ -23480,16 +23564,35 @@ function SettoRepeater({
23480
23564
  if (keys.length <= 1) return;
23481
23565
  store?.removeListItem(itemsKey, itemKey, lng);
23482
23566
  };
23567
+ const canRemove = keys.length > 1;
23483
23568
  return /* @__PURE__ */ jsxs("div", { className, "data-setto-repeater": true, children: [
23484
- editMode ? /* @__PURE__ */ jsx(
23569
+ keys.map((itemKey, index) => /* @__PURE__ */ jsxs("div", { className: "setto-repeater-item", style: { position: "relative" }, children: [
23570
+ editable && canRemove ? /* @__PURE__ */ jsx(
23571
+ "button",
23572
+ {
23573
+ type: "button",
23574
+ "data-setto-ui": true,
23575
+ className: "setto-repeater-remove",
23576
+ "aria-label": `Fjern ${itemLabel}`,
23577
+ title: `Fjern ${itemLabel}`,
23578
+ onClick: (e) => {
23579
+ e.stopPropagation();
23580
+ handleRemove(itemKey);
23581
+ },
23582
+ style: removeBtnStyle,
23583
+ children: "×"
23584
+ }
23585
+ ) : null,
23586
+ children(itemKey, index)
23587
+ ] }, itemKey)),
23588
+ editable ? /* @__PURE__ */ jsx(
23485
23589
  "div",
23486
23590
  {
23487
23591
  "data-setto-ui": true,
23488
23592
  style: {
23489
23593
  display: "flex",
23490
23594
  justifyContent: "flex-end",
23491
- marginBottom: 8,
23492
- gap: 8
23595
+ marginTop: 8
23493
23596
  },
23494
23597
  children: /* @__PURE__ */ jsxs(
23495
23598
  "button",
@@ -23500,7 +23603,7 @@ function SettoRepeater({
23500
23603
  e.stopPropagation();
23501
23604
  handleAdd();
23502
23605
  },
23503
- style: controlBtnStyle,
23606
+ style: addBtnStyle,
23504
23607
  children: [
23505
23608
  "+ Legg til ",
23506
23609
  itemLabel
@@ -23508,37 +23611,10 @@ function SettoRepeater({
23508
23611
  }
23509
23612
  )
23510
23613
  }
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))
23614
+ ) : null
23539
23615
  ] });
23540
23616
  }
23541
- const controlBtnStyle = {
23617
+ const addBtnStyle = {
23542
23618
  padding: "6px 10px",
23543
23619
  background: "#f7f7f7",
23544
23620
  border: "1px solid #e0e0e0",
@@ -23548,22 +23624,27 @@ const controlBtnStyle = {
23548
23624
  cursor: "pointer",
23549
23625
  fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
23550
23626
  };
23551
- function editModeUrl(pathname = "/") {
23552
- const u = new URL(window.location.href);
23553
- u.pathname = pathname;
23554
- u.search = "";
23555
- u.searchParams.set("setto", "edit");
23556
- return u.toString();
23557
- }
23558
- function isAdminRoute() {
23559
- return window.location.pathname.startsWith("/admin");
23560
- }
23561
- function isAdminDeploymentView() {
23562
- return isAdminRoute() && new URLSearchParams(window.location.search).has("deployment");
23563
- }
23564
- function adminRedirectUrl() {
23565
- return `${window.location.origin}/admin`;
23566
- }
23627
+ const removeBtnStyle = {
23628
+ position: "absolute",
23629
+ top: 4,
23630
+ right: 4,
23631
+ zIndex: 2,
23632
+ display: "flex",
23633
+ alignItems: "center",
23634
+ justifyContent: "center",
23635
+ width: 24,
23636
+ height: 24,
23637
+ padding: 0,
23638
+ background: "#ffffff",
23639
+ border: "1px solid #e0e0e0",
23640
+ borderRadius: "50%",
23641
+ fontSize: 16,
23642
+ lineHeight: 1,
23643
+ color: "#444",
23644
+ cursor: "pointer",
23645
+ boxShadow: "0 1px 4px rgba(0,0,0,0.18)",
23646
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
23647
+ };
23567
23648
  function authCallbackType() {
23568
23649
  const hash = window.location.hash.replace(/^#/, "");
23569
23650
  if (!hash) return null;
@@ -23844,7 +23925,7 @@ const loadingStyle = {
23844
23925
  };
23845
23926
  function deploymentAdminUrl(deploymentId) {
23846
23927
  const u = new URL(window.location.href);
23847
- u.pathname = "/admin";
23928
+ u.pathname = SETTO_BASE;
23848
23929
  u.search = "";
23849
23930
  u.searchParams.set("deployment", deploymentId);
23850
23931
  return u.pathname + u.search;
@@ -23957,7 +24038,7 @@ function DeploymentList({ siteId }) {
23957
24038
  try {
23958
24039
  await api.cancelDeployment(config.siteId, focusId);
23959
24040
  setFocusId(null);
23960
- window.history.replaceState(null, "", "/admin");
24041
+ window.history.replaceState(null, "", SETTO_BASE);
23961
24042
  } catch (err) {
23962
24043
  setCancelError(err instanceof Error ? err.message : "Kunne ikke avbryte");
23963
24044
  } finally {
@@ -24185,6 +24266,8 @@ function depDotStyle(status) {
24185
24266
  }
24186
24267
  export {
24187
24268
  AuthGate,
24269
+ GuestEditProvider,
24270
+ SETTO_BASE,
24188
24271
  SettoAdminApp,
24189
24272
  SettoBlock,
24190
24273
  SettoIcon,
@@ -24193,6 +24276,7 @@ export {
24193
24276
  SettoRepeater,
24194
24277
  SettoSection,
24195
24278
  T,
24279
+ useGuestEdit,
24196
24280
  useSectionTheme,
24197
24281
  useSetto
24198
24282
  };