@vertz/ui 0.2.14 → 0.2.16

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 (32) hide show
  1. package/README.md +49 -0
  2. package/dist/shared/chunk-14eqne2a.js +10 -0
  3. package/dist/shared/{chunk-dksg08fq.js → chunk-1rxa2fz4.js} +2 -8
  4. package/dist/shared/{chunk-8hsz5y4a.js → chunk-4fwcwxn6.js} +14 -4
  5. package/dist/shared/{chunk-hw67ckr3.js → chunk-4mtn7af6.js} +230 -19
  6. package/dist/shared/{chunk-2sth83bd.js → chunk-6jyt4ycw.js} +67 -2
  7. package/dist/shared/{chunk-83g4h38e.js → chunk-6wd36w21.js} +1 -0
  8. package/dist/shared/{chunk-h89w580h.js → chunk-afawz764.js} +1 -1
  9. package/dist/shared/{chunk-nn9v1zmk.js → chunk-b0qqqk03.js} +86 -21
  10. package/dist/shared/{chunk-c9xxsrat.js → chunk-dhehvmj0.js} +179 -10
  11. package/dist/shared/{chunk-mj7b4t40.js → chunk-fkbgbf3n.js} +98 -11
  12. package/dist/shared/{chunk-c30eg6wn.js → chunk-j09yyh34.js} +79 -6
  13. package/dist/shared/chunk-mtsvrj9e.js +23 -0
  14. package/dist/shared/chunk-pnv25zep.js +7 -0
  15. package/dist/shared/{chunk-j6qyxfdc.js → chunk-vndfjfdy.js} +3 -3
  16. package/dist/src/auth/public.d.ts +69 -9
  17. package/dist/src/auth/public.js +217 -13
  18. package/dist/src/css/public.d.ts +110 -2
  19. package/dist/src/css/public.js +8 -4
  20. package/dist/src/form/public.d.ts +33 -7
  21. package/dist/src/form/public.js +2 -2
  22. package/dist/src/index.d.ts +311 -20
  23. package/dist/src/index.js +161 -14
  24. package/dist/src/internals.d.ts +141 -5
  25. package/dist/src/internals.js +17 -9
  26. package/dist/src/query/public.js +4 -3
  27. package/dist/src/router/public.d.ts +39 -9
  28. package/dist/src/router/public.js +17 -11
  29. package/dist/src/test/index.d.ts +26 -23
  30. package/dist/src/test/index.js +5 -4
  31. package/package.json +3 -3
  32. package/reactivity.json +1 -11
@@ -1,7 +1,15 @@
1
1
  import {
2
- __classList,
2
+ __classList
3
+ } from "./chunk-1rxa2fz4.js";
4
+ import {
5
+ RouterContext
6
+ } from "./chunk-mtsvrj9e.js";
7
+ import {
3
8
  __on
4
- } from "./chunk-dksg08fq.js";
9
+ } from "./chunk-pnv25zep.js";
10
+ import {
11
+ isBrowser
12
+ } from "./chunk-14eqne2a.js";
5
13
  import {
6
14
  __append,
7
15
  __element,
@@ -9,18 +17,19 @@ import {
9
17
  __exitChildren,
10
18
  __staticText,
11
19
  getIsHydrating
12
- } from "./chunk-j6qyxfdc.js";
20
+ } from "./chunk-vndfjfdy.js";
13
21
  import {
14
22
  _tryOnCleanup,
15
23
  createContext,
16
24
  domEffect,
25
+ getSSRContext,
17
26
  popScope,
18
27
  pushScope,
19
28
  runCleanups,
20
29
  signal,
21
30
  untrack,
22
31
  useContext
23
- } from "./chunk-8hsz5y4a.js";
32
+ } from "./chunk-4fwcwxn6.js";
24
33
 
25
34
  // src/router/link.ts
26
35
  var DANGEROUS_SCHEMES = ["javascript:", "data:", "vbscript:"];
@@ -65,8 +74,10 @@ function createLink(currentPath, navigate, factoryOptions) {
65
74
  } else {
66
75
  __append(el, result);
67
76
  }
68
- } else {
77
+ } else if (typeof children === "string") {
69
78
  __append(el, __staticText(children));
79
+ } else {
80
+ __append(el, children);
70
81
  }
71
82
  __exitChildren();
