@levo-so/site-runtime 0.3.7 → 0.3.9

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.
Files changed (45) hide show
  1. package/CollectionField-C-6a1ud4.js +270 -0
  2. package/{CollectionInput-B1qV4QEH.js → CollectionInput-BJ__qcbk.js} +40 -40
  3. package/DateTimeWidget-LfaosVVW.js +255 -0
  4. package/{Dialog-DVB4WpBS.js → Dialog-dT7y641z.js} +43 -43
  5. package/{KBChatWidget-Cnq0DCOQ.js → KBChatWidget-CuAtGhHT.js} +240 -239
  6. package/{LevoProvider-nmIbMlsk.js → LevoProvider-Bsjkcwc8.js} +8 -8
  7. package/PhoneWidget-D-CcDuie.js +99 -0
  8. package/{Popover-C7zZHBIc.js → Popover-BTdlOQP_.js} +20 -20
  9. package/{PopupBody-DZVwDLkg-yrot8Uqb.js → PopupBody-BI-MJqmd-C0qNyN7V.js} +299 -182
  10. package/PopupRenderer-D66Gx63H-C6RnOnl2.js +388 -0
  11. package/{RichTextWidget-BOrVIR2j.js → RichTextWidget-BFWVevY7.js} +140 -140
  12. package/ThemeProvider-DutrjSTF.js +116 -0
  13. package/{Tooltip-BIgrpetu.js → Tooltip-CQB0LyxK.js} +20 -20
  14. package/{VerifyButton-CA1GtEZJ.js → VerifyButton-BUT70lgz.js} +65 -65
  15. package/{dist-Cb6sb7nO.js → dist-Rjs__H25.js} +14 -14
  16. package/dist-orLT2eNz.js +3663 -0
  17. package/index.d.ts +1071 -3
  18. package/index.js +1365 -733
  19. package/inlineBaseTheme-BB9e6h5f.js +9 -0
  20. package/{inlineCollectionTheme-DYYMrmcE.js → inlineCollectionTheme-CWcMCUti.js} +1 -1
  21. package/{kbChat-DIb4AS7E.js → kbChat-L55effMi.js} +28 -28
  22. package/{nodeAlignment-D70kOcui.js → nodeAlignment-COvc-rIj.js} +1 -1
  23. package/package.json +37 -27
  24. package/popups.js +1 -1
  25. package/{react-6kO0cqgr.js → react-B9yvlbH0.js} +1 -1
  26. package/routes-CyKmkoRM.js +316 -0
  27. package/start.d.ts +38 -0
  28. package/start.js +19 -0
  29. package/styles.css +1 -1
  30. package/{generateRadiusScale-54mE9ikx.js → themeVars.cssm-ByhhC2Jm.js} +17 -17
  31. package/usePopupRedirect-CAAdcpaW-D9XoiVhG.js +82 -0
  32. package/CollectionField-C59pmeQw.js +0 -270
  33. package/DateTimeWidget-Cctsvcrs.js +0 -255
  34. package/PhoneWidget-J3VTMWJH.js +0 -99
  35. package/PopupRenderer-BjW1jg70-Biwj2bIc.js +0 -387
  36. package/ThemeProvider-BQmIB5tr.js +0 -116
  37. package/dist-C8gLeklT.js +0 -3559
  38. package/inlineBaseTheme-B6E9zzyp.js +0 -9
  39. package/usePopupRedirect-BThP_YPW-DoixMAB9.js +0 -80
  40. /package/{createThemeExtension-DhGk81nV.js → createThemeExtension-4AUn51iJ.js} +0 -0
  41. /package/{es-CztjLuNy.js → es-Bj3CsMJz.js} +0 -0
  42. /package/{pillChrome-BAu5AfCT.js → pillChrome-uieiq_SK.js} +0 -0
  43. /package/{popupState-JmpeKzgM-qhNTV79U.js → popupState-JmpeKzgM-D_77LiAa.js} +0 -0
  44. /package/{rteTheme.cssm-xg0gJ28J.js → rteTheme.cssm-CaRzrwtu.js} +0 -0
  45. /package/{theme.cssm-mkQwezuj.js → theme.cssm-DUzmx76l.js} +0 -0
package/index.d.ts CHANGED
@@ -1,9 +1,19 @@
1
1
  import { ILemaCollection } from '@levo-so/core';
2
2
  import { ILevoClient } from '@levo-so/core';
3
3
  import { ILevoInsights } from '@levo-so/insights';
4
+ import { ILevoMembership } from '@levo-so/core';
4
5
  import { JSX } from 'react/jsx-runtime';
5
6
  import { LevoQuery } from '@levo-so/core';
6
7
 
8
+ /**
9
+ * Build the canonical + hreflang link set for one page.
10
+ *
11
+ * The canonical is ALWAYS self-referential — a page canonicals to its own URL, never to a
12
+ * different variant. That single rule is what stops the variants fragmenting into
13
+ * duplicate content, which is the problem the whole design exists to avoid.
14
+ */
15
+ export declare const buildHreflangLinks: (options: IBuildHreflangOptions) => IHreflangLink[];
16
+
7
17
  /** Color-mode preference. `system` follows the OS `prefers-color-scheme`. */
8
18
  export declare type ColorMode = "dark" | "light" | "system";
9
19
 
@@ -39,7 +49,7 @@ export declare const ColorModeProvider: React.FC<IColorModeProviderProps>;
39
49
  export declare const createLevoHead: (site: ISiteJson) => (meta?: IPlatformMetaFile, dynamic?: ILevoDynamicHead) => (ctx?: IHeadCtx) => IComposedHead;
40
50
 
41
51
  /**
42
- * Build the one-line route loader (`loader: levoLoader(dataSources)`): prefetch the
52
+ * Build the one-line route loader (`loader: levoLoader(dataSources, meta)`): prefetch the
43
53
  * page's declared data sources on the server so SSR ships fully-rendered HTML, and
44
54
  * return them keyed by source key — that map is the route's `loaderData`, which
45
55
  * `levoHead`'s dynamic config reads for per-record heads. An empty `dataSources` ({})
@@ -48,14 +58,43 @@ export declare const createLevoHead: (site: ISiteJson) => (meta?: IPlatformMetaF
48
58
  *
49
59
  * Takes the raw `site.json` — same single input as `createLevoHead` and
50
60
  * `LevoSiteProvider` — and derives the client config itself.
61
+ *
62
+ * GATED PAGES PREFETCH NOTHING. Pass the page's meta file (the same one `levoHead` gets)
63
+ * and a page with enabled `access` rules skips prefetch entirely. The prefetch runs on the
64
+ * SERVER, where the visitor's session cookie isn't available — so it would fetch as an
65
+ * anonymous caller and stream the result into the SSR payload of a page that has not been
66
+ * cleared to show it. Per-collection visibility means a truly private collection returns
67
+ * nothing to that caller, but a members-only page over public content would still have
68
+ * shipped its records to anyone who viewed source. The page's hooks fetch client-side once
69
+ * the gate opens, which is the only point at which we know who is asking.
70
+ *
71
+ * `meta` is optional so sites generated before this shipped keep working unchanged — they
72
+ * simply prefetch as they always did (and have no gates to protect).
51
73
  */
