@levo-so/site-runtime 0.3.11 → 0.3.13

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.
@@ -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-BdNHqoWi.js").then((e) => e.t).then((e) => ({ default: e.CollectionFormBuilder }))), re = E({ baseSpacing: ".625rem" }), ie = { style: a({ form: { gap: "1rem" } }) }, ae = {
63
+ }, te = /* @__PURE__ */ O({ PopupBody: () => oe }), ne = B(() => import("./dist-CEdznbFW.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-DW3qWKam.js").then((e) => e.n).then((e) => ({ default: e.PopupBody }))), fe = (e) => {
106
+ }, de = I(() => import("./PopupBody-BI-MJqmd-DgTQEzda.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-BdNHqoWi.js";
2
+ import { r as e } from "./dist-CEdznbFW.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-Cl_sKk9y.js";
5
+ import { t as r } from "./dist-6AIgW_iB.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 LevoRichText };
1644
+ export { _n as LevoBlogContent };
@@ -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-BnOmWsyj.js").then((e) => e.t).then((e) => ({ default: e.PopupRenderer }))), x = (e) => {
17
+ }, b = p(() => import("./PopupRenderer-D66Gx63H-Cd9xJv33.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;
@@ -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(), [y, b] = Q({
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
- }), [x, S] = Q(!1), [C, w] = Q(!1), [T, E] = Q(!1), D = se({}), O = se({}), k = (e) => {
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
- }, A = () => {
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 = k(i);
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
- D.current = {
2354
+ O.current = {
2355
2355
  ...t,
2356
2356
  ...r,
2357
- ...D.current
2357
+ ...O.current
2358
2358
  };
2359
- }, M = V({
2359
+ }, N = V({
2360
2360
  mode: "onTouched",
2361
2361
  disabled: m
2362
- }), { formState: N } = M, P = (e) => {
2363
- D.current = {
2364
- ...D.current || {},
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 (A(), !y.contentId && !y.publishedId && !v) {
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
- S(!0), c?.collection?.getDraftEntry(o?.key).then((t) => {
2374
+ C(!0), c?.collection?.getDraftEntry(o?.key).then((t) => {
2375
2375
  let n = t?.content?.data;
2376
2376
  if (n === null) {
2377
- w(!0);
2377
+ T(!0);
2378
2378
  return;
2379
2379
  }
2380
- w(!1);
2380
+ T(!1);
2381
2381
  let r = e && Array.isArray(n) && n?.find((e) => e?._status === "published") || null;
2382
- if (r?._id) D.current = {
2383
- ...D.current || {},
2382
+ if (r?._id) O.current = {
2383
+ ...O.current || {},
2384
2384
  ...r
2385
- }, b({
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 && (D.current = {
2392
- ...D.current || {},
2391
+ t?._id && (O.current = {
2392
+ ...O.current || {},
2393
2393
  ...t
2394
- }, b({
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
- O.current = { ...D.current }, M.reset({ ...D?.current }), i?.(D.current), S(!1);
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 && (D.current = {
2406
- ...D.current || {},
2405
+ t && (O.current = {
2406
+ ...O.current || {},
2407
2407
  ...t
2408
2408
  });
2409
2409
  }
2410
2410
  }
2411
- O.current = { ...D.current }, M.reset({ ...D?.current }), i?.(D.current);
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 } = N;
2415
- e && t && (O.current = { ...D.current }, M.reset({
2416
- ...D?.current,
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?.(D.current));
2419
- }, [N]), ae(() => {
2420
- o && (y.contentId || y.publishedId) && ai(o?.key);
2421
- }, [y]), {
2422
- methods: M,
2423
- isLoading: x,
2424
- isError: C,
2425
- contentId: y.contentId,
2426
- publishedId: y.publishedId,
2427
- allFormValues: D.current,
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 !== y.contentId && !y.publishedId && b({
2429
+ e && e !== b.contentId && !b.publishedId && x({
2430
2430
  contentId: e,
2431
2431
  publishedId: null
2432
2432
  });
2433
2433
  },
2434
- appendFormValues: P,
2434
+ appendFormValues: I,
2435
2435
  submitToRemote: async (e) => {
2436
- if (P(e), !(v || !c?.collection || !s)) try {
2437
- let e = et(D?.current, s, O.current), t = null;
2438
- if (t = y.publishedId ? await c?.collection?.editDraftEntry(s?.key, y.publishedId, e) : y.contentId ? await c?.collection?.submitDraftEntry(s?.key, e) : await c?.collection?.saveCollectionData({
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."), D.current = {}, A(), ai(s?.key), b({
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
- }), E(!0), u && u({
2447
- schemaId: s?.id,
2448
- response: t?.content?.data,
2449
- meta: t?.content?.meta
2450
- }), n && n();
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, M.setError), r && r(e.fieldErrors || []), f && f(e)) : f && f(e?.response?.data || e);
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: T,
2459
- dismissSuccessOverlay: () => E(!1)
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;
@@ -3625,15 +3630,15 @@ var ka = (e) => {
3625
3630
  let t = W(17), n, r, i, a;
3626
3631
  t[0] === e ? (n = t[1], r = t[2], i = t[3], a = t[4]) : ({collection_id: n, hiddenFields: r, enableVerification: a, ...i} = e, t[0] = e, t[1] = n, t[2] = r, t[3] = i, t[4] = a);
3627
3632
  let o = a === void 0 ? !0 : a, { collection: s } = g(), [c, l] = Q(!0), [u, d] = Q(null), f;
3628
- t[5] !== s || t[6] !== n || t[7] !== r || t[8] !== i?.workspace_id ? (f = () => {
3629
- n ? s?.getCollectionByIDExpanded(n, { headers: { "Levo-Workspace": i?.workspace_id } }).then((e) => {
3633
+ t[5] !== s || t[6] !== n || t[7] !== r || t[8] !== i ? (f = () => {
3634
+ n ? s?.getCollectionByIDExpanded(n, { headers: { "Levo-Workspace": i.workspace_id } }).then((e) => {
3630
3635
  let t = e?.content?.data;
3631
3636
  if (t) t.sections = t?.sections?.map((e) => (e.fields = e?.fields?.map((e) => (r?.includes(e?.key) && (e.hidden = !0), e)), e)), d(t);
3632
3637
  else throw Error(`API response invalid: ${t}`);
3633
3638
  }).catch(ja).finally(() => {
3634
3639
  l(!1);
3635
3640
  }) : (console.error("[CollectionFormBuilder] Collection ID or Workspace ID is missing."), l(!1));
3636
- }, t[5] = s, t[6] = n, t[7] = r, t[8] = i?.workspace_id, t[9] = f) : f = t[9];
3641
+ }, t[5] = s, t[6] = n, t[7] = r, t[8] = i, t[9] = f) : f = t[9];
3637
3642
  let p;
3638
3643
  t[10] === n ? p = t[11] : (p = [n], t[10] = n, t[11] = p), ae(f, p);
3639
3644
  let m;
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?: "default" | "single-field" | "steps" | "steps-2";
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
@@ -835,6 +902,15 @@ export declare interface ILevoRecordConfig {
835
902
  module: ILevoSourceModule;
836
903
  /** What the module's `identifier` names — see {@link ILevoSourceModule}. Round-tripped; the event single endpoint is flat (slug only). */
837
904
  identifier: string;
905
+ /**
906
+ * Route param this binding's slug comes from — `"slug"` on `/blog/$slug`, `"events"` on
907
+ * `/legal-events/$events`. Every generated record binding carries it: it is the mapping a
908
+ * multi-param route needs, since with two variable segments nothing else in the file says
909
+ * which binding fills which slot. Round-tripped through the editor binding like the rest.
910
+ *
911
+ * Optional only for the pages written before it existed — see {@link recordSlug}.
912
+ */
913
+ param?: string;
838
914
  /**
839
915
  * Reserved for field projection. The slug endpoints return the whole record today, so
840
916
  * this is round-tripped through the binding but not yet sent to the API.
@@ -851,6 +927,45 @@ export declare interface ILevoRecordResult<T = Record<string, unknown>> {
851
927
  };
852
928
  }
853
929
 
930
+ /** What the platform's search index knows a result to be. */
931
+ export declare type ILevoSearchKind = "page" | "collection" | "blog" | "event" | "community";
932
+
933
+ /** One hit. `url` is ready to link to — the index stores the visitor-facing path. */
934
+ export declare interface ILevoSearchResult {
935
+ content_type: ILevoSearchKind;
936
+ content_id: string;
937
+ title: string | null;
938
+ /** A short snippet. May contain HTML from a rich-text body — render it as TEXT. */
939
+ description: string | null;
940
+ url: string;
941
+ thumbnail: IMediaObject | null;
942
+ /** Relevance, 0–1. Results arrive sorted, so this is only worth showing if you want to. */
943
+ score: number;
944
+ }
945
+
946
+ export declare interface ILevoSearchState {
947
+ /** The live input value — bind straight to the field. */
948
+ query: string;
949
+ setQuery: (value: string) => void;
950
+ /** Hits for the settled query. Empty until one has been searched. */
951
+ results: ILevoSearchResult[];
952
+ /** A request is in flight. Previous results stay on screen while it is, so nothing flickers. */
953
+ isLoading: boolean;
954
+ /** Human-readable failure, or null. */
955
+ error: string | null;
956
+ /** The query was searched and came back with nothing — the "no results" case, specifically. */
957
+ isEmpty: boolean;
958
+ /** The query is long enough to search (2+ characters, the server's own floor). */
959
+ isSearchable: boolean;
960
+ /** Clear the query, the results, and the error. */
961
+ reset: () => void;
962
+ /** Dialog open state, and ⌘K if `hotkey` is on. Ignore it for an on-page search. */
963
+ isOpen: boolean;
964
+ setOpen: (open: boolean) => void;
965
+ open: () => void;
966
+ close: () => void;
967
+ }
968
+
854
969
  /** Site-level Levo config — the scaffold shim fills this from Vite env. */
855
970
  export declare interface ILevoSiteConfig {
856
971
  /** Origin of the Levo public API / proxy the site's data calls go through. */
@@ -1530,6 +1645,18 @@ export declare interface IUseLevoOtpInputParams {
1530
1645
  disabled?: boolean;
1531
1646
  }
1532
1647
 
1648
+ export declare interface IUseLevoSearchOptions {
1649
+ /** Max results per request (server caps at 50). Default 20. */
1650
+ limit?: number;
1651
+ /** Milliseconds of typing quiet before a request goes out. Default 300. */
1652
+ debounceMs?: number;
1653
+ /**
1654
+ * Bind ⌘K / Ctrl+K to toggle `isOpen`. Default true — turn it off for a search that lives
1655
+ * ON the page (a /search route) rather than in a dialog.
1656
+ */
1657
+ hotkey?: boolean;
1658
+ }
1659
+
1533
1660
  /** Every method a page may list in `options.methods`. */
1534
1661
  export declare const LEVO_AUTH_METHODS: ILevoAuthMethod[];
1535
1662
 
@@ -1609,6 +1736,11 @@ export declare const LevoEventRegister: ({ eventID, title, external, buttonText,
1609
1736
  * (from its SSR-prefetched schema) and submits entries into that collection. The AI drops it
1610
1737
  * in and never rebuilds the form engine.
1611
1738
  *
1739
+ * `source` alone is a finished form: it collects, validates, submits, handles spam, and shows
1740
+ * a thank-you screen with nothing else wired up. The props below the divider exist for the
1741
+ * minority of forms that are a step in a flow rather than the end of one — a value prefilled
1742
+ * from the route, a redirect on submit, a confirmation the page renders itself.
1743
+ *
1612
1744
  * Colors follow the site theme (`src/styles/theme.css`) by default, so a form matches the
1613
1745
  * brand out of the box; `theme` overrides per form. `CollectionFormRenderer` resolves its
1614
1746
  * client via `@levo-so/react`'s `useLevo()` — the same context `LevoSiteProvider` mounts at
@@ -1659,6 +1791,20 @@ export declare const LevoProfileForm: React.FC<ILevoProfileFormProps>;
1659
1791
  */
1660
1792
  export declare const LevoSiteProvider: React.FC<{
1661
1793
  site?: ISiteJson;
1794
+ /**
1795
+ * Send analytics. Pass `import.meta.env.PROD` from the site's own shim: a preview is the
1796
+ * dev server and a published site is a built bundle, so that flag is the only thing that
1797
+ * can tell them apart — this package is built ONCE as a library, long before either
1798
+ * exists, so it cannot read its own `import.meta.env` and get an answer about the site.
1799
+ *
1800
+ * False does NOT tear analytics out. Trackers and identity still run with zero network
1801
+ * (see below), because popup triggers and every other store-only consumer wait on
1802
+ * `$isIdentityContextReady`, and a preview with dead popups is a worse preview.
1803
+ *
1804
+ * Defaults to true: a site whose shim predates this prop keeps the behaviour it shipped
1805
+ * with, rather than silently losing analytics in production.
1806
+ */
1807
+ analytics?: boolean;
1662
1808
  children: React.ReactNode;
1663
1809
  }>;
1664
1810
 
@@ -1794,7 +1940,7 @@ export declare const prefetchLevoSources: ({ queryClient, config, sources, param
1794
1940
  config: ILevoSiteConfig | undefined;
1795
1941
  /** The page's `dataSources` export (undefined = page has none). */
1796
1942
  sources: ILevoDataSources | undefined;
1797
- /** Route params; record bindings read `params.slug`. */
1943
+ /** Route params; each record binding reads the one its `param` names, else the sole segment. */
1798
1944
  params?: Record<string, string>;
1799
1945
  }) => Promise<Record<string, unknown>>;
1800
1946
 
@@ -1952,6 +2098,26 @@ export declare const useLevoOtpInput: ({ value, onChange, length, onComplete, au
1952
2098
  */
1953
2099
  export declare const useLevoRecord: <T = Record<string, unknown>>(config: ILevoRecordConfig, slug: string | undefined) => ILevoRecordResult<T>;
1954
2100
 
2101
+ /**
2102
+ * Site-wide search — everything except the look.
2103
+ *
2104
+ * ```tsx
2105
+ * const search = useLevoSearch();
2106
+ * <input value={search.query} onChange={(e) => search.setQuery(e.target.value)} />
2107
+ * {search.results.map((hit) => <a href={hit.url} key={hit.content_id}>{hit.title}</a>)}
2108
+ * ```
2109
+ *
2110
+ * What it searches is the platform's index of this SITE: blog posts, collection records,
2111
+ * events and forum posts in full, and pages by title and description. A v3 page's body copy
2112
+ * lives in its route file, which the indexer never reads — so a page is found by what its
2113
+ * meta says it is, not by a phrase buried in its markup.
2114
+ *
2115
+ * Client-only and self-throttling: nothing is requested during SSR, below two characters, or
2116
+ * on a site with no backend. Results are cached per query, so re-opening the dialog and
2117
+ * retyping the same thing costs nothing.
2118
+ */
2119
+ export declare const useLevoSearch: (options?: IUseLevoSearchOptions) => ILevoSearchState;
2120
+
1955
2121
  /**
1956
2122
  * The site's Levo client, or `null` when the site isn't wired to a Levo backend yet
1957
2123
  * (no `api_url` in `site.json` — e.g. not stamped at provision). Consumers no-op on null.