@sevenfold/setto-client 0.6.1 → 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 {};
@@ -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) {
@@ -21175,21 +21175,38 @@ const EMPTY = {
21175
21175
  };
21176
21176
  function useSiteAccess(supabase, session, siteId) {
21177
21177
  const [state, setState] = useState(EMPTY);
21178
+ const cachedUserIdRef = useRef(null);
21178
21179
  useEffect(() => {
21179
21180
  if (!session) {
21181
+ cachedUserIdRef.current = null;
21180
21182
  setState({ sites: null, currentSite: null, loading: false, error: null });
21181
21183
  return;
21182
21184
  }
21185
+ const userId = session.user.id;
21186
+ const sameUser = cachedUserIdRef.current === userId;
21183
21187
  let cancelled = false;
21184
- 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
+ });
21185
21200
  supabase.from("sites").select("*").then(({ data, error }) => {
21186
21201
  if (cancelled) return;
21187
21202
  if (error) {
21203
+ cachedUserIdRef.current = userId;
21188
21204
  setState({ sites: [], currentSite: null, loading: false, error: error.message });
21189
21205
  return;
21190
21206
  }
21191
21207
  const rows = data ?? [];
21192
21208
  const current = rows.find((s) => s.id === siteId) ?? null;
21209
+ cachedUserIdRef.current = userId;
21193
21210
  setState({ sites: rows, currentSite: current, loading: false, error: null });
21194
21211
  });
21195
21212
  return () => {
@@ -21217,6 +21234,7 @@ const STYLE_ID$1 = "setto-edit-layout";
21217
21234
  const HTML_CLASS = "setto-edit-mode";
21218
21235
  function useSettoDocumentLayout(active) {
21219
21236
  useEffect(() => {
21237
+ if (!active) return;
21220
21238
  const html = document.documentElement;
21221
21239
  html.classList.add(HTML_CLASS);
21222
21240
  html.style.setProperty("--setto-fab-size", `${FAB_SIZE}px`);
@@ -22765,12 +22783,18 @@ function UndoIcon() {
22765
22783
  /* @__PURE__ */ jsx("path", { d: "M21 17a9 9 0 0 0-15-6.7L3 13" })
22766
22784
  ] });
22767
22785
  }
22768
- function EditModeShell({ children, themeStore }) {
22769
- useSettoDocumentLayout(true);
22786
+ function EditModeShell({ children, themeStore, active }) {
22787
+ useSettoDocumentLayout(active);
22770
22788
  const [toolbarRoot, setToolbarRoot] = useState(null);
22771
22789
  const [hintRoot, setHintRoot] = useState(null);
22772
22790
  const [sectionToolbarRoot, setSectionToolbarRoot] = useState(null);
22773
22791
  useEffect(() => {
22792
+ if (!active) {
22793
+ setToolbarRoot(null);
22794
+ setHintRoot(null);
22795
+ setSectionToolbarRoot(null);
22796
+ return;
22797
+ }
22774
22798
  const toolbar = document.createElement("div");
22775
22799
  toolbar.id = "setto-toolbar-root";
22776
22800
  document.body.appendChild(toolbar);
@@ -22790,12 +22814,12 @@ function EditModeShell({ children, themeStore }) {
22790
22814
  hint.remove();
22791
22815
  sectionBar.remove();
22792
22816
  };
22793
- }, []);
22817
+ }, [active]);
22794
22818
  return /* @__PURE__ */ jsxs(Fragment, { children: [
22795
22819
  children,
22796
- toolbarRoot ? createPortal(/* @__PURE__ */ jsx(EditToolbar, {}), toolbarRoot) : null,
22797
- hintRoot ? createPortal(/* @__PURE__ */ jsx(EditLinkHint, {}), hintRoot) : null,
22798
- 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
22799
22823
  ] });
22800
22824
  }
22801
22825
  const SettoContext = createContext(null);
@@ -22855,7 +22879,8 @@ function SettoProvider({ config, children }) {
22855
22879
  themeStore: storeReady ? themeStoreRef.current : null,
22856
22880
  assetStore: storeReady ? assetStoreRef.current : null
22857
22881
  };
22858
- 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 }) }) });
22859
22884
  }
22860
22885
  function useSetto() {
22861
22886
  const ctx = useContext(SettoContext);
@@ -23207,6 +23232,7 @@ const SettoSection = forwardRef(
23207
23232
  "section",
23208
23233
  {
23209
23234
  ref,
23235
+ id: sectionId,
23210
23236
  "data-setto-section": sectionId,
23211
23237
  className,
23212
23238
  style: mergedStyle,
@@ -23716,25 +23742,27 @@ function SettoRepeater({
23716
23742
  };
23717
23743
  const canRemove = keys.length > 1;
23718
23744
  return /* @__PURE__ */ jsxs("div", { className, "data-setto-repeater": true, children: [
23719
- keys.map((itemKey, index) => /* @__PURE__ */ jsxs("div", { className: "setto-repeater-item", style: { position: "relative" }, children: [
23720
- editable && canRemove ? /* @__PURE__ */ jsx(
23721
- "button",
23722
- {
23723
- type: "button",
23724
- "data-setto-ui": true,
23725
- className: "setto-repeater-remove",
23726
- "aria-label": `Fjern ${itemLabel}`,
23727
- title: `Fjern ${itemLabel}`,
23728
- onClick: (e) => {
23729
- e.stopPropagation();
23730
- handleRemove(itemKey);
23731
- },
23732
- style: removeBtnStyle,
23733
- children: "×"
23734
- }
23735
- ) : null,
23736
- children(itemKey, index)
23737
- ] }, 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
+ ),
23738
23766
  editable ? /* @__PURE__ */ jsx(
23739
23767
  "div",
23740
23768
  {