@levo-so/site-runtime 0.3.21 → 0.3.23

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 (4) hide show
  1. package/CHANGELOG.md +352 -0
  2. package/index.d.ts +56 -1
  3. package/index.js +414 -367
  4. package/package.json +3 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,352 @@
1
+ # @levo-so/site-runtime
2
+
3
+ ## 0.3.23
4
+
5
+ ### Patch Changes
6
+
7
+ - Two records in one URL segment. `levoParams(key, pattern, [siblings])` declares how a route's single param splits into several record variables, e.g. `/health-packages/<package>-in-<city>`:
8
+
9
+ - `const split = levoParams("packages_in_cities", "{$packages}-in-{$cities}")` goes on the route as `params: split`; the page reads `packages` and `cities` from `Route.useParams()`.
10
+ - The segment splits on its rightmost separator. A segment that does not fit is declined, so a plain sibling page (`$packages.tsx`) keeps serving `/health-packages/<package>`.
11
+ - `split.link(values)` is the `params` of a `<Link>` to the page (TanStack types it from every parsed param, so the segment alone does not compile); `split.segment(values)` is the string form.
12
+ - A plain variable beside the split in the same route is named in the third argument: `levoParams("c_in_d", "{$c}-in-{$d}", ["a"])`.
13
+ - Both arguments must be string literals, or top-level consts holding them: the platform reads the declaration out of the route file to enumerate the page's sitemap URLs and pairs each variable with the record binding whose `param` names it.
14
+
15
+ Loud failures for URL and binding mismatches:
16
+
17
+ - A record binding whose `param` names no param of the route throws on staging previews with the route's params listed, and logs on production sites while the record resolves to null as before.
18
+ - A `levoParams` key the route does not have is declined and reported once in the server log; the type check and the platform's publish check refuse it.
19
+ - Pattern errors (no variable, duplicate name, two variables with nothing between them, stray braces) throw when the route module initialises.
20
+
21
+ Requires the matching platform release for sitemap enumeration and publish validation; sites must pin this version for the builder AI to use the feature.
22
+
23
+ - @levo-so/core@0.3.23
24
+ - @levo-so/insights@0.3.23
25
+
26
+ ## 0.3.22
27
+
28
+ ### Internal
29
+
30
+ - The package now ships its `CHANGELOG.md`, which Studio's runtime update hands to Levo AI.
31
+ - The data client drops the `richtext` field type, which nothing used. Nothing to change in site code.
32
+
33
+ ## 0.3.21
34
+
35
+ ### Behaviour changes
36
+
37
+ - **Collection forms take bookings on generated sites.** The booking flow — billing, booking card, payment — moved into the shared form library, so a `LevoForm` bound to a priced collection now goes on to billing and payment instead of saving the entry and stopping. It is styled with the form's own theme, so the site's brand colour and dark mode carry through into billing and the ticket.
38
+ - Whether a submission is a booking or an enquiry is decided before billing. One with no matching price row is saved and gets the form's thank-you screen instead of a blank billing screen. When unsure (fixed price, older settings, a failed lookup) it is treated as a booking.
39
+ - A failed booking keeps the visitor on billing to retry against the same entry, and the redirect after payment follows the form's success action.
40
+ - Requests start their 30-second timeout when the call is made, not when the page loaded, so an action taken more than 30 seconds after page load no longer aborts before it is sent.
41
+
42
+ ### Fixed
43
+
44
+ - The Stripe payment window keeps keyboard focus inside itself; Tab no longer walks onto the page behind it.
45
+
46
+ ### Internal
47
+
48
+ - Dependency security updates. The upgrade's install picks them up; nothing to change in site code.
49
+
50
+ ## 0.3.20
51
+
52
+ ### Behaviour changes
53
+
54
+ - **Event registration is one sheet.** `LevoEventRegister` opens a single scrolling sheet instead of a two-step modal: ticket choice with nothing preselected, a collapsible order summary, inline coupons, attendee details, payment, and an in-sheet confirmation with add-to-calendar that survives a reload. A failed payment returns to the filled-in form, and "Pay again" retries after a closed or failed payment. Signed-in visitors are prefilled. It uses the site's theme colours, so it follows the brand and dark mode.
55
+ - Cashfree checkout for event registration, alongside Razorpay and Stripe.
56
+ - The registration form's fields follow the event's `registration_fields`.
57
+
58
+ ### New
59
+
60
+ - `LevoEventRegister` optional props `startsAt`, `endsAt` (the event's `starts_at` / `ends_at`, shown in the visitor's timezone and used for the calendar entry) and `eventUrl` (the joining link, attached to the calendar entry).
61
+ - `?register=1` on a page URL opens the registration sheet on load — for campaign links and second calls to action.
62
+
63
+ ### Site changes (optional)
64
+
65
+ - Where a page renders `LevoEventRegister` with the event record at hand, pass `startsAt={event.starts_at}`, `endsAt={event.ends_at}` and `eventUrl={event.url}` so the sheet shows the time and the calendar entry has the link. Without them it works as before.
66
+
67
+ ### Fixed
68
+
69
+ - Restores the form fixes from 0.3.16, 0.3.17 and 0.3.18 that 0.3.19 was published without (see 0.3.19, Known issue).
70
+
71
+ ## 0.3.19
72
+
73
+ ### Breaking
74
+
75
+ - **The runtime no longer adds the favicon.** `levoHead` (`createLevoHead`) used to emit `<link rel="icon">` from `site.json`; it no longer does, so a site that relied on it shows the browser's fallback icon (the React logo from `public/favicon.ico`, if that file is still there). Declare exactly one icon in `src/routes/__root.tsx`, in `head().links`, replacing any existing icon link:
76
+ `{ rel: "icon", href: site.icon?.location ?? "https://app.levo.so/favicon.ico" }`
77
+ with `import { LevoSiteProvider, site } from "@/lib/levo/runtime";`. If `src/lib/levo/runtime.tsx` has no `site` export (sites created before August), add it there: `export const site: ISiteJson = siteJson;`, importing `type ISiteJson` from `@levo-so/site-runtime`.
78
+ - `ISiteMeta.icon` is removed. A `levoHead`/`pageHead` call that passes `icon` fails typecheck — delete the field.
79
+
80
+ ### Site changes (optional)
81
+
82
+ - Add `{ title: site.name }` to the root route's `head().meta`, the fallback title for 404s.
83
+ - Delete `public/favicon.ico`, `public/logo192.png`, `public/logo512.png` and `public/manifest.json` if nothing references them — leftovers from the React template.
84
+
85
+ ### Known issue
86
+
87
+ - This build was published without the form fixes from 0.3.16, 0.3.17 and 0.3.18: forms behave as they did in 0.3.15. 0.3.20 restores them. Nothing to change in site code.
88
+
89
+ ## 0.3.18
90
+
91
+ ### Fixed
92
+
93
+ - Forms (`LevoForm`, `LevoEntryForm`, `LevoProfileForm`, popup forms) hide fields the collection marks as not publicly viewable or submittable (`allow_public_view: false`, `allow_public_submit: false`), including sub-fields. A `hiddenFields` list that arrives after the first render is respected.
94
+
95
+ ## 0.3.17
96
+
97
+ ### Fixed
98
+
99
+ - A phone, date-time or rich-text field revealed by form logic no longer blanks and resets the whole form.
100
+
101
+ ## 0.3.16
102
+
103
+ ### Fixed
104
+
105
+ - Form logic: a section whose fields are all hidden no longer leaves an empty gap; step forms skip steps that would be empty; hiding is transitive (if A hides B, fields that depend on B hide too); hidden fields are cleared on submit.
106
+
107
+ ## 0.3.15
108
+
109
+ ### Fixed
110
+
111
+ - Multi-language sites: hovering a link to an unprefixed path (`/pricing` instead of `/en/pricing`) froze the tab. `localeBeforeLoad` now redirects without a loop and keeps the query string. Nothing to change in site code.
112
+ - `LevoBlogContent` renders self-hosted video blocks; YouTube and iframe embeds honour a percent width and left/center/right alignment; YouTube embeds keep 16:9 instead of a fixed 480px height; iframes are at least 100px tall.
113
+
114
+ ### Site changes (optional)
115
+
116
+ - `src/components/language-switcher.tsx`, if present: put `key={locale}` before `{...linkProps}` on the `<a>`. Stops a React "unique key" warning on every page load.
117
+
118
+ ### Internal
119
+
120
+ - Chat widget dependencies (`@assistant-ui/*`) bumped. The upgrade's install picks them up.
121
+
122
+ ## 0.3.14
123
+
124
+ ### Fixed
125
+
126
+ - Links inside `LevoBlogContent` no longer carry `rel="nofollow"` or `rel="noreferrer"`; `rel="noopener"` is added only next to `target="_blank"`. Applies to posts already published.
127
+ - Blockquotes inside blog content use the body text size and colour instead of a smaller muted grey — a visible change on posts with quotes.
128
+
129
+ ## 0.3.13
130
+
131
+ ### Breaking
132
+
133
+ - The data client's blog and event reads now require their options bag: `blog.getAllBlogs(key, query, options)`, `blog.getSingleBlog(key, slug, options)`, `event.getAllEvents(seriesKey, query, options)`, `event.getSingleEvent(slug, options)`, where `options` is `{ workspace_id: string; params?: Record<string, string> }`. Only code that calls the client from `useLevoSite()` directly is affected — `useLevoSource`, `useLevoRecord` and route `dataSources` are not. Those calls were already failing at runtime (events answered 400, blog lists came back empty).
134
+
135
+ ### Site changes
136
+
137
+ - Record bindings take `param`, the route param they read, spelled exactly like the `$segment` in the route's file name: `{ mode: "record", module: "blog", identifier: "so_levo_blog", param: "slug" }`. **Required on a route with two or more `$params`** (e.g. `src/routes/stores/$state/$city.tsx`): without it the loader looks for `params.slug`, finds nothing, and the page renders with no server-side data, title or share image. A route with a single `$param` falls back to it, so `param` is optional there.
138
+
139
+ ### Fixed
140
+
141
+ - Blog and event data sources send the workspace, so event pages stop answering 400 and blog lists stop coming back empty.
142
+ - A detail route whose param isn't `slug` (e.g. `/legal-events/$events`) now prefetches its record, so it gets its per-record title, description and share image.
143
+ - Visitor chat: tables in replies get a "Copy table" button.
144
+
145
+ ## 0.3.12
146
+
147
+ ### Breaking
148
+
149
+ - **`LevoRichText` is renamed `LevoBlogContent`**, and its subpath moves: `@levo-so/site-runtime/rich-text` → `@levo-so/site-runtime/blog-content`; `ILevoRichTextProps` → `ILevoBlogContentProps`. Same props (`content` plus div attributes). No alias: the old import fails typecheck. Fix: `Grep` for `LevoRichText` and `site-runtime/rich-text`, then rename the import and the tag. Where it wraps a plain string (a title, an excerpt, a description), remove the wrapper and render the string instead — the component is only for a stored editor document, i.e. a blog post's `content` or a rich-text collection field.
150
+
151
+ ### New
152
+
153
+ - `useLevoSearch({ limit?, debounceMs?, hotkey? })` — site-wide search behaviour: `query`/`setQuery`, debounced `results`, `isLoading`, `error`, `isEmpty`, and a ⌘K open state (`isOpen`, `open`, `close`). Renders nothing; must sit under `LevoSiteProvider`. Each result is `ILevoSearchResult { content_type, content_id, title, description, url, thumbnail, score }`.
154
+ - `LevoForm` optional props: `defaultValues` (prefill by collection field key), `onSuccess(result)` and `onError(error)`, `showSuccessScreen={false}` (keep the form instead of the thank-you card), `disabled`. Types `ILevoFormSubmitResult`, `ILevoFormSuccessConfig`.
155
+ - `LevoSiteProvider` takes `analytics?: boolean` (default `true`). Sites created from this version pass `analytics={import.meta.env.PROD}` in `src/lib/levo/runtime.tsx` so previews stop counting as visits.
156
+
157
+ ### Site changes (optional)
158
+
159
+ - Sites created from this version ship `src/components/search/SiteSearch.tsx`, `src/components/access/LoginModal.tsx` and `src/components/access/UserMenu.tsx`. An older site does not have them — never import one without checking it exists; build the feature from the hooks instead.
160
+
161
+ ### Behaviour changes
162
+
163
+ - Inside the Studio editor preview, the chat launcher and the "Made with Levo" badge are hidden. The preview URL and the live site are unchanged.
164
+ - The editor can force a gated page's outcome (granted, sign-in, blocked) to design it. On the live site the real session still decides, and the data behind the gate stays protected.
165
+
166
+ ## 0.3.11
167
+
168
+ ### Behaviour changes
169
+
170
+ - Analytics now creates visitor ids in the browser and sets first-party cookies `id_i_cd` (device) and `id_i_cs` (session) plus localStorage `lv_insights_identity`; `lv_aud_*` and `lv_insights_sf` are no longer written. Update any cookie banner or privacy page that names cookies. No code changes.
171
+
172
+ ## 0.3.10
173
+
174
+ ### New
175
+
176
+ - Data sources read blog categories, tags and authors (`module: "blog_category" | "blog_tag" | "blog_author"`, `identifier` = the parent blog's key) and community forum posts (`module: "community"`, `identifier` = the forum key). Type `ILevoSourceModule`.
177
+ - `LevoRichText` from `@levo-so/site-runtime/rich-text` — renamed in 0.3.12; do not use this name.
178
+
179
+ ### Behaviour changes
180
+
181
+ - `LevoSiteProvider` shows a small "Made with Levo" badge in the bottom-right corner when the workspace's plan requires branding (`site.json` → `feature_flag.branding`). Remove any hand-built Levo badge so it doesn't show twice.
182
+ - `@levo-so/site-runtime/setup.css` now includes the blog-content prose styles.
183
+ - Visitor chat shows when each message was sent ("9:42 AM", "Aug 19 · 9:42 AM").
184
+
185
+ ### Site changes (optional)
186
+
187
+ - `__root.tsx` `head().links`: `{ rel: "preconnect", href: "https://static.levocdn.com" }` makes Levo-hosted images start loading sooner.
188
+
189
+ ## 0.3.9
190
+
191
+ ### New
192
+
193
+ - Multi-language sites. Inert until the workspace has two or more locales; single-language sites are unaffected.
194
+ - `@levo-so/site-runtime/start` (new subpath): `createLocaleMiddleware(config, routeTree)`, used in `src/start.ts`.
195
+ - `readLocaleConfig(site)`, `localeBeforeLoad(config, routeTree)`, `SiteLocaleProvider`, `LocaleHreflang`, `useUrlLocale(config)`, `useLocale()`, `useLanguageOptions()` (link props for a language switcher).
196
+ - `ISiteJson` gains `locales` and `default_locale`.
197
+ - Do not add this wiring during an upgrade. It is only needed once a workspace adds a second language, and switching it on moves every URL under a language prefix. When wiring it, never import `@/routeTree.gen` into `__root.tsx` — the import cycle makes every server render fail.
198
+
199
+ ### Behaviour changes
200
+
201
+ - Canonical URLs drop the trailing slash (`/blog/` → `/blog`; the root keeps its `/`). Visible to search engines.
202
+ - New-entry forms prefill the collection's default values. Clearing a relation field now saves (it is sent as empty instead of left out).
203
+
204
+ ### Internal
205
+
206
+ - Tiptap bumped to 3.30, Stripe and Razorpay pinned; new optional peer `@tanstack/react-start`, which every site already has. The upgrade's install picks them up.
207
+
208
+ ## 0.3.8
209
+
210
+ ### New
211
+
212
+ - Members-only pages (page gating):
213
+ - `useLevoAccess({ id, access })` — the page gate: `state` (`checking | granted | blocked`), `code`, `rule`, `authMethods`, `recheck`. Stays closed on any failure.
214
+ - `useLevoAuth()` (the visitor's session: `account`, `isLoggedIn`, `isLoaded`, `refresh`, `signOut`), `useLevoLogin()` (`sendCode`, `resendCode`, `verifyCode`, `sendMagicLink`, each returning `{ ok, data | error }`), `useLevoOAuth(...)` (Google, Microsoft, LinkedIn popup sign-in), `useLevoOtpInput(...)`.
215
+ - `LevoEntryForm` (request access to a collection), `LevoProfileForm` (complete the member profile).
216
+ - Validators `isValidEmail`, `isValidOtp`, `isValidPhone`, `normalizePhone`; `LEVO_OTP_LENGTH`.
217
+ - The loader from `createLevoLoader(site)` takes the route's meta as an optional second argument: `loader: levoLoader(dataSources, meta)`. A gated page then skips its server-side prefetch, so its data never reaches a visitor the gate would refuse.
218
+
219
+ ### Site changes (only for gated pages)
220
+
221
+ - Sites created from this version ship the gate UI in `src/components/access/` (`AccessGate.tsx`, `LoginCard.tsx` and the rest). An older site does not have that folder — never import from `@/components/access/` without checking it exists; build the gate from `useLevoAccess` and `useLevoLogin` instead.
222
+
223
+ ### Fixed
224
+
225
+ - A form with a phone, date-time or rich-text field no longer blanks the page while those fields load.
226
+
227
+ ## 0.3.7
228
+
229
+ ### Behaviour changes
230
+
231
+ - Visitor chat: on a site with several chat agents, the agent follows the page while the conversation carries over; reloading the page starts a fresh conversation; suggested questions come from the current agent and fill the input instead of sending; while an answer streams, Stop is the only action.
232
+ - Event registration (`LevoEventRegister`): the tax/GST block is hidden when nothing is payable, and the dialog scrolls inside itself on short screens.
233
+ - Popups: a redirect popup's cover image is clickable; popup content loads when the popup opens.
234
+
235
+ ### Internal
236
+
237
+ - Chat widget dependencies (`@assistant-ui/*`) move from 0.12 to 0.15. The chat panel breaks on first open if the site's lockfile keeps the old versions; the upgrade's install picks up the new ones.
238
+
239
+ ## 0.3.1
240
+
241
+ ### Patch Changes
242
+
243
+ - 88218da: Sites now start insights, expose the runtime handles, and carry their page id.
244
+
245
+ - `LevoSiteProvider` calls `insights.init()` after mount. Nothing on a code-routed site did — Levo-hosted pages get it from `LevoPage`'s `LevoInsightsPage` — so `$isIdentityContextReady` never flipped and every store-only consumer (popup triggers, DOM trackers) waited forever. Popups were silently dead on generated sites.
246
+ - `window.levo_client` / `window.levo_insights` are set on generated sites, matching what `LevoPage` exposes on Levo-hosted ones.
247
+ - Page identity flows route → runtime: `meta/<route>.json`'s `id` (new `IPlatformMetaFile.id`) is stamped into the head by `levoHead` and read back by the provider on mount and on every client navigation (`readPageId` / `PAGE_ID_META`, both exported). It feeds analytics `page_id` and `LevoPopups`' new `pageId` prop, which makes page-bound popup targeting work on code-routed sites. Sites whose meta files carry no id are unaffected — URL-path targeting behaves as before.
248
+
249
+ ### Patch Changes
250
+
251
+ - insights and popup fixes
252
+
253
+ ## 0.3.0
254
+
255
+ ### Minor Changes
256
+
257
+ - 1e929c2: Inspector: selection captures for the editor's AI chat.
258
+
259
+ - `capture-selected` message — rasterizes the currently selected element to a PNG data URL (html-to-image, lazy-loaded chunk) and replies `capture-result`.
260
+ - Select-mode marquee drag (region select): press-drag draws a dashed rubber-band; release reports `region-selected` with the dragged rect, the topmost source-stamped elements substantially inside it (capped at 12), and a PNG of exactly that area. Sub-threshold presses remain single-element click picks; Esc mid-drag cancels the marquee only. Driven by pointer events (the swallow layer's `pointerdown` preventDefault suppresses all compatibility mouse events — mouse-based drag listeners never fire in select mode). Pointer capture at drag activation makes the gesture edge-tolerant: overshooting the iframe boundary clamps the marquee to the frame instead of cancelling, and releasing outside still finalizes.
261
+ - Region highlights: after a drag, every included element gets a persistent green ring (fixed pool, part of the inspector's own overlay set) — repositioned on scroll/resize, suppressed in text mode, cleared by the same paths as the single-pick ring (new pick, clear-selection, route change).
262
+ - Correct capture backgrounds: renders are subtree-only, so ancestor-painted backgrounds used to come out transparent ("wrong bg"). Element captures now fill with the effective ancestor background color; region captures climb to the nearest background-painting ancestor as the render root (bounded at 4 viewport-areas) so crops include real gradient/image backgrounds, with the color fill as fallback. The intermediate region raster is capped at a 4000px longest edge so a small drag inside a large painted section can't spike memory or stall the preview.
263
+
264
+ ### Patch Changes
265
+
266
+ - select element ss
267
+
268
+ ## 0.2.3
269
+
270
+ ### Patch Changes
271
+
272
+ - dark mode, popups, site agents
273
+
274
+ ## 0.2.2
275
+
276
+ ### Patch Changes
277
+
278
+ - image edit and better shortcut support
279
+
280
+ ## 0.2.1
281
+
282
+ ### Patch Changes
283
+
284
+ - fa254dc: Studio v3 sites can now render events and take registrations.
285
+
286
+ - Data sources: `module: "event"` works with `useLevoSource` (events in a series, keyed
287
+ by the series id) and `useLevoRecord` (one event detail page by slug).
288
+ - New `LevoEventRegister` component — a self-contained ticket booking + payment widget
289
+ (Razorpay by default, Stripe Embedded Checkout when the booking API returns
290
+ `provider: "stripe"`). Generated event pages drop this in and never rebuild the flow.
291
+ - Adds `@stripe/stripe-js`, `@stripe/react-stripe-js`, `react-razorpay` dependencies and
292
+ `lucide-react` + `react-dom` peer dependencies. The registration dialogs are built on
293
+ Base UI (`@base-ui/react`, an optional peer) for focus trapping, scroll lock, and
294
+ accessible dialog semantics.
295
+
296
+ - fa254dc: Collection-backed forms. Generated sites can now collect submissions into a collection.
297
+
298
+ - New `mode: "form"` data source — the route loader prefetches the collection SCHEMA
299
+ (`getCollectionByIDExpanded`) SSR via `prefetchLevoSources`, and `useLevoForm(config)`
300
+ reads it from the warm cache.
301
+ - New `LevoForm` component — renders the collection's fields, validates, submits, shows a
302
+ success screen, and handles captcha. It reuses the shared `CollectionFormRenderer`, which
303
+ resolves its client from `@levo-so/react`'s `useLevo()` context. `LevoSiteProvider` now
304
+ mounts that context at the site root (it delegates to `@levo-so/react`'s `LevoProvider`,
305
+ a trivial SSR-safe context after the P5 SDK diet), so the renderer works with no per-form
306
+ provider bridge. `useLevoSite`/`useLevoInsights` are null-safe re-exports of
307
+ `@levo-so/react`'s `useLevoClient`/`useLevoInsights`.
308
+ - `LevoForm` auto-themes to the site: it maps the renderer's full color contract onto the
309
+ starter's semantic tokens (`--color-brand`, `--color-ink`, `--color-border`…) via levocn's
310
+ `inlineBaseTheme`, so a form matches the brand out of the box and re-skins when `theme.css`
311
+ changes. Its prop surface is intentionally narrow (`source`, `submitLabel`, `hiddenFields`,
312
+ `formVariant`, `successConfig`, `theme`, `className`) rather than exposing the renderer's
313
+ full context; `theme` allows per-form color/radius overrides.
314
+ - The form renderer (`@levo.libraries/react-collection` + `editor-core` + `levocn`) and the
315
+ `@levo-so/react` client context it depends on are now BUNDLED into this package's dist
316
+ (mirroring how `@levo-so/studio` ships). A consuming site installs only
317
+ `@levo-so/site-runtime` from public npm — no private registry, and `@levo-so/react` never
318
+ appears as a dependency. The ~40 public leaf deps (tiptap, yup, react-select, sonner…) are
319
+ hoisted into this package. Bundled styles ship as `@levo-so/site-runtime/setup.css`.
320
+ - Build change: switched from per-module output to studio-style bundled output
321
+ (`publishConfig.directory: "dist"`, generated `dist/package.json`, rolled-up types).
322
+
323
+ - full cleanup
324
+ - Updated dependencies [fa254dc]
325
+ - Updated dependencies
326
+ - @levo-so/core@0.2.1
327
+ - @levo-so/insights@0.2.1
328
+
329
+ ## 0.2.0
330
+
331
+ ### Minor Changes
332
+
333
+ - 54e5118: Package the scaffold contract so starters stay thin shims: `createLevoHead(siteJson)` —
334
+ each route imports its own `meta/<route>.json` (`@meta/` alias) and wires
335
+ `head: levoHead(meta)`; composes the platform meta keys (incl. `ld_json`, `canonical`,
336
+ `twitter_card`, `head_code`, `disable_indexing`) with site.json fallbacks
337
+ ("Page name | Site name") into the document head. Plus `createLevoLoader` (the SSR
338
+ data-source route loader) and `siteConfigFromJson` (site.json → ILevoSiteConfig). The
339
+ starter's single `src/lib/levo/runtime.tsx` shim only feeds in `site.json`; every future
340
+ behavior change ships by bumping this package.
341
+
342
+ ### Patch Changes
343
+
344
+ - updated meta folders
345
+
346
+ ## 0.1.113
347
+
348
+ ### Patch Changes
349
+
350
+ - Updated dependencies [d885aa0]
351
+ - @levo-so/core@0.1.113
352
+ - @levo-so/insights@0.1.113
package/index.d.ts CHANGED
@@ -883,6 +883,51 @@ export declare interface ILevoOtpSlot {
883
883
  };
884
884
  }
885
885
 
886
+ /**
887
+ * One split segment, declared once: the route's `params` option IS this object, and every
888
+ * link to the route reuses the same const, so the pattern is never spelled twice.
889
+ * `TSiblings` names the route's other required params, `never` when the segment is the only
890
+ * one (see `levoParams`).
891
+ */
892
+ export declare interface ILevoParams<TSegment extends string, TPattern extends string, TSiblings extends string = never> {
893
+ /**
894
+ * TanStack's `params.parse`: the variables split out of the segment beside the raw key and
895
+ * the siblings, or `false` for a segment the pattern does not fit, which declines the route
896
+ * so the sibling route with no pattern serves the URL. Throws when the key is not a param of
897
+ * the route at all (the first argument misspells the file's `$param`): see `parse` below.
898
+ */
899
+ parse: (raw: Record<TSegment | NoInfer<TSiblings>, string>) => ILevoParsedParams<TSegment, TPattern, TSiblings> | false;
900
+ /**
901
+ * The segment the values compose (`"full-body-in-mumbai"`), for a string href: a JSON-LD
902
+ * url, a sitemap-style index. A missing or empty value throws: a link with a hole is a bug,
903
+ * not a URL.
904
+ */
905
+ segment: (values: Record<ILevoPatternVars<TPattern>, string>) => string;
906
+ /**
907
+ * The `params` of a `<Link>` to the route: the values plus the composed segment under the
908
+ * raw key, with the sibling values passed through so a `<Link to="/$a/$c_in_d">` gets `a`
909
+ * beside the segment. Exists because TanStack types `Link params` from the route's PARSED
910
+ * params, so the variables are required beside the segment and the segment alone does not
911
+ * compile. Throws for a missing or empty value, as `segment` does.
912
+ */
913
+ link: (values: Record<ILevoPatternVars<TPattern> | TSiblings, string>) => ILevoParsedParams<TSegment, TPattern, TSiblings>;
914
+ }
915
+
916
+ /**
917
+ * What `parse` returns and `link` composes: the route's raw segment param (TanStack's
918
+ * parsed-params type must keep it), the route's other required params named as siblings
919
+ * (TanStack types the route's params from this alone, so a sibling not named here drops out
920
+ * of the route's types), plus one string per pattern variable.
921
+ */
922
+ export declare type ILevoParsedParams<TSegment extends string, TPattern extends string, TSiblings extends string = never> = Record<TSegment | TSiblings, string> & Record<ILevoPatternVars<TPattern>, string>;
923
+
924
+ /**
925
+ * The variable names of a pattern as a union of literals:
926
+ * `ILevoPatternVars<"{$packages}-in-{$cities}">` is `"packages" | "cities"`.
927
+ * A pattern that is not a literal has no inferable names (`never`).
928
+ */
929
+ export declare type ILevoPatternVars<TPattern extends string> = TPattern extends `${string}{$${infer TName}}${infer TRest}` ? TName | ILevoPatternVars<TRest> : never;
930
+
886
931
  export declare interface ILevoPopupsProps {
887
932
  /**
888
933
  * The site's id (`site.json` `id`) — scopes the active-popups fetch. Absent → the
@@ -921,7 +966,9 @@ export declare interface ILevoRecordConfig {
921
966
  * Route param this binding's slug comes from — `"slug"` on `/blog/$slug`, `"events"` on
922
967
  * `/legal-events/$events`. Every generated record binding carries it: it is the mapping a
923
968
  * multi-param route needs, since with two variable segments nothing else in the file says
924
- * which binding fills which slot. Round-tripped through the editor binding like the rest.
969
+ * which binding fills which slot. On a `levoParams` page it names one pattern variable
970
+ * (`"packages"` for `{$packages}-in-{$cities}`), which is how the platform pairs each variable
971
+ * with a collection. Round-tripped through the editor binding like the rest.
925
972
  *
926
973
  * Optional only for the pages written before it existed — see {@link recordSlug}.
927
974
  */
@@ -1755,6 +1802,8 @@ export declare const LevoEventRegister: ({ eventID, title, external, buttonText,
1755
1802
  */
1756
1803
  export declare const LevoForm: React.FC<ILevoFormProps>;
1757
1804
 
1805
+ export declare const levoParams: <TSegment extends string, TPattern extends string, TSiblings extends string = never>(key: TSegment, pattern: TPattern, _siblings?: readonly TSiblings[]) => ILevoParams<TSegment, TPattern, TSiblings>;
1806
+
1758
1807
  /**
1759
1808
  * Mounts the site's popups, wired from `site.json`. Rendered once from the scaffold's
1760
1809
  * `__root` (via the `SitePopups` shim), inside `<LevoSiteProvider>` — `PopupLoader` reads
@@ -1936,6 +1985,12 @@ export declare const parseRawHead: (raw: string) => IComposedHead;
1936
1985
  * source resolves to null so it doesn't blank the whole page — the hook surfaces that
1937
1986
  * binding's error while the rest render.
1938
1987
  *
1988
+ * One failure is NOT absorbed outside production: a record binding whose `param` names no
1989
+ * param of the route. That is a wiring mistake in the page (never a transient fetch error),
1990
+ * it renders as a not-found record that no one can tell from a bad slug, and the preview is
1991
+ * where it gets fixed — so there the loader rejects with the message and the route's error
1992
+ * boundary shows it. See the branch below for what "preview" can mean here.
1993
+ *
1939
1994
  * Returns the fetched values keyed by source key (records as the record object, lists as
1940
1995
  * `{data, meta}`). The route loader returns this map, making it the route's `loaderData` —
1941
1996
  * which `levoHead`'s dynamic config reads for per-record titles/descriptions/share images.