@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 +4 -0
- package/dist/SettoRepeater.d.ts +17 -0
- package/dist/SettoSection.d.ts +8 -0
- package/dist/edit-mode/mount.d.ts +6 -1
- package/dist/lib/api.d.ts +2 -0
- package/dist/setto-client.js +71 -37
- package/dist/setto-client.js.map +1 -1
- package/package.json +1 -1
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).
|
package/dist/SettoRepeater.d.ts
CHANGED
|
@@ -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;
|
package/dist/SettoSection.d.ts
CHANGED
|
@@ -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>;
|
package/dist/setto-client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect, useSyncExternalStore,
|
|
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) =>
|
|
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(
|
|
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
|
-
|
|
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(
|
|
23711
|
-
|
|
23712
|
-
|
|
23713
|
-
|
|
23714
|
-
|
|
23715
|
-
|
|
23716
|
-
|
|
23717
|
-
|
|
23718
|
-
|
|
23719
|
-
|
|
23720
|
-
e
|
|
23721
|
-
|
|
23722
|
-
|
|
23723
|
-
|
|
23724
|
-
|
|
23725
|
-
|
|
23726
|
-
|
|
23727
|
-
|
|
23728
|
-
|
|
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" ? "
|
|
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
|
-
|
|
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");
|