72
83
  if (activeClass) {
@@ -88,22 +99,49 @@ function createLink(currentPath, navigate, factoryOptions) {
88
99
  return el;
89
100
  };
90
101
  }
91
-
92
- // src/router/router-context.ts
93
- var RouterContext = createContext(undefined, "@vertz/ui::RouterContext");
94
- function useRouter() {
102
+ function Link({ href, children, activeClass, className }) {
95
103
  const router = useContext(RouterContext);
96
104
  if (!router) {
97
- throw new Error("useRouter() must be called within RouterContext.Provider");
105
+ throw new Error("Link must be used within a RouterContext.Provider (via createRouter)");
98
106
  }
99
- return router;
100
- }
101
- function useParams() {
102
- const router = useContext(RouterContext);
103
- if (!router) {
104
- throw new Error("useParams() must be called within RouterContext.Provider");
107
+ const safeHref = isSafeUrl(href) ? href : "#";
108
+ const handleClick = (event) => {
109
+ const mouseEvent = event;
110
+ if (mouseEvent.ctrlKey || mouseEvent.metaKey || mouseEvent.shiftKey || mouseEvent.altKey) {
111
+ return;
112
+ }
113
+ mouseEvent.preventDefault();
114
+ router.navigate({ to: safeHref });
115
+ };
116
+ const props = { href: safeHref };
117
+ if (className) {
118
+ props.class = className;
105
119
  }
106
- return router.current?.parsedParams ?? router.current?.params ?? {};
120
+ const el = __element("a", props);
121
+ __on(el, "click", handleClick);
122
+ __enterChildren(el);
123
+ if (typeof children === "function") {
124
+ const result = children();
125
+ if (typeof result === "string") {
126
+ __append(el, __staticText(result));
127
+ } else {
128
+ __append(el, result);
129
+ }
130
+ } else if (typeof children === "string") {
131
+ __append(el, __staticText(children));
132
+ } else {
133
+ __append(el, children);
134
+ }
135
+ __exitChildren();
136
+ if (activeClass) {
137
+ __classList(el, {
138
+ [activeClass]: () => {
139
+ router.current;
140
+ return isBrowser() ? window.location.pathname === safeHref : false;
141
+ }
142
+ });
143
+ }
144
+ return el;
107
145
  }
108
146
 
109
147
  // src/router/outlet.ts
@@ -249,17 +287,44 @@ function buildLevels(matched) {
249
287
  }));
250
288
  }
251
289
  function buildInsideOutFactory(matched, levels, startAt, router) {
252
- let factory = matched[matched.length - 1].route.component;
290
+ const ssrCtx = getSSRContext();
291
+ const wrapForSSR = (route, routeIndex) => {
292
+ if (!ssrCtx)
293
+ return route.component;
294
+ const resolved = ssrCtx.resolvedComponents?.get(route);
295
+ if (resolved)
296
+ return resolved;
297
+ return () => {
298
+ const result = route.component();
299
+ if (result instanceof Promise) {
300
+ if (!ssrCtx.pendingRouteComponents) {
301
+ ssrCtx.pendingRouteComponents = new Map;
302
+ }
303
+ ssrCtx.pendingRouteComponents.set(route, result);
304
+ for (let j = routeIndex + 1;j < matched.length; j++) {
305
+ const childRoute = matched[j].route;
306
+ if (!ssrCtx.pendingRouteComponents.has(childRoute)) {
307
+ const childResult = childRoute.component();
308
+ if (childResult instanceof Promise) {
309
+ ssrCtx.pendingRouteComponents.set(childRoute, childResult);
310
+ }
311
+ }
312
+ }
313
+ }
314
+ return result;
315
+ };
316
+ };
317
+ let factory = wrapForSSR(matched[matched.length - 1].route, matched.length - 1);
253
318
  for (let i = matched.length - 2;i >= startAt; i--) {
254
319
  const level = levels[i];
255
320
  const childFactory = factory;
256
321
  level.childSignal.value = childFactory;
257
- const parentRoute = level.route;
322
+ const parentComponent = wrapForSSR(level.route, i);
258
323
  const cs = level.childSignal;
259
324
  factory = () => {
260
325
  let result;
261
326
  OutletContext.Provider({ childComponent: cs, router }, () => {
262
- result = parentRoute.component();
327
+ result = parentComponent();
263
328
  });
264
329
  return result;
265
330
  };
@@ -285,4 +350,4 @@ function useSearchParams(searchSignal) {
285
350
  return searchSignal.value;
286
351
  }
287
352
 
288
- export { createLink, RouterContext, useRouter, useParams, OutletContext, Outlet, RouterView, parseSearchParams, useSearchParams };
353
+ export { createLink, Link, OutletContext, Outlet, RouterView, parseSearchParams, useSearchParams };
@@ -3,10 +3,10 @@ import {
3
3
  __element,
4
4
  __enterChildren,
5
5
  __exitChildren
6
- } from "./chunk-j6qyxfdc.js";
6
+ } from "./chunk-vndfjfdy.js";
7
7
  import {
8
8
  getSSRContext
9
- } from "./chunk-8hsz5y4a.js";
9
+ } from "./chunk-4fwcwxn6.js";
10
10
 
