@vertz/ui 0.2.1 → 0.2.5

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.
@@ -6,7 +6,7 @@ import {
6
6
  globalCss,
7
7
  s,
8
8
  variants
9
- } from "../shared/chunk-0p5f7gmg.js";
9
+ } from "../shared/chunk-qacth5ah.js";
10
10
  import"../shared/chunk-ryb49346.js";
11
11
  import"../shared/chunk-g4rch80a.js";
12
12
  import"../shared/chunk-hrd0mft1.js";
package/dist/index.d.ts CHANGED
@@ -72,6 +72,12 @@ interface Computed<T> extends ReadonlySignal<T> {
72
72
  /** Dispose function returned by effect(). */
73
73
  type DisposeFn = () => void;
74
74
  /**
75
+ * A snapshot of context values at a point in time.
76
+ * Each Provider creates a new scope that inherits from the parent.
77
+ * Effects capture this scope so that useContext works in async callbacks.
78
+ */
79
+ type ContextScope = Map<Context<unknown>, unknown>;
80
+ /**
75
81
  * Props for the JSX pattern of Context.Provider.
76
82
  *
77
83
  * `children` accepts both raw values (what TypeScript sees in JSX) and
@@ -414,6 +420,34 @@ interface VariantFunction<V extends VariantDefinitions> {
414
420
  * @returns A function that accepts variant props and returns a className string.
415
421
  */
416
422
  declare function variants<V extends VariantDefinitions>(config: VariantsConfig<V>): VariantFunction<V>;
423
+ declare const DialogStackContext: Context<DialogStack>;
424
+ declare function useDialogStack(): DialogStack;
425
+ interface DialogHandle<TResult> {
426
+ close(...args: void extends TResult ? [] : [result: TResult]): void;
427
+ }
428
+ type DialogComponent<
429
+ TResult,
430
+ TProps = Record<string, never>
431
+ > = (props: TProps & {
432
+ dialog: DialogHandle<TResult>;
433
+ }) => Node;
434
+ declare class DialogDismissedError extends Error {
435
+ constructor();
436
+ }
437
+ interface DialogStack {
438
+ open<
439
+ TResult,
440
+ TProps
441
+ >(component: DialogComponent<TResult, TProps>, props: TProps): Promise<TResult>;
442
+ /** @internal — used by useDialogStack() to pass captured context scope */
443
+ openWithScope<
444
+ TResult,
445
+ TProps
446
+ >(component: DialogComponent<TResult, TProps>, props: TProps, scope: ContextScope | null): Promise<TResult>;
447
+ readonly size: number;
448
+ closeAll(): void;
449
+ }
450
+ declare function createDialogStack(container: HTMLElement): DialogStack;
417
451
  /**
418
452
  * Brand symbol for render nodes.
419
453
  * SSR nodes add this to their prototype for fast identification.
@@ -729,6 +763,17 @@ interface QueryOptions<T> {
729
763
  cache?: CacheStore<T>;
730
764
  /** Timeout in ms for SSR data loading. Default: 300. Set to 0 to disable. */
731
765
  ssrTimeout?: number;
766
+ /**
767
+ * Polling interval in ms, or a function for dynamic intervals.
768
+ *
769
+ * - `number` — fixed interval in ms
770
+ * - `false` or `0` — disabled
771
+ * - `(data, iteration) => number | false` — called after each fetch to
772
+ * determine the next interval. Return `false` to stop polling.
773
+ * `iteration` counts polls since the last start/restart (resets to 0
774
+ * when the function returns `false`).
775
+ */
776
+ refetchInterval?: number | false | ((data: T | undefined, iteration: number) => number | false);
732
777
  }
733
778
  /** The reactive object returned by query(). */
734
779
  interface QueryResult<
@@ -1004,6 +1049,8 @@ interface PrefetchHandle {
1004
1049
  abort: () => void;
1005
1050
  /** Resolves when SSE stream completes (data or done event). */
1006
1051
  done?: Promise<void>;
1052
+ /** Resolves when the first SSE event of any type arrives. */
1053
+ firstEvent?: Promise<void>;
1007
1054
  }
1008
1055
  /** Options for createRouter(). */
