@sevenfold/setto-client 0.6.0 → 0.6.2

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
@@ -387,3 +387,7 @@ Produces `dist/setto-client.js` and `.d.ts` via Vite library mode. Only needed b
387
387
  - Section colour toolbar only offers `brandColors` — no custom hex/rgba input.
388
388
  - CTA card colours and nested component colours are not section-themeable yet.
389
389
  - Single editor per site (no concurrent-edit conflict handling).
390
+
391
+ ## Knowledge base
392
+
393
+ `.wiki/` is this repo's LLM-maintained knowledge bundle (Karpathy LLM-wiki pattern, OKF core): one page per component, edit-mode feature, block-tree module, store and workflow, each with code citations, contracts, failure modes and doc-drift notes. Start at [`.wiki/index.md`](.wiki/index.md) (catalog) or [`.wiki/overview.md`](.wiki/overview.md) (the one page to read first). Keep it in sync with code changes as described in [`.wiki/AGENTS.md`](.wiki/AGENTS.md).
@@ -12,5 +12,22 @@ export interface SettoRepeaterProps {
12
12
  /**
13
13
  * Renders a dynamic list from i18n. In edit mode, editors can add items (button
14
14
  * at the end of the list) and remove items (round × revealed on hover).
15
+ *
16
+ * The published site gets no per-item wrapper — only the editor does.
17
+ *
18
+ * The wrapper exists to give the remove button something to be absolutely
19
+ * positioned against, which is edit chrome and has no business in the output.
20
+ * Leaving it in cost real layout: an item styled `border-b py-7 first:pt-0` is
21
+ * the *only* child of its own wrapper, so `:first-child` matched every item and
22
+ * `pt-0` won on all of them — every row in a repeated list sat flush on the
23
+ * rule above it instead of clearing it by 28px. The same trap catches `last:`,
24
+ * `odd:`, `even:` and `:not(:last-child)`; `last:border-b-0` would have erased
25
+ * every divider on the page.
26
+ *
27
+ * Worse, it was invisible while authoring: in edit mode the remove button is
28
+ * rendered before the children, so the item is not `:first-child` there and the
29
+ * padding appears. The editor showed the correct layout and the published site
30
+ * shipped the broken one. Dropping the wrapper from the published output is
31
+ * what brings the two back into agreement.
15
32
  */
16
33
  export declare function SettoRepeater({ itemsKey, defaultItem, itemLabel, children, className, }: SettoRepeaterProps): import("react/jsx-runtime").JSX.Element;
@@ -7,5 +7,13 @@ export interface SettoSectionProps extends ComponentPropsWithoutRef<'section'> {
7
7
  * Wraps a page section with theme-driven background colour and edit-mode
8
8
  * selection. Click the section chrome (not editable text or nested blocks)
9
9
  * to open the colour toolbar.
10
+ *
11
+ * The section also carries `id={sectionId}`, which is what makes a header's
12
+ * `href="#produkter"` land anywhere. Every generated site so far wrote that nav
13
+ * and shipped it dead: the only handle on the element was
14
+ * `data-setto-section`, an attribute no fragment identifier can address, and a
15
+ * menu that silently does nothing looks exactly like a menu that works until
16
+ * somebody taps it. An explicit `id` prop still wins — it is spread after this
17
+ * one — so a page that needs a different anchor can say so.
10
18
  */
11
19
  export declare const SettoSection: import("react").ForwardRefExoticComponent<SettoSectionProps & import("react").RefAttributes<HTMLElement>>;
@@ -3,12 +3,17 @@ import type { ThemeStore } from '../lib/theme-store';
3
3
  interface EditModeShellProps {
4
4
  children: ReactNode;
5
5
  themeStore: ThemeStore | null;
6
+ /** When false, children render unchanged — no portals or document layout. */
7
+ active: boolean;
6
8
  }
7
9
  /**
8
10
  * Edit mode: full-width page with a floating Setto FAB anchored bottom-right.
9
11
  * Text is edited inline via contentEditable on `<T>` — no sidebar.
12
+ *
13
+ * Always mounted around the host app so toggling edit mode does not remount
14
+ * page content (which would replay GSAP / scroll-driven animations).
10
15
  */
11
- export declare function EditModeShell({ children, themeStore }: EditModeShellProps): import("react/jsx-runtime").JSX.Element;
16
+ export declare function EditModeShell({ children, themeStore, active }: EditModeShellProps): import("react/jsx-runtime").JSX.Element;
12
17
  /** @deprecated Use EditModeShell */
13
18
  export declare const EditModeMount: typeof EditModeShell;
14
19
  export {};
package/dist/lib/api.d.ts CHANGED
@@ -16,5 +16,7 @@ export declare function createApi(args: {
16
16
  expired: number;
17
17
  }>;
18
18
  cancelDeployment(siteId: string, deploymentId: string): Promise<void>;
19
+ /** Request a password-reset email (link goes via setto-server, then site /setto). */
20
+ requestPasswordReset(siteId: string, email: string): Promise<void>;
19
21
  };
20
22
  export type SettoApi = ReturnType<typeof createApi>;
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
- import { useState, useEffect, useSyncExternalStore, useRef, useLayoutEffect, createContext, useContext, useCallback, useMemo, useId, forwardRef } from "react";
2
+ import { useState, useRef, useEffect, useSyncExternalStore, useLayoutEffect, createContext, useContext, useCallback, useMemo, useId, forwardRef, Fragment as Fragment$1 } from "react";
3
3
  import { useTranslation } from "react-i18next";
4
4
  import { createPortal } from "react-dom";
5
5
  function __rest(s, e) {
@@ -20805,6 +20805,18 @@ function createApi(args) {
20805
20805
  }
20806
20806
  );
20807
20807
  if (!res.ok) throw await asError(res, "cancelDeployment failed");
20808
+ },
20809
+ /** Request a password-reset email (link goes via setto-server, then site /setto). */
20810
+ async requestPasswordReset(siteId, email) {
20811
+ const res = await fetch(
20812
+ `${apiUrl}/sites/${encodeURIComponent(siteId)}/auth/password-reset`,
20813
+ {
20814
+ method: "POST",
20815
+ headers: { "Content-Type": "application/json" },
20816
+ body: JSON.stringify({ email })
20817
+ }
20818
+ );
20819
+ if (!res.ok) throw await asError(res, "requestPasswordReset failed");
20808
20820
  }
20809
20821
  };