11
11
  // src/component/children.ts
12
12
  var MAX_RESOLVE_DEPTH = 100;
@@ -82,6 +82,7 @@ var PROPERTY_MAP = {
82
82
  leading: { properties: ["line-height"], valueType: "line-height" },
83
83
  tracking: { properties: ["letter-spacing"], valueType: "raw" },
84
84
  decoration: { properties: ["text-decoration"], valueType: "raw" },
85
+ list: { properties: ["list-style"], valueType: "raw" },
85
86
  ring: { properties: ["outline"], valueType: "ring" },
86
87
  cursor: { properties: ["cursor"], valueType: "raw" },
87
88
  transition: { properties: ["transition"], valueType: "raw" },
@@ -115,7 +116,9 @@ var KEYWORD_MAP = {
115
116
  "select-none": [{ property: "user-select", value: "none" }],
116
117
  "pointer-events-none": [{ property: "pointer-events", value: "none" }],
117
118
  "whitespace-nowrap": [{ property: "white-space", value: "nowrap" }],
118
- "shrink-0": [{ property: "flex-shrink", value: "0" }]
119
+ "shrink-0": [{ property: "flex-shrink", value: "0" }],
120
+ italic: [{ property: "font-style", value: "italic" }],
121
+ "not-italic": [{ property: "font-style", value: "normal" }]
119
122
  };
120
123
  var DISPLAY_MAP = {
121
124
  flex: "flex",
@@ -399,6 +402,9 @@ function resolveToken(parsed) {
399
402
  if (property === "ring") {
400
403
  return { declarations: resolveRingMulti(value), pseudo };
401
404
  }
405
+ if (property === "list") {
406
+ return { declarations: resolveList(value), pseudo };
407
+ }
402
408
  const resolvedValue = resolveValue(value, mapping.valueType, property);
403
409
  const declarations = mapping.properties.map((prop) => ({
404
410
  property: prop,
@@ -551,6 +557,17 @@ function resolveRingMulti(value) {
551
557
  }
552
558
  return [{ property: "outline-color", value: resolveColor(value, "ring") }];
553
559
  }
560
+ var LIST_STYLE_KEYWORDS = new Set(["none", "disc", "decimal"]);
561
+ var LIST_POSITION_KEYWORDS = new Set(["inside", "outside"]);
562
+ function resolveList(value) {
563
+ if (LIST_STYLE_KEYWORDS.has(value)) {
564
+ return [{ property: "list-style", value }];
565
+ }
566
+ if (LIST_POSITION_KEYWORDS.has(value)) {
567
+ return [{ property: "list-style-position", value }];
568
+ }
569
+ throw new TokenResolveError(`Invalid list value '${value}'. Use: none, disc, decimal, inside, outside.`, `list:${value}`);
570
+ }
554
571
  function resolveRaw(value, property) {
555
572
  if (property === "border-r" || property === "border-l" || property === "border-t" || property === "border-b") {
556
573
  const num = Number(value);
@@ -740,6 +757,143 @@ ${props}
740
757
  }`;
741
758
  }
742
759
 
760
+ // src/css/sanitize.ts
761
+ function sanitizeCssValue(value) {
762
+ return value.replace(/[;{}<>']/g, "").replace(/url\s*\(/gi, "").replace(/expression\s*\(/gi, "").replace(/@import/gi, "");
763
+ }
764
+ function escapeHtmlAttr(value) {
765
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/'/g, "&#39;");
766
+ }
767
+
768
+ // src/css/font.ts
769
+ function font(family, options) {
770
+ return {
771
+ __brand: "FontDescriptor",
772
+ family,
773
+ weight: String(options.weight),
774
+ style: options.style ?? "normal",
775
+ display: options.display ?? "swap",
776
+ src: options.src,
777
+ fallback: options.fallback ?? [],
778
+ subsets: options.subsets ?? ["latin"],
779
+ unicodeRange: options.unicodeRange,
780
+ adjustFontFallback: options.adjustFontFallback ?? true
781
+ };
782
+ }
783
+ function toCssWeight(weight) {
784
+ return sanitizeCssValue(weight).replace("..", " ");
785
+ }
786
+ function buildFontFace(family, src, weight, style, display, unicodeRange) {
787
+ const safeFamily = sanitizeCssValue(family);
788
+ const safeSrc = sanitizeCssValue(src);
789
+ const safeStyle = sanitizeCssValue(style);
790
+ const safeDisplay = sanitizeCssValue(display);
791
+ const lines = [
792
+ "@font-face {",
793
+ ` font-family: '${safeFamily}';`,
794
+ ` font-style: ${safeStyle};`,
795
+ ` font-weight: ${toCssWeight(weight)};`,
796
+ ` font-display: ${safeDisplay};`,
797
+ ` src: url(${safeSrc}) format('woff2');`
798
+ ];
799
+ if (unicodeRange) {
800
+ lines.push(` unicode-range: ${sanitizeCssValue(unicodeRange)};`);
801
+ }
802
+ lines.push("}");
803
+ return lines.join(`
804
+ `);
805
+ }
806
+ var PERCENT_RE = /^\d+\.\d+%$/;
807
+ function sanitizeMetricValue(value) {
808
+ if (!PERCENT_RE.test(value)) {
809
+ throw new Error(`Invalid font metric override value: "${value}"`);
810
+ }
811
+ return value;
812
+ }
813
+ function buildFallbackFontFace(family, metrics) {
814
+ const safeFamily = sanitizeCssValue(family);
815
+ const lines = [
816
+ "@font-face {",
817
+ ` font-family: '${safeFamily} Fallback';`,
818
+ ` src: local('${metrics.fallbackFont}');`,
819
+ ` ascent-override: ${sanitizeMetricValue(metrics.ascentOverride)};`,
820
+ ` descent-override: ${sanitizeMetricValue(metrics.descentOverride)};`,
821
+ ` line-gap-override: ${sanitizeMetricValue(metrics.lineGapOverride)};`,
822
+ ` size-adjust: ${sanitizeMetricValue(metrics.sizeAdjust)};`,
823
+ "}"
824
+ ];
825
+ return lines.join(`
826
+ `);
827
+ }
828
+ function validateWoff2Src(path) {
829
+ if (!path.toLowerCase().endsWith(".woff2")) {
830
+ throw new Error(`Font src "${path}" is not a .woff2 file. Only woff2 format is currently supported.`);
831
+ }
832
+ }
833
+ function compileFonts(fonts, options) {
834
+ const fontFaces = [];
835
+ const cssVars = [];
836
+ const preloadPaths = [];
837
+ const fallbackMetrics = options?.fallbackMetrics;
838
+ for (const [key, descriptor] of Object.entries(fonts)) {
839
+ if (!/^[a-zA-Z0-9-]+$/.test(key)) {
840
+ throw new Error(`Font key "${key}" contains invalid CSS identifier characters. Use only [a-zA-Z0-9-].`);
841
+ }
842
+ const { family, weight, style, display, src, fallback, unicodeRange } = descriptor;
843
+ const adjustFontFallback = descriptor.adjustFontFallback ?? true;
844
+ const metrics = fallbackMetrics?.[key];
845
+ const shouldGenerateFallback = metrics && src && adjustFontFallback !== false;
846
+ const safeFamily = sanitizeCssValue(family);
847
+ const safeFallbacks = fallback.map(sanitizeCssValue);
848
+ const fallbackFontName = shouldGenerateFallback ? `'${safeFamily} Fallback'` : undefined;
849
+ const familyParts = [
850
+ `'${safeFamily}'`,
851
+ ...fallbackFontName ? [fallbackFontName] : [],
852
+ ...safeFallbacks
853
+ ];
854
+ const familyValue = familyParts.join(", ");
855
+ cssVars.push(` --font-${sanitizeCssValue(key)}: ${familyValue};`);
856
+ if (!src)
857
+ continue;
858
+ if (typeof src === "string") {
859
+ validateWoff2Src(src);
860
+ fontFaces.push(buildFontFace(family, src, weight, style, display, unicodeRange));
861
+ preloadPaths.push(src);
862
+ } else {
863
+ for (const entry of src) {
864
+ validateWoff2Src(entry.path);
865
+ const entryWeight = entry.weight != null ? String(entry.weight) : weight;
866
+ const entryStyle = entry.style ?? style;
867
+ fontFaces.push(buildFontFace(family, entry.path, entryWeight, entryStyle, display, unicodeRange));
868
+ }
869
+ const first = src[0];
870
+ if (first) {
871
+ preloadPaths.push(first.path);
872
+ }
873
+ }
874
+ if (shouldGenerateFallback) {
875
+ fontFaces.push(buildFallbackFontFace(family, metrics));
876
+ }
877
+ }
878
+ const fontFaceCss = fontFaces.join(`
879
+
880
+ `);
881
+ const cssVarLines = cssVars;
882
+ const cssVarsCss = cssVars.length > 0 ? `:root {
883
+ ${cssVars.join(`
884
+ `)}
885
+ }` : "";
886
+ const preloadTags = preloadPaths.map((p) => `<link rel="preload" href="${escapeHtmlAttr(p)}" as="font" type="font/woff2" crossorigin>`).join(`
887
+ `);
888
+ const preloadItems = preloadPaths.map((href) => ({
889
+ href,
890
+ as: "font",
891
+ type: "font/woff2",
892
+ crossorigin: true
893
+ }));
894
+ return { fontFaceCss, cssVarsCss, cssVarLines, preloadTags, preloadItems };
895
+ }
896
+
743
897
  // src/css/global-css.ts
744
898
  function globalCss(input) {
745
899
  const rules = [];
@@ -793,16 +947,14 @@ class InlineStyleError extends Error {
793
947
  }
794
948
 
795
949
  // src/css/theme.ts
796
- function sanitizeCssValue(value) {
797
- return value.replace(/[;{}]/g, "").replace(/url\s*\(/gi, "").replace(/expression\s*\(/gi, "").replace(/@import/gi, "");
798
- }
799
950
  function defineTheme(input) {
800
951
  return {
801
952
  colors: input.colors,
802
- spacing: input.spacing
953
+ spacing: input.spacing,
954
+ fonts: input.fonts
803
955
  };
804
956
  }
805
- function compileTheme(theme) {
957
+ function compileTheme(theme, options) {
806
958
  const rootVars = [];
807
959
  const darkVars = [];
808
960
  const tokenPaths = [];
@@ -847,7 +999,22 @@ function compileTheme(theme) {
847
999
  tokenPaths.push(`spacing.${name}`);
848
1000
  }
849
1001
  }
1002
+ let fontFaceCss = "";
1003
+ let preloadTags = "";
1004
+ let preloadItems = [];
1005
+ if (theme.fonts) {
1006
+ const compiled = compileFonts(theme.fonts, {
1007
+ fallbackMetrics: options?.fallbackMetrics
1008
+ });
1009
+ fontFaceCss = compiled.fontFaceCss;
1010
+ preloadTags = compiled.preloadTags;
1011
+ preloadItems = compiled.preloadItems;
1012
+ rootVars.push(...compiled.cssVarLines);
1013
+ }
850
1014
  const blocks = [];
1015
+ if (fontFaceCss) {
1016
+ blocks.push(fontFaceCss);
1017
+ }
851
1018
  if (rootVars.length > 0) {
852
1019
  blocks.push(`:root {
853
1020
  ${rootVars.join(`
@@ -863,7 +1030,9 @@ ${darkVars.join(`
863
1030
  return {
864
1031
  css: blocks.join(`
865
1032
  `),
866
- tokens: tokenPaths
1033
+ tokens: tokenPaths,
1034
+ preloadTags,
1035
+ preloadItems
867
1036
  };
868
1037
  }
869
1038
 
@@ -1000,4 +1169,4 @@ function variants(config) {
1000
1169
  return fn;
1001
1170
  }
1002
1171
 
1003
- export { resolveChildren, children, PROPERTY_MAP, KEYWORD_MAP, DISPLAY_MAP, SPACING_SCALE, RADIUS_SCALE, SHADOW_SCALE, FONT_SIZE_SCALE, FONT_WEIGHT_SCALE, LINE_HEIGHT_SCALE, ALIGNMENT_MAP, SIZE_KEYWORDS, HEIGHT_AXIS_PROPERTIES, COLOR_NAMESPACES, CSS_COLOR_KEYWORDS, CONTENT_MAP, PSEUDO_PREFIXES, PSEUDO_MAP, injectCSS, resetInjectedStyles, getInjectedCSS, css, globalCss, s, defineTheme, compileTheme, ThemeProvider, variants };
1172
+ export { resolveChildren, children, PROPERTY_MAP, KEYWORD_MAP, DISPLAY_MAP, SPACING_SCALE, RADIUS_SCALE, SHADOW_SCALE, FONT_SIZE_SCALE, FONT_WEIGHT_SCALE, LINE_HEIGHT_SCALE, ALIGNMENT_MAP, SIZE_KEYWORDS, HEIGHT_AXIS_PROPERTIES, COLOR_NAMESPACES, CSS_COLOR_KEYWORDS, CONTENT_MAP, PSEUDO_PREFIXES, PSEUDO_MAP, injectCSS, resetInjectedStyles, getInjectedCSS, css, font, compileFonts, globalCss, s, defineTheme, compileTheme, ThemeProvider, variants };
@@ -1,35 +1,95 @@
1
1
  import {
2
2
  executeLoaders,
3
3
  matchRoute
4
- } from "./chunk-83g4h38e.js";
4
+ } from "./chunk-6wd36w21.js";
5
5
  import {
6
6
  prefetchNavData
7
7
  } from "./chunk-jrtrk5z4.js";
8
+ import {
9
+ isBrowser
10
+ } from "./chunk-14eqne2a.js";
8
11
  import {
9
12
  getSSRContext,
10
13
  signal
11
- } from "./chunk-8hsz5y4a.js";
14
+ } from "./chunk-4fwcwxn6.js";
12
15
 
13
16
  // src/router/navigate.ts
14
17
  var DEFAULT_NAV_THRESHOLD_MS = 500;
15
- function createRouter(routes, initialUrl, options) {
18
+ function interpolatePath(path, params) {
19
+ const segments = path.split("/");
20
+ return segments.map((segment) => {
21
+ if (segment.startsWith(":")) {
22
+ const paramName = segment.slice(1);
23
+ const value = params?.[paramName];
24
+ if (value === undefined) {
25
+ throw new TypeError(`Missing route param "${paramName}" for path "${path}"`);
26
+ }
27
+ return encodeURIComponent(value);
28
+ }
29
+ if (segment === "*") {
30
+ const value = params?.["*"];
31
+ if (value === undefined) {
32
+ throw new TypeError(`Missing wildcard param "*" for path "${path}"`);
33
+ }
34
+ return value.replace(/^\/+|\/+$/g, "").split("/").map((part) => encodeURIComponent(part)).join("/");
35
+ }
36
+ return segment;
37
+ }).join("/");
38
+ }
39
+ function buildSearch(search) {
40
+ if (!search)
41
+ return "";
42
+ if (typeof search === "string") {
43
+ if (search === "")
44
+ return "";
45
+ return search.startsWith("?") ? search : `?${search}`;
46
+ }
47
+ const params = search instanceof URLSearchParams ? new URLSearchParams(search) : new URLSearchParams;
48
+ if (!(search instanceof URLSearchParams)) {
49
+ const entries = Object.entries(search).sort(([left], [right]) => left.localeCompare(right));
50
+ for (const [key, rawValue] of entries) {
51
+ const values = Array.isArray(rawValue) ? rawValue : [rawValue];
52
+ for (const value of values) {
53
+ if (value === undefined || value === null)
54
+ continue;
55
+ params.append(key, String(value));
56
+ }
57
+ }
58
+ }
59
+ const query = params.toString();
60
+ return query ? `?${query}` : "";
61
+ }
62
+ function buildNavigationUrl(to, options) {
63
+ return `${interpolatePath(to, options?.params)}${buildSearch(options?.search)}`;
64
+ }
65
+ function createRouter(routes, initialUrlOrOptions, maybeOptions) {
66
+ const initialUrl = typeof initialUrlOrOptions === "string" ? initialUrlOrOptions : undefined;
67
+ const options = typeof initialUrlOrOptions === "object" ? initialUrlOrOptions : maybeOptions;
16
68
  const ssrCtx = getSSRContext();
17
- const isSSR = ssrCtx !== undefined;
18
- if (isSSR || typeof window === "undefined") {
69
+ if (!isBrowser()) {
70
+ let registerRoutesForDiscovery = function(ctx) {
71
+ if (!ctx.discoveredRoutes) {
72
+ ctx.discoveredRoutes = collectRoutePatterns(routes);
73
+ }
74
+ };
19
75
  const ssrUrl = initialUrl ?? ssrCtx?.url ?? "/";
20
76
  const fallbackMatch = matchRoute(routes, ssrUrl);
21
77
  return {
22
78
  current: {
23
79
  get value() {
24
80
  const ctx = getSSRContext();
25
- if (ctx)
81
+ if (ctx) {
82
+ registerRoutesForDiscovery(ctx);
26
83
  return matchRoute(routes, ctx.url);
84
+ }
27
85
  return fallbackMatch;
28
86
  },
29
87
  peek() {
30
88
  const ctx = getSSRContext();
31
- if (ctx)
89
+ if (ctx) {
90
+ registerRoutesForDiscovery(ctx);
32
91
  return matchRoute(routes, ctx.url);
92
+ }
33
93
  return fallbackMatch;
34
94
  },
35
95
  notify() {}
@@ -82,8 +142,12 @@ function createRouter(routes, initialUrl, options) {
82
142
  const current = {
83
143
  get value() {
84
144
  const ctx = getSSRContext();
85
- if (ctx)
145
+ if (ctx) {
146
+ if (!ctx.discoveredRoutes) {
147
+ ctx.discoveredRoutes = collectRoutePatterns(routes);
148
+ }
86
149
  return matchRoute(routes, ctx.url);
150
+ }
87
151
  return _current.value;
88
152
  },
89
153
  set value(v) {
@@ -91,8 +155,12 @@ function createRouter(routes, initialUrl, options) {
91
155
  },
92
156
  peek() {
93
157
  const ctx = getSSRContext();
94
- if (ctx)
158
+ if (ctx) {
159
+ if (!ctx.discoveredRoutes) {
160
+ ctx.discoveredRoutes = collectRoutePatterns(routes);
161
+ }
95
162
  return matchRoute(routes, ctx.url);
163
+ }
96
164
  return _current.peek();
97
165
  },
98
166
  notify() {
@@ -195,10 +263,11 @@ function createRouter(routes, initialUrl, options) {
195
263
  }
196
264
  }
197
265
  }
198
- async function navigate(navUrl, navOptions) {
266
+ async function navigate(input) {
267
+ const navUrl = buildNavigationUrl(input.to, input);
199
268
  const gen = ++navigateGen;
200
269
  const handle = startPrefetch(navUrl);
201
- if (navOptions?.replace) {
270
+ if (input.replace) {
202
271
  window.history.replaceState(null, "", navUrl);
203
272
  } else {
204
273
  window.history.pushState(null, "", navUrl);
@@ -250,5 +319,23 @@ function createRouter(routes, initialUrl, options) {
250
319
  searchParams
251
320
  };
252
321
  }
322
+ function collectRoutePatterns(routes, prefix = "") {
323
+ const patterns = [];
324
+ for (const route of routes) {
325
+ const fullPattern = joinPatterns(prefix, route.pattern);
326
+ patterns.push(fullPattern);
327
+ if (route.children) {
328
+ patterns.push(...collectRoutePatterns(route.children, fullPattern));
329
+ }
330
+ }
331
+ return patterns;
332
+ }
333
+ function joinPatterns(parent, child) {
334
+ if (!parent || parent === "/")
335
+ return child;
336
+ if (child === "/")
337
+ return parent;
338
+ return `${parent.replace(/\/$/, "")}/${child.replace(/^\//, "")}`;
339
+ }
253
340
 
254
341
  export { createRouter };