@levo-so/site-runtime 0.3.11 → 0.3.12
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/{PopupBody-BI-MJqmd-DW3qWKam.js → PopupBody-BI-MJqmd-Dy94OeBQ.js} +1 -1
- package/{PopupRenderer-D66Gx63H-BnOmWsyj.js → PopupRenderer-D66Gx63H-DkprAQH7.js} +1 -1
- package/blog-content.d.ts +31 -0
- package/{rich-text.js → blog-content.js} +3 -3
- package/{dist-BdNHqoWi.js → dist-1r8apbDv.js} +56 -51
- package/{dist-Cl_sKk9y.js → dist-B7c9xjWu.js} +1 -1
- package/index.d.ts +167 -10
- package/index.js +628 -531
- package/package.json +6 -6
- package/popups.js +1 -1
- package/rich-text.d.ts +0 -25
|
@@ -60,7 +60,7 @@ var Q = [
|
|
|
60
60
|
...r,
|
|
61
61
|
children: a
|
|
62
62
|
}) : null;
|
|
63
|
-
}, te = /* @__PURE__ */ O({ PopupBody: () => oe }), ne = B(() => import("./dist-
|
|
63
|
+
}, te = /* @__PURE__ */ O({ PopupBody: () => oe }), ne = B(() => import("./dist-1r8apbDv.js").then((e) => e.t).then((e) => ({ default: e.CollectionFormBuilder }))), re = E({ baseSpacing: ".625rem" }), ie = { style: a({ form: { gap: "1rem" } }) }, ae = {
|
|
64
64
|
left: D,
|
|
65
65
|
center: M,
|
|
66
66
|
right: A
|
|
@@ -103,7 +103,7 @@ var se = (e) => (e ?? "16:9").replace(":", " / "), ce = (e) => {
|
|
|
103
103
|
...a,
|
|
104
104
|
children: r
|
|
105
105
|
}), n[15] = r, n[16] = a, n[17] = p, n[18] = m) : m = n[18], m;
|
|
106
|
-
}, de = I(() => import("./PopupBody-BI-MJqmd-
|
|
106
|
+
}, de = I(() => import("./PopupBody-BI-MJqmd-Dy94OeBQ.js").then((e) => e.n).then((e) => ({ default: e.PopupBody }))), fe = (e) => {
|
|
107
107
|
let t = A(7), { size: n } = e, r = n ?? "medium", i;
|
|
108
108
|
t[0] === r ? i = t[1] : (i = k({ size: r }), t[0] = r, t[1] = i);
|
|
109
109
|
let a, o, s;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare interface ILevoBlogContentProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "content"> {
|
|
2
|
+
/**
|
|
3
|
+
* A blog post's `content`, or a collection field the dashboard edits with the rich-text
|
|
4
|
+
* editor — exactly as the API returns it. Every shape the platform has ever stored is
|
|
5
|
+
* accepted; see {@link LevoBlogContent}.
|
|
6
|
+
*/
|
|
7
|
+
content: unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Render a stored Levo editor DOCUMENT — a blog post's body, or a collection field the
|
|
12
|
+
* dashboard edits with the rich-text editor.
|
|
13
|
+
*
|
|
14
|
+
* Narrow on purpose: this is for the `{ json: { type: "doc", … } }` value those two fields
|
|
15
|
+
* hold, NOT for text in general. Plain strings — excerpts, descriptions, headings, anything
|
|
16
|
+
* a `<p>` can hold — render as ordinary JSX; reaching for this component there pulls the
|
|
17
|
+
* whole editor node set onto the page for nothing. Where it IS the right call there is no
|
|
18
|
+
* alternative: `String(post.content)` prints `[object Object]` and `dangerouslySetInnerHTML`
|
|
19
|
+
* has nothing to insert.
|
|
20
|
+
*
|
|
21
|
+
* Renders the same node set the Levo editor writes and Levo-hosted pages show — headings,
|
|
22
|
+
* lists, code blocks, images, bookmarks, buttons, embedded collections — server-side, so the
|
|
23
|
+
* article is in the SSR HTML where crawlers read it.
|
|
24
|
+
*
|
|
25
|
+
* Typography comes from the shared prose theme in `@levo-so/site-runtime/setup.css` (already
|
|
26
|
+
* loaded by the site shell). Style the wrapper through `className` as usual; content the field
|
|
27
|
+
* doesn't contain renders nothing, so an empty body is an empty region, never a crash.
|
|
28
|
+
*/
|
|
29
|
+
export declare const LevoBlogContent: React.FC<ILevoBlogContentProps>;
|
|
30
|
+
|
|
31
|
+
export { }
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { r as e } from "./dist-
|
|
2
|
+
import { r as e } from "./dist-1r8apbDv.js";
|
|
3
3
|
import { t } from "./LevoProvider-Bsjkcwc8.js";
|
|
4
4
|
import { t as n } from "./inlineBaseTheme-BB9e6h5f.js";
|
|
5
|
-
import { t as r } from "./dist-
|
|
5
|
+
import { t as r } from "./dist-B7c9xjWu.js";
|
|
6
6
|
import { _ as i, a, c as o, d as s, f as c, g as l, h as u, i as d, l as f, m as p, n as m, o as h, p as g, r as _, s as v, t as y, u as b, v as x } from "./react-Cj_U6VZQ.js";
|
|
7
7
|
import { i as S, n as C, r as w, t as T } from "./content.cssm-XdMsTGIB.js";
|
|
8
8
|
import { t as E } from "./RteThemeProvider-BQmfSXSl.js";
|
|
@@ -1641,4 +1641,4 @@ var Yt = "_10kj0ro0", Xt = "_10kj0ro1", Zt = "_10kj0ro2", Qt = (e) => e ? typeof
|
|
|
1641
1641
|
}) : null;
|
|
1642
1642
|
};
|
|
1643
1643
|
//#endregion
|
|
1644
|
-
export { _n as
|
|
1644
|
+
export { _n as LevoBlogContent };
|
|
@@ -2321,14 +2321,14 @@ var zr = ((e) => (t) => t ? Rr(e, t) : e)({
|
|
|
2321
2321
|
}, ai = (e) => {
|
|
2322
2322
|
localStorage.removeItem(`${e}-${ni.FORM_VALUES}`);
|
|
2323
2323
|
}, oi = ({ schema: e, originalSchema: t, onSubmitSuccess: n, onSubmitError: r, onInit: i, setFormErrorAfterSubmit: a = !0 }) => {
|
|
2324
|
-
let o = oe(() => e, [e]), s = oe(() => t || o, [t, o]), c = g(), { toastConfig: l, onSuccess: u, onError: f, isLoggedIn: p, isDisabled: m, externalInitialValues: h = {}, account: _, preview: v } = ei(), [
|
|
2324
|
+
let o = oe(() => e, [e]), s = oe(() => t || o, [t, o]), c = g(), { toastConfig: l, onSuccess: u, onError: f, isLoggedIn: p, isDisabled: m, externalInitialValues: h = {}, account: _, preview: v, uiConfig: y } = ei(), [b, x] = Q({
|
|
2325
2325
|
contentId: null,
|
|
2326
2326
|
publishedId: null
|
|
2327
|
-
}), [
|
|
2327
|
+
}), [S, C] = Q(!1), [w, T] = Q(!1), [E, D] = Q(!1), O = se({}), k = se({}), A = (e) => {
|
|
2328
2328
|
if (!o?.sections) return null;
|
|
2329
2329
|
for (let t of o.sections) for (let n of t?.fields ?? []) if (n?.key === e) return n?.kind ?? null;
|
|
2330
2330
|
return null;
|
|
2331
|
-
},
|
|
2331
|
+
}, M = () => {
|
|
2332
2332
|
let e = {};
|
|
2333
2333
|
try {
|
|
2334
2334
|
if (typeof window < "u") {
|
|
@@ -2336,7 +2336,7 @@ var zr = ((e) => (t) => t ? Rr(e, t) : e)({
|
|
|
2336
2336
|
for (let [n, r] of t.entries()) {
|
|
2337
2337
|
let [t, i] = n.split("-");
|
|
2338
2338
|
if (t === "lf" && i && r) try {
|
|
2339
|
-
let t = JSON.parse(r), n =
|
|
2339
|
+
let t = JSON.parse(r), n = A(i);
|
|
2340
2340
|
e[i] = typeof t == "number" && !(n === "number" || n === "currency") ? r : t;
|
|
2341
2341
|
} catch {
|
|
2342
2342
|
e[i] = r;
|
|
@@ -2351,112 +2351,117 @@ var zr = ((e) => (t) => t ? Rr(e, t) : e)({
|
|
|
2351
2351
|
...n,
|
|
2352
2352
|
...e
|
|
2353
2353
|
};
|
|
2354
|
-
|
|
2354
|
+
O.current = {
|
|
2355
2355
|
...t,
|
|
2356
2356
|
...r,
|
|
2357
|
-
...
|
|
2357
|
+
...O.current
|
|
2358
2358
|
};
|
|
2359
|
-
},
|
|
2359
|
+
}, N = V({
|
|
2360
2360
|
mode: "onTouched",
|
|
2361
2361
|
disabled: m
|
|
2362
|
-
}), { formState:
|
|
2363
|
-
|
|
2364
|
-
...
|
|
2362
|
+
}), { formState: F } = N, I = (e) => {
|
|
2363
|
+
O.current = {
|
|
2364
|
+
...O.current || {},
|
|
2365
2365
|
...e
|
|
2366
2366
|
};
|
|
2367
2367
|
};
|
|
2368
2368
|
return ae(() => {
|
|
2369
2369
|
if (o && c?.collection) {
|
|
2370
|
-
if (
|
|
2370
|
+
if (M(), !b.contentId && !b.publishedId && !v) {
|
|
2371
2371
|
let e = o?.allow_multiple === !1;
|
|
2372
2372
|
if (o?.allow_draft) {
|
|
2373
2373
|
if (p) {
|
|
2374
|
-
|
|
2374
|
+
C(!0), c?.collection?.getDraftEntry(o?.key).then((t) => {
|
|
2375
2375
|
let n = t?.content?.data;
|
|
2376
2376
|
if (n === null) {
|
|
2377
|
-
|
|
2377
|
+
T(!0);
|
|
2378
2378
|
return;
|
|
2379
2379
|
}
|
|
2380
|
-
|
|
2380
|
+
T(!1);
|
|
2381
2381
|
let r = e && Array.isArray(n) && n?.find((e) => e?._status === "published") || null;
|
|
2382
|
-
if (r?._id)
|
|
2383
|
-
...
|
|
2382
|
+
if (r?._id) O.current = {
|
|
2383
|
+
...O.current || {},
|
|
2384
2384
|
...r
|
|
2385
|
-
},
|
|
2385
|
+
}, x({
|
|
2386
2386
|
contentId: null,
|
|
2387
2387
|
publishedId: r._id
|
|
2388
2388
|
});
|
|
2389
2389
|
else {
|
|
2390
2390
|
let e = Array.isArray(n) ? n?.filter((e) => e?._status === "draft") : [], t = e?.[e.length - 1] || null;
|
|
2391
|
-
t?._id && (
|
|
2392
|
-
...
|
|
2391
|
+
t?._id && (O.current = {
|
|
2392
|
+
...O.current || {},
|
|
2393
2393
|
...t
|
|
2394
|
-
},
|
|
2394
|
+
}, x({
|
|
2395
2395
|
contentId: t._id,
|
|
2396
2396
|
publishedId: null
|
|
2397
2397
|
}));
|
|
2398
2398
|
}
|
|
2399
2399
|
}).catch((e) => (c?.logger?.error("Failed to get draft entry", { exception: e }), null)).finally(() => {
|
|
2400
|
-
|
|
2400
|
+
k.current = { ...O.current }, N.reset({ ...O?.current }), i?.(O.current), C(!1);
|
|
2401
2401
|
});
|
|
2402
2402
|
return;
|
|
2403
2403
|
}
|
|
2404
2404
|
let t = ri(o?.key);
|
|
2405
|
-
t && (
|
|
2406
|
-
...
|
|
2405
|
+
t && (O.current = {
|
|
2406
|
+
...O.current || {},
|
|
2407
2407
|
...t
|
|
2408
2408
|
});
|
|
2409
2409
|
}
|
|
2410
2410
|
}
|
|
2411
|
-
|
|
2411
|
+
k.current = { ...O.current }, N.reset({ ...O?.current }), i?.(O.current);
|
|
2412
2412
|
}
|
|
2413
2413
|
}, [o, p]), ae(() => {
|
|
2414
|
-
let { isSubmitSuccessful: e, isValid: t } =
|
|
2415
|
-
e && t && (
|
|
2416
|
-
...
|
|
2414
|
+
let { isSubmitSuccessful: e, isValid: t } = F;
|
|
2415
|
+
e && t && (k.current = { ...O.current }, N.reset({
|
|
2416
|
+
...O?.current,
|
|
2417
2417
|
__captcha: "verified"
|
|
2418
|
-
}), i?.(
|
|
2419
|
-
}, [
|
|
2420
|
-
o && (
|
|
2421
|
-
}, [
|
|
2422
|
-
methods:
|
|
2423
|
-
isLoading:
|
|
2424
|
-
isError:
|
|
2425
|
-
contentId:
|
|
2426
|
-
publishedId:
|
|
2427
|
-
allFormValues:
|
|
2418
|
+
}), i?.(O.current));
|
|
2419
|
+
}, [F]), ae(() => {
|
|
2420
|
+
o && (b.contentId || b.publishedId) && ai(o?.key);
|
|
2421
|
+
}, [b]), {
|
|
2422
|
+
methods: N,
|
|
2423
|
+
isLoading: S,
|
|
2424
|
+
isError: w,
|
|
2425
|
+
contentId: b.contentId,
|
|
2426
|
+
publishedId: b.publishedId,
|
|
2427
|
+
allFormValues: O.current,
|
|
2428
2428
|
handleContentId: (e) => {
|
|
2429
|
-
e && e !==
|
|
2429
|
+
e && e !== b.contentId && !b.publishedId && x({
|
|
2430
2430
|
contentId: e,
|
|
2431
2431
|
publishedId: null
|
|
2432
2432
|
});
|
|
2433
2433
|
},
|
|
2434
|
-
appendFormValues:
|
|
2434
|
+
appendFormValues: I,
|
|
2435
2435
|
submitToRemote: async (e) => {
|
|
2436
|
-
if (
|
|
2437
|
-
let e = et(
|
|
2438
|
-
if (t =
|
|
2436
|
+
if (I(e), !(v || !c?.collection || !s)) try {
|
|
2437
|
+
let e = et(O?.current, s, k.current), t = null;
|
|
2438
|
+
if (t = b.publishedId ? await c?.collection?.editDraftEntry(s?.key, b.publishedId, e) : b.contentId ? await c?.collection?.submitDraftEntry(s?.key, e) : await c?.collection?.saveCollectionData({
|
|
2439
2439
|
collection_id: s?.id,
|
|
2440
2440
|
data: e
|
|
2441
2441
|
}), t?.content?.data) {
|
|
2442
2442
|
let e = t?.content?.meta?.booking_config?.enabled;
|
|
2443
|
-
l?.showSuccessToast && !(l?.suppressOnBooking && e) && d(l.successToastText || "Form submitted successfully."),
|
|
2443
|
+
l?.showSuccessToast && !(l?.suppressOnBooking && e) && d(l.successToastText || "Form submitted successfully."), O.current = {}, M(), ai(s?.key), x({
|
|
2444
2444
|
contentId: null,
|
|
2445
2445
|
publishedId: null
|
|
2446
|
-
}),
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2446
|
+
}), y?.hideSuccessScreen || D(!0);
|
|
2447
|
+
try {
|
|
2448
|
+
u && u({
|
|
2449
|
+
schemaId: s?.id,
|
|
2450
|
+
response: t?.content?.data,
|
|
2451
|
+
meta: t?.content?.meta
|
|
2452
|
+
}), n && n();
|
|
2453
|
+
} catch (e) {
|
|
2454
|
+
c?.logger?.error("Form success callback threw after a saved entry", { exception: P(e) });
|
|
2455
|
+
}
|
|
2451
2456
|
return;
|
|
2452
2457
|
}
|
|
2453
2458
|
throw Error("Failed to save form details, API response invalid");
|
|
2454
2459
|
} catch (e) {
|
|
2455
|
-
Xt(e), e instanceof j ? (e?.hasFieldErrors && a && ti(e?.fieldErrors,
|
|
2460
|
+
Xt(e), e instanceof j ? (e?.hasFieldErrors && a && ti(e?.fieldErrors, N.setError), r && r(e.fieldErrors || []), f && f(e)) : f && f(e?.response?.data || e);
|
|
2456
2461
|
}
|
|
2457
2462
|
},
|
|
2458
|
-
showSuccessOverlay:
|
|
2459
|
-
dismissSuccessOverlay: () =>
|
|
2463
|
+
showSuccessOverlay: E,
|
|
2464
|
+
dismissSuccessOverlay: () => D(!1)
|
|
2460
2465
|
};
|
|
2461
2466
|
}, si = "_1jed92w2", ci = "_1jed92w3 _1jed92w1", li = "_1jed92w4 _1jed92w1", ui = "_1jed92w5 _1jed92w1", di = "_1jed92w6 _1jed92w1", fi = () => {
|
|
2462
2467
|
let e = W(1), t;
|
|
@@ -14,7 +14,7 @@ var v = /* @__PURE__ */ e({
|
|
|
14
14
|
}), y = async (e, t) => {
|
|
15
15
|
let n = {};
|
|
16
16
|
return t?.site_id && (n.site_id = t.site_id), t?.locale && (n.locale = t.locale), t?.country && (n.country = t.country), (await e.url("/v1/studio/popup/list-active").query(n).get().json())?.content?.data ?? [];
|
|
17
|
-
}, b = p(() => import("./PopupRenderer-D66Gx63H-
|
|
17
|
+
}, b = p(() => import("./PopupRenderer-D66Gx63H-DkprAQH7.js").then((e) => e.t).then((e) => ({ default: e.PopupRenderer }))), x = (e) => {
|
|
18
18
|
let n = u(9), { siteId: r, pageId: i } = e, { fetch: a, logger: s } = t(), [c, l] = g(null), p = h(!1), v, x;
|
|
19
19
|
if (n[0] !== a || n[1] !== s || n[2] !== r ? (v = () => {
|
|
20
20
|
if (p.current) return;
|
package/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { ILemaCollection } from '@levo-so/core';
|
|
|
2
2
|
import { ILevoClient } from '@levo-so/core';
|
|
3
3
|
import { ILevoInsights } from '@levo-so/insights';
|
|
4
4
|
import { ILevoMembership } from '@levo-so/core';
|
|
5
|
+
import { IMediaObject } from '@levo-so/core';
|
|
5
6
|
import { JSX } from 'react/jsx-runtime';
|
|
6
7
|
import { LevoQuery } from '@levo-so/core';
|
|
7
8
|
|
|
@@ -117,7 +118,7 @@ export declare const createSiteClient: (config?: ILevoSiteConfig) => ILevoClient
|
|
|
117
118
|
* anything the client sends, so collection lookups and profile conditions are evaluated
|
|
118
119
|
* where they can be trusted.
|
|
119
120
|
*/
|
|
120
|
-
export declare const decideAccess: ({ rules, invalid, pageId, hasClient, isReady, isLoggedIn, check, }: IAccessDecisionInput) => IAccessDecision;
|
|
121
|
+
export declare const decideAccess: ({ rules, invalid, pageId, hasClient, isReady, isLoggedIn, check, preview, }: IAccessDecisionInput) => IAccessDecision;
|
|
121
122
|
|
|
122
123
|
/**
|
|
123
124
|
* Main entry: extract visitor country + language preferences.
|
|
@@ -171,6 +172,8 @@ export declare interface IAccessDecisionInput {
|
|
|
171
172
|
isSuccess: boolean;
|
|
172
173
|
data: IAccessCheckResponse | null;
|
|
173
174
|
};
|
|
175
|
+
/** A stage forced from the Studio editor, or null. Client-only, never set on a real visit. */
|
|
176
|
+
preview?: ILevoAccessPreviewStage | null;
|
|
174
177
|
}
|
|
175
178
|
|
|
176
179
|
/**
|
|
@@ -254,6 +257,16 @@ export declare type IEditorToPreview = {
|
|
|
254
257
|
} | {
|
|
255
258
|
type: "set-text-edit";
|
|
256
259
|
enabled: boolean;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Force a page's access gate to a given outcome, or `null` to decide normally. The only
|
|
263
|
+
* message here that isn't about inspecting — it rides this channel because this channel IS
|
|
264
|
+
* "the editor is driving this preview", and a gated page is otherwise uneditable. Re-sent
|
|
265
|
+
* on every `ready`, like the two mode flags above.
|
|
266
|
+
*/
|
|
267
|
+
| {
|
|
268
|
+
type: "set-access-preview";
|
|
269
|
+
stage: ILevoAccessPreviewStage | null;
|
|
257
270
|
} | {
|
|
258
271
|
type: "clear-selection";
|
|
259
272
|
} | {
|
|
@@ -418,6 +431,21 @@ export declare interface ILevoAccessGatingOptions {
|
|
|
418
431
|
hidden?: string[];
|
|
419
432
|
}
|
|
420
433
|
|
|
434
|
+
/**
|
|
435
|
+
* A gate outcome forced from the Studio editor, so someone can work on a page they have no
|
|
436
|
+
* account for — and design the blocked screens without arranging a matching account.
|
|
437
|
+
*
|
|
438
|
+
* Editor-only: it arrives over the editor's postMessage channel and is never persisted, so
|
|
439
|
+
* a real visit has no way to be in one of these states. `granted` is the one that fails
|
|
440
|
+
* OPEN, which is the whole point of it — see `decideAccess`, and the note there on what
|
|
441
|
+
* that does and does not expose.
|
|
442
|
+
*
|
|
443
|
+
* A stage whose rule the page doesn't configure is IGNORED rather than forced: a page with
|
|
444
|
+
* no `gating` rule has no entry form to show, and inventing one would preview a screen the
|
|
445
|
+
* real site can never render.
|
|
446
|
+
*/
|
|
447
|
+
export declare type ILevoAccessPreviewStage = "granted" | "signed_out" | "entry_missing" | "entry_under_review" | "profile_incomplete";
|
|
448
|
+
|
|
421
449
|
/**
|
|
422
450
|
* `kind: "profile"` — account access: signed in AND the visitor's OWN membership account
|
|
423
451
|
* satisfies `condition`. There is no collection here (the membership account is resolved
|
|
@@ -627,19 +655,35 @@ export declare interface ILevoFormProps {
|
|
|
627
655
|
/** Field keys to hide from the rendered form (matched against field key/id, recursively into nested fields). */
|
|
628
656
|
hiddenFields?: string[];
|
|
629
657
|
/** Layout: all fields at once, one-at-a-time (Typeform), or stepped by section. */
|
|
630
|
-
formVariant?:
|
|
658
|
+
formVariant?: ILevoFormVariant;
|
|
631
659
|
/** Success-screen copy + the post-submit action (show a message, let them submit another, or redirect). */
|
|
632
|
-
successConfig?:
|
|
633
|
-
title?: string;
|
|
634
|
-
message?: string;
|
|
635
|
-
action?: {
|
|
636
|
-
type: "none" | "submit_another" | "redirect";
|
|
637
|
-
value?: string;
|
|
638
|
-
};
|
|
639
|
-
};
|
|
660
|
+
successConfig?: ILevoFormSuccessConfig;
|
|
640
661
|
/** Brand the form. Optional — defaults to the site theme; only override the few colors you want to diverge. */
|
|
641
662
|
theme?: ILevoFormTheme;
|
|
642
663
|
className?: string;
|
|
664
|
+
/**
|
|
665
|
+
* Values the form opens with, keyed by collection field key. Merged over the collection's
|
|
666
|
+
* own field defaults and under anything the URL supplies, so a `?plan=pro` link still wins.
|
|
667
|
+
* Keys the collection doesn't have are dropped at submit. Read ONCE when the schema
|
|
668
|
+
* resolves — a later change to this object does not re-seed a form the visitor is filling in.
|
|
669
|
+
*/
|
|
670
|
+
defaultValues?: Record<string, unknown>;
|
|
671
|
+
/** Render every field read-only (a confirmation view, or a form gated behind another step). */
|
|
672
|
+
disabled?: boolean;
|
|
673
|
+
/**
|
|
674
|
+
* Show the built-in thank-you screen after a submission. Default `true`. Turn it off ONLY
|
|
675
|
+
* alongside an `onSuccess` that does something visible (navigates, reveals your own
|
|
676
|
+
* confirmation) — otherwise a successful submit looks like it did nothing. The form submits
|
|
677
|
+
* and resets either way; it just stays on screen instead of being replaced.
|
|
678
|
+
*/
|
|
679
|
+
showSuccessScreen?: boolean;
|
|
680
|
+
/** Fires after the entry is saved — navigate, track a conversion, reveal your own confirmation. */
|
|
681
|
+
onSuccess?: (result: ILevoFormSubmitResult) => void;
|
|
682
|
+
/**
|
|
683
|
+
* Fires when a submission fails. Field-level validation errors are already shown on the
|
|
684
|
+
* fields themselves; use this for page-level messaging or tracking, not to re-render errors.
|
|
685
|
+
*/
|
|
686
|
+
onError?: (error: unknown) => void;
|
|
643
687
|
}
|
|
644
688
|
|
|
645
689
|
export declare interface ILevoFormResult {
|
|
@@ -653,6 +697,26 @@ export declare interface ILevoFormResult {
|
|
|
653
697
|
};
|
|
654
698
|
}
|
|
655
699
|
|
|
700
|
+
/** What a successful submission produced. */
|
|
701
|
+
export declare interface ILevoFormSubmitResult {
|
|
702
|
+
/** The collection the entry landed in. */
|
|
703
|
+
schemaId: string;
|
|
704
|
+
/** The created record, exactly as the API returned it. */
|
|
705
|
+
record: unknown;
|
|
706
|
+
/** Submission metadata (booking config, payment intent, …) when the collection emits any. */
|
|
707
|
+
meta?: unknown;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** Success-screen copy + what happens after a submission. */
|
|
711
|
+
export declare interface ILevoFormSuccessConfig {
|
|
712
|
+
title?: string;
|
|
713
|
+
message?: string;
|
|
714
|
+
action?: {
|
|
715
|
+
type: "none" | "submit_another" | "redirect";
|
|
716
|
+
value?: string;
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
656
720
|
/**
|
|
657
721
|
* The bridge between the collection form renderer's color contract and the generated site's
|
|
658
722
|
* semantic theme tokens. Shared by every form the runtime renders — the page's `<LevoForm>`,
|
|
@@ -687,6 +751,9 @@ export declare interface ILevoFormTheme {
|
|
|
687
751
|
gap?: string;
|
|
688
752
|
}
|
|
689
753
|
|
|
754
|
+
/** How the form lays its fields out. */
|
|
755
|
+
export declare type ILevoFormVariant = "default" | "single-field" | "steps" | "steps-2";
|
|
756
|
+
|
|
690
757
|
export declare interface ILevoLoginActions {
|
|
691
758
|
/**
|
|
692
759
|
* Send a verification code. `identifier` is the email address (medium `"email"`) or the
|
|
@@ -851,6 +918,45 @@ export declare interface ILevoRecordResult<T = Record<string, unknown>> {
|
|
|
851
918
|
};
|
|
852
919
|
}
|
|
853
920
|
|
|
921
|
+
/** What the platform's search index knows a result to be. */
|
|
922
|
+
export declare type ILevoSearchKind = "page" | "collection" | "blog" | "event" | "community";
|
|
923
|
+
|
|
924
|
+
/** One hit. `url` is ready to link to — the index stores the visitor-facing path. */
|
|
925
|
+
export declare interface ILevoSearchResult {
|
|
926
|
+
content_type: ILevoSearchKind;
|
|
927
|
+
content_id: string;
|
|
928
|
+
title: string | null;
|
|
929
|
+
/** A short snippet. May contain HTML from a rich-text body — render it as TEXT. */
|
|
930
|
+
description: string | null;
|
|
931
|
+
url: string;
|
|
932
|
+
thumbnail: IMediaObject | null;
|
|
933
|
+
/** Relevance, 0–1. Results arrive sorted, so this is only worth showing if you want to. */
|
|
934
|
+
score: number;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
export declare interface ILevoSearchState {
|
|
938
|
+
/** The live input value — bind straight to the field. */
|
|
939
|
+
query: string;
|
|
940
|
+
setQuery: (value: string) => void;
|
|
941
|
+
/** Hits for the settled query. Empty until one has been searched. */
|
|
942
|
+
results: ILevoSearchResult[];
|
|
943
|
+
/** A request is in flight. Previous results stay on screen while it is, so nothing flickers. */
|
|
944
|
+
isLoading: boolean;
|
|
945
|
+
/** Human-readable failure, or null. */
|
|
946
|
+
error: string | null;
|
|
947
|
+
/** The query was searched and came back with nothing — the "no results" case, specifically. */
|
|
948
|
+
isEmpty: boolean;
|
|
949
|
+
/** The query is long enough to search (2+ characters, the server's own floor). */
|
|
950
|
+
isSearchable: boolean;
|
|
951
|
+
/** Clear the query, the results, and the error. */
|
|
952
|
+
reset: () => void;
|
|
953
|
+
/** Dialog open state, and ⌘K if `hotkey` is on. Ignore it for an on-page search. */
|
|
954
|
+
isOpen: boolean;
|
|
955
|
+
setOpen: (open: boolean) => void;
|
|
956
|
+
open: () => void;
|
|
957
|
+
close: () => void;
|
|
958
|
+
}
|
|
959
|
+
|
|
854
960
|
/** Site-level Levo config — the scaffold shim fills this from Vite env. */
|
|
855
961
|
export declare interface ILevoSiteConfig {
|
|
856
962
|
/** Origin of the Levo public API / proxy the site's data calls go through. */
|
|
@@ -1530,6 +1636,18 @@ export declare interface IUseLevoOtpInputParams {
|
|
|
1530
1636
|
disabled?: boolean;
|
|
1531
1637
|
}
|
|
1532
1638
|
|
|
1639
|
+
export declare interface IUseLevoSearchOptions {
|
|
1640
|
+
/** Max results per request (server caps at 50). Default 20. */
|
|
1641
|
+
limit?: number;
|
|
1642
|
+
/** Milliseconds of typing quiet before a request goes out. Default 300. */
|
|
1643
|
+
debounceMs?: number;
|
|
1644
|
+
/**
|
|
1645
|
+
* Bind ⌘K / Ctrl+K to toggle `isOpen`. Default true — turn it off for a search that lives
|
|
1646
|
+
* ON the page (a /search route) rather than in a dialog.
|
|
1647
|
+
*/
|
|
1648
|
+
hotkey?: boolean;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1533
1651
|
/** Every method a page may list in `options.methods`. */
|
|
1534
1652
|
export declare const LEVO_AUTH_METHODS: ILevoAuthMethod[];
|
|
1535
1653
|
|
|
@@ -1609,6 +1727,11 @@ export declare const LevoEventRegister: ({ eventID, title, external, buttonText,
|
|
|
1609
1727
|
* (from its SSR-prefetched schema) and submits entries into that collection. The AI drops it
|
|
1610
1728
|
* in and never rebuilds the form engine.
|
|
1611
1729
|
*
|
|
1730
|
+
* `source` alone is a finished form: it collects, validates, submits, handles spam, and shows
|
|
1731
|
+
* a thank-you screen with nothing else wired up. The props below the divider exist for the
|
|
1732
|
+
* minority of forms that are a step in a flow rather than the end of one — a value prefilled
|
|
1733
|
+
* from the route, a redirect on submit, a confirmation the page renders itself.
|
|
1734
|
+
*
|
|
1612
1735
|
* Colors follow the site theme (`src/styles/theme.css`) by default, so a form matches the
|
|
1613
1736
|
* brand out of the box; `theme` overrides per form. `CollectionFormRenderer` resolves its
|
|
1614
1737
|
* client via `@levo-so/react`'s `useLevo()` — the same context `LevoSiteProvider` mounts at
|
|
@@ -1659,6 +1782,20 @@ export declare const LevoProfileForm: React.FC<ILevoProfileFormProps>;
|
|
|
1659
1782
|
*/
|
|
1660
1783
|
export declare const LevoSiteProvider: React.FC<{
|
|
1661
1784
|
site?: ISiteJson;
|
|
1785
|
+
/**
|
|
1786
|
+
* Send analytics. Pass `import.meta.env.PROD` from the site's own shim: a preview is the
|
|
1787
|
+
* dev server and a published site is a built bundle, so that flag is the only thing that
|
|
1788
|
+
* can tell them apart — this package is built ONCE as a library, long before either
|
|
1789
|
+
* exists, so it cannot read its own `import.meta.env` and get an answer about the site.
|
|
1790
|
+
*
|
|
1791
|
+
* False does NOT tear analytics out. Trackers and identity still run with zero network
|
|
1792
|
+
* (see below), because popup triggers and every other store-only consumer wait on
|
|
1793
|
+
* `$isIdentityContextReady`, and a preview with dead popups is a worse preview.
|
|
1794
|
+
*
|
|
1795
|
+
* Defaults to true: a site whose shim predates this prop keeps the behaviour it shipped
|
|
1796
|
+
* with, rather than silently losing analytics in production.
|
|
1797
|
+
*/
|
|
1798
|
+
analytics?: boolean;
|
|
1662
1799
|
children: React.ReactNode;
|
|
1663
1800
|
}>;
|
|
1664
1801
|
|
|
@@ -1952,6 +2089,26 @@ export declare const useLevoOtpInput: ({ value, onChange, length, onComplete, au
|
|
|
1952
2089
|
*/
|
|
1953
2090
|
export declare const useLevoRecord: <T = Record<string, unknown>>(config: ILevoRecordConfig, slug: string | undefined) => ILevoRecordResult<T>;
|
|
1954
2091
|
|
|
2092
|
+
/**
|
|
2093
|
+
* Site-wide search — everything except the look.
|
|
2094
|
+
*
|
|
2095
|
+
* ```tsx
|
|
2096
|
+
* const search = useLevoSearch();
|
|
2097
|
+
* <input value={search.query} onChange={(e) => search.setQuery(e.target.value)} />
|
|
2098
|
+
* {search.results.map((hit) => <a href={hit.url} key={hit.content_id}>{hit.title}</a>)}
|
|
2099
|
+
* ```
|
|
2100
|
+
*
|
|
2101
|
+
* What it searches is the platform's index of this SITE: blog posts, collection records,
|
|
2102
|
+
* events and forum posts in full, and pages by title and description. A v3 page's body copy
|
|
2103
|
+
* lives in its route file, which the indexer never reads — so a page is found by what its
|
|
2104
|
+
* meta says it is, not by a phrase buried in its markup.
|
|
2105
|
+
*
|
|
2106
|
+
* Client-only and self-throttling: nothing is requested during SSR, below two characters, or
|
|
2107
|
+
* on a site with no backend. Results are cached per query, so re-opening the dialog and
|
|
2108
|
+
* retyping the same thing costs nothing.
|
|
2109
|
+
*/
|
|
2110
|
+
export declare const useLevoSearch: (options?: IUseLevoSearchOptions) => ILevoSearchState;
|
|
2111
|
+
|
|
1955
2112
|
/**
|
|
1956
2113
|
* The site's Levo client, or `null` when the site isn't wired to a Levo backend yet
|
|
1957
2114
|
* (no `api_url` in `site.json` — e.g. not stamped at provision). Consumers no-op on null.
|