@levo-so/site-runtime 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -4,6 +4,27 @@ import { ILevoInsights } from '@levo-so/insights';
4
4
  import { JSX } from 'react/jsx-runtime';
5
5
  import { LevoQuery } from '@levo-so/core';
6
6
 
7
+ /** Color-mode preference. `system` follows the OS `prefers-color-scheme`. */
8
+ export declare type ColorMode = "dark" | "light" | "system";
9
+
10
+ /**
11
+ * Site-wide light/dark color mode for a generated Studio site. This does NOT define the
12
+ * theme (colors/tokens live in `theme.css`) — it only toggles a `light`/`dark` class on
13
+ * `<html>`; `theme.css` keys its dark token overrides off `.dark` (via `@custom-variant
14
+ * dark`), so every semantic utility (`bg-page`, `text-ink`, …) flips with zero page changes.
15
+ *
16
+ * Mounted once from the scaffold's `__root` (wrapping the page) — deliberately NOT from
17
+ * `LevoSiteProvider`: that one returns bare children on an unwired site (no `api_url`), and
18
+ * light/dark can't depend on whether a backend is stamped. `defaultMode` is also a per-site
19
+ * design decision the generated site owns, not something the runtime should pick.
20
+ *
21
+ * SSR-safe: the initial
22
+ * render uses `defaultMode` and emits only the `ScriptOnce` guard; the stored preference is
23
+ * read and applied post-mount. Put `suppressHydrationWarning` on `<html>` since the guard
24
+ * mutates it before hydration.
25
+ */
26
+ export declare const ColorModeProvider: React.FC<IColorModeProviderProps>;
27
+
7
28
  /**
8
29
  * Build the site's `levoHead` from its `site.json`. Each route passes its OWN imported
9
30
  * meta file (`head: levoHead(meta)`) — no path matching anywhere; the import IS the
@@ -24,8 +45,11 @@ export declare const createLevoHead: (site: ISiteJson) => (meta?: IPlatformMetaF
24
45
  * `levoHead`'s dynamic config reads for per-record heads. An empty `dataSources` ({})
25
46
  * resolves to an empty map. Context is typed structurally (just `queryClient`) so the
26
47
  * scaffold's router types never enter the package.
48
+ *
49
+ * Takes the raw `site.json` — same single input as `createLevoHead` and
50
+ * `LevoSiteProvider` — and derives the client config itself.
27
51
  */