1009
1056
  interface RouterOptions {
@@ -1084,6 +1131,17 @@ interface RouterViewProps {
1084
1131
  router: Router;
1085
1132
  fallback?: () => Node;
1086
1133
  }
1134
+ /**
1135
+ * Renders the matched route's component inside a container div.
1136
+ *
1137
+ * Handles sync and async (lazy-loaded) components, stale resolution guards,
1138
+ * page cleanup on navigation, and RouterContext propagation.
1139
+ *
1140
+ * Uses __element() so the container is claimed from SSR during hydration.
1141
+ * On the first hydration render, children are already in the DOM — the
1142
+ * domEffect runs the component factory (to attach reactivity/event handlers)
1143
+ * but skips clearing the container.
1144
+ */
1087
1145
  declare function RouterView({ router, fallback }: RouterViewProps): HTMLElement;
1088
1146
  /**
1089
1147
  * Parse URLSearchParams into a typed object, optionally through a schema.
@@ -1149,7 +1207,7 @@ interface EntityStoreOptions {
1149
1207
  }
1150
1208
  /**
1151
1209
  * EntityStore - Normalized, signal-backed entity cache for @vertz/ui.
1152
- *
1210
+ *
1153
1211
  * Stores entities by type and ID, with signal-per-entity reactivity.
1154
1212
  * Supports field-level merge, SSR hydration, and query result indices.
1155
1213
  */
@@ -1209,10 +1267,10 @@ declare class EntityStore {
1209
1267
  }
1210
1268
  /**
1211
1269
  * Create a pre-populated EntityStore for testing.
1212
- *
1270
+ *
1213
1271
  * @param data - Entity data keyed by type → id → entity
1214
1272
  * @returns A new EntityStore with the data already merged
1215
- *
1273
+ *
1216
1274
  * @example
1217
1275
  * ```ts
1218
1276
  * const store = createTestStore({
@@ -1221,9 +1279,9 @@ declare class EntityStore {
1221
1279
  * '2': { id: '2', name: 'Bob' }
1222
1280
  * }
1223
1281
  * });
1224
- *
1282
+ *
1225
1283
  * expect(store.get('User', '1').value).toEqual({ id: '1', name: 'Alice' });
1226
1284
  * ```
1227
1285
  */
1228
1286
  declare function createTestStore(data: Record<string, Record<string, unknown>>): EntityStore;
1229
- export { zoomOut, zoomIn, variants, validate, useSearchParams, useRouter, useParams, useContext, untrack, slideOutToTop, slideOutToRight, slideOutToLeft, slideOutToBottom, slideInFromTop, slideInFromRight, slideInFromLeft, slideInFromBottom, signal, setAdapter, s, resolveChildren, resetInjectedStyles, ref, queryMatch, query, parseSearchParams, palettes, onMount2 as onMount, mount, keyframes, isRenderNode, isQueryDescriptor, injectCSS, hydrate, globalCss, getInjectedCSS, getAdapter, formDataToObject, form, fadeOut, fadeIn, defineTheme, defineRoutes, css, createTestStore, createRouter, createLink, createFieldState, createDOMAdapter, createContext, computed, compileTheme, children, batch, accordionUp, accordionDown, __staticText, __exitChildren, __enterChildren, __element, __append, VariantsConfig, VariantProps, VariantFunction, ValidationResult, UnwrapSignals, TypedRoutes, TypedRouter, ThemeProviderProps, ThemeProvider, ThemeInput, Theme, SuspenseProps, Suspense, StyleValue, StyleEntry, Signal, SerializedStore, SearchParamSchema, SdkMethodWithMeta, SdkMethod, RouterViewProps, RouterView, RouterOptions, RouterContext, Router, RoutePaths, RouteMatch, RouteDefinitionMap, RouteConfig, RenderText, RenderNode, RenderElement, RenderAdapter, Ref, ReadonlySignal, RawDeclaration, RENDER_NODE_BRAND, QueryResult, QueryOptions, QueryMatchHandlers, QueryDescriptor2 as QueryDescriptor, PresenceProps, Presence, PathWithParams, OutletContextValue, OutletContext, Outlet, NavigateOptions, MountOptions, MountHandle, MatchedRoute, LoaderData, LinkProps, LinkFactoryOptions, InferRouteMap, GlobalCSSOutput, GlobalCSSInput, FormSchema, FormOptions, FormInstance, FormDataOptions, FieldState, ExtractParams, ErrorBoundaryProps, ErrorBoundary, EntityStoreOptions, EntityStore, DisposeFn, DisposalScopeError, Context, Computed, ComponentRegistry, ComponentLoader, ComponentFunction, CompiledTheme, CompiledRoute, ColorPalette, ChildrenAccessor, ChildValue, CacheStore, CSSOutput, CSSInput, ANIMATION_EASING, ANIMATION_DURATION };
1287
+ export { zoomOut, zoomIn, variants, validate, useSearchParams, useRouter, useParams, useDialogStack, useContext, untrack, slideOutToTop, slideOutToRight, slideOutToLeft, slideOutToBottom, slideInFromTop, slideInFromRight, slideInFromLeft, slideInFromBottom, signal, setAdapter, s, resolveChildren, resetInjectedStyles, ref, queryMatch, query, parseSearchParams, palettes, onMount2 as onMount, mount, keyframes, isRenderNode, isQueryDescriptor, injectCSS, hydrate, globalCss, getInjectedCSS, getAdapter, formDataToObject, form, fadeOut, fadeIn, defineTheme, defineRoutes, css, createTestStore, createRouter, createLink, createFieldState, createDialogStack, createDOMAdapter, createContext, computed, compileTheme, children, batch, accordionUp, accordionDown, __staticText, __exitChildren, __enterChildren, __element, __append, VariantsConfig, VariantProps, VariantFunction, ValidationResult, UnwrapSignals, TypedRoutes, TypedRouter, ThemeProviderProps, ThemeProvider, ThemeInput, Theme, SuspenseProps, Suspense, StyleValue, StyleEntry, Signal, SerializedStore, SearchParamSchema, SdkMethodWithMeta, SdkMethod, RouterViewProps, RouterView, RouterOptions, RouterContext, Router, RoutePaths, RouteMatch, RouteDefinitionMap, RouteConfig, RenderText, RenderNode, RenderElement, RenderAdapter, Ref, ReadonlySignal, RawDeclaration, RENDER_NODE_BRAND, QueryResult, QueryOptions, QueryMatchHandlers, QueryDescriptor2 as QueryDescriptor, PresenceProps, Presence, PathWithParams, OutletContextValue, OutletContext, Outlet, NavigateOptions, MountOptions, MountHandle, MatchedRoute, LoaderData, LinkProps, LinkFactoryOptions, InferRouteMap, GlobalCSSOutput, GlobalCSSInput, FormSchema, FormOptions, FormInstance, FormDataOptions, FieldState, ExtractParams, ErrorBoundaryProps, ErrorBoundary, EntityStoreOptions, EntityStore, DisposeFn, DisposalScopeError, DialogStackContext, DialogStack, DialogHandle, DialogDismissedError, DialogComponent, Context, Computed, ComponentRegistry, ComponentLoader, ComponentFunction, CompiledTheme, CompiledRoute, ColorPalette, ChildrenAccessor, ChildValue, CacheStore, CSSOutput, CSSInput, ANIMATION_EASING, ANIMATION_DURATION };
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  slideOutToTop,
21
21
  zoomIn,
22
22
  zoomOut
23
- } from "./shared/chunk-pp3a6xbn.js";
23
+ } from "./shared/chunk-n91rwj2r.js";
24
24
  import {
25
25
  Outlet,
26
26
  OutletContext,
@@ -31,11 +31,11 @@ import {
31
31
  useParams,
32
32
  useRouter,
33
33
  useSearchParams
34
- } from "./shared/chunk-nmjyj8p9.js";
34
+ } from "./shared/chunk-0xcmwgdb.js";
35
35
  import"./shared/chunk-v3yyf79g.js";