52
- export declare const createLevoLoader: (site: ISiteJson) => (sources: ILevoDataSources) => ({ context, params, }: {
74
+ export declare const createLevoLoader: (site: ISiteJson) => (sources: ILevoDataSources, meta?: IPlatformMetaFile) => ({ context, params, }: {
53
75
  context: {
54
76
  queryClient: IQueryClientLike;
55
77
  };
56
78
  params?: Record<string, string>;
57
79
  }) => Promise<Record<string, unknown>>;
58
80
 
81
+ /**
82
+ * Build a route-existence matcher from a generated `routeTree`.
83
+ *
84
+ * LAZY on purpose — the walk runs on the first lookup, not here — and the reason is import
85
+ * order, not router construction. `__root.tsx` is imported BY `routeTree.gen.ts`, so anything
86
+ * it evaluates at module scope runs while the tree is still half-built: walk it there and the
87
+ * list comes back short or empty. Deferring to the first lookup (inside a request middleware
88
+ * or a `beforeLoad`, both of which run long after every module has evaluated) sidesteps that
89
+ * entirely.
90
+ *
91
+ * Two bugs of this exact shape have already shipped, and both presented identically: the server
92
+ * render was correct, the client fell back to the default language, and the visitor watched the
93
+ * page flip a second after load. Before making this eager, confirm where the starter calls it
94
+ * from — at module scope in `__root.tsx`, eager is wrong.
95
+ */
96
+ export declare const createRouteTreeMatcher: (routeTree: unknown) => ((pathname: string) => boolean);
97
+
59
98
  /**
60
99
  * Build the site's core Levo client from config, or `null` when the site isn't wired to a
61
100
  * backend yet (no `apiUrl`). SSR-safe — `createLevoClient` guards its window/document reads.
@@ -65,6 +104,95 @@ export declare const createLevoLoader: (site: ISiteJson) => (sources: ILevoDataS
65
104
  */
66
105
  export declare const createSiteClient: (config?: ILevoSiteConfig) => ILevoClient | null;
67
106
 
107
+ /**
108
+ * Three layers, cheapest first:
109
+ *
110
+ * 1. NO GATES — granted synchronously, on the server too, with zero network. The path
111
+ * ~every page takes, so an ungated site pays nothing for the gate existing.
112
+ * 2. NOT SIGNED IN — decided locally from the session. Every gate kind implies a login
113
+ * (authentication by definition, profile explicitly, gating because entries are matched
114
+ * by their `_account`), so a signed-out visitor gets the sign-in card immediately rather
115
+ * than after a round-trip that could only have said the same thing.
116
+ * 3. SIGNED IN — the server's verdict. It reads the rules from the page ROW, never from
117
+ * anything the client sends, so collection lookups and profile conditions are evaluated
118
+ * where they can be trusted.
119
+ */
120
+ export declare const decideAccess: ({ rules, invalid, pageId, hasClient, isReady, isLoggedIn, check, }: IAccessDecisionInput) => IAccessDecision;
121
+
122
+ /**
123
+ * Main entry: extract visitor country + language preferences.
124
+ * Accepts any header carrier — normalization is internal.
125
+ */
126
+ export declare const extractGeoAndLanguageData: (headers: Record<string, string | string[]> | Headers | null | undefined) => {
127
+ bestCountry: ICountryInfo | null;
128
+ languageData: IParsedLanguageData;
129
+ };
130
+
131
+ /**
132
+ * The gate's policy, as a pure function.
133
+ *
134
+ * Split from `useLevoAccess` on purpose: this is the most safety-critical logic in the
135
+ * feature — it decides whether a page renders — and as a hook it could only ever be
136
+ * exercised through React. Here it is a table of inputs to a verdict, readable top to bottom
137
+ * and testable without a renderer. The hook's remaining job is gathering the inputs.
138
+ *
139
+ * FAIL CLOSED. Every path that cannot produce a definite "yes" resolves to `blocked`. A
140
+ * gated page never falls open because something upstream was having a bad minute.
141
+ */
142
+ /** What the check-access endpoint returns. */
143
+ export declare interface IAccessCheckResponse {
144
+ has_access: boolean;
145
+ code?: ILevoAccessServerCode;
146
+ message?: string;
147
+ }
148
+
149
+ export declare interface IAccessDecision {
150
+ state: ILevoAccessState;
151
+ code: ILevoAccessCode | null;
152
+ message: string | null;
153
+ rule: ILevoAccessRule | null;
154
+ }
155
+
156
+ export declare interface IAccessDecisionInput {
157
+ /** Enabled, parseable rules on this page. Empty = ungated. */
158
+ rules: ILevoAccessRule[];
159
+ /** The page declared rules, but none of them parsed. */
160
+ invalid: boolean;
161
+ /** The platform page id from the meta file; `""` when unstamped. */
162
+ pageId: string;
163
+ /** The site is wired to a Levo backend. */
164
+ hasClient: boolean;
165
+ /** Hydrated AND the session question has been answered. False = undecided. */
166
+ isReady: boolean;
167
+ isLoggedIn: boolean;
168
+ /** The server check, passed through from the query. */
169
+ check: {
170
+ isError: boolean;
171
+ isSuccess: boolean;
172
+ data: IAccessCheckResponse | null;
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Everything needed to annotate ONE page. Describes that page across all languages, so the path
178
+ * is the language-neutral one and the locale list is the site's, not the page's.
179
+ */
180
+ export declare interface IBuildHreflangOptions {
181
+ /** Neutral path, no locale prefix — `/pricing`. */
182
+ neutralPath: string;
183
+ /** Site origin, e.g. `https://acme.com`. hreflang requires absolute URLs. */
184
+ origin: string;
185
+ locales: string[];
186
+ /**
187
+ * The locale prefix currently in the URL, or null on a neutral URL. Decides which URL is
188
+ * canonical: a prefixed visitor self-canonicals to their prefixed URL, a neutral visitor
189
+ * self-canonicals to the neutral one.
190
+ */
191
+ activePrefix?: string | null;
192
+ /** Route-existence check — a locale with no route file for this page gets no alternate. */
193
+ hasRoute?: (pathname: string) => boolean;
194
+ }
195
+
68
196
  export declare interface IColorModeProviderProps {
69
197
  children: React.ReactNode;
70
198
  /** Fallback when nothing is stored yet. */
@@ -92,6 +220,33 @@ export declare interface IComposedHead {
92
220
  scripts: IHeadEntry[];
93
221
  }
94
222
 
223
+ /**
224
+ * GeoIP header extractor.
225
+ *
226
+ * Pulls visitor country + language from HTTP headers across CDN providers
227
+ * (Cloudflare, Vercel, AWS CloudFront, Azure, BunnyCDN), nginx-ingress, and
228
+ * Traefik GeoIP2. Only surfaces `bestCountry` + `languageData` — the helpers
229
+ * that were historically exported (client IPs, provider-specific getters,
230
+ * raw header maps) are no longer used by any consumer.
231
+ *
232
+ * DELIBERATE DUPLICATE of `external/next-middleware/src/geoIp.ts`. The two must not be
233
+ * merged: next-middleware is a Next.js package serving V2 slate apps, and having a V2 app
234
+ * depend on the V3 site runtime (or vice versa) would couple two release trains that ship
235
+ * independently. The file has no framework imports, so copying is cheap — but a header
236
+ * added to ONE copy is a silent gap in the other. Change both.
237
+ *
238
+ * On v3 the live providers are nginx-ingress (`x-country-code`) and, on paper, Traefik
239
+ * (`x-geoip2-country`); our ingress sets both. See docs/studio-v3/site-localization.md.
240
+ */
241
+ export declare interface ICountryInfo {
242
+ countryCode: string;
243
+ city?: string;
244
+ region?: string;
245
+ latitude?: string;
246
+ longitude?: string;
247
+ source: string;
248
+ }
249
+
95
250
  /** Editor → preview (received by `onMessage`; dashboard's `IOutboundMessage`). */
96
251
  export declare type IEditorToPreview = {
97
252
  type: "set-inspect";
@@ -178,6 +333,48 @@ declare interface IHeadEntry {
178
333
 
179
334
  export declare type IHistoryFn = History["pushState"];
180
335
 
336
+ /**
337
+ * hreflang + canonical link generation for a localized site.
338
+ *
339
+ * These are the `<head>` annotations that tell Google which URL serves which language.
340
+ * They need NO database: the site knows its locale list (`site.json`) and which variants
341
+ * exist (the route tree), so correct annotations ship from day one — ahead of the sibling-row
342
+ * DB sync that the SITEMAP hreflang depends on.
343
+ *
344
+ * Known gap until that sync lands: these tags declare the relationships, but nothing points
345
+ * a crawler AT the prefixed URLs, so variants won't be indexed yet. See
346
+ * docs/studio-v3/site-localization.md.
347
+ */
348
+ /** One `<link>` to render into `<head>` — either this page's canonical or one language alternate. */
349
+ export declare interface IHreflangLink {
350
+ rel: "alternate" | "canonical";
351
+ href: string;
352
+ hrefLang?: string;
353
+ }
354
+
355
+ export declare interface ILanguageInfo {
356
+ locale: string;
357
+ language: string;
358
+ region?: string;
359
+ quality: number;
360
+ }
361
+
362
+ /** One selectable language, with the props a switcher needs to render it correctly. */
363
+ export declare interface ILanguageOption {
364
+ /** The locale tag, e.g. `fr-FR`. Label it however you like. */
365
+ locale: string;
366
+ isActive: boolean;
367
+ /**
368
+ * Spread onto an `<a>`. Contains the canonical `href`, the click interception, and
369
+ * `aria-current` — see `useLanguageOptions` for why each matters.
370
+ */
371
+ linkProps: {
372
+ href: string;
373
+ "aria-current": "true" | undefined;
374
+ onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
375
+ };
376
+ }
377
+
181
378
  export declare interface ILayersNode {
182
379
  tag: string;
183
380
  source?: string;
@@ -191,6 +388,168 @@ export declare interface ILayersNode {
191
388
  children: ILayersNode[];
192
389
  }
193
390
 
391
+ /**
392
+ * `kind: "authentication"` — the visitor must be signed in, via one of `methods`.
393
+ * `primary_method` picks which passwordless form the login card leads with; the OAuth
394
+ * entries in `methods` become the buttons above it.
395
+ */
396
+ export declare interface ILevoAccessAuthenticationOptions {
397
+ methods?: ILevoAuthMethod[];
398
+ primary_method?: ILevoPrimaryAuthMethod;
399
+ }
400
+
401
+ export declare type ILevoAccessCode = ILevoAccessServerCode | ILevoAccessRuntimeCode;
402
+
403
+ /** The `access` array in a page's `meta/<route>.json`. Every enabled rule must pass. */
404
+ export declare type ILevoAccessConfig = ILevoAccessRule[];
405
+
406
+ /**
407
+ * `kind: "gating"` — the visitor must own an entry in `collection` that satisfies
408
+ * `condition`. Ownership is by the entry's `_account`, so this is always login-implying.
409
+ * No entry → the site shows its entry form; entry that fails the condition → the
410
+ * "under review" message.
411
+ */
412
+ export declare interface ILevoAccessGatingOptions {
413
+ /** Collection key the visitor's entry must live in. */
414
+ collection: string;
415
+ /** LevoQuery `where` the entry must satisfy. Absent/empty = any entry passes. */
416
+ condition?: LevoQuery.Where;
417
+ /** Field keys hidden in the entry form the site renders when no entry exists. */
418
+ hidden?: string[];
419
+ }
420
+
421
+ /**
422
+ * `kind: "profile"` — account access: signed in AND the visitor's OWN membership account
423
+ * satisfies `condition`. There is no collection here (the membership account is resolved
424
+ * server-side); failing accounts get the profile-completion form.
425
+ */
426
+ export declare interface ILevoAccessProfileOptions {
427
+ /** "Profile is complete when" — a LevoQuery `where` over the account. Empty = always passes. */
428
+ condition?: LevoQuery.Where;
429
+ /** Extra account field keys to hide in the profile-completion form. */
430
+ hidden?: string[];
431
+ }
432
+
433
+ /** What `useLevoAccess` hands a page. */
434
+ export declare interface ILevoAccessResult {
435
+ state: ILevoAccessState;
436
+ /** Null only while `checking` (and on a page with no gates). */
437
+ code: ILevoAccessCode | null;
438
+ /** The server's human-readable reason, when it produced one. Cards may use it as a fallback. */
439
+ message: string | null;
440
+ /** The rule that blocked — the card reads its `options` (collection, hidden fields, …). */
441
+ rule: ILevoAccessRule | null;
442
+ /** Login methods for the sign-in card, resolved from this page's config. */
443
+ authMethods: ILevoAuthMethods;
444
+ /** Re-run the gate — pass as `onSuccess` to any card that can change the outcome. */
445
+ recheck: () => void;
446
+ /** A `recheck()` (or the very first check) is in flight. */
447
+ isChecking: boolean;
448
+ }
449
+
450
+ /**
451
+ * One gate. `enabled` defaults to true when the key is absent — an author writing a rule
452
+ * down means it should apply; only an explicit `false` parks it.
453
+ */
454
+ export declare type ILevoAccessRule = {
455
+ kind: "authentication";
456
+ enabled?: boolean;
457
+ options?: ILevoAccessAuthenticationOptions;
458
+ } | {
459
+ kind: "gating";
460
+ enabled?: boolean;
461
+ options: ILevoAccessGatingOptions;
462
+ } | {
463
+ kind: "profile";
464
+ enabled?: boolean;
465
+ options?: ILevoAccessProfileOptions;
466
+ };
467
+
468
+ /**
469
+ * Codes the RUNTIME raises without asking the server. Namespaced `access.*` so they can
470
+ * never collide with a server code, present or future.
471
+ *
472
+ * - `CONFIG_INVALID` — the page has an `access` array we could not parse into a single
473
+ * valid rule. Fail-closed: a typo in hand-edited JSON must not silently un-gate a page.
474
+ * - `PAGE_NOT_IDENTIFIED` — the meta file carries no platform `id`, so there is nothing to
475
+ * ask the server about (a page whose row hasn't been stamped yet).
476
+ * - `UNWIRED` — the site has no Levo backend at all (`site.json` without `api_url`).
477
+ * - `CHECK_FAILED` — the check-access call itself failed (network, 5xx).
478
+ */
479
+ export declare type ILevoAccessRuntimeCode = "access.CONFIG_INVALID" | "access.PAGE_NOT_IDENTIFIED" | "access.UNWIRED" | "access.CHECK_FAILED";
480
+
481
+ /** Codes returned by the server's check-access evaluation. */
482
+ export declare type ILevoAccessServerCode = "authentication.NOT_CONFIGURED" | "authentication.NOT_LOGGED_IN" | "authentication.SUCCESS" | "gating.NOT_CONFIGURED" | "gating.IDENTIFIER_NOT_FOUND" | "gating.ENTRY_NOT_FOUND" | "gating.ENTRY_CONDITION_NOT_FULFILLED" | "gating.SUCCESS" | "profile.NOT_LOGGED_IN" | "profile.CONDITION_NOT_FULFILLED" | "profile.SUCCESS";
483
+
484
+ /**
485
+ * The gate: reads a page's own `meta/<route>.json` and answers whether this visitor may see
486
+ * this page. The page hands the result to whichever card matches the code — this renders
487
+ * nothing itself.
488
+ *
489
+ * This hook only GATHERS (session, config, server check); the verdict is `decideAccess`, a
490
+ * pure function next door. Keeping them apart is what makes the policy readable and testable
491
+ * on its own.
492
+ */
493
+ /**
494
+ * The page identity + gates this hook needs. A route's imported `meta/<route>.json`
495
+ * satisfies it as-is — `access` is typed `unknown` on purpose, because a JSON import widens
496
+ * string literals (`kind: string`, not `kind: "gating"`) and would never structurally match
497
+ * the rule union. Validation happens at runtime in `parseAccess`, which is where a
498
+ * hand-edited file has to be checked anyway.
499
+ */
500
+ export declare interface ILevoAccessSource {
501
+ /** The platform page id, stamped into the meta file. Empty on a page whose row is unstamped. */
502
+ id?: string;
503
+ /** The page's `access` array, straight from the meta file. */
504
+ access?: unknown;
505
+ }
506
+
507
+ /**
508
+ * Where the gate currently stands.
509
+ *
510
+ * - `checking` — undecided. Always the state during SSR and the first client frame for a
511
+ * gated page, which is what keeps gated content out of the server-rendered HTML.
512
+ * - `granted` — render the page.
513
+ * - `blocked` — render the card for `code`.
514
+ *
515
+ * There is no fourth "errored" state on purpose: every way a check can fail resolves to
516
+ * `blocked` with an `access.*` code, so a caller that handles the three states handles
517
+ * everything.
518
+ */
519
+ export declare type ILevoAccessState = "checking" | "granted" | "blocked";
520
+
521
+ export declare type ILevoAuthMethod = ILevoPrimaryAuthMethod | ILevoOAuthMethod;
522
+
523
+ /**
524
+ * The login methods a card should offer, resolved from the page's OWN config (never from
525
+ * the server response) so the card renders identically whether the block was decided
526
+ * locally or server-side.
527
+ */
528
+ export declare interface ILevoAuthMethods {
529
+ /** The passwordless form to lead with. Falls back to `otp` when a page configures none. */
530
+ primary: ILevoPrimaryAuthMethod;
531
+ /** OAuth providers to offer above the primary form. Empty when none are configured. */
532
+ oauth: ILevoOAuthMethod[];
533
+ }
534
+
535
+ export declare interface ILevoAuthState {
536
+ /** The signed-in membership account, or null. */
537
+ account: ILevoMembership.Account | null;
538
+ isLoggedIn: boolean;
539
+ /**
540
+ * The session question has been answered. False during SSR, the first client frame, and
541
+ * while the first `getMe()` is in flight — treat it as "don't know yet", never as
542
+ * "signed out".
543
+ */
544
+ isLoaded: boolean;
545
+ /** A session request is in flight (first load or a refresh). */
546
+ isLoading: boolean;
547
+ /** Re-read the session — call after any sign-in that happened outside this hook. */
548
+ refresh: () => Promise<void>;
549
+ signOut: () => Promise<void>;
550
+ isSigningOut: boolean;
551
+ }
552
+
194
553
  export declare interface ILevoChatProps {
195
554
  /** The site's root `site.json` — all chat props are derived from it. */
196
555
  site: ISiteJson;
@@ -211,6 +570,9 @@ export declare interface ILevoChatProps {
211
570
  path?: string;
212
571
  }
213
572
 
573
+ /** How the verification code reaches the visitor. */
574
+ export declare type ILevoCodeMedium = "email" | "whatsapp";
575
+
214
576
  /** The shape a page exports so its route loader can prefetch every binding it renders. */
215
577
  export declare type ILevoDataSources = Record<string, ILevoSourceDef>;
216
578
 
@@ -232,6 +594,19 @@ export declare interface ILevoDynamicHead {
232
594
  image?: string;
233
595
  }
234
596
 
597
+ export declare interface ILevoEntryFormProps {
598
+ /** The gated collection's key — take it from the blocking rule's `options.collection`. */
599
+ collection: string;
600
+ /** Field keys to hide — the rule's `options.hidden`. */
601
+ hidden?: string[];
602
+ submitLabel?: string;
603
+ /** Called once the entry is saved. Wire it to the gate's `recheck` so the page re-evaluates. */
604
+ onSuccess?: () => void;
605
+ /** Per-form color/radius overrides. Rarely needed — the form already follows the site theme. */
606
+ theme?: ILevoFormTheme;
607
+ className?: string;
608
+ }
609
+
235
610
  /**
236
611
  * A form binding — the fields + submit for a collection. `LevoForm` renders it (the schema
237
612
  * drives which inputs appear); submissions go to the collection. Forms only bind to
@@ -278,12 +653,18 @@ export declare interface ILevoFormResult {
278
653
  };
279
654
  }
280
655
 
656
+ /**
657
+ * The bridge between the collection form renderer's color contract and the generated site's
658
+ * semantic theme tokens. Shared by every form the runtime renders — the page's `<LevoForm>`,
659
+ * and the access cards' entry + profile forms — so a `theme.css` edit re-skins all of them
660
+ * together and none of them can drift into looking like a different product.
661
+ */
281
662
  /**
282
663
  * Per-form color/radius overrides. Every value is any CSS color/length — pass a site token
283
664
  * (`"var(--color-brand)"`) or a literal (`"#0ea5e9"`). Omitted keys fall through to the site
284
665
  * theme (see `DEFAULT_FORM_TOKENS`), so a form auto-matches the brand without any theme prop.
285
666
  */
286
- declare interface ILevoFormTheme {
667
+ export declare interface ILevoFormTheme {
287
668
  colors?: Partial<{
288
669
  /** Buttons, focus accents. */
289
670
  primary: string;
@@ -306,6 +687,120 @@ declare interface ILevoFormTheme {
306
687
  gap?: string;
307
688
  }
308
689
 
690
+ export declare interface ILevoLoginActions {
691
+ /**
692
+ * Send a verification code. `identifier` is the email address (medium `"email"`) or the
693
+ * E.164 phone number (medium `"whatsapp"`) — normalize it with `normalizePhone` first.
694
+ */
695
+ sendCode: (params: {
696
+ identifier: string;
697
+ medium?: ILevoCodeMedium;
698
+ }) => Promise<ILevoLoginOutcome>;
699
+ /** Same call as `sendCode`; separate so a form can show "Resending…" without touching the send button. */
700
+ resendCode: (params: {
701
+ identifier: string;
702
+ medium?: ILevoCodeMedium;
703
+ }) => Promise<ILevoLoginOutcome>;
704
+ /** Exchange the code for a session. On success the shared session cache updates immediately. */
705
+ verifyCode: (params: {
706
+ identifier: string;
707
+ code: string;
708
+ }) => Promise<ILevoLoginOutcome<ILevoMembership.Account | null>>;
709
+ /** Email a magic link back to the current URL. */
710
+ sendMagicLink: (email: string) => Promise<ILevoLoginOutcome>;
711
+ isSendingCode: boolean;
712
+ isResendingCode: boolean;
713
+ isVerifying: boolean;
714
+ isSendingMagicLink: boolean;
715
+ }
716
+
717
+ /** A failed action, flattened to what a form can render without knowing about LevoError. */
718
+ export declare interface ILevoLoginError {
719
+ code: string;
720
+ title: string;
721
+ message: string;
722
+ /** Per-field validation messages, keyed by field name. Empty when the error isn't a validation one. */
723
+ fieldErrors: Record<string, string>;
724
+ /** True when the visitor's account is awaiting approval — show the pending screen, not an error. */
725
+ isPendingApproval: boolean;
726
+ }
727
+
728
+ export declare type ILevoLoginOutcome<T = void> = {
729
+ ok: true;
730
+ data: T;
731
+ } | {
732
+ ok: false;
733
+ error: ILevoLoginError;
734
+ };
735
+
736
+ export declare interface ILevoOAuthHandle {
737
+ provider: ILevoOAuthMethod;
738
+ /** Opens the provider's popup. Must be called straight from a click — browsers block otherwise. */
739
+ signIn: () => void;
740
+ }
741
+
742
+ export declare type ILevoOAuthMethod = (typeof LEVO_OAUTH_METHODS)[number];
743
+
744
+ export declare interface ILevoOAuthState {
745
+ /**
746
+ * One handle per provider that is BOTH configured on the page and enabled in the
747
+ * workspace. Empty while the URLs load, so buttons appear only once they can work.
748
+ */
749
+ providers: ILevoOAuthHandle[];
750
+ isLoading: boolean;
751
+ /** The provider whose popup is open, if any — for a per-button spinner. */
752
+ pendingProvider: ILevoOAuthMethod | null;
753
+ /** Last failure (loading the URLs, or the provider rejecting the sign-in). */
754
+ error: ILevoLoginError | null;
755
+ clearError: () => void;
756
+ }
757
+
758
+ export declare interface ILevoOtpInputState {
759
+ slots: ILevoOtpSlot[];
760
+ /** Spread onto the element wrapping the boxes — announces them as one field. */
761
+ containerProps: {
762
+ role: "group";
763
+ "aria-label": string;
764
+ };
765
+ /** Clear every box and return focus to the first — for a "wrong code, try again" reset. */
766
+ clear: () => void;
767
+ }
768
+
769
+ /**
770
+ * A segmented verification-code input, minus the markup.
771
+ *
772
+ * The site owns how the boxes LOOK; this owns how they BEHAVE — and the behaviour is where
773
+ * these go wrong. Auto-advance, backspace stepping back, arrow keys, paste, and the browser
774
+ * dropping an entire autofilled code into the first box are five separate bugs waiting to
775
+ * happen, and none of them are visible when you restyle a box. So they live here, where a
776
+ * design change can't reach them.
777
+ *
778
+ * The value is always LEFT-PACKED: `"12"` means boxes 1–2 are filled and 3–4 are empty, and
779
+ * there is no way to represent a gap. Focusing a box beyond the end snaps back to the first
780
+ * empty one, which is what keeps that invariant true no matter where the visitor clicks.
781
+ */
782
+ export declare interface ILevoOtpSlot {
783
+ /** The digit in this box, or `""`. */
784
+ char: string;
785
+ /** This box has focus — for a ring or a caret bar. */
786
+ isActive: boolean;
787
+ /** Spread onto the box's `<input>`. Includes the ref, so don't set your own. */
788
+ inputProps: {
789
+ ref: (element: HTMLInputElement | null) => void;
790
+ value: string;
791
+ inputMode: "numeric";
792
+ autoComplete: "one-time-code" | "off";
793
+ type: "text";
794
+ "aria-label": string;
795
+ onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
796
+ onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;
797
+ onPaste: (event: React.ClipboardEvent<HTMLInputElement>) => void;
798
+ onFocus: () => void;
799
+ onBlur: () => void;
800
+ disabled?: boolean;
801
+ };
802
+ }
803
+
309
804
  export declare interface ILevoPopupsProps {
310
805
  /**
311
806
  * The site's id (`site.json` `id`) — scopes the active-popups fetch. Absent → the
@@ -320,6 +815,21 @@ export declare interface ILevoPopupsProps {
320
815
  pageId?: string;
321
816
  }
322
817
 
818
+ export declare type ILevoPrimaryAuthMethod = (typeof LEVO_PRIMARY_AUTH_METHODS)[number];
819
+
820
+ export declare interface ILevoProfileFormProps {
821
+ /** Extra account field keys to hide — the blocking rule's `options.hidden`. */
822
+ hidden?: string[];
823
+ submitLabel?: string;
824
+ /** Called after the account updates. Wire it to the gate's `recheck`. */
825
+ onSuccess?: () => void;
826
+ /** Class for the inline submit-error line. Defaults to the theme's danger color. */
827
+ errorClassName?: string;
828
+ /** Per-form color/radius overrides. Rarely needed — the form already follows the site theme. */
829
+ theme?: ILevoFormTheme;
830
+ className?: string;
831
+ }
832
+
323
833
  /** A single-record binding — one collection record / blog post / event by `slug` (a detail page). */
324
834
  export declare interface ILevoRecordConfig {
325
835
  module: "collection" | "blog" | "event";
@@ -399,6 +909,142 @@ export declare interface ILevoSourceResult<T = Record<string, unknown>> {
399
909
  };
400
910
  }
401
911
 
912
+ export declare interface ILocaleConfig {
913
+ /** Every locale the site publishes. Empty or single-entry on a non-localized site. */
914
+ locales: string[];
915
+ /** The locale served at unprefixed URLs. */
916
+ defaultLocale: string;
917
+ /**
918
+ * Whether this site does localization at all. Every site is single-locale until a workspace
919
+ * adds a second one, and the whole mechanism is inert while this is false.
920
+ */
921
+ isLocalized: boolean;
922
+ /**
923
+ * Absolute origin for hreflang, or null when the site has no domain stamped yet. hreflang
924
+ * requires absolute URLs, so an unstamped `site.json` means no tags rather than broken ones.
925
+ */
926
+ origin: string | null;
927
+ }
928
+
929
+ export declare type ILocaleDecision = ILocaleRender | ILocaleRedirect;
930
+
931
+ /** Mount once in `<head>`; the current page comes from router state, not from props. */
932
+ export declare interface ILocaleHreflangProps {
933
+ config: ILocaleConfig;
934
+ /**
935
+ * The generated route tree. Supplying it drops alternates for variants that have no route
936
+ * file, so an advertised URL never 404s. Omit and every locale is advertised.
937
+ */
938
+ routeTree?: unknown;
939
+ }
940
+
941
+ /**
942
+ * What the provider needs to expose a language to page code. Everything except `locale` is
943
+ * site-wide config; prefer `<SiteLocaleProvider>`, which derives all of it from `ILocaleConfig`.
944
+ */
945
+ export declare interface ILocaleProviderProps {
946
+ children: React.ReactNode;
947
+ /** The locale being rendered, from the server's resolution. */
948
+ locale: string;
949
+ /** Every locale the site publishes. */
950
+ locales?: string[];
951
+ /** Locale served at unprefixed URLs. */
952
+ defaultLocale?: string;
953
+ /** How `locale` was chosen — `"prefix"` means the visitor is on a canonical URL. */
954
+ source?: LocaleSource;
955
+ /**
956
+ * Does a route file exist at this path? Supplying it stops the switcher offering a language
957
+ * this page was never translated into — see `useLanguageOptions`.
958
+ */
959
+ hasRoute?: (pathname: string) => boolean;
960
+ }
961
+
962
+ /** Send the visitor to their language's URL. */
963
+ export declare interface ILocaleRedirect {
964
+ kind: "redirect";
965
+ locale: string;
966
+ source: LocaleSource;
967
+ /** Path only — `localeRedirectResponse` appends the request's own `search`/`hash`. */
968
+ path: string;
969
+ status: 302;
970
+ /**
971
+ * Every header the redirect must carry: `location`, the load-bearing `cache-control`, and
972
+ * `set-cookie` when the choice is worth remembering. Send them as-is; nothing is missing.
973
+ */
974
+ headers: Record<string, string>;
975
+ }
976
+
977
+ /** Serve the URL the visitor asked for. */
978
+ export declare interface ILocaleRender {
979
+ kind: "render";
980
+ locale: string;
981
+ source: LocaleSource;
982
+ }
983
+
984
+ export declare interface ILocaleRequestOptions {
985
+ /** The pathname the visitor asked for, e.g. `/pricing` or `/fr-FR/pricing`. */
986
+ pathname: string;
987
+ /** Every locale the site publishes, from `site.json`. */
988
+ locales: string[];
989
+ /** The locale served at unprefixed URLs. */
990
+ defaultLocale: string;
991
+ /**
992
+ * Request headers. Server path only — an in-app navigation never reaches the server, so the
993
+ * client path has none and negotiates from the cookie alone.
994
+ */
995
+ headers?: Record<string, string | string[]> | Headers | null;
996
+ /**
997
+ * A raw cookie string. Omit on the server, where it is read out of `headers`; pass
998
+ * `document.cookie` on the client, which has no headers to read it from.
999
+ */
1000
+ cookie?: string | null;
1001
+ /**
1002
+ * Does a route file exist at this path? Without it a visitor can be sent to a translation
1003
+ * nobody wrote. Build it with `createRouteTreeMatcher(routeTree)`.
1004
+ */
1005
+ hasRoute?: (pathname: string) => boolean;
1006
+ }
1007
+
1008
+ /**
1009
+ * What `useLocale()` returns: the language being rendered, the site's full list, and the one
1010
+ * mutation page code may perform. Read-only apart from `setLocale` — the URL, not this object,
1011
+ * is what decides the language.
1012
+ */
1013
+ export declare interface ILocaleState {
1014
+ locale: string;
1015
+ locales: string[];
1016
+ defaultLocale: string;
1017
+ source: LocaleSource;
1018
+ /** True once the site publishes more than one locale — gate switcher UI on this. */
1019
+ isMultiLocale: boolean;
1020
+ /**
1021
+ * Switch language: remember the choice and go to that language's URL for this page.
1022
+ *
1023
+ * It NAVIGATES rather than swapping content in place, because with the redirect model a
1024
+ * language IS a URL — `/hi-IN/pricing` is the Hindi page and the only place it exists. That
1025
+ * also means the result is shareable and cacheable, which staying on a neutral URL could
1026
+ * not give you.
1027
+ */
1028
+ setLocale: (locale: string) => void;
1029
+ /**
1030
+ * Route-existence check for this site, when the provider was given one. Consumed by
1031
+ * `useLanguageOptions`; page code should not need it.
1032
+ */
1033
+ hasRoute?: (pathname: string) => boolean;
1034
+ /**
1035
+ * Prefix a path with a locale. For the LANGUAGE SWITCHER only — the one place a visitor is
1036
+ * explicitly choosing a variant.
1037
+ *
1038
+ * Do NOT use it in shared chrome (navbar, footer, logo). Those render on every variant, and
1039
+ * building their links from the current page's language traps a visitor in the wrong one:
1040
+ * someone who landed on `/en` because no Hindi home exists then gets `/en/...` links
1041
+ * everywhere, and never finds their way back to `/hi/about` even though it exists. Shared
1042
+ * components link to the NEUTRAL path and let the doorway route them — it knows the cookie
1043
+ * and which translations exist, and a component knows neither.
1044
+ */
1045
+ pathFor: (path: string, locale: string) => string;
1046
+ }
1047
+
402
1048
  export declare function Inspector(): null;
403
1049
 
404
1050
  /** One page's head metadata. Every field optional — site defaults fill gaps per-field. */
@@ -450,6 +1096,24 @@ export declare interface IPageSource {
450
1096
  label: string;
451
1097
  }
452
1098
 
1099
+ /** What `parseAccess` decided about a page's `access` block. */
1100
+ export declare interface IParsedAccess {
1101
+ /** Every enabled, usable rule. Empty when the page is ungated. */
1102
+ rules: ILevoAccessRule[];
1103
+ /**
1104
+ * True when `access` held entries but not one of them parsed — a malformed block.
1105
+ * The gate treats this as BLOCKED, never as ungated: silently serving a page whose
1106
+ * author wrote a gate for it is the one failure mode worth being loud about.
1107
+ */
1108
+ invalid: boolean;
1109
+ }
1110
+
1111
+ export declare interface IParsedLanguageData {
1112
+ languages: ILanguageInfo[];
1113
+ primaryLanguage: string;
1114
+ inferredRegions: string[];
1115
+ }
1116
+
453
1117
  export declare interface IPatchedHistoryFn {
454
1118
  (this: History, ...args: Parameters<IHistoryFn>): ReturnType<IHistoryFn>;
455
1119
  __inspectorPatched?: boolean;
@@ -468,6 +1132,16 @@ export declare interface IPlatformMetaFile {
468
1132
  title?: string;
469
1133
  description?: string;
470
1134
  meta?: IPlatformPageMeta;
1135
+ /**
1136
+ * The page's access gates — see `./access/types`. FILE-owned like `meta`: the AI writes
1137
+ * it, and the platform mirrors it into the page row, which is what the server evaluates.
1138
+ *
1139
+ * Typed `unknown` deliberately. A JSON import widens string literals (`kind: string`, not
1140
+ * `kind: "gating"`), so a precise type here would reject every real meta file; the block
1141
+ * is validated at runtime by `parseAccess` instead, which is where a hand-edited file has
1142
+ * to be checked regardless.
1143
+ */
1144
+ access?: unknown;
471
1145
  }
472
1146
 
473
1147
  /** The platform's per-page SEO object — the `meta` key of `meta/<route>.json`. */
@@ -593,6 +1267,63 @@ export declare interface IRegionElement {
593
1267
  text?: string;
594
1268
  }
595
1269
 
1270
+ export declare interface IResolvedLocale {
1271
+ /** The locale to render, always a member of `locales` (or the default). */
1272
+ locale: string;
1273
+ source: LocaleSource;
1274
+ /**
1275
+ * The locale prefix stripped off the incoming pathname, if any — `/fr-FR/pricing` →
1276
+ * `fr-FR`. Null on a neutral URL. This is what tells the caller the visitor is on a
1277
+ * canonical URL rather than a negotiated one.
1278
+ */
1279
+ prefix: string | null;
1280
+ /** The pathname with any locale prefix removed — `/fr-FR/pricing` → `/pricing`. */
1281
+ neutralPath: string;
1282
+ }
1283
+
1284
+ export declare interface IResolveLocaleOptions {
1285
+ /** Full locale list from `site.json` (`workspace.locales`). */
1286
+ locales?: string[];
1287
+ /** Locale served at unprefixed URLs (`workspace.default_locale`). */
1288
+ defaultLocale?: string;
1289
+ /** The request pathname, e.g. `/fr-FR/pricing`. */
1290
+ pathname: string;
1291
+ /** Request headers. Server-side only; omit on the client. */
1292
+ headers?: Record<string, string | string[]> | Headers | null;
1293
+ /** Value of the `levo_locale` cookie, if the visitor has one. */
1294
+ cookieLocale?: string | null;
1295
+ /**
1296
+ * Skip cookie/header/geo negotiation and pin to the default. Set for crawlers, so a
1297
+ * neutral URL genuinely IS its canonical language from Google's point of view.
1298
+ */
1299
+ forceDefault?: boolean;
1300
+ }
1301
+
1302
+ export declare interface IResolveLocaleRedirectOptions {
1303
+ /** The locale the visitor should see, from `resolveLocale`. */
1304
+ locale: string;
1305
+ /** Locale served at unprefixed URLs. */
1306
+ defaultLocale: string;
1307
+ /** The locale prefix already present in the URL, or null on a neutral URL. */
1308
+ prefix: string | null;
1309
+ /** The request path with any locale prefix removed. */
1310
+ neutralPath: string;
1311
+ /** Every locale the site publishes. */
1312
+ locales: string[];
1313
+ /**
1314
+ * Does a route file exist at this path? Prevents redirecting to a translation that was
1315
+ * never written: `/hi-IN/careers` absent means the visitor stays on `/careers` and reads
1316
+ * the default language, rather than being bounced to a 404.
1317
+ */
1318
+ hasRoute?: (pathname: string) => boolean;
1319
+ /**
1320
+ * The pathname exactly as the visitor asked for it. Only used to detect a prefix whose
1321
+ * spelling differs from the site's own, e.g. `/EN/pricing` for a site publishing `en`.
1322
+ * Omit and that correction is skipped.
1323
+ */
1324
+ currentPath?: string;
1325
+ }
1326
+
596
1327
  /** The element-selected payload — see `selectedPayload`. */
597
1328
  export declare interface ISelectedElement {
598
1329
  tag: string;
@@ -672,6 +1403,22 @@ export declare interface ISiteJson {
672
1403
  insights_url?: string | null;
673
1404
  app_mode?: string;
674
1405
  };
1406
+ /**
1407
+ * Every locale the site publishes, as dash-form full locales (`en-GB`, `fr-FR`, `ar-AE`)
1408
+ * to match how `page.language` stores them. Projected from `workspace.locales` — locales
1409
+ * are a WORKSPACE-level setting, so every site in a workspace shares this list.
1410
+ *
1411
+ * Absent or length <= 1 → localization is entirely inert: no rewrite, no hreflang, no
1412
+ * cookie. That is every site today, which is what lets the mechanism ship before the
1413
+ * builder learns to generate `/fr-FR/*` routes.
1414
+ */
1415
+ locales?: string[];
1416
+ /**
1417
+ * The locale served at UNPREFIXED URLs. `/pricing` is this locale's canonical home, and
1418
+ * it deliberately has no `/en-GB/pricing` twin — a prefixed default would be a duplicate
1419
+ * needing its own canonical. Projected from `workspace.default_locale`.
1420
+ */
1421
+ default_locale?: string;
675
1422
  }
676
1423
 
677
1424
  /** Site-wide defaults + site-wide raw head, from `src/pages/_site.meta.ts`. */
@@ -685,6 +1432,14 @@ export declare interface ISiteMeta extends IPageMeta {
685
1432
  icon?: string;
686
1433
  }
687
1434
 
1435
+ /**
1436
+ * Pending approval is not a failure to show in a red box — it means "your request landed,
1437
+ * a human has to act". The sign-in card swaps to its own screen for it, so every entry
1438
+ * point (OTP, magic link, OAuth) has to recognise it the same way. Matched loosely because
1439
+ * the code has been namespaced differently across services.
1440
+ */
1441
+ export declare const isPendingApprovalError: (code?: string | null) => boolean;
1442
+
688
1443
  export declare type IStagedEntry = IStagedStyleEntry | IStagedTextEntry | IStagedImageEntry;
689
1444
 
690
1445
  declare interface IStagedImageEntry {
@@ -710,6 +1465,18 @@ export declare interface IStagedTextEntry {
710
1465
  value: string;
711
1466
  }
712
1467
 
1468
+ /**
1469
+ * Deliberately permissive: `something@something.tld`, no attempt to enumerate valid TLDs or
1470
+ * implement RFC 5322. The address is proven by the visitor receiving the code — the only
1471
+ * job here is catching a typo before we spend a round-trip on it.
1472
+ */
1473
+ export declare const isValidEmail: (value: string) => boolean;
1474
+
1475
+ export declare const isValidOtp: (value: string) => boolean;
1476
+
1477
+ /** E.164 shape: a country code that can't start with 0, then 7–14 more digits. */
1478
+ export declare const isValidPhone: (value: string) => boolean;
1479
+
713
1480
  /** The element-text-changed payload — see `commit`. */
714
1481
  export declare interface ITextChangedElement {
715
1482
  source: string | null;
@@ -720,6 +1487,62 @@ export declare interface ITextChangedElement {
720
1487
  occurrences: number;
721
1488
  }
722
1489
 
1490
+ export declare interface IUseLevoOAuthParams {
1491
+ /** Providers this page offers, from its access config. Anything else is ignored. */
1492
+ methods: ILevoOAuthMethod[];
1493
+ /** Popup window title — usually the site name. */
1494
+ title?: string;
1495
+ /** Fired after a successful sign-in; the session cache is already refreshed by then. */
1496
+ onSuccess?: () => void;
1497
+ /** Fired on failure. Check `error.isPendingApproval` before rendering it as an error. */
1498
+ onError?: (error: ILevoLoginError) => void;
1499
+ }
1500
+
1501
+ export declare interface IUseLevoOtpInputParams {
1502
+ value: string;
1503
+ onChange: (value: string) => void;
1504
+ /** Digits in the code. Defaults to the platform's length. */
1505
+ length?: number;
1506
+ /**
1507
+ * Fired the moment the last box fills, with the complete code. Use it to submit — waiting
1508
+ * for a button press after the final digit is a step nobody wants. The code is passed as an
1509
+ * argument because the caller's state hasn't re-rendered yet when this runs.
1510
+ */
1511
+ onComplete?: (code: string) => void;
1512
+ /** Focus the first box on mount. */
1513
+ autoFocus?: boolean;
1514
+ disabled?: boolean;
1515
+ }
1516
+
1517
+ /** Every method a page may list in `options.methods`. */
1518
+ export declare const LEVO_AUTH_METHODS: ILevoAuthMethod[];
1519
+
1520
+ /**
1521
+ * The visitor's session, shared across every component on the page.
1522
+ *
1523
+ * Backed by TanStack Query rather than a provider: the site already mounts a QueryClient
1524
+ * for data sources, so one cache entry gives every card, navbar, and gate the same account
1525
+ * with one request — and any of them can invalidate it after a sign-in.
1526
+ *
1527
+ * CLIENT-ONLY BY CONSTRUCTION. The session lives in a cookie the SSR pass cannot see, so a
1528
+ * server-side `getMe()` would resolve "signed out" for a signed-in visitor and bake that
1529
+ * into the HTML. Gating it on `useMounted` means a gated page renders its `checking` state
1530
+ * on the server — which is also what keeps gated content out of the server-rendered HTML.
1531
+ */
1532
+ export declare const LEVO_AUTH_QUERY_KEY: readonly ["levo-auth", "session"];
1533
+
1534
+ /** OAuth providers — rendered as buttons ALONGSIDE the primary form, any number of them. */
1535
+ export declare const LEVO_OAUTH_METHODS: readonly ["google", "microsoft", "linkedin"];
1536
+
1537
+ /** Digits in the verification code the platform sends. */
1538
+ export declare const LEVO_OTP_LENGTH = 4;
1539
+
1540
+ /** The error code the platform returns when the account exists but awaits admin approval. */
1541
+ export declare const LEVO_PENDING_APPROVAL_CODE = "membership.account.ACCOUNT_PENDING_APPROVAL";
1542
+
1543
+ /** Passwordless methods — exactly one of these is the login card's PRIMARY form. */
1544
+ export declare const LEVO_PRIMARY_AUTH_METHODS: readonly ["otp", "magiclink", "whatsapp"];
1545
+
723
1546
  /**
724
1547
  * Drops the site's AI chat widget onto every page, wired from `site.json`. Mounted once
725
1548
  * from the scaffold's `__root` (via the `SiteChat` shim) alongside `<Inspector />`. Self-
@@ -729,6 +1552,22 @@ export declare interface ITextChangedElement {
729
1552
  */
730
1553
  export declare const LevoChat: React.FC<ILevoChatProps>;
731
1554
 
1555
+ /**
1556
+ * The "request access" form for a `gating` rule — the collection's own fields, submitted as
1557
+ * an entry owned by the signed-in visitor.
1558
+ *
1559
+ * Ownership is what makes the gate work: the server matches entries by `_account`, so this
1560
+ * form only ever renders for a signed-in visitor and the submission carries their session.
1561
+ * The visitor's email/mobile prefill the schema's designated contact fields, which is both a
1562
+ * convenience and what keeps the entry findable by a human reviewing the collection.
1563
+ *
1564
+ * Distinct from the page-level `<LevoForm>` in one way that matters: the schema is fetched
1565
+ * client-side, because a gated page's route loader never prefetches it — the form only
1566
+ * exists on the blocked path, and prefetching it on every visit would tell the SSR pass
1567
+ * something about a page it is not allowed to render.
1568
+ */
1569
+ export declare const LevoEntryForm: React.FC<ILevoEntryFormProps>;
1570
+
732
1571
  /**
733
1572
  * Self-contained event registration widget for generated Studio v3 sites — ticket
734
1573
  * selection, coupons, attendee details, payment (Razorpay by default; Stripe Embedded
@@ -776,6 +1615,20 @@ export declare const LevoForm: React.FC<ILevoFormProps>;
776
1615
  */
777
1616
  export declare const LevoPopups: React.FC<ILevoPopupsProps>;
778
1617
 
1618
+ /**
1619
+ * The profile-completion form for a `profile` rule.
1620
+ *
1621
+ * Submits through `membership.updateMe` — it edits the visitor's OWN account, and never
1622
+ * creates a collection entry (that's what `gating` does). Only visible, non-readonly fields
1623
+ * are sent: echoing back the whole form would push system fields and the locked identity
1624
+ * fields through an endpoint that has no reason to receive them.
1625
+ *
1626
+ * Renders its own submit error inline rather than handing it upward, because an error that
1627
+ * depends on a site component remembering to display it is an error that eventually isn't
1628
+ * displayed.
1629
+ */
1630
+ export declare const LevoProfileForm: React.FC<ILevoProfileFormProps>;
1631
+
779
1632
  /**
780
1633
  * Mounts the Levo client + insights for a generated site, and the site-wide overlays that
781
1634
  * ride on top of it (AI chat + popups). Rendered once from the scaffold's `__root` (via the
@@ -793,6 +1646,69 @@ export declare const LevoSiteProvider: React.FC<{
793
1646
  children: React.ReactNode;
794
1647
  }>;
795
1648
 
1649
+ /** Cookie the resolved locale is persisted under. Readable by JS — the switcher writes it. */
1650
+ export declare const LOCALE_COOKIE = "levo_locale";
1651
+
1652
+ /** Cache-Control for the doorway redirect. Never let a shared cache store this — see above. */
1653
+ export declare const LOCALE_REDIRECT_CACHE_CONTROL = "private, no-store";
1654
+
1655
+ /**
1656
+ * Build the `beforeLoad` guard for a site's root route.
1657
+ *
1658
+ * `routeTree` is passed here rather than carried on the config because `__root.tsx` is imported
1659
+ * by the generated tree; the matcher it produces reads the tree lazily, on the first navigation,
1660
+ * which is what keeps that circular import safe.
1661
+ */
1662
+ export declare const localeBeforeLoad: (config: ILocaleConfig, routeTree: unknown) => ({ location }: {
1663
+ location: {
1664
+ pathname: string;
1665
+ searchStr?: string;
1666
+ };
1667
+ }) => void;
1668
+
1669
+ export declare const LocaleHreflang: React.FC<ILocaleHreflangProps>;
1670
+
1671
+ /**
1672
+ * The URL a page has in a given variant.
1673
+ *
1674
+ * On a multi-variant site EVERY variant is prefixed — including the site's default. That is
1675
+ * what makes each page cacheable: `/en/pricing` is always English and `/fr-FR/pricing` is
1676
+ * always French, so a plain URL-keyed cache is correct with no configuration. The unprefixed
1677
+ * `/pricing` is not a page at all under this model; it is the doorway that redirects.
1678
+ *
1679
+ * A single-variant site gets no prefixes — `/en/` on every URL of a site that only speaks
1680
+ * English is noise, and there is nothing to disambiguate.
1681
+ *
1682
+ * The default locale takes a prefix like every other, so this needs no knowledge of which one
1683
+ * it is. (It did while the default was unprefixed; that inverted on 2026-08-18.)
1684
+ */
1685
+ export declare const localePath: (path: string, locale: string, locales: string[]) => string;
1686
+
1687
+ export declare const LocaleProvider: React.FC<ILocaleProviderProps>;
1688
+
1689
+ /**
1690
+ * Build the HTTP response for a redirect decision.
1691
+ *
1692
+ * Exists so the server path cannot assemble it wrongly: status and headers come straight off the
1693
+ * decision, and `302` (never `301`) is fixed here — the target depends on who is asking, so a
1694
+ * browser must never remember it as permanent.
1695
+ */
1696
+ export declare const localeRedirectResponse: (decision: ILocaleRedirect, search?: string, hash?: string) => Response;
1697
+
1698
+ /**
1699
+ * How a locale was chosen. Surfaced so callers can act on provenance rather than re-deriving
1700
+ * it: `prefix` means the visitor is on a canonical locale URL (don't redirect, don't restamp
1701
+ * the cookie), and `default` on a multi-locale site means nothing matched.
1702
+ */
1703
+ export declare type LocaleSource = "prefix" | "cookie" | "header" | "geo" | "default";
1704
+
1705
+ /**
1706
+ * To E.164: strip every separator, keep one leading `+`. `"+91 98765 43210"` and
1707
+ * `"0091-98765-43210"` both normalize to `"+919876543210"` — the format the OTP endpoint
1708
+ * expects, and the one Studio's phone widget emits.
1709
+ */
1710
+ export declare const normalizePhone: (value: string) => string;
1711
+
796
1712
  /** `name` of the head tag carrying the page id. Written by `levoHead`, read by `readPageId`. */
797
1713
  export declare const PAGE_ID_META = "levo:page-id";
798
1714
 
@@ -821,6 +1737,22 @@ export declare function PageHead({ meta, site }: {
821
1737
  */
822
1738
  export declare const pageHead: (meta?: IPageMeta, site?: ISiteMeta) => IComposedHead;
823
1739
 
1740
+ /**
1741
+ * Read a page's `access` block defensively.
1742
+ *
1743
+ * The three outcomes are deliberately distinct:
1744
+ * - absent / `[]` / every rule `enabled: false` → `{rules: [], invalid: false}` = ungated.
1745
+ * All three are an author SAYING "no gate", so the page renders with no network call.
1746
+ * - at least one usable rule → `{rules: [...], invalid: false}` = gated.
1747
+ * - entries present, none usable → `{rules: [], invalid: true}` = blocked.
1748
+ * A typo (`kind: "athentication"`, a gating rule missing its collection) must not read
1749
+ * the same as "no gate" — that would turn one wrong character into a public page.
1750
+ *
1751
+ * Note that a rule explicitly disabled is NOT invalid: it parsed fine, the author just
1752
+ * turned it off. Only entries that fail to parse at all count toward `invalid`.
1753
+ */
1754
+ export declare const parseAccess: (raw: unknown) => IParsedAccess;
1755
+
824
1756
  /**
825
1757
  * Tolerant extraction of `<script>` / `<meta>` / `<link>` tags from a raw head snippet.
826
1758
  * Unsupported tags are DROPPED (documented v1 limitation) — silently at runtime; the
@@ -850,6 +1782,9 @@ export declare const prefetchLevoSources: ({ queryClient, config, sources, param
850
1782
  params?: Record<string, string>;
851
1783
  }) => Promise<Record<string, unknown>>;
852
1784
 
1785
+ /** Read the locale config out of a site's `site.json`. */
1786
+ export declare const readLocaleConfig: (site: ISiteJson) => ILocaleConfig;
1787
+
853
1788
  /**
854
1789
  * The id the currently matched route stamped into the head, or null when the site's
855
1790
  * platform hasn't stamped ids into its meta files yet (the field ships empty). Client-only —
@@ -857,9 +1792,54 @@ export declare const prefetchLevoSources: ({ queryClient, config, sources, param
857
1792
  */
858
1793
  export declare const readPageId: () => string | null;
859
1794
 
1795
+ /**
1796
+ * The login methods the sign-in card should offer for this page.
1797
+ *
1798
+ * Resolved from the page's own rules rather than the server's response so the card looks
1799
+ * the same whether the block was decided locally (not signed in) or server-side. A page
1800
+ * that gates on `profile` alone still needs a sign-in card — hence the fallback to email
1801
+ * OTP when no authentication rule names any method.
1802
+ */
1803
+ export declare const resolveAuthMethods: (rules: ILevoAccessConfig) => ILevoAuthMethods;
1804
+
860
1805
  /** What `system` resolves to — the class actually on `<html>`. */
861
1806
  export declare type ResolvedColorMode = "dark" | "light";
862
1807
 
1808
+ /**
1809
+ * Resolve which locale to serve.
1810
+ *
1811
+ * Precedence: explicit URL prefix → cookie → `Accept-Language` → GeoIP country → default.
1812
+ *
1813
+ * The prefix wins over the cookie deliberately: a `/fr-FR/pricing` link shared with someone
1814
+ * whose cookie says `de-DE` must render French, or shared links stop meaning anything.
1815
+ *
1816
+ * GeoIP sits BELOW `Accept-Language` because country is a weaker signal for language than
1817
+ * the visitor's own stated browser preference — someone in Belgium may want either Dutch or
1818
+ * French, and their browser already answered that.
1819
+ */
1820
+ export declare const resolveLocale: (options: IResolveLocaleOptions) => IResolvedLocale;
1821
+
1822
+ /**
1823
+ * The path to redirect a visitor to, or `null` to serve the current URL as-is.
1824
+ *
1825
+ * Returns null — meaning "no redirect" — in every one of these cases:
1826
+ * - the visitor is already on a prefixed URL (they asked for that language explicitly)
1827
+ * - the site publishes one locale or none
1828
+ * - no route file exists for this page in the visitor's language
1829
+ */
1830
+ export declare const resolveLocaleRedirect: (options: IResolveLocaleRedirectOptions) => string | null;
1831
+
1832
+ /**
1833
+ * Decide what to do with one request to a possibly-unprefixed URL.
1834
+ *
1835
+ * Bot detection is derived here rather than asked of the caller. Crawlers must always see the
1836
+ * default language at a neutral URL — it is what makes that URL genuinely *be* its canonical
1837
+ * language — and that is a property of the design, not a decision each doorway should be making
1838
+ * for itself. `resolveLocale`'s `forceDefault` stays available for anything that needs to pin it
1839
+ * directly.
1840
+ */
1841
+ export declare const resolveLocaleRequest: (options: ILocaleRequestOptions) => ILocaleDecision;
1842
+
863
1843
  /**
864
1844
  * The site's Levo client config from its `site.json` — the only place that mapping lives, so
865
1845
  * the provider and the SSR route loader can't drift apart.
@@ -869,9 +1849,69 @@ export declare type ResolvedColorMode = "dark" | "light";
869
1849
  */
870
1850
  export declare const siteConfigFromJson: (site: ISiteJson) => ILevoSiteConfig;
871
1851
 
1852
+ /**
1853
+ * Mount the locale context from a site's config.
1854
+ *
1855
+ * Prefer this over `<LocaleProvider>` in a generated site: it derives the active language from
1856
+ * the URL itself, so there is no second copy of that derivation to fall out of step with the
1857
+ * `<html lang>` or the hreflang tags.
1858
+ */
1859
+ export declare const SiteLocaleProvider: React.FC<{
1860
+ config: ILocaleConfig;
1861
+ children: React.ReactNode;
1862
+ /**
1863
+ * The generated route tree. Supplying it lets the switcher hide languages this page has no
1864
+ * translation into — the difference between offering a link and offering a 404.
1865
+ */
1866
+ routeTree?: unknown;
1867
+ }>;
1868
+
1869
+ /**
1870
+ * Split a locale prefix off a pathname. Only strips a segment that is a KNOWN locale, so a
1871
+ * site with a page genuinely named `/fr-FR` (or any other collision) is unaffected — the
1872
+ * segment has to be in the site's own locale list to count.
1873
+ */
1874
+ export declare const splitLocalePrefix: (pathname: string, locales: string[]) => {
1875
+ prefix: string | null;
1876
+ neutralPath: string;
1877
+ };
1878
+
872
1879
  /** Read/set the current color mode. Throws outside a `<ColorModeProvider>`. */
873
1880
  export declare function useColorMode(): IColorModeState;
874
1881
 
1882
+ /**
1883
+ * The behaviour half of a language switcher, so the starter only owns the look.
1884
+ *
1885
+ * Three things here are load-bearing and were previously comments in a starter component asking
1886
+ * not to be "simplified":
1887
+ *
1888
+ * 1. `href` is a real canonical URL, so a crawler can follow it. A switcher built from buttons
1889
+ * and a click handler is invisible to search, and the site's own hreflang tags then point at
1890
+ * pages nothing links to.
1891
+ * 2. A plain left-click is intercepted into a cookie write plus a navigation, so the choice is
1892
+ * remembered. Modifier and middle clicks are deliberately NOT intercepted, so "open in new
1893
+ * tab" lands on the real URL like any other link.
1894
+ * 3. The path is stripped back to its neutral form before each variant's URL is built, or the
1895
+ * hrefs come out doubled (`/de-DE/fr-FR/pricing`).
1896
+ *
1897
+ * Render `options` however you want; drop `linkProps` onto an `<a>` and none of the above can
1898
+ * be lost.
1899
+ *
1900
+ * Languages this page has no translation into are OMITTED, when the provider was given a route
1901
+ * tree. Their URL would be a real 404: a prefixed URL is an explicit request, so the doorway
1902
+ * correctly declines to redirect it, and there is no page behind it. This is the same rule the
1903
+ * hreflang tags follow — a switcher offering what hreflang refuses to advertise is the two
1904
+ * halves disagreeing about which pages exist.
1905
+ *
1906
+ * Returns an empty list when fewer than two languages remain, so a switcher self-hides on a
1907
+ * single-locale site and on a page that only exists in one language.
1908
+ */
1909
+ export declare function useLanguageOptions(): ILanguageOption[];
1910
+
1911
+ export declare const useLevoAccess: (source: ILevoAccessSource) => ILevoAccessResult;
1912
+
1913
+ export declare const useLevoAuth: () => ILevoAuthState;
1914
+
875
1915
  /**
876
1916
  * Load a collection's SCHEMA so `LevoForm` can render its fields. Like the other hooks the
877
1917
  * config is a static literal (the editor binding); it reads the warm cache the route loader
@@ -883,6 +1923,12 @@ export declare const useLevoForm: (config: ILevoFormConfig) => ILevoFormResult;
883
1923
  /** The site's insights (analytics/track), or `null` on an unwired site. */
884
1924
  export declare const useLevoInsights: () => ILevoInsights | null;
885
1925
 
1926
+ export declare const useLevoLogin: () => ILevoLoginActions;
1927
+
1928
+ export declare const useLevoOAuth: ({ methods, title, onSuccess, onError, }: IUseLevoOAuthParams) => ILevoOAuthState;
1929
+
1930
+ export declare const useLevoOtpInput: ({ value, onChange, length, onComplete, autoFocus, disabled, }: IUseLevoOtpInputParams) => ILevoOtpInputState;
1931
+
886
1932
  /**
887
1933
  * Render ONE record by slug — a detail page. `slug` is a RUNTIME arg (the page's route
888
1934
  * param), kept OUT of the config literal so the binding stays static and editable. No slug
@@ -911,4 +1957,26 @@ export declare const useLevoSite: () => ILevoClient | null;
911
1957
  */
912
1958
  export declare const useLevoSource: <T = Record<string, unknown>>(config: ILevoSourceConfig) => ILevoSourceResult<T>;
913
1959
 
1960
+ /**
1961
+ * Read the active locale and switch languages.
1962
+ *
1963
+ * Returns an inert single-locale state outside a `<LocaleProvider>` rather than throwing —
1964
+ * unlike `useColorMode`, which throws. A switcher is page content the AI may drop onto a
1965
+ * site whose provider was never wired, and crashing the whole page over a language picker is
1966
+ * a worse failure than rendering nothing.
1967
+ */
1968
+ export declare function useLocale(): ILocaleState;
1969
+
1970
+ /**
1971
+ * The language the current URL serves.
1972
+ *
1973
+ * Read off the URL, not negotiated: every variant is a prefixed URL under the redirect model,
1974
+ * so `/hi-IN/pricing` is Hindi and nothing else, and server and browser cannot disagree about
1975
+ * it. (When locales were applied as a rewrite they could, and did — three separate bugs where
1976
+ * the page rendered in one language and hydrated in another.)
1977
+ *
1978
+ * For `<html lang>`, which sits above the provider and so cannot use `useLocale()`.
1979
+ */
1980
+ export declare function useUrlLocale(config: ILocaleConfig): string;
1981
+
914
1982
  export { }