@sevenfold/setto-client 0.3.4 → 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
  */
@@ -5,9 +5,9 @@ interface GuestEditContextValue {
5
5
  stop: () => void;
6
6
  }
7
7
  /**
8
- * Lets visitors inline-edit copy on a marketing site (e.g. setto.no) without
9
- * auth. Changes are session-only and never published. Real Setto edit mode
10
- * always takes precedence.
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
11
  */
12
12
  export declare function GuestEditProvider({ children }: {
13
13
  children: ReactNode;
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export { SettoRepeater } from './SettoRepeater';
11
11
  export type { SettoRepeaterProps } from './SettoRepeater';
12
12
  export { useSectionTheme } from './use-section-theme';
13
13
  export { SettoAdminApp } from './admin/App';
14
+ export { SETTO_BASE } from './lib/urls';
14
15
  export { AuthGate } from './edit-mode/auth-gate';
15
16
  export type { SettoConfig, BrandColor, DeploymentRow, SiteRow, ContentFile, PublishFile, PublishResult, } from './types';
16
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: [
@@ -22699,7 +22716,8 @@ function useSetto() {
22699
22716
  }
22700
22717
  const GuestEditContext = createContext(null);
22701
22718
  function GuestEditProvider({ children }) {
22702
- const { editMode } = useSetto();
22719
+ const { editMode, themeStore } = useSetto();
22720
+ const { selectSection } = useSectionEdit();
22703
22721
  const [active, setActive] = useState(false);
22704
22722
  useEffect(() => {
22705
22723
  if (editMode && active) setActive(false);
@@ -22709,8 +22727,14 @@ function GuestEditProvider({ children }) {
22709
22727
  return () => document.documentElement.classList.remove("setto-guest-editing");
22710
22728
  }, [active]);
22711
22729
  const start = useCallback(() => setActive(true), []);
22712
- const stop = useCallback(() => setActive(false), []);
22713
- return /* @__PURE__ */ jsx(GuestEditContext.Provider, { value: { active, start, stop }, children });
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
+ ] });
22714
22738
  }
22715
22739
  function useGuestEdit() {
22716
22740
  const ctx = useContext(GuestEditContext);
@@ -22852,9 +22876,9 @@ function T({ k }) {
22852
22876
  const value = store ? store.get(k, i18n.language) : t(k);
22853
22877
  useEffect(() => {
22854
22878
  const el = ref.current;
22855
- if (!el || focused || !editMode) return;
22879
+ if (!el || focused || !editable) return;
22856
22880
  if (el.textContent !== value) el.textContent = value;
22857
- }, [value, focused, editMode]);
22881
+ }, [value, focused, editable]);
22858
22882
  if (!editable) return /* @__PURE__ */ jsx(Fragment, { children: value });
22859
22883
  const handleMouseDown = (e) => {
22860
22884
  e.stopPropagation();
@@ -22901,7 +22925,6 @@ function T({ k }) {
22901
22925
  commit(e.currentTarget);
22902
22926
  };
22903
22927
  const handleInput = (e) => {
22904
- if (!editMode) return;
22905
22928
  store?.set(k, i18n.language, e.currentTarget.textContent ?? "");
22906
22929
  };
22907
22930
  const handlePaste = (e) => {
@@ -22924,7 +22947,7 @@ function T({ k }) {
22924
22947
  contentEditable: true,
22925
22948
  suppressContentEditableWarning: true,
22926
22949
  lang,
22927
- spellCheck: true,
22950
+ spellCheck: false,
22928
22951
  role: "textbox",
22929
22952
  tabIndex: 0,
22930
22953
  onFocus: (e) => {
@@ -22977,9 +23000,9 @@ function isNestedThemeTarget(target, container) {
22977
23000
  const hit = target.closest("[data-setto-section]");
22978
23001
  return !!(hit && hit !== container);
22979
23002
  }
22980
- function handleThemeTargetClick(e, editMode, themeId, selected, selectSection, onClick) {
23003
+ function handleThemeTargetClick(e, editable, themeId, selected, selectSection, onClick) {
22981
23004
  onClick?.(e);
22982
- if (!editMode || e.defaultPrevented) return;
23005
+ if (!editable || e.defaultPrevented) return;
22983
23006
  const target = e.target;
22984
23007
  if (isTextEditClick(target)) return;
22985
23008
  if (target.closest("a[href], button, input, textarea, select")) return;
@@ -22987,11 +23010,11 @@ function handleThemeTargetClick(e, editMode, themeId, selected, selectSection, o
22987
23010
  e.stopPropagation();
22988
23011
  selectSection(selected ? null : themeId);
22989
23012
  }
22990
- function themeTargetEditStyle(editMode, selected) {
22991
- if (!editMode) return {};
23013
+ function themeTargetEditStyle(editable, selected, accent = "#640AFF") {
23014
+ if (!editable) return {};
22992
23015
  return {
22993
23016
  cursor: "pointer",
22994
- outline: selected ? "2px solid #640AFF" : void 0,
23017
+ outline: selected ? `2px solid ${accent}` : void 0,
22995
23018
  outlineOffset: selected ? -2 : void 0
22996
23019
  };
22997
23020
  }
@@ -23009,23 +23032,26 @@ function useSectionTheme(sectionId) {
23009
23032
  const SettoSection = forwardRef(
23010
23033
  function SettoSection2({ sectionId, className, style, children, onClick, ...rest }, ref) {
23011
23034
  const { editMode } = useSetto();
23035
+ const guest = useGuestEdit();
23036
+ const editable = editMode || guest.active;
23037
+ const accent = editMode ? "#640AFF" : "#C4502A";
23012
23038
  const { selectedId, selectSection } = useSectionEdit();
23013
23039
  const colors = useSectionTheme(sectionId);
23014
23040
  const selected = selectedId === sectionId;
23015
23041
  const handleClick = useCallback(
23016
23042
  (e) => {
23017
- if (editMode) {
23043
+ if (editable) {
23018
23044
  const target = e.target;
23019
23045
  if (isNestedThemeTarget(target, e.currentTarget)) return;
23020
23046
  }
23021
- handleThemeTargetClick(e, editMode, sectionId, selected, selectSection, onClick);
23047
+ handleThemeTargetClick(e, editable, sectionId, selected, selectSection, onClick);
23022
23048
  },
23023
- [editMode, onClick, selectSection, selected, sectionId]
23049
+ [editable, onClick, selectSection, selected, sectionId]
23024
23050
  );
23025
23051
  const mergedStyle = {
23026
23052
  ...style,
23027
23053
  ...colors.background ? { backgroundColor: colors.background } : {},
23028
- ...themeTargetEditStyle(editMode, selected)
23054
+ ...themeTargetEditStyle(editable, selected, accent)
23029
23055
  };
23030
23056
  return /* @__PURE__ */ jsx(
23031
23057
  "section",
@@ -23044,6 +23070,9 @@ const SettoSection = forwardRef(
23044
23070
  const SettoBlock = forwardRef(
23045
23071
  function SettoBlock2({ blockId, className, style, children, onClick, ...rest }, ref) {
23046
23072
  const { editMode } = useSetto();
23073
+ const guest = useGuestEdit();
23074
+ const editable = editMode || guest.active;
23075
+ const accent = editMode ? "#640AFF" : "#C4502A";
23047
23076
  const { selectedId, selectSection } = useSectionEdit();
23048
23077
  const colors = useSectionTheme(blockId);
23049
23078
  const selected = selectedId === blockId;
@@ -23051,19 +23080,19 @@ const SettoBlock = forwardRef(
23051
23080
  (e) => {
23052
23081
  handleThemeTargetClick(
23053
23082
  e,
23054
- editMode,
23083
+ editable,
23055
23084
  blockId,
23056
23085
  selected,
23057
23086
  selectSection,
23058
23087
  onClick
23059
23088
  );
23060
23089
  },
23061
- [editMode, blockId, selected, selectSection, onClick]
23090
+ [editable, blockId, selected, selectSection, onClick]
23062
23091
  );
23063
23092
  const mergedStyle = {
23064
23093
  ...style,
23065
23094
  ...colors.background ? { backgroundColor: colors.background } : {},
23066
- ...themeTargetEditStyle(editMode, selected)
23095
+ ...themeTargetEditStyle(editable, selected, accent)
23067
23096
  };
23068
23097
  return /* @__PURE__ */ jsx(
23069
23098
  "div",
@@ -23175,6 +23204,9 @@ function FloatingPopover({
23175
23204
  function SettoIcon({ k, icons, size = 24, className, style }) {
23176
23205
  const { t, i18n } = useTranslation();
23177
23206
  const { editMode, store } = useSetto();
23207
+ const guest = useGuestEdit();
23208
+ const editable = editMode || guest.active;
23209
+ const accent = editMode ? "#640AFF" : "#C4502A";
23178
23210
  const anchorRef = useRef(null);
23179
23211
  const [open, setOpen] = useState(false);
23180
23212
  const [search, setSearch] = useState("");
@@ -23186,7 +23218,7 @@ function SettoIcon({ k, icons, size = 24, className, style }) {
23186
23218
  const iconName = store ? store.get(k, i18n.language) : t(k);
23187
23219
  const Icon = icons[iconName] ?? icons[Object.keys(icons)[0] ?? ""] ?? null;
23188
23220
  if (!Icon) return null;
23189
- if (!editMode) {
23221
+ if (!editable) {
23190
23222
  return /* @__PURE__ */ jsx(Icon, { size, className, style });
23191
23223
  }
23192
23224
  const handleClick = (e) => {
@@ -23217,7 +23249,7 @@ function SettoIcon({ k, icons, size = 24, className, style }) {
23217
23249
  style: {
23218
23250
  display: "inline-flex",
23219
23251
  cursor: "pointer",
23220
- outline: open ? "2px solid #640AFF" : void 0,
23252
+ outline: open ? `2px solid ${accent}` : void 0,
23221
23253
  outlineOffset: 2,
23222
23254
  borderRadius: 4
23223
23255
  },
@@ -23484,6 +23516,28 @@ function SettoImage({ srcKey, alt, className, style, ...rest }) {
23484
23516
  )
23485
23517
  ] });
23486
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
+ }
23487
23541
  function SettoRepeater({
23488
23542
  itemsKey,
23489
23543
  defaultItem,
@@ -23493,7 +23547,10 @@ function SettoRepeater({
23493
23547
  }) {
23494
23548
  const { i18n } = useTranslation();
23495
23549
  const { editMode, store } = useSetto();
23550
+ const guest = useGuestEdit();
23551
+ const editable = editMode || guest.active;
23496
23552
  const lng = i18n.language;
23553
+ useRepeaterStyles();
23497
23554
  const [, force] = useState(0);
23498
23555
  useEffect(() => {
23499
23556
  if (!store) return;
@@ -23507,16 +23564,35 @@ function SettoRepeater({
23507
23564
  if (keys.length <= 1) return;
23508
23565
  store?.removeListItem(itemsKey, itemKey, lng);
23509
23566
  };
23567
+ const canRemove = keys.length > 1;
23510
23568
  return /* @__PURE__ */ jsxs("div", { className, "data-setto-repeater": true, children: [
23511
- 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(
23512
23589
  "div",
23513
23590
  {
23514
23591
  "data-setto-ui": true,
23515
23592
  style: {
23516
23593
  display: "flex",
23517
23594
  justifyContent: "flex-end",
23518
- marginBottom: 8,
23519
- gap: 8
23595
+ marginTop: 8
23520
23596
  },
23521
23597
  children: /* @__PURE__ */ jsxs(
23522
23598
  "button",
@@ -23527,7 +23603,7 @@ function SettoRepeater({
23527
23603
  e.stopPropagation();
23528
23604
  handleAdd();
23529
23605
  },
23530
- style: controlBtnStyle,
23606
+ style: addBtnStyle,
23531
23607
  children: [
23532
23608
  "+ Legg til ",
23533
23609
  itemLabel
@@ -23535,37 +23611,10 @@ function SettoRepeater({
23535
23611
  }
23536
23612
  )
23537
23613
  }
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))
23614
+ ) : null
23566
23615
  ] });
23567
23616
  }
23568
- const controlBtnStyle = {
23617
+ const addBtnStyle = {
23569
23618
  padding: "6px 10px",
23570
23619
  background: "#f7f7f7",
23571
23620
  border: "1px solid #e0e0e0",
@@ -23575,22 +23624,27 @@ const controlBtnStyle = {
23575
23624
  cursor: "pointer",
23576
23625
  fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
23577
23626
  };
23578
- function editModeUrl(pathname = "/") {
23579
- const u = new URL(window.location.href);
23580
- u.pathname = pathname;
23581
- u.search = "";
23582
- u.searchParams.set("setto", "edit");
23583
- return u.toString();
23584
- }
23585
- function isAdminRoute() {
23586
- return window.location.pathname.startsWith("/admin");
23587
- }
23588
- function isAdminDeploymentView() {
23589
- return isAdminRoute() && new URLSearchParams(window.location.search).has("deployment");
23590
- }
23591
- function adminRedirectUrl() {
23592
- return `${window.location.origin}/admin`;
23593
- }
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
+ };
23594
23648
  function authCallbackType() {
23595
23649
  const hash = window.location.hash.replace(/^#/, "");
23596
23650
  if (!hash) return null;
@@ -23871,7 +23925,7 @@ const loadingStyle = {
23871
23925
  };
23872
23926
  function deploymentAdminUrl(deploymentId) {
23873
23927
  const u = new URL(window.location.href);
23874
- u.pathname = "/admin";
23928
+ u.pathname = SETTO_BASE;
23875
23929
  u.search = "";
23876
23930
  u.searchParams.set("deployment", deploymentId);
23877
23931
  return u.pathname + u.search;
@@ -23984,7 +24038,7 @@ function DeploymentList({ siteId }) {
23984
24038
  try {
23985
24039
  await api.cancelDeployment(config.siteId, focusId);
23986
24040
  setFocusId(null);
23987
- window.history.replaceState(null, "", "/admin");
24041
+ window.history.replaceState(null, "", SETTO_BASE);
23988
24042
  } catch (err) {
23989
24043
  setCancelError(err instanceof Error ? err.message : "Kunne ikke avbryte");
23990
24044
  } finally {
@@ -24213,6 +24267,7 @@ function depDotStyle(status) {
24213
24267
  export {
24214
24268
  AuthGate,
24215
24269
  GuestEditProvider,
24270
+ SETTO_BASE,
24216
24271
  SettoAdminApp,
24217
24272
  SettoBlock,
24218
24273
  SettoIcon,