28
- export declare const createLevoLoader: (config: ILevoSiteConfig) => (sources: ILevoDataSources) => ({ context, params, }: {
52
+ export declare const createLevoLoader: (site: ISiteJson) => (sources: ILevoDataSources) => ({ context, params, }: {
29
53
  context: {
30
54
  queryClient: IQueryClientLike;
31
55
  };
@@ -41,6 +65,27 @@ export declare const createLevoLoader: (config: ILevoSiteConfig) => (sources: IL
41
65
  */
42
66
  export declare const createSiteClient: (config?: ILevoSiteConfig) => ILevoClient | null;
43
67
 
68
+ export declare interface IColorModeProviderProps {
69
+ children: React.ReactNode;
70
+ /** Fallback when nothing is stored yet. */
71
+ defaultMode?: ColorMode;
72
+ /** localStorage key the preference is persisted under. */
73
+ storageKey?: string;
74
+ }
75
+
76
+ export declare interface IColorModeState {
77
+ /** The stored preference (`system` before it resolves to light/dark). */
78
+ mode: ColorMode;
79
+ /**
80
+ * The mode actually in effect — `system` already resolved against the OS. Use this to
81
+ * render a two-state toggle so no site has to re-implement the media query. Before mount
82
+ * it reflects `defaultMode` (with `system` read as light), matching what the server sent.
83
+ */
84
+ resolvedMode: ResolvedColorMode;
85
+ /** Persist a new preference and re-apply it immediately. */
86
+ setMode: (mode: ColorMode) => void;
87
+ }
88
+
44
89
  export declare interface IComposedHead {
45
90
  meta: IHeadEntry[];
46
91
  links: IHeadEntry[];
@@ -88,6 +133,11 @@ export declare type IEditorToPreview = {
88
133
  stageId: string;
89
134
  element: IElementLocator;
90
135
  value: string;
136
+ } | {
137
+ type: "stage-image";
138
+ stageId: string;
139
+ element: IElementLocator;
140
+ value: string;
91
141
  } | {
92
142
  type: "clear-staged";
93
143
  stageId?: string;
@@ -131,6 +181,11 @@ export declare interface ILayersNode {
131
181
  children: ILayersNode[];
132
182
  }
133
183
 
184
+ export declare interface ILevoChatProps {
185
+ /** The site's root `site.json` — all chat props are derived from it. */
186
+ site: ISiteJson;
187
+ }
188
+
134
189
  /** The shape a page exports so its route loader can prefetch every binding it renders. */
135
190
  export declare type ILevoDataSources = Record<string, ILevoSourceDef>;
136
191
 
@@ -226,6 +281,14 @@ declare interface ILevoFormTheme {
226
281
  gap?: string;
227
282
  }
228
283
 
284
+ export declare interface ILevoPopupsProps {
285
+ /**
286
+ * The site's id (`site.json` `id`) — scopes the active-popups fetch. Absent → the
287
+ * workspace-wide active popups are returned.
288
+ */
289
+ siteId?: string;
290
+ }
291
+
229
292
  /** A single-record binding — one collection record / blog post / event by `slug` (a detail page). */
230
293
  export declare interface ILevoRecordConfig {
231
294
  module: "collection" | "blog" | "event";
@@ -480,17 +543,47 @@ export declare interface ISelectedElement {
480
543
  };
481
544
  }
482
545
 
483
- /** The slice of the project's root `site.json` the runtime reads. */
546
+ /**
547
+ * The project's root `site.json` — the ONE input to the whole runtime. The platform stamps
548
+ * it at provision and re-syncs it on settings saves; the scaffold shim imports it and hands
549
+ * the same object to every factory (`createLevoHead`, `createLevoLoader`,
550
+ * `<LevoSiteProvider site>`).
551
+ *
552
+ * It lives in its own leaf module because four modules read it (client, scaffold, provider,
553
+ * chat) and every one of them sits at a different depth of the import graph — hanging the
554
+ * type off any of them makes the graph cyclic.
555
+ *
556
+ * Every field is optional on purpose: a site provisioned before a given block shipped is
557
+ * missing it entirely, and the runtime must degrade (inert client, hidden widget) rather
558
+ * than crash.
559
+ */
484
560
  export declare interface ISiteJson {
485
561
  id?: string;
486
562
  workspace_id?: string;
563
+ /** Site slug — tags the visitor-chat "Powered by" link's utm_source. */
564
+ slug?: string;
487
565
  name?: string;
488
566
  description?: string;
489
567
  /** The site's live URL host (with or without protocol) — feeds auto-canonicals. */
490
568
  domain?: string;
569
+ /**
570
+ * Favicon media object. Its `.location` becomes the site's `<link rel="icon">` and the
571
+ * visitor-chat header icon. Absent → the site keeps the starter's default `favicon.ico`.
572
+ */
573
+ icon?: {
574
+ location?: string;
575
+ } | null;
491
576
  settings?: {
492
577
  head_code?: string;
493
578
  };
579
+ /**
580
+ * Workspace feature flags stamped into the site. `branding` gates the visitor-chat
581
+ * "Powered by levo.so" attribution — same flag as the "Made with Levo" page badge.
582
+ * Absent (not yet stamped) → coerced false → attribution hidden (white-label-safe).
583
+ */
584
+ feature_flag?: {
585
+ branding?: boolean;
586
+ };
494
587
  levo?: {
495
588
  api_url?: string;
496
589
  insights_url?: string | null;
@@ -502,9 +595,22 @@ export declare interface ISiteJson {
502
595
  export declare interface ISiteMeta extends IPageMeta {
503
596
  /** "%s · Acme" — %s is the page title. Applied only when the page provides a title. */
504
597
  titleTemplate?: string;
598
+ /**
599
+ * Favicon URL → `<link rel="icon">`. Site-level only (a per-page favicon isn't a thing),
600
+ * which is why it lives here rather than on `IPageMeta`.
601
+ */
602
+ icon?: string;
505
603
  }
506
604
 
507
- export declare type IStagedEntry = IStagedStyleEntry | IStagedTextEntry;
605
+ export declare type IStagedEntry = IStagedStyleEntry | IStagedTextEntry | IStagedImageEntry;
606
+
607
+ declare interface IStagedImageEntry {
608
+ kind: "image";
609
+ source: string;
610
+ index: number;
611
+ /** New image URL applied as the element's `src`; its original `src`/`srcset` are stashed. */
612
+ value: string;
613
+ }
508
614
 
509
615
  export declare interface IStagedStyleEntry {
510
616
  kind: "style";
@@ -531,13 +637,22 @@ export declare interface ITextChangedElement {
531
637
  occurrences: number;
532
638
  }
533
639
 
640
+ /**
641
+ * Drops the site's AI chat widget onto every page, wired from `site.json`. Mounted once
642
+ * from the scaffold's `__root` (via the `SiteChat` shim) alongside `<Inspector />`. Self-
643
+ * gates: renders nothing until the workspace resolves a servable active agent, so an
644
+ * unwired or agent-less site paints no launcher. `apiBaseUrl` is intentionally left at the
645
+ * KBChat default (the same-origin `/.levo` proxy) — the serving layer owns that route.
646
+ */
647
+ export declare const LevoChat: React.FC<ILevoChatProps>;
648
+
534
649
  /**
535
650
  * Self-contained event registration widget for generated Studio v3 sites — ticket
536
651
  * selection, coupons, attendee details, payment (Razorpay by default; Stripe Embedded
537
652
  * Checkout when the booking API returns `provider: "stripe"`), and a booking confirmation.
538
653
  *
539
654
  * The AI drops this onto an event page and NEVER rebuilds the flow: it owns the entire
540
- * booking + payment path against the Levo event API. The site client (`useLevoSite`)
655
+ * booking + payment path against the Levo event API. The site client (`useLevoClient`)
541
656
  * auto-stamps the workspace on every request, so no auth/workspace wiring is needed.
542
657
  * `external` turns it into a plain link out to a third-party ticketing URL.
543
658
  */
@@ -565,13 +680,32 @@ export declare const LevoEventRegister: ({ eventID, title, external, buttonText,
565
680
  export declare const LevoForm: React.FC<ILevoFormProps>;
566
681
 
567
682
  /**
568
- * Mounts the Levo client + insights for a generated site. Rendered once from the
569
- * scaffold's `__root` (via the env-reading shim); SSR-safe — construction guards its
570
- * window/document reads, and `insights.init()` (client-side only by contract) runs in
571
- * an effect after mount.
683
+ * Mounts the site's popups, wired from `site.json`. Rendered once from the scaffold's
684
+ * `__root` (via the `SitePopups` shim), inside `<LevoSiteProvider>` — `PopupLoader` reads
685
+ * the Levo client through `useLevo()`, which that provider mounts. The library handles
686
+ * everything else: it fetches active popups, evaluates page/referrer targeting against the
687
+ * current URL, lazy-loads the renderer only when popups exist, and self-hides otherwise.
688
+ *
689
+ * No `pageId` is threaded: that is a studio DB page id with no equivalent in a code-routed
690
+ * site, so page-bound rules don't apply here — site-wide and URL-path targeting (the
691
+ * `is_all_pages` / `contains` / `begins_with` matchers) do.
692
+ */
693
+ export declare const LevoPopups: React.FC<ILevoPopupsProps>;
694
+
695
+ /**
696
+ * Mounts the Levo client + insights for a generated site, and the site-wide overlays that
697
+ * ride on top of it (AI chat + popups). Rendered once from the scaffold's `__root` (via the
698
+ * env-reading shim); SSR-safe — construction guards its window/document reads, and
699
+ * `insights.init()` (client-side only by contract) runs in an effect after mount.
700
+ *
701
+ * Takes the raw `site.json` and derives everything from it: the data client's config, and
702
+ * the site-wide overlays' metadata (workspace/name/slug/icon/branding for chat, id for
703
+ * popups). The overlays render only on a wired site, so they always have a client in scope;
704
+ * each is client-only, lazy, and self-hiding, so they add nothing to the initial page. Omit
705
+ * `site` to mount a bare (null) client context.
572
706
  */
573
707
  export declare const LevoSiteProvider: React.FC<{
574
- config?: ILevoSiteConfig;
708
+ site?: ISiteJson;
575
709
  children: React.ReactNode;
576
710
  }>;
577
711
 
@@ -610,7 +744,7 @@ export declare const parseRawHead: (raw: string) => IComposedHead;
610
744
  /**
611
745
  * Prefetch every binding a page declares into the router's QueryClient — call it (awaited)
612
746
  * from the route `loader` so SSR renders fully-populated HTML. Builds its own client from
613
- * the site config (loaders run outside React, so `useLevoSite` isn't available). Record
747
+ * the site config (loaders run outside React, so the client context isn't available). Record
614
748
  * bindings pull their slug from the route `params` (`$slug`). Resilient: a single failed
615
749
  * source resolves to null so it doesn't blank the whole page — the hook surfaces that
616
750
  * binding's error while the rest render.
@@ -629,13 +763,21 @@ export declare const prefetchLevoSources: ({ queryClient, config, sources, param
629
763
  params?: Record<string, string>;
630
764
  }) => Promise<Record<string, unknown>>;
631
765
 
766
+ /** What `system` resolves to — the class actually on `<html>`. */
767
+ export declare type ResolvedColorMode = "dark" | "light";
768
+
632
769
  /**
633
- * The site's Levo client config from its `site.json`. The platform stamps that file at
634
- * provision and re-syncs it on settings saves; the `?.` on `levo` is load-bearing — a
635
- * site provisioned before the block shipped degrades to an inert client, never a crash.
770
+ * The site's Levo client config from its `site.json` — the only place that mapping lives, so
771
+ * the provider and the SSR route loader can't drift apart.
772
+ *
773
+ * The `?.` on `levo` is load-bearing: a site provisioned before the block shipped degrades to
774
+ * an inert client, never a crash.
636
775
  */
637
776
  export declare const siteConfigFromJson: (site: ISiteJson) => ILevoSiteConfig;
638
777
 
778
+ /** Read/set the current color mode. Throws outside a `<ColorModeProvider>`. */
779
+ export declare function useColorMode(): IColorModeState;
780
+
639
781
  /**
640
782
  * Load a collection's SCHEMA so `LevoForm` can render its fields. Like the other hooks the
641
783
  * config is a static literal (the editor binding); it reads the warm cache the route loader
@@ -656,7 +798,11 @@ export declare const useLevoRecord: <T = Record<string, unknown>>(config: ILevoR
656
798
 
657
799
  /**
658
800
  * The site's Levo client, or `null` when the site isn't wired to a Levo backend yet
659
- * (no `apiUrl` in config — e.g. env not injected at provision). Consumers no-op on null.
801
+ * (no `api_url` in `site.json` — e.g. not stamped at provision). Consumers no-op on null.
802
+ *
803
+ * Site-facing alias only. This package's own components call `@levo-so/react`'s
804
+ * `useLevoClient()` directly so that nothing internal imports THIS module — which keeps the
805
+ * provider, and the chat/popup widgets it mounts, off the SSR route-loader's import path.
660
806
  *
661
807
  * Explicit return type on purpose: inference would inline core-internal names
662
808
  * (`IPageContext`) that the rolled-up d.ts doesn't export → TS4023 on declaration emit.