@voltro/web 0.51.0 → 0.53.0

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/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ import { Ref } from 'react';
11
11
  import { ResolvableHeaders } from '@voltro/client';
12
12
  import { Rpc } from '@effect/rpc';
13
13
  import { RpcGroup } from '@effect/rpc';
14
+ import { Schema } from 'effect';
14
15
 
15
16
  /** Decision outcome for a document-level anchor click. */
16
17
  export declare type AnchorNavigationDecision = {
@@ -167,6 +168,13 @@ declare interface CompiledRoute extends PageDescriptor {
167
168
 
168
169
  export declare const compileRoute: (route: PageDescriptor) => CompiledRoute;
169
170
 
171
+ /**
172
+ * Decode a query string against a page's `searchParams` schema. Total: an
173
+ * invalid query yields the schema's defaults; only a schema that cannot even
174
+ * decode `{}` (a required field with no default) throws, naming the fix.
175
+ */
176
+ export declare const decodeSearchParams: <A>(schema: SearchParamsSchema<A>, usp: URLSearchParams) => A;
177
+
170
178
  /** The English defaults. A deployment overrides any subtree via
171
179
  * {@link FallbackStringsProvider} or a per-component `strings` prop;
172
180
  * unspecified keys fall through to these. */
@@ -221,6 +229,18 @@ export declare interface DevStatus {
221
229
 
222
230
  export declare type DevStatusKind = 'compiling' | 'loading' | 'reconnecting' | 'error';
223
231
 
232
+ /**
233
+ * Canonical link-side encode — the schema-free half. `routes.generated.ts`
234
+ * types a link's params through a TYPE-ONLY import; at runtime the values are
235
+ * serialised canonically: strings pass through, numbers/booleans via
236
+ * `String()`, arrays as repeated keys, `undefined` omitted. `Date` (or any
237
+ * object) is refused loudly: a link must encode to the exact string form the
238
+ * schema's decode side accepts, and there is no canonical Date string the type
239
+ * layer could guarantee — declare the field as a string/number transform
240
+ * instead.
241
+ */
242
+ export declare const encodeSearchParams: (params: Readonly<Record<string, unknown>>) => URLSearchParams;
243
+
224
244
  export declare interface ErrorBoundaryProps {
225
245
  readonly error: unknown;
226
246
  readonly reset: () => void;
@@ -322,12 +342,20 @@ export declare const getStatuses: () => ReadonlyArray<DevStatus>;
322
342
 
323
343
  /**
324
344
  * Client-side runtime: scan the document for `[data-voltro-island]`
325
- * markers and schedule each for hydration per its strategy. Called once
326
- * at mount time by `@voltro/web/mount` when the page declares
327
- * `interactive: 'islands'`. Idempotent running twice is a no-op (we
328
- * mark elements as visited).
345
+ * markers and schedule each for hydration per its strategy. Called by the
346
+ * per-page islands entry, and by `@voltro/web/mount` when an islands page
347
+ * is reached through the full app bundle (SPA navigation). Idempotent —
348
+ * running twice is a no-op (we mark elements as visited).
329
349
  */
330
- export declare const hydrateIslandsOnPage: () => void;
350
+ export declare const hydrateIslandsOnPage: (options?: HydrateIslandsOptions) => void;
351
+
352
+ export declare interface HydrateIslandsOptions {
353
+ /** Wrap every island root — the per-page islands entry of a page whose
354
+ * islands use framework hooks passes a `<VoltroRuntimeProvider>` here so
355
+ * a `useSubscription` island gets a live runtime. Purely presentational
356
+ * entries pass nothing and never load the client core. */
357
+ readonly wrap?: (children: ReactNode) => ReactNode;
358
+ }
331
359
 
332
360
  export declare type HydrateStrategy =
333
361
  /** Hydrate as soon as the client runtime mounts (after main script load). */
@@ -341,9 +369,14 @@ export declare type HydrateStrategy =
341
369
  | 'interaction'
342
370
  /** Never hydrate. Useful for fully-static islands (e.g. SSR-only data
343
371
  * display that never changes). */
344
- | 'never';
345
-
346
- declare const Image_2: ({ src, alt, width, height, fill, sizes, priority, loader, placeholder, blurDataURL, style, ...rest }: ImageProps) => ReactElement;
372
+ | 'never'
373
+ /** CLIENT-ONLY: the server renders an empty placeholder (never the
374
+ * component a browser-only lib touching `window` in render would
375
+ * crash the SSR pass), and the client mounts fresh with `createRoot`
376
+ * instead of hydrating. Astro's `client:only`. */
377
+ | 'only';
378
+
379
+ declare const Image_2: ({ src, alt, width, height, fill, sizes, priority, loader, quality, placeholder, blurDataURL, style, ...rest }: ImageProps) => ReactElement;
347
380
  export { Image_2 as Image }
348
381
 
349
382
  /** App-level default loader for every `<Image>` below it. A per-image
@@ -355,14 +388,16 @@ export declare const ImageConfigProvider: ({ loader, children, }: {
355
388
 
356
389
  /** Maps a logical src + a target pixel width to a concrete URL. The hook
357
390
  * for on-the-fly resizing — e.g. `({src,width}) => \`${src}?w=${width}\``
358
- * against an image CDN or the storage serve endpoint. */
391
+ * against an image CDN or the storage serve endpoint. `quality` is set when
392
+ * the `<Image quality>` prop is — forward it if your CDN supports it. */
359
393
  export declare type ImageLoader = (params: {
360
394
  readonly src: string;
361
395
  readonly width: number;
396
+ readonly quality?: number;
362
397
  }) => string;
363
398
 
364
399
  export declare interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src' | 'width' | 'height' | 'loading' | 'srcSet'> {
365
- readonly src: string;
400
+ readonly src: string | OptimizedImageAsset;
366
401
  /** Required — accessibility. Use `alt=""` for purely decorative images. */
367
402
  readonly alt: string;
368
403
  /** Intrinsic width in px. Required unless `fill`. */
@@ -379,7 +414,14 @@ export declare interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElem
379
414
  readonly priority?: boolean;
380
415
  /** Per-image URL loader (overrides the ImageConfigProvider default). */
381
416
  readonly loader?: ImageLoader;
382
- /** `'blur'` paints `blurDataURL` behind the image until it loads. */
417
+ /** Encode quality hint 1–100 forwarded to the URL loader (a hero wants 90,
418
+ * a thumbnail 50). For a build-optimized `?image` asset the quality was
419
+ * baked at build time (`images.quality` in app.config.ts) — this prop
420
+ * applies to loader/CDN-resolved URLs. */
421
+ readonly quality?: number;
422
+ /** `'blur'` paints `blurDataURL` behind the image until it loads. A
423
+ * build-optimized asset carries its own blur and defaults to `'blur'`;
424
+ * pass `'empty'` to opt out. */
383
425
  readonly placeholder?: 'blur' | 'empty';
384
426
  readonly blurDataURL?: string;
385
427
  }
@@ -446,7 +488,7 @@ export declare const lazyPageRoute: (pattern: string, load: () => Promise<Record
446
488
  readonly chain?: ReadonlyArray<RouteSegment>;
447
489
  }) => PageDescriptor;
448
490
 
449
- export declare const Link: ({ to, children, onClick, prefetch: prefetchProp, replace, onMouseEnter, onFocus, ref, ...anchorProps }: LinkProps) => ReactNode;
491
+ export declare const Link: ({ to, children, onClick, prefetch: prefetchProp, replace, transition, onMouseEnter, onFocus, ref, ...anchorProps }: LinkProps) => ReactNode;
450
492
 
451
493
  export declare interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'onClick'> {
452
494
  readonly to: VoltroUrl;
@@ -457,6 +499,11 @@ export declare interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLA
457
499
  /** Swap the current history entry instead of pushing a new one —
458
500
  * mirror of `NavigateOptions.replace`. */
459
501
  readonly replace?: boolean;
502
+ /** Run this link's route swap through `document.startViewTransition`
503
+ * (`true`) or force it off (`false`), overriding the app-wide
504
+ * `router.viewTransitions` default — mirror of
505
+ * `NavigateOptions.transition`. */
506
+ readonly transition?: boolean;
460
507
  /**
461
508
  * Forwarded to the underlying `<a>`.
462
509
  *
@@ -638,6 +685,21 @@ export declare type LoaderFn<T = unknown, Procedures = unknown> = (ctx: LoaderCo
638
685
  */
639
686
  export declare type LoaderQuery<Procedures = unknown> = Procedures extends ProcedureTypeMap ? <Tag extends keyof Procedures & string>(tag: Tag, ...input: Record<string, never> extends ProcedureInput<Procedures[Tag]> ? [input?: ProcedureInput<Procedures[Tag]>] : [input: ProcedureInput<Procedures[Tag]>]) => Promise<ProcedureOutput<Procedures[Tag]>> : <T = unknown>(tag: string, input?: Record<string, unknown>) => Promise<T>;
640
687
 
688
+ /**
689
+ * Reference a family declared in `app.config.ts` `fonts:`.
690
+ *
691
+ * const inter = localFont('Inter')
692
+ * <main style={{ fontFamily: inter.fontFamily }}>…</main>
693
+ */
694
+ export declare const localFont: (family: string) => LocalFontHandle;
695
+
696
+ export declare interface LocalFontHandle {
697
+ /** The CSS custom property the shell defines: `--font-<slug>`. */
698
+ readonly variable: string;
699
+ /** Ready-to-use `font-family` value: `var(--font-<slug>)`. */
700
+ readonly fontFamily: string;
701
+ }
702
+
641
703
  /** The URL path a logical page takes for a given locale under URL-prefix
642
704
  * routing: the default locale keeps the bare path, every other locale gets a
643
705
  * `/<locale>` prefix. `/about` @ `de` → `/de/about`; @ the default → `/about`. */
@@ -707,6 +769,14 @@ export declare interface NavigateOptions {
707
769
  readonly replace?: boolean;
708
770
  /** Set to `false` to preserve the current scroll position. Defaults to true. */
709
771
  readonly scroll?: boolean;
772
+ /**
773
+ * Run this navigation's visual route swap through
774
+ * `document.startViewTransition` (`true`) or force it off (`false`),
775
+ * overriding the app-wide `router.viewTransitions` default for this one
776
+ * navigation. Browsers without the API and users with
777
+ * `prefers-reduced-motion: reduce` always get the plain swap regardless.
778
+ */
779
+ readonly transition?: boolean;
710
780
  }
711
781
 
712
782
  export declare const NavigationIndicator: () => ReactNode;
@@ -742,6 +812,31 @@ export declare class NotFoundError extends Error {
742
812
 
743
813
  export declare const noticeIslandsShipTheFullBundle: (isDev: boolean) => void;
744
814
 
815
+ /**
816
+ * A build-optimized static asset — what a `?image` import resolves to:
817
+ *
818
+ * import hero from './hero.jpg?image'
819
+ * <Image src={hero} alt="…" />
820
+ *
821
+ * The CLI's sharp pipeline produced the variants at build time (on demand in
822
+ * dev); `<Image>` renders them as a `<picture>` with one `<source>` per
823
+ * modern format and infers width/height/blur — the CLS-required intrinsic
824
+ * size stops being a hand-written prop for imported assets.
825
+ */
826
+ export declare interface OptimizedImageAsset {
827
+ readonly src: string;
828
+ readonly width: number;
829
+ readonly height: number;
830
+ readonly blurDataURL: string;
831
+ /** Fallback-format srcSet (jpeg/png). */
832
+ readonly srcSet: string;
833
+ /** Modern-format sources, in preference order (avif before webp). */
834
+ readonly sources: ReadonlyArray<{
835
+ readonly type: string;
836
+ readonly srcSet: string;
837
+ }>;
838
+ }
839
+
745
840
  export declare const PAGE_SLOT_ATTR = "data-voltro-page-slot";
746
841
 
747
842
  export declare interface PageDescriptor<TLoaderData = unknown> {
@@ -794,6 +889,19 @@ export declare interface PageDescriptor<TLoaderData = unknown> {
794
889
  * set this to `'islands'` so only marked interactive zones
795
890
  * hydrate; the rest stays pure static HTML. */
796
891
  readonly interactive?: InteractiveMode | undefined;
892
+ /**
893
+ * Intercepting route (modal-with-URL): when a SOFT navigation reaches this
894
+ * page FROM one of the `from` route patterns, the page renders as an
895
+ * OVERLAY above the still-mounted origin page instead of replacing it —
896
+ * URL changes, Back closes the overlay, a hard load of the same URL
897
+ * renders the page standalone. `from` is one or more route PATTERNS
898
+ * (`'/photos'`, `'/albums/[id]'`); a soft navigation from any other route
899
+ * renders standalone. Full-hydration pages only (`interactive: 'full'`) —
900
+ * islands/none pages have no client router to intercept with.
901
+ */
902
+ readonly intercept?: {
903
+ readonly from: string | ReadonlyArray<string>;
904
+ } | undefined;
797
905
  }
798
906
 
799
907
  export declare interface PageMeta {
@@ -964,6 +1072,9 @@ export declare type RenderMode = (typeof RENDER_MODES)[number];
964
1072
 
965
1073
  /* Excluded from this release type: renderRouterProviderTree */
966
1074
 
1075
+ /** Test seam. */
1076
+ export declare const resetScriptRegistryForTest: () => void;
1077
+
967
1078
  /**
968
1079
  * Decide whether a click on (or inside) an anchor should be handled by the
969
1080
  * SPA router or left to the browser. Pure — no side effects, no
@@ -1005,7 +1116,7 @@ export declare interface RouteMatch {
1005
1116
  readonly params: Readonly<Record<string, string>>;
1006
1117
  }
1007
1118
 
1008
- export declare const Router: ({ routes, notFounds, notFound: NotFound, errorFallback: ErrorFallback, }: RouterProps) => ReactNode;
1119
+ export declare const Router: ({ routes, notFounds, notFound: NotFound, errorFallback: ErrorFallback, viewTransitions, }: RouterProps) => ReactNode;
1009
1120
 
1010
1121
  /* Excluded from this release type: RouterContext */
1011
1122
 
@@ -1040,6 +1151,15 @@ export declare interface RouterProps {
1040
1151
  readonly notFound?: ComponentType;
1041
1152
  /** Fallback ErrorBoundary used when neither the leaf nor its chain supplies one. */
1042
1153
  readonly errorFallback?: ComponentType<ErrorBoundaryProps>;
1154
+ /**
1155
+ * Run SPA route swaps through `document.startViewTransition` — the browser
1156
+ * cross-fades old and new page (customizable via `::view-transition-*` CSS).
1157
+ * Set from `app.config.ts` `router.viewTransitions`; individual navigations
1158
+ * override it via `navigate(to, { transition })` / `<Link transition>`.
1159
+ * Browsers without the API and `prefers-reduced-motion: reduce` fall back to
1160
+ * the exact untransitioned behavior.
1161
+ */
1162
+ readonly viewTransitions?: boolean;
1043
1163
  }
1044
1164
 
1045
1165
  /**
@@ -1081,10 +1201,55 @@ declare interface RouteSnapshot {
1081
1201
  readonly loaderData: unknown;
1082
1202
  }
1083
1203
 
1204
+ /**
1205
+ * Declarative third-party script. Renders NOTHING into the HTML — the tag is
1206
+ * injected client-side per `strategy`, deduplicated process-wide by `src`
1207
+ * (external) or `id` (inline). Behavior per `interactive` mode is decided,
1208
+ * not accidental: on an `interactive: 'none'` page the app bundle never runs,
1209
+ * so a `<Script>` never fires (the build warns by name); on an islands page
1210
+ * only `<Script>` INSIDE an island ever mounts — a `visible` island's script
1211
+ * loads when the island hydrates (the build warns about `<Script>` in the
1212
+ * static part).
1213
+ */
1214
+ export declare const Script: ({ src, children, id, strategy, onLoad, onError, nonce, attributes, }: ScriptProps) => ReactElement | null;
1215
+
1216
+ export declare interface ScriptProps {
1217
+ /** External script URL. Exactly one of `src` / `children` is required. */
1218
+ readonly src?: string;
1219
+ /** Inline script body. Requires `id` (the dedupe key — inline code has no
1220
+ * src to key on). */
1221
+ readonly children?: string;
1222
+ /** Dedupe key for inline scripts; optional label for external ones. */
1223
+ readonly id?: string;
1224
+ /** `afterInteractive` (default): inject once this component mounts —
1225
+ * i.e. after hydration. `lazyOnload`: wait for browser idle. */
1226
+ readonly strategy?: 'afterInteractive' | 'lazyOnload';
1227
+ /** Fires when the script has executed — ALSO on a re-mount of the same
1228
+ * script, answered from the registry (the script itself is never
1229
+ * re-fetched or re-run). */
1230
+ readonly onLoad?: () => void;
1231
+ readonly onError?: (error: unknown) => void;
1232
+ /** CSP nonce. Absent → the document's nonce is propagated when one exists. */
1233
+ readonly nonce?: string;
1234
+ /** Extra attributes for the tag (`data-*`, `crossorigin`, …). */
1235
+ readonly attributes?: Readonly<Record<string, string>>;
1236
+ }
1237
+
1238
+ declare const SEARCH_PARAMS_BRAND: unique symbol;
1239
+
1084
1240
  /** The next search-params value: either the params object directly, or an
1085
1241
  * updater that receives the CURRENT `URLSearchParams` and returns the next. */
1086
1242
  export declare type SearchParamsInit = URLSearchParams | Readonly<Record<string, string>> | ((current: URLSearchParams) => URLSearchParams | Record<string, string>);
1087
1243
 
1244
+ /** A search-params schema: decodes a plain `Record<string, string | string[]>`
1245
+ * (the canonical URLSearchParams view) into the page's typed shape. */
1246
+ export declare type SearchParamsSchema<A> = Schema.Schema<A, any, never>;
1247
+
1248
+ /** The canonical object view of a query string: repeated keys become arrays. */
1249
+ export declare const searchParamsToObject: (usp: URLSearchParams, opts?: {
1250
+ readonly arrayKeys?: ReadonlySet<string>;
1251
+ }) => Record<string, string | ReadonlyArray<string>>;
1252
+
1088
1253
  /**
1089
1254
  * Cache key for a layout (chain segment) loader. Distinct from the page's
1090
1255
  * key for the same (pattern, params) so a layout's data and the page's
@@ -1155,23 +1320,6 @@ export declare interface ServerLogRelayOptions {
1155
1320
  readonly originLabel: string;
1156
1321
  }
1157
1322
 
1158
- export declare const ServerRequestContext: Context<ServerRequestContextValue | null>;
1159
-
1160
- export declare interface ServerRequestContextValue {
1161
- readonly cookies: Readonly<Record<string, string>>;
1162
- readonly headers: Readonly<Record<string, string>>;
1163
- /** Raw request URL as it came off the wire (path + query). Useful
1164
- * for SSR pages that need to read `?q=…` style search params
1165
- * without touching anything client-only. Empty string for build-
1166
- * time SSG renders where there is no incoming request. */
1167
- readonly url: string;
1168
- }
1169
-
1170
- export declare const ServerRequestProvider: ({ value, children, }: {
1171
- readonly value: ServerRequestContextValue;
1172
- readonly children: ReactNode;
1173
- }) => ReactNode;
1174
-
1175
1323
  /** Push a new route snapshot. Router calls this from its render
1176
1324
  * effect; observers re-render on the next microtask. */
1177
1325
  export declare const setRouteSnapshot: (next: RouteSnapshot) => void;
@@ -1337,10 +1485,27 @@ export declare const useReconnect: () => (() => void);
1337
1485
  * to router-pushed query changes without a reload should re-render via
1338
1486
  * the router (e.g. `useNavigate`/`useLocation`) — this hook then
1339
1487
  * re-resolves on that render.
1488
+ *
1489
+ * Pass the page's own `searchParams` schema export to get the DECODED, typed
1490
+ * shape instead (defaults applied; an invalid query falls back to the
1491
+ * defaults — see `decodeSearchParams`). The schema value is module-local by
1492
+ * design: the page imports its own export, so no generated module ever holds
1493
+ * schema VALUES and links stay typed through type-only imports.
1494
+ *
1495
+ * ```tsx
1496
+ * export const searchParams = Schema.Struct({
1497
+ * page: Schema.optionalWith(Schema.NumberFromString, { default: () => 1 }),
1498
+ * })
1499
+ * const { page } = useSearchParams(searchParams) // number, SSR-aware
1500
+ * ```
1501
+ *
1502
+ * Overload ORDER is load-bearing (the same rule `@voltro/client` pins): the
1503
+ * schema overload must come after the zero-arg one, or every existing
1504
+ * zero-arg call site would re-infer against the generic signature.
1340
1505
  */
1341
- export declare const useSearchParams: () => URLSearchParams;
1506
+ export declare function useSearchParams(): URLSearchParams;
1342
1507
 
1343
- export declare const useServerRequest: () => ServerRequestContextValue | null;
1508
+ export declare function useSearchParams<A>(schema: SearchParamsSchema<A>): A;
1344
1509
 
1345
1510
  /**
1346
1511
  * The WRITE half of `useSearchParams` — returns a setter that updates the
@@ -1359,8 +1524,25 @@ export declare const useServerRequest: () => ServerRequestContextValue | null;
1359
1524
  * pass `{ push: true }` for a distinct history entry. Client-only — during SSR
1360
1525
  * there is no history to write, so read `useSearchParams()` off the request URL
1361
1526
  * instead and mutate on the client after hydration.
1527
+ *
1528
+ * Pass the page's `searchParams` schema to get the TYPED setter. Its object
1529
+ * form REPLACES the query — same semantics as the untyped form, an absent
1530
+ * field decodes to its default on the next read — and its updater form
1531
+ * receives the CURRENT decoded params, so a merge is an explicit spread:
1532
+ *
1533
+ * ```tsx
1534
+ * const setParams = useSetSearchParams(searchParams)
1535
+ * setParams({ page: 2 }) // → ?page=2 (filter dropped)
1536
+ * setParams((p) => ({ ...p, page: p.page + 1 })) // keeps ?filter — typed merge
1537
+ * ```
1538
+ *
1539
+ * Overload ORDER is load-bearing (same rule as `useSearchParams`): the schema
1540
+ * overload comes after the zero-arg one, or every existing zero-arg call site
1541
+ * would re-infer against the generic signature.
1362
1542
  */
1363
- export declare const useSetSearchParams: () => ((next: SearchParamsInit, opts?: SetSearchParamsOptions) => void);
1543
+ export declare function useSetSearchParams(): (next: SearchParamsInit, opts?: SetSearchParamsOptions) => void;
1544
+
1545
+ export declare function useSetSearchParams<A extends Readonly<Record<string, unknown>>>(schema: SearchParamsSchema<A>): (next: Partial<A> | ((current: A) => Partial<A>), opts?: SetSearchParamsOptions) => void;
1364
1546
 
1365
1547
  /**
1366
1548
  * Read + set the app theme, backed by the SAME `voltro:theme` cookie and `.dark`
@@ -1382,9 +1564,22 @@ export declare type VoltroExternalUrl = string & {
1382
1564
  readonly [EXTERNAL_URL_BRAND]: 'external';
1383
1565
  };
1384
1566
 
1385
- /** A URL minted by the app's generated `routes` builder. */
1386
- export declare type VoltroRouteUrl = string & {
1567
+ /**
1568
+ * A URL minted by the app's generated `routes` builder.
1569
+ *
1570
+ * `TSearch` is a compile-time-only phantom: for a route whose page exports a
1571
+ * `searchParams` schema, the codegen brands the builder's return as
1572
+ * `VoltroRouteUrl<Schema.Schema.Type<typeof searchParams>>` through a
1573
+ * TYPE-ONLY import — no schema value ever enters `routes.generated.ts`, so
1574
+ * code-splitting stays intact — and `withQuery` then type-checks the params
1575
+ * against the page's contract. The default is `unknown`, which every branded
1576
+ * URL is assignable to, so a route without a schema (and every existing call
1577
+ * site) is untouched.
1578
+ */
1579
+ export declare type VoltroRouteUrl<TSearch = unknown> = string & {
1387
1580
  readonly [ROUTE_URL_BRAND]: 'route';
1581
+ } & {
1582
+ readonly [SEARCH_PARAMS_BRAND]?: TSearch;
1388
1583
  };
1389
1584
 
1390
1585
  export declare type VoltroUrl = VoltroRouteUrl | VoltroExternalUrl;
@@ -1401,8 +1596,24 @@ export declare const withHash: (url: VoltroRouteUrl, hash: string) => VoltroRout
1401
1596
  *
1402
1597
  * withQuery(routes['/_/p/[orgSlug]/[projectSlug]']({…}), { env: 'prod' })
1403
1598
  * //→ '/_/p/acme/web?env=prod' (typed as VoltroRouteUrl)
1599
+ *
1600
+ * For a route whose page exports a `searchParams` schema, the URL carries the
1601
+ * decoded shape as a phantom (see {@link VoltroRouteUrl}) and the params here
1602
+ * type-check against it — a misspelt key or a wrong value type is a compile
1603
+ * error. The ENCODE stays canonical and schema-free (strings pass through,
1604
+ * number/boolean via String(), arrays as repeated keys) so the generated
1605
+ * builder never needs the schema VALUE; the searchParams roundtrip test pins
1606
+ * that this canonical form is exactly what the schema's decode accepts.
1607
+ *
1608
+ * ONE generic signature with a CONDITIONAL params type — deliberately not two
1609
+ * overloads. With overloads, a wrong key on a typed URL fails the strict
1610
+ * overload and silently falls through to the permissive one, so the compile
1611
+ * error the phantom exists for never fires (the `.test-d.ts` pins exactly
1612
+ * that). The conditional keeps one resolution path: a schema phantom infers
1613
+ * `Partial<TSearch>`, the bare brand (phantom `unknown`) infers the free
1614
+ * record.
1404
1615
  */
1405
- export declare const withQuery: (url: VoltroRouteUrl, params: Readonly<Record<string, string | number | undefined>>) => VoltroRouteUrl;
1616
+ export declare function withQuery<TSearch = unknown>(url: VoltroRouteUrl<TSearch>, params: [TSearch] extends [Readonly<Record<string, unknown>>] ? Partial<TSearch> : Readonly<Record<string, string | number | boolean | ReadonlyArray<string | number | boolean> | undefined>>): VoltroRouteUrl<TSearch>;
1406
1617
 
1407
1618
 
1408
1619
  export * from "@voltro/client";