36
36
  import {
37
37
  createRouter
38
- } from "./shared/chunk-cq7xg4xe.js";
38
+ } from "./shared/chunk-ka5ked7n.js";
39
39
  import {
40
40
  defineRoutes
41
41
  } from "./shared/chunk-9e92w0wt.js";
@@ -48,8 +48,8 @@ import {
48
48
  import {
49
49
  query,
50
50
  queryMatch
51
- } from "./shared/chunk-wv6kkj1w.js";
52
- import"./shared/chunk-vx0kzack.js";
51
+ } from "./shared/chunk-hh0dhmb4.js";
52
+ import"./shared/chunk-jrtrk5z4.js";
53
53
  import {
54
54
  ThemeProvider,
55
55
  children,
@@ -63,7 +63,7 @@ import {
63
63
  resolveChildren,
64
64
  s,
65
65
  variants
66
- } from "./shared/chunk-0p5f7gmg.js";
66
+ } from "./shared/chunk-qacth5ah.js";
67
67
  import {
68
68
  __append,
69
69
  __element,
@@ -87,9 +87,11 @@ import {
87
87
  computed,
88
88
  createContext,
89
89
  domEffect,
90
+ getContextScope,
90
91
  popScope,
91
92
  pushScope,
92
93
  runCleanups,
94
+ setContextScope,
93
95
  signal,
94
96
  untrack,
95
97
  useContext
@@ -290,6 +292,145 @@ function Suspense(props) {
290
292
  return placeholder;
291
293
  }
292
294
  }
295
+ // src/dialog/dialog-stack.ts
296
+ var DialogStackContext = createContext();
297
+ function useDialogStack() {
298
+ const stack = useContext(DialogStackContext);
299
+ if (!stack) {
300
+ throw new Error("useDialogStack() must be called within DialogStackProvider");
301
+ }
302
+ const capturedScope = getContextScope();
303
+ return {
304
+ open(component, props) {
305
+ return stack.openWithScope(component, props, capturedScope);
306
+ },
307
+ openWithScope: stack.openWithScope,
308
+ get size() {
309
+ return stack.size;
310
+ },
311
+ closeAll() {
312
+ stack.closeAll();
313
+ }
314
+ };
315
+ }
316
+
317
+ class DialogDismissedError extends Error {
318
+ constructor() {
319
+ super("Dialog was dismissed");
320
+ this.name = "DialogDismissedError";
321
+ }
322
+ }
323
+ function createDialogStack(container) {
324
+ const entries = [];
325
+ let nextId = 0;
326
+ function open(component, props, capturedScope) {
327
+ return new Promise((resolve, reject) => {
328
+ if (entries.length > 0) {
329
+ entries[entries.length - 1].wrapper.setAttribute("data-state", "background");
330
+ }
331
+ const wrapper = document.createElement("div");
332
+ wrapper.setAttribute("data-dialog-wrapper", "");
333
+ wrapper.setAttribute("data-state", "open");
334
+ wrapper.setAttribute("data-dialog-depth", "0");
335
+ const entry = {
336
+ id: nextId++,
337
+ wrapper,
338
+ node: null,
339
+ resolve,
340
+ reject,
341
+ cleanups: [],
342
+ dismissible: true
343
+ };
344
+ const prevScope = setContextScope(capturedScope ?? null);
345
+ const scope = pushScope();
346
+ const handle = {
347
+ close: (...args) => {
348
+ closeEntry(entry, args[0]);
349
+ }
350
+ };
351
+ entry.node = component({ ...props, dialog: handle });
352
+ entry.cleanups = [...scope];
353
+ popScope();
354
+ setContextScope(prevScope);
355
+ if (entry.dismissible) {
356
+ wrapper.addEventListener("keydown", (e) => {
357
+ if (e.key === "Escape" && entries[entries.length - 1] === entry) {
358
+ e.preventDefault();
359
+ e.stopPropagation();
360
+ dismissEntry(entry);
361
+ }
362
+ });
363
+ }
364
+ wrapper.appendChild(entry.node);
365
+ container.appendChild(wrapper);
366
+ entries.push(entry);
367
+ updateDepthAttributes();
368
+ });
369
+ }
370
+ function closeEntry(entry, result) {
371
+ const idx = entries.indexOf(entry);
372
+ if (idx === -1)
373
+ return;
374
+ entry.wrapper.setAttribute("data-state", "closed");
375
+ onAnimationsComplete(entry.wrapper, () => {
376
+ runCleanups(entry.cleanups);
377
+ if (entry.wrapper.parentNode === container) {
378
+ container.removeChild(entry.wrapper);
379
+ }
380
+ const entryIdx = entries.indexOf(entry);
381
+ if (entryIdx !== -1) {
382
+ entries.splice(entryIdx, 1);
383
+ }
384
+ if (entries.length > 0) {
385
+ entries[entries.length - 1].wrapper.setAttribute("data-state", "open");
386
+ }
387
+ updateDepthAttributes();
388
+ entry.resolve(result);
389
+ });
390
+ }
391
+ function updateDepthAttributes() {
392
+ for (let i = 0;i < entries.length; i++) {
393
+ entries[i].wrapper.setAttribute("data-dialog-depth", String(entries.length - 1 - i));
394
+ }
395
+ }
396
+ return {
397
+ open(component, props) {
398
+ return open(component, props);
399
+ },
400
+ openWithScope(component, props, scope) {
401
+ return open(component, props, scope);
402
+ },
403
+ get size() {
404
+ return entries.length;
405
+ },
406
+ closeAll() {
407
+ for (let i = entries.length - 1;i >= 0; i--) {
408
+ dismissEntry(entries[i]);
409
+ }
410
+ }
411
+ };
412
+ function dismissEntry(entry) {
413
+ const idx = entries.indexOf(entry);
414
+ if (idx === -1)
415
+ return;
416
+ entry.wrapper.setAttribute("data-state", "closed");
417
+ onAnimationsComplete(entry.wrapper, () => {
418
+ runCleanups(entry.cleanups);
419
+ if (entry.wrapper.parentNode === container) {
420
+ container.removeChild(entry.wrapper);
421
+ }
422
+ const entryIdx = entries.indexOf(entry);
423
+ if (entryIdx !== -1) {
424
+ entries.splice(entryIdx, 1);
425
+ }
426
+ if (entries.length > 0) {
427
+ entries[entries.length - 1].wrapper.setAttribute("data-state", "open");
428
+ }
429
+ updateDepthAttributes();
430
+ entry.reject(new DialogDismissedError);
431
+ });
432
+ }
433
+ }
293
434
  // src/mount.ts
294
435
  function mount(app, selector, options) {
295
436
  if (typeof selector !== "string" && !(selector instanceof HTMLElement)) {
@@ -568,6 +709,7 @@ export {
568
709
  useSearchParams,
569
710
  useRouter,
570
711
  useParams,
712
+ useDialogStack,
571
713
  useContext,
572
714
  untrack,
573
715
  slideOutToTop,
@@ -609,6 +751,7 @@ export {
609
751
  createRouter,
610
752
  createLink,
611
753
  createFieldState,
754
+ createDialogStack,
612
755
  createDOMAdapter,
613
756
  createContext,
614
757
  computed,
@@ -633,6 +776,8 @@ export {
633
776
  ErrorBoundary,
634
777
  EntityStore,
635
778
  DisposalScopeError,
779
+ DialogStackContext,
780
+ DialogDismissedError,
636
781
  ANIMATION_EASING,
637
782
  ANIMATION_DURATION
638
783
  };
package/dist/internals.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  deserializeProps,
3
3
  onAnimationsComplete,
4
4
  resolveComponent
5
- } from "./shared/chunk-pp3a6xbn.js";
5
+ } from "./shared/chunk-n91rwj2r.js";
6
6
  import {
7
7
  __attr,
8
8
  __classList,
@@ -17,8 +17,8 @@ import {
17
17
  import {
18
18
  MemoryCache,
19
19
  deriveKey
20
- } from "./shared/chunk-wv6kkj1w.js";
21
- import"./shared/chunk-vx0kzack.js";
20
+ } from "./shared/chunk-hh0dhmb4.js";
21
+ import"./shared/chunk-jrtrk5z4.js";
22
22
  import {
23
23
  ALIGNMENT_MAP,
24
24
  COLOR_NAMESPACES,
@@ -38,7 +38,7 @@ import {
38
38
  SIZE_KEYWORDS,
39
39
  SPACING_SCALE,
40
40
  compileTheme
41
- } from "./shared/chunk-0p5f7gmg.js";
41
+ } from "./shared/chunk-qacth5ah.js";
42
42
  import {
43
43
  __append,
44
44
  __child,
@@ -45,6 +45,17 @@ interface QueryOptions<T> {
45
45
  cache?: CacheStore<T>;
46
46
  /** Timeout in ms for SSR data loading. Default: 300. Set to 0 to disable. */
47
47
  ssrTimeout?: number;
48
+ /**
49
+ * Polling interval in ms, or a function for dynamic intervals.
50
+ *
51
+ * - `number` — fixed interval in ms
52
+ * - `false` or `0` — disabled
53
+ * - `(data, iteration) => number | false` — called after each fetch to
54
+ * determine the next interval. Return `false` to stop polling.
55
+ * `iteration` counts polls since the last start/restart (resets to 0
56
+ * when the function returns `false`).
57
+ */
58
+ refetchInterval?: number | false | ((data: T | undefined, iteration: number) => number | false);
48
59
  }
49
60
  /** The reactive object returned by query(). */
50
61
  interface QueryResult<
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  query,
3
3
  queryMatch
4
- } from "../shared/chunk-wv6kkj1w.js";
5
- import"../shared/chunk-vx0kzack.js";
4
+ } from "../shared/chunk-hh0dhmb4.js";
5
+ import"../shared/chunk-jrtrk5z4.js";
6
6
  import"../shared/chunk-g4rch80a.js";
7
7
  import"../shared/chunk-hrd0mft1.js";
8
8
 
@@ -251,6 +251,8 @@ interface PrefetchHandle {
251
251
  abort: () => void;
252
252
  /** Resolves when SSE stream completes (data or done event). */
253
253
  done?: Promise<void>;
254
+ /** Resolves when the first SSE event of any type arrives. */
255
+ firstEvent?: Promise<void>;
254
256
  }
255
257
  /** Options for createRouter(). */
256
258
  interface RouterOptions {
@@ -354,6 +356,17 @@ interface RouterViewProps {
354
356
  router: Router;
355
357
  fallback?: () => Node;
356
358
  }
359
+ /**
360
+ * Renders the matched route's component inside a container div.
361
+ *
362
+ * Handles sync and async (lazy-loaded) components, stale resolution guards,
363
+ * page cleanup on navigation, and RouterContext propagation.
364
+ *
365
+ * Uses __element() so the container is claimed from SSR during hydration.
366
+ * On the first hydration render, children are already in the DOM — the
367
+ * domEffect runs the component factory (to attach reactivity/event handlers)
368
+ * but skips clearing the container.
369
+ */
357
370
  declare function RouterView({ router, fallback }: RouterViewProps): HTMLElement;
358
371
  /**
359
372
  * Parse URLSearchParams into a typed object, optionally through a schema.
@@ -8,15 +8,15 @@ import {
8
8
  useParams,
9
9
  useRouter,
10
10
  useSearchParams
11
- } from "../shared/chunk-nmjyj8p9.js";
11
+ } from "../shared/chunk-0xcmwgdb.js";
12
12
  import"../shared/chunk-v3yyf79g.js";
13
13
  import {
14
14
  createRouter
15
- } from "../shared/chunk-cq7xg4xe.js";
15
+ } from "../shared/chunk-ka5ked7n.js";
16
16
  import {
17
17
  defineRoutes
18
18
  } from "../shared/chunk-9e92w0wt.js";
19
- import"../shared/chunk-vx0kzack.js";
19
+ import"../shared/chunk-jrtrk5z4.js";
20
20
  import"../shared/chunk-ryb49346.js";
21
21
  import"../shared/chunk-g4rch80a.js";
22
22
  import"../shared/chunk-hrd0mft1.js";
@@ -23,6 +23,17 @@ import {
23
23
  } from "./chunk-hrd0mft1.js";
24
24
 
25
25
  // src/router/link.ts
26
+ var DANGEROUS_SCHEMES = ["javascript:", "data:", "vbscript:"];
27
+ function isSafeUrl(url) {
28
+ const normalized = url.replace(/\s/g, "").toLowerCase();
29
+ if (normalized.startsWith("//"))
30
+ return false;
31
+ for (const scheme of DANGEROUS_SCHEMES) {
32
+ if (normalized.startsWith(scheme))
33
+ return false;
34
+ }
35
+ return true;
36
+ }
26
37
  function createLink(currentPath, navigate, factoryOptions) {
27
38
  return function Link({
28
39
  href,
@@ -31,15 +42,16 @@ function createLink(currentPath, navigate, factoryOptions) {
31
42
  className,
32
43
  prefetch
33
44
  }) {
45
+ const safeHref = isSafeUrl(href) ? href : "#";
34
46
  const handleClick = (event) => {
35
47
  const mouseEvent = event;
36
48
  if (mouseEvent.ctrlKey || mouseEvent.metaKey || mouseEvent.shiftKey || mouseEvent.altKey) {
37
49
  return;
38
50
  }
39
51
  mouseEvent.preventDefault();
40
- navigate(href);
52
+ navigate(safeHref);
41
53
  };
42
- const props = { href };
54
+ const props = { href: safeHref };
43
55
  if (className) {
44
56
  props.class = className;
45
57
  }
@@ -59,7 +71,7 @@ function createLink(currentPath, navigate, factoryOptions) {
59
71
  __exitChildren();
60
72
  if (activeClass) {
61
73
  __classList(el, {
62
- [activeClass]: () => currentPath.value === href
74
+ [activeClass]: () => currentPath.value === safeHref
63
75
  });
64
76
  }
65
77
  if (prefetch === "hover" && factoryOptions?.onPrefetch) {
@@ -68,7 +80,7 @@ function createLink(currentPath, navigate, factoryOptions) {
68
80
  if (prefetched)
69
81
  return;
70
82
  prefetched = true;
71
- factoryOptions.onPrefetch?.(href);
83
+ factoryOptions.onPrefetch?.(safeHref);
72
84
  };
73
85
  __on(el, "mouseenter", triggerPrefetch);
74
86
  __on(el, "focus", triggerPrefetch);
@@ -152,16 +164,6 @@ function Outlet() {
152
164
  }
153
165
 
154
166
  // src/router/router-view.ts
155
- function hasViewTransition(doc) {
156
- return "startViewTransition" in doc;
157
- }
158
- function withTransition(fn) {
159
- if (typeof document !== "undefined" && hasViewTransition(document)) {
160
- document.startViewTransition(fn);
161
- } else {
162
- fn();
163
- }
164
- }
165
167
  function RouterView({ router, fallback }) {
166
168
  const container = __element("div");
167
169
  let isFirstHydrationRender = getIsHydrating();
@@ -230,11 +232,7 @@ function RouterView({ router, fallback }) {
230
232
  prevLevels = levels;
231
233
  popScope();
232
234
  };
233
- if (isFirstHydrationRender) {
234
- doRender();
235
- } else {
236
- withTransition(doRender);
237
- }
235
+ doRender();
238
236
  });
239
237
  });
240
238
  __exitChildren();
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isNavPrefetchActive
3
- } from "./chunk-vx0kzack.js";
3
+ } from "./chunk-jrtrk5z4.js";
4
4
  import {
5
5
  getAdapter,
6
6
  isRenderNode
@@ -51,20 +51,24 @@ function hashString(str) {
51
51
  import { isQueryDescriptor } from "@vertz/fetch";
52
52
 
53
53
  // src/query/ssr-hydration.ts
54
- function hydrateQueryFromSSR(key, resolve) {
54
+ function hydrateQueryFromSSR(key, resolve, options) {
55
55
  const ssrData = globalThis.__VERTZ_SSR_DATA__;
56
56
  if (!ssrData)
57
57
  return null;
58
+ const persistent = options?.persistent ?? false;
58
59
  const existing = ssrData.find((entry) => entry.key === key);
59
60
  if (existing) {
60
61
  resolve(existing.data);
61
- return () => {};
62
+ if (!persistent)
63
+ return () => {};
62
64
  }
63
65
  const handler = (event) => {
64
66
  const detail = event.detail;
65
67
  if (detail.key === key) {
66
68
  resolve(detail.data);
67
- document.removeEventListener("vertz:ssr-data", handler);
69
+ if (!persistent) {
70
+ document.removeEventListener("vertz:ssr-data", handler);
71
+ }
68
72
  }
69
73
  };
70
74
  document.addEventListener("vertz:ssr-data", handler);
@@ -169,12 +173,13 @@ function query(source, options = {}) {
169
173
  let navPrefetchDeferred = false;
170
174
  if (!isSSR() && enabled && initialData === undefined) {
171
175
  const hydrationKey = customKey ?? baseKey;
176
+ const isNavigation = isNavPrefetchActive();
172
177
  ssrHydrationCleanup = hydrateQueryFromSSR(hydrationKey, (result) => {
173
178
  data.value = result;
174
179
  loading.value = false;
175
180
  cache.set(hydrationKey, result);
176
181
  ssrHydrated = true;
177
- });
182
+ }, { persistent: isNavigation });
178
183
  if (!ssrHydrated && ssrHydrationCleanup !== null && isNavPrefetchActive()) {
179
184
  if (customKey) {
180
185
  const cached = cache.get(customKey);
@@ -189,7 +194,7 @@ function query(source, options = {}) {
189
194
  navPrefetchDeferred = true;
190
195
  const doneHandler = () => {
191
196
  document.removeEventListener("vertz:nav-prefetch-done", doneHandler);
192
- if (data.peek() === undefined) {
197
+ if (data.peek() === undefined && !ssrHydrated) {
193
198
  refetchTrigger.value = refetchTrigger.peek() + 1;
194
199
  }
195
200
  };
@@ -203,8 +208,45 @@ function query(source, options = {}) {
203
208
  }
204
209
  let fetchId = 0;
205
210
  let debounceTimer;
211
+ let intervalTimer;
212
+ const refetchIntervalOption = options.refetchInterval;
213
+ const hasInterval = typeof refetchIntervalOption === "function" || typeof refetchIntervalOption === "number" && refetchIntervalOption > 0;
214
+ let intervalIteration = 0;
206
215
  const inflightKeys = new Set;
207
216
  const refetchTrigger = signal(0);
217
+ let intervalPaused = false;
218
+ function scheduleInterval() {
219
+ if (!hasInterval || isSSR() || intervalPaused)
220
+ return;
221
+ let ms;
222
+ if (typeof refetchIntervalOption === "function") {
223
+ ms = refetchIntervalOption(data.peek(), intervalIteration);
224
+ } else {
225
+ ms = refetchIntervalOption;
226
+ }
227
+ if (ms === false || ms <= 0) {
228
+ intervalIteration = 0;
229
+ return;
230
+ }
231
+ intervalIteration++;
232
+ clearTimeout(intervalTimer);
233
+ intervalTimer = setTimeout(() => {
234
+ refetch();
235
+ }, ms);
236
+ }
237
+ let visibilityHandler;
238
+ if (hasInterval && enabled && !isSSR() && typeof document !== "undefined") {
239
+ visibilityHandler = () => {
240
+ if (document.visibilityState === "hidden") {
241
+ intervalPaused = true;
242
+ clearTimeout(intervalTimer);
243
+ } else {
244
+ intervalPaused = false;
245
+ refetch();
246
+ }
247
+ };
248
+ document.addEventListener("visibilitychange", visibilityHandler);
249
+ }
208
250
  function handleFetchPromise(promise, id, key) {
209
251
  promise.then((result) => {
210
252
  inflight.delete(key);
@@ -215,6 +257,7 @@ function query(source, options = {}) {
215
257
  data.value = result;
216
258
  loading.value = false;
217
259
  revalidating.value = false;
260
+ scheduleInterval();
218
261
  }, (err) => {
219
262
  inflight.delete(key);
220
263
  inflightKeys.delete(key);
@@ -223,6 +266,7 @@ function query(source, options = {}) {
223
266
  error.value = err;
224
267
  loading.value = false;
225
268
  revalidating.value = false;
269
+ scheduleInterval();
226
270
  });
227
271
  }
228
272
  function startFetch(fetchPromise, key) {
@@ -270,6 +314,19 @@ function query(source, options = {}) {
270
314
  isFirst = false;
271
315
  return;
272
316
  }
317
+ } else {
318
+ const trackPromise = callThunkWithCapture();
319
+ trackPromise.catch(() => {});
320
+ const derivedKey = untrack(() => getCacheKey());
321
+ const cached = untrack(() => cache.get(derivedKey));
322
+ if (cached !== undefined) {
323
+ untrack(() => {
324
+ data.value = cached;
325
+ loading.value = false;
326
+ });
327
+ isFirst = false;
328
+ return;
329
+ }
273
330
  }
274
331
  isFirst = false;
275
332
  return;
@@ -311,7 +368,8 @@ function query(source, options = {}) {
311
368
  return;
312
369
  }
313
370
  }
314
- const shouldCheckCache = isFirst ? !!customKey : !customKey;
371
+ const isNavigation = ssrHydrationCleanup !== null;
372
+ const shouldCheckCache = isNavigation || (isFirst ? !!customKey : !customKey);
315
373
  if (shouldCheckCache) {
316
374
  const cached = untrack(() => cache.get(key));
317
375
  if (cached !== undefined) {
@@ -322,12 +380,14 @@ function query(source, options = {}) {
322
380
  error.value = undefined;
323
381
  });
324
382
  isFirst = false;
383
+ scheduleInterval();
325
384
  return;
326
385
  }
327
386
  }
328
387
  if (isFirst && initialData !== undefined) {
329
388
  promise.catch(() => {});
330
389
  isFirst = false;
390
+ scheduleInterval();
331
391
  return;
332
392
  }
333
393
  isFirst = false;
@@ -346,6 +406,10 @@ function query(source, options = {}) {
346
406
  disposeFn?.();
347
407
  ssrHydrationCleanup?.();
348
408
  clearTimeout(debounceTimer);
409
+ clearTimeout(intervalTimer);
410
+ if (visibilityHandler && typeof document !== "undefined") {
411
+ document.removeEventListener("visibilitychange", visibilityHandler);
412
+ }
349
413
  fetchId++;
350
414
  for (const key of inflightKeys) {
351
415
  inflight.delete(key);
@@ -48,18 +48,36 @@ function dispatchPrefetchDone() {
48
48
  setNavPrefetchActive(false);
49
49
  document.dispatchEvent(new CustomEvent("vertz:nav-prefetch-done"));
50
50
  }
51
+ var prefetchGen = 0;
51
52
  function prefetchNavData(url, options) {
52
53
  const controller = new AbortController;
53
54
  const timeout = options?.timeout ?? 5000;
55
+ const gen = ++prefetchGen;
54
56
  ensureSSRDataBus();
55
57
  setNavPrefetchActive(true);
56
58
  const timeoutId = setTimeout(() => controller.abort(), timeout);
59
+ let resolveFirstEvent = null;
60
+ const firstEvent = new Promise((r) => {
61
+ resolveFirstEvent = r;
62
+ });
63
+ function onFirstEvent() {
64
+ if (resolveFirstEvent) {
65
+ resolveFirstEvent();
66
+ resolveFirstEvent = null;
67
+ }
68
+ }
69
+ function finishIfCurrent() {
70
+ if (gen === prefetchGen) {
71
+ dispatchPrefetchDone();
72
+ }
73
+ }
57
74
  const done = fetch(url, {
58
75
  headers: { "X-Vertz-Nav": "1" },
59
76
  signal: controller.signal
60
77
  }).then(async (response) => {
61
78
  if (!response.body) {
62
- dispatchPrefetchDone();
79
+ onFirstEvent();
80
+ finishIfCurrent();
63
81
  return;
64
82
  }
65
83
  const reader = response.body.getReader();
@@ -73,21 +91,24 @@ function prefetchNavData(url, options) {
73
91
  const parsed = parseSSE(buffer);
74
92
  buffer = parsed.remaining;
75
93
  for (const event of parsed.events) {
94
+ onFirstEvent();
76
95
  if (event.type === "data") {
77
96
  try {
78
97
  const { key, data } = JSON.parse(event.data);
79
98
  pushNavData(key, data);
80
99
  } catch {}
81
100
  } else if (event.type === "done") {
82
- dispatchPrefetchDone();
101
+ finishIfCurrent();
83
102
  clearTimeout(timeoutId);
84
103
  return;
85
104
  }
86
105
  }
87
106
  }
88
- dispatchPrefetchDone();
107
+ onFirstEvent();
108
+ finishIfCurrent();
89
109
  }).catch(() => {
90
- dispatchPrefetchDone();
110
+ onFirstEvent();
111
+ finishIfCurrent();
91
112
  }).finally(() => {
92
113
  clearTimeout(timeoutId);
93
114
  });
@@ -96,7 +117,8 @@ function prefetchNavData(url, options) {
96
117
  controller.abort();
97
118
  clearTimeout(timeoutId);
98
119
  },
99
- done
120
+ done,
121
+ firstEvent
100
122
  };
101
123
  }
102
124
 
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-9e92w0wt.js";
5
5
  import {
6
6
  prefetchNavData
7
- } from "./chunk-vx0kzack.js";
7
+ } from "./chunk-jrtrk5z4.js";
8
8
  import {
9
9
  signal
10
10
  } from "./chunk-hrd0mft1.js";
@@ -22,19 +22,38 @@ function createRouter(routes, initialUrl, options) {
22
22
  url = window.location.pathname + window.location.search;
23
23
  }
24
24
  const initialMatch = matchRoute(routes, url);
25
+ function normalizeUrl(rawUrl) {
26
+ const qIdx = rawUrl.indexOf("?");
27
+ if (qIdx === -1)
28
+ return rawUrl;
29
+ const pathname = rawUrl.slice(0, qIdx);
30
+ const params = new URLSearchParams(rawUrl.slice(qIdx + 1));
31
+ params.sort();
32
+ const sorted = params.toString();
33
+ return sorted ? `${pathname}?${sorted}` : pathname;
34
+ }
35
+ const visitedUrls = new Set;
36
+ if (initialMatch)
37
+ visitedUrls.add(normalizeUrl(url));
25
38
  const current = signal(initialMatch);
26
39
  const loaderData = signal([]);
27
40
  const loaderError = signal(null);
28
41
  const searchParams = signal(initialMatch?.search ?? {});
29
42
  let navigationGen = 0;
43
+ let navigateGen = 0;
30
44
  let currentAbort = null;
31
45
  const serverNavEnabled = !!options?.serverNav;
32
46
  const serverNavTimeout = typeof options?.serverNav === "object" ? options.serverNav.timeout : undefined;
33
47
  const prefetchFn = options?._prefetchNavData ?? (serverNavEnabled ? prefetchNavData : null);
34
48
  let activePrefetch = null;
49
+ let activePrefetchUrl = null;
35
50
  function startPrefetch(navUrl) {
36
51
  if (!serverNavEnabled || !prefetchFn)
37
52
  return null;
53
+ const normalized = normalizeUrl(navUrl);
54
+ if (activePrefetch && activePrefetchUrl === normalized) {
55
+ return activePrefetch;
56
+ }
38
57
  if (activePrefetch) {
39
58
  activePrefetch.abort();
40
59
  }
@@ -43,15 +62,14 @@ function createRouter(routes, initialUrl, options) {
43
62
  prefetchOpts.timeout = serverNavTimeout;
44
63
  }
45
64
  activePrefetch = prefetchFn(navUrl, prefetchOpts);
65
+ activePrefetchUrl = normalized;
46
66
  return activePrefetch;
47
67
  }
48
68
  async function awaitPrefetch(handle) {
49
- if (!handle?.done)
69
+ const target = handle?.firstEvent ?? handle?.done;
70
+ if (!target)
50
71
  return;
51
- await Promise.race([
52
- handle.done,
53
- new Promise((r) => setTimeout(r, DEFAULT_NAV_THRESHOLD_MS))
54
- ]);
72
+ await Promise.race([target, new Promise((r) => setTimeout(r, DEFAULT_NAV_THRESHOLD_MS))]);
55
73
  }
56
74
  if (isSSR) {
57
75
  const g = globalThis;
@@ -93,6 +111,7 @@ function createRouter(routes, initialUrl, options) {
93
111
  const match = matchRoute(routes, url2);
94
112
  current.value = match;
95
113
  if (match) {
114
+ visitedUrls.add(normalizeUrl(url2));
96
115
  searchParams.value = match.search;
97
116
  await runLoaders(match, gen, abort.signal);
98
117
  } else {
@@ -104,6 +123,7 @@ function createRouter(routes, initialUrl, options) {
104
123
  }
105
124
  }
106
125
  async function navigate(navUrl, navOptions) {
126
+ const gen = ++navigateGen;
107
127
  const handle = startPrefetch(navUrl);
108
128
  if (!isSSR) {
109
129
  if (navOptions?.replace) {
@@ -112,9 +132,12 @@ function createRouter(routes, initialUrl, options) {
112
132
  window.history.pushState(null, "", navUrl);
113
133
  }
114
134
  }
115
- if (handle?.done) {
135
+ const isCachedNav = visitedUrls.has(normalizeUrl(navUrl));
136
+ if (!isCachedNav && (handle?.firstEvent || handle?.done)) {
116
137
  await awaitPrefetch(handle);
117
138
  }
139
+ if (gen !== navigateGen)
140
+ return;
118
141
  await applyNavigation(navUrl);
119
142
  }
120
143
  async function revalidate() {
@@ -148,6 +171,7 @@ function createRouter(routes, initialUrl, options) {
148
171
  if (activePrefetch) {
149
172
  activePrefetch.abort();
150
173
  activePrefetch = null;
174
+ activePrefetchUrl = null;
151
175
  }
152
176
  }
153
177
  return {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  injectCSS
3
- } from "./chunk-0p5f7gmg.js";
3
+ } from "./chunk-qacth5ah.js";
4
4
 
5
5
  // src/dom/animation.ts
6
6
  function onAnimationsComplete(el, callback) {
@@ -788,6 +788,9 @@ class InlineStyleError extends Error {
788
788
  }
789
789
 
790
790
  // src/css/theme.ts
791
+ function sanitizeCssValue(value) {
792
+ return value.replace(/[;{}]/g, "").replace(/url\s*\(/gi, "").replace(/expression\s*\(/gi, "").replace(/@import/gi, "");
793
+ }
791
794
  function defineTheme(input) {
792
795
  return {
793
796
  colors: input.colors,
@@ -817,17 +820,17 @@ function compileTheme(theme) {
817
820
  for (const [key, value] of Object.entries(values)) {
818
821
  if (key === "DEFAULT") {
819
822
  const varName = `--color-${name}`;
820
- rootVars.push(` ${varName}: ${value};`);
823
+ rootVars.push(` ${varName}: ${sanitizeCssValue(value)};`);
821
824
  tokenPaths.push(name);
822
825
  } else if (key.startsWith("_")) {
823
826
  const variant = key.slice(1);
824
827
  const varName = `--color-${name}`;
825
828
  if (variant === "dark") {
826
- darkVars.push(` ${varName}: ${value};`);
829
+ darkVars.push(` ${varName}: ${sanitizeCssValue(value)};`);
827
830
  }
828
831
  } else {
829
832
  const varName = `--color-${name}-${key}`;
830
- rootVars.push(` ${varName}: ${value};`);
833
+ rootVars.push(` ${varName}: ${sanitizeCssValue(value)};`);
831
834
  tokenPaths.push(`${name}.${key}`);
832
835
  }
833
836
  }
@@ -835,7 +838,7 @@ function compileTheme(theme) {
835
838
  if (theme.spacing) {
836
839
  for (const [name, value] of Object.entries(theme.spacing)) {
837
840
  const varName = `--spacing-${name}`;
838
- rootVars.push(` ${varName}: ${value};`);
841
+ rootVars.push(` ${varName}: ${sanitizeCssValue(value)};`);
839
842
  tokenPaths.push(`spacing.${name}`);
840
843
  }
841
844
  }
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  createRouter
3
- } from "../shared/chunk-cq7xg4xe.js";
3
+ } from "../shared/chunk-ka5ked7n.js";
4
4
  import {
5
5
  defineRoutes
6
6
  } from "../shared/chunk-9e92w0wt.js";
7
- import"../shared/chunk-vx0kzack.js";
7
+ import"../shared/chunk-jrtrk5z4.js";
8
8
  import"../shared/chunk-hrd0mft1.js";
9
9
 
10
10
  // src/test/interactions.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertz/ui",
3
- "version": "0.2.1",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Vertz UI framework — signals, components, JSX runtime",
@@ -63,12 +63,12 @@
63
63
  "typecheck": "tsc --noEmit"
64
64
  },
65
65
  "dependencies": {
66
- "@vertz/fetch": "0.2.1"
66
+ "@vertz/fetch": "^0.2.1"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@happy-dom/global-registrator": "^20.7.0",
70
- "@vertz/schema": "0.2.1",
71
- "bunup": "latest",
70
+ "@vertz/schema": "^0.2.4",
71
+ "bunup": "^0.16.31",
72
72
  "happy-dom": "^20.7.0",
73
73
  "typescript": "^5.7.0"
74
74
  },