20810
20822
  }
@@ -21163,21 +21175,38 @@ const EMPTY = {
21163
21175
  };
21164
21176
  function useSiteAccess(supabase, session, siteId) {
21165
21177
  const [state, setState] = useState(EMPTY);
21178
+ const cachedUserIdRef = useRef(null);
21166
21179
  useEffect(() => {
21167
21180
  if (!session) {
21181
+ cachedUserIdRef.current = null;
21168
21182
  setState({ sites: null, currentSite: null, loading: false, error: null });
21169
21183
  return;
21170
21184
  }
21185
+ const userId = session.user.id;
21186
+ const sameUser = cachedUserIdRef.current === userId;
21171
21187
  let cancelled = false;
21172
- setState((prev) => ({ ...prev, loading: true, error: null }));
21188
+ setState((prev) => {
21189
+ if (sameUser && prev.sites !== null && !prev.loading) {
21190
+ return prev;
21191
+ }
21192
+ return {
21193
+ ...prev,
21194
+ loading: true,
21195
+ error: null,
21196
+ sites: sameUser ? prev.sites : null,
21197
+ currentSite: sameUser ? prev.currentSite : null
21198
+ };
21199
+ });
21173
21200
  supabase.from("sites").select("*").then(({ data, error }) => {
21174
21201
  if (cancelled) return;
21175
21202
  if (error) {
21203
+ cachedUserIdRef.current = userId;
21176
21204
  setState({ sites: [], currentSite: null, loading: false, error: error.message });
21177
21205
  return;
21178
21206
  }
21179
21207
  const rows = data ?? [];
21180
21208
  const current = rows.find((s) => s.id === siteId) ?? null;
21209
+ cachedUserIdRef.current = userId;
21181
21210
  setState({ sites: rows, currentSite: current, loading: false, error: null });
21182
21211
  });
21183
21212
  return () => {
@@ -21199,15 +21228,13 @@ function isAdminRoute() {
21199
21228
  function isAdminDeploymentView() {
21200
21229
  return isAdminRoute() && new URLSearchParams(window.location.search).has("deployment");
21201
21230
  }
21202
- function adminRedirectUrl() {
21203
- return `${window.location.origin}${SETTO_BASE}`;
21204
- }
21205
21231
  const FAB_SIZE = 52;
21206
21232
  const FAB_INSET = 16;
21207
21233
  const STYLE_ID$1 = "setto-edit-layout";
21208
21234
  const HTML_CLASS = "setto-edit-mode";
21209
21235
  function useSettoDocumentLayout(active) {
21210
21236
  useEffect(() => {
21237
+ if (!active) return;
21211
21238
  const html = document.documentElement;
21212
21239
  html.classList.add(HTML_CLASS);
21213
21240
  html.style.setProperty("--setto-fab-size", `${FAB_SIZE}px`);
@@ -22756,12 +22783,18 @@ function UndoIcon() {
22756
22783
  /* @__PURE__ */ jsx("path", { d: "M21 17a9 9 0 0 0-15-6.7L3 13" })
22757
22784
  ] });
22758
22785
  }
22759
- function EditModeShell({ children, themeStore }) {
22760
- useSettoDocumentLayout(true);
22786
+ function EditModeShell({ children, themeStore, active }) {
22787
+ useSettoDocumentLayout(active);
22761
22788
  const [toolbarRoot, setToolbarRoot] = useState(null);
22762
22789
  const [hintRoot, setHintRoot] = useState(null);
22763
22790
  const [sectionToolbarRoot, setSectionToolbarRoot] = useState(null);
22764
22791
  useEffect(() => {
22792
+ if (!active) {
22793
+ setToolbarRoot(null);
22794
+ setHintRoot(null);
22795
+ setSectionToolbarRoot(null);
22796
+ return;
22797
+ }
22765
22798
  const toolbar = document.createElement("div");
22766
22799
  toolbar.id = "setto-toolbar-root";
22767
22800
  document.body.appendChild(toolbar);
@@ -22781,12 +22814,12 @@ function EditModeShell({ children, themeStore }) {
22781
22814
  hint.remove();
22782
22815
  sectionBar.remove();
22783
22816
  };
22784
- }, []);
22817
+ }, [active]);
22785
22818
  return /* @__PURE__ */ jsxs(Fragment, { children: [
22786
22819
  children,
22787
- toolbarRoot ? createPortal(/* @__PURE__ */ jsx(EditToolbar, {}), toolbarRoot) : null,
22788
- hintRoot ? createPortal(/* @__PURE__ */ jsx(EditLinkHint, {}), hintRoot) : null,
22789
- sectionToolbarRoot && themeStore ? createPortal(/* @__PURE__ */ jsx(SectionToolbar, {}), sectionToolbarRoot) : null
22820
+ active && toolbarRoot ? createPortal(/* @__PURE__ */ jsx(EditToolbar, {}), toolbarRoot) : null,
22821
+ active && hintRoot ? createPortal(/* @__PURE__ */ jsx(EditLinkHint, {}), hintRoot) : null,
22822
+ active && sectionToolbarRoot && themeStore ? createPortal(/* @__PURE__ */ jsx(SectionToolbar, {}), sectionToolbarRoot) : null
22790
22823
  ] });
22791
22824
  }
22792
22825
  const SettoContext = createContext(null);
@@ -22846,7 +22879,8 @@ function SettoProvider({ config, children }) {
22846
22879
  themeStore: storeReady ? themeStoreRef.current : null,
22847
22880
  assetStore: storeReady ? assetStoreRef.current : null
22848
22881
  };
22849
- return /* @__PURE__ */ jsx(SettoContext.Provider, { value, children: /* @__PURE__ */ jsx(SectionEditProvider, { children: editMode && value.store ? /* @__PURE__ */ jsx(EditModeShell, { themeStore: value.themeStore, children }) : children }) });
22882
+ const editShellActive = editMode && !!value.store;
22883
+ return /* @__PURE__ */ jsx(SettoContext.Provider, { value, children: /* @__PURE__ */ jsx(SectionEditProvider, { children: /* @__PURE__ */ jsx(EditModeShell, { active: editShellActive, themeStore: value.themeStore, children }) }) });
22850
22884
  }
22851
22885
  function useSetto() {
22852
22886
  const ctx = useContext(SettoContext);
@@ -23198,6 +23232,7 @@ const SettoSection = forwardRef(
23198
23232
  "section",
23199
23233
  {
23200
23234
  ref,
23235
+ id: sectionId,
23201
23236
  "data-setto-section": sectionId,
23202
23237
  className,
23203
23238
  style: mergedStyle,
@@ -23707,25 +23742,27 @@ function SettoRepeater({
23707
23742
  };
23708
23743
  const canRemove = keys.length > 1;
23709
23744
  return /* @__PURE__ */ jsxs("div", { className, "data-setto-repeater": true, children: [
23710
- keys.map((itemKey, index) => /* @__PURE__ */ jsxs("div", { className: "setto-repeater-item", style: { position: "relative" }, children: [
23711
- editable && canRemove ? /* @__PURE__ */ jsx(
23712
- "button",
23713
- {
23714
- type: "button",
23715
- "data-setto-ui": true,
23716
- className: "setto-repeater-remove",
23717
- "aria-label": `Fjern ${itemLabel}`,
23718
- title: `Fjern ${itemLabel}`,
23719
- onClick: (e) => {
23720
- e.stopPropagation();
23721
- handleRemove(itemKey);
23722
- },
23723
- style: removeBtnStyle,
23724
- children: "×"
23725
- }
23726
- ) : null,
23727
- children(itemKey, index)
23728
- ] }, itemKey)),
23745
+ keys.map(
23746
+ (itemKey, index) => editable ? /* @__PURE__ */ jsxs("div", { className: "setto-repeater-item", style: { position: "relative" }, children: [
23747
+ canRemove ? /* @__PURE__ */ jsx(
23748
+ "button",
23749
+ {
23750
+ type: "button",
23751
+ "data-setto-ui": true,
23752
+ className: "setto-repeater-remove",
23753
+ "aria-label": `Fjern ${itemLabel}`,
23754
+ title: `Fjern ${itemLabel}`,
23755
+ onClick: (e) => {
23756
+ e.stopPropagation();
23757
+ handleRemove(itemKey);
23758
+ },
23759
+ style: removeBtnStyle,
23760
+ children: "×"
23761
+ }
23762
+ ) : null,
23763
+ children(itemKey, index)
23764
+ ] }, itemKey) : /* @__PURE__ */ jsx(Fragment$1, { children: children(itemKey, index) }, itemKey)
23765
+ ),
23729
23766
  editable ? /* @__PURE__ */ jsx(
23730
23767
  "div",
23731
23768
  {
@@ -24034,10 +24071,10 @@ function consumeInviteErrorFromUrl() {
24034
24071
  params.delete("setto_invite_error");
24035
24072
  const qs = params.toString();
24036
24073
  window.history.replaceState(null, "", `${window.location.pathname}${qs ? `?${qs}` : ""}`);
24037
- return code === "invalid" ? "Invitasjonslenken er ugyldig eller utløpt. Be administratoren om en ny." : "Kunne ikke aktivere invitasjonen.";
24074
+ return code === "invalid" ? "Lenken er ugyldig eller utløpt. Be om en ny nullstilling eller invitasjon." : "Kunne ikke aktivere lenken.";
24038
24075
  }
24039
24076
  function AuthGate({ children }) {
24040
- const { supabase, session, authLoading } = useSetto();
24077
+ const { supabase, api, config, session, authLoading } = useSetto();
24041
24078
  const [mode, setMode] = useState("signin");
24042
24079
  const [email, setEmail] = useState("");
24043
24080
  const [password, setPassword] = useState("");
@@ -24088,10 +24125,7 @@ function AuthGate({ children }) {
24088
24125
  setError(null);
24089
24126
  setInfo(null);
24090
24127
  try {
24091
- const { error: resetError } = await supabase.auth.resetPasswordForEmail(email, {
24092
- redirectTo: adminRedirectUrl()
24093
- });
24094
- if (resetError) throw resetError;
24128
+ await api.requestPasswordReset(config.siteId, email);
24095
24129
  setInfo("Vi har sendt en lenke for å nullstille passordet. Sjekk innboksen.");
24096
24130
  } catch (err) {
24097
24131
  setError(err instanceof Error ? err.message : "Ukjent feil");