@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.
@@ -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-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-DW3qWKam.js").then((e) => e.n).then((e) => ({ default: e.PopupBody }))), fe = (e) => {
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-BdNHqoWi.js";
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-Cl_sKk9y.js";
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 LevoRichText };
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(), [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;
@@ -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-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?: "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
@@ -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.