@resistdesign/voltra 3.0.0-alpha.45 → 3.0.0-alpha.46
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/README.md +13 -0
- package/native/index.js +72 -55
- package/native/utils/History.d.ts +25 -3
- package/native/utils/Route.d.ts +19 -58
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -242,6 +242,7 @@ const coords = layout.computeNativeCoords({
|
|
|
242
242
|
|
|
243
243
|
Voltra routing uses the same `Route` API across app/web/native.
|
|
244
244
|
Use the platform barrel for root `Route` so runtime mechanics are auto-wired.
|
|
245
|
+
The routing model is path/history based on every platform.
|
|
245
246
|
|
|
246
247
|
Reference example: `examples/routing/app-routing.ts`
|
|
247
248
|
|
|
@@ -271,10 +272,22 @@ import { Route } from "@resistdesign/voltra/native";
|
|
|
271
272
|
</Route>;
|
|
272
273
|
```
|
|
273
274
|
|
|
275
|
+
On native, Voltra provides the missing browser-like pieces:
|
|
276
|
+
|
|
277
|
+
- history-style path state for environments without the browser History API
|
|
278
|
+
- deep-link URL ingress so app opens behave like web navigations
|
|
279
|
+
- hardware back wiring into that same history model
|
|
280
|
+
- the `native` barrel auto-selects browser behavior on React Native web targets
|
|
281
|
+
and native-history behavior on mobile targets
|
|
282
|
+
|
|
274
283
|
How it works:
|
|
275
284
|
|
|
276
285
|
- Root `<Route>` (no `path`) is provider mode.
|
|
277
286
|
- Nested `<Route path="...">` entries are matcher mode.
|
|
287
|
+
- Shared app routing matches normalized path strings on every platform.
|
|
288
|
+
- Web already receives a browser pathname.
|
|
289
|
+
- Native provides the browser-like history/path source that mobile lacks.
|
|
290
|
+
- Native deep-link ingress is mapped into that same path/history model.
|
|
278
291
|
- Strategy is auto-selected:
|
|
279
292
|
- DOM + History API => browser history strategy.
|
|
280
293
|
- Otherwise => in-memory native strategy.
|
package/native/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { computeTrackPixels } from '../chunk-TJFTWPXQ.js';
|
|
2
|
-
import { createFormRenderer, createHistoryBackHandler, buildHistoryPath, AutoFormView, AutoForm, parseTemplate, validateAreas, computeAreaBounds, parseHistoryPath, createMemoryHistory, Route,
|
|
2
|
+
import { createFormRenderer, createHistoryBackHandler, buildHistoryPath, AutoFormView, AutoForm, parseTemplate, validateAreas, computeAreaBounds, parseHistoryPath, createMemoryHistory, createBrowserRouteAdapter, Route, createRouteAdapterFromHistory } from '../chunk-44BMFTKD.js';
|
|
3
3
|
import { ERROR_MESSAGE_CONSTANTS } from '../chunk-YCTVEW2I.js';
|
|
4
|
-
import
|
|
4
|
+
import '../chunk-WNFRDIBW.js';
|
|
5
5
|
import '../chunk-I2KLQ2HA.js';
|
|
6
|
-
import { createElement, useMemo } from 'react';
|
|
6
|
+
import { createElement, useMemo, useRef } from 'react';
|
|
7
7
|
import { Text, View, Platform, Switch, TextInput, Pressable, BackHandler } from 'react-native';
|
|
8
8
|
import { jsx } from 'react/jsx-runtime';
|
|
9
9
|
|
|
@@ -580,10 +580,13 @@ var createNativeHistory = (options = {}) => {
|
|
|
580
580
|
adapter,
|
|
581
581
|
initialPath = "/",
|
|
582
582
|
onIncomingURL = "replace",
|
|
583
|
-
mapURLToPath = mapNativeURLToPath
|
|
583
|
+
mapURLToPath = mapNativeURLToPath,
|
|
584
|
+
backHandler
|
|
584
585
|
} = options;
|
|
585
586
|
const history = createMemoryHistory(initialPath);
|
|
587
|
+
const historyBackHandler = createHistoryBackHandler(history);
|
|
586
588
|
let unsubscribe;
|
|
589
|
+
let stopBackHandler;
|
|
587
590
|
let started = false;
|
|
588
591
|
const applyIncomingURL = (url) => {
|
|
589
592
|
if (!url) {
|
|
@@ -620,6 +623,20 @@ var createNativeHistory = (options = {}) => {
|
|
|
620
623
|
return;
|
|
621
624
|
}
|
|
622
625
|
started = true;
|
|
626
|
+
if (backHandler && !stopBackHandler) {
|
|
627
|
+
const listener = () => historyBackHandler.handle();
|
|
628
|
+
const subscription = backHandler.addEventListener(
|
|
629
|
+
"hardwareBackPress",
|
|
630
|
+
listener
|
|
631
|
+
);
|
|
632
|
+
stopBackHandler = () => {
|
|
633
|
+
if (typeof subscription?.remove === "function") {
|
|
634
|
+
subscription.remove();
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
backHandler.removeEventListener?.("hardwareBackPress", listener);
|
|
638
|
+
};
|
|
639
|
+
}
|
|
623
640
|
if (!adapter) {
|
|
624
641
|
return;
|
|
625
642
|
}
|
|
@@ -651,23 +668,13 @@ var createNativeHistory = (options = {}) => {
|
|
|
651
668
|
}
|
|
652
669
|
unsubscribe?.();
|
|
653
670
|
unsubscribe = void 0;
|
|
671
|
+
stopBackHandler?.();
|
|
672
|
+
stopBackHandler = void 0;
|
|
654
673
|
started = false;
|
|
655
674
|
}
|
|
656
675
|
};
|
|
657
676
|
};
|
|
658
677
|
var createNativeBackHandler = (history) => createHistoryBackHandler(history);
|
|
659
|
-
var createNavigationStateRouteAdapter = (options) => {
|
|
660
|
-
const getPath = () => options.toPath(options.getState());
|
|
661
|
-
const resolvePath = (path) => resolveRouteAdapterPath(getPath(), path);
|
|
662
|
-
return {
|
|
663
|
-
getPath,
|
|
664
|
-
subscribe: (listener) => options.subscribe(() => {
|
|
665
|
-
listener(getPath());
|
|
666
|
-
}),
|
|
667
|
-
push: options.navigate ? (path) => options.navigate?.(resolvePath(path)) : void 0,
|
|
668
|
-
replace: options.replace ? (path) => options.replace?.(resolvePath(path)) : void 0
|
|
669
|
-
};
|
|
670
|
-
};
|
|
671
678
|
var createNativeHardwareBackHandler = (adapter) => {
|
|
672
679
|
return () => {
|
|
673
680
|
if (adapter.canGoBack?.()) {
|
|
@@ -696,53 +703,63 @@ var createNativeRouteBackIntegration = (backHandler) => {
|
|
|
696
703
|
setup: (adapter) => registerNativeHardwareBackHandler(adapter, backHandler)
|
|
697
704
|
};
|
|
698
705
|
};
|
|
706
|
+
var createNativeHistoryRouteAdapter = (initialPath, ingress, backHandler) => {
|
|
707
|
+
const history = createNativeHistory({
|
|
708
|
+
initialPath,
|
|
709
|
+
backHandler,
|
|
710
|
+
...ingress ? {
|
|
711
|
+
adapter: {
|
|
712
|
+
getInitialURL: async () => await ingress.getInitialURL?.() ?? null,
|
|
713
|
+
subscribe: (listener) => ingress.subscribe?.(listener) ?? (() => {
|
|
714
|
+
})
|
|
715
|
+
},
|
|
716
|
+
onIncomingURL: ingress.onIncomingURL,
|
|
717
|
+
mapURLToPath: ingress.mapURLToPath
|
|
718
|
+
} : {}
|
|
719
|
+
});
|
|
720
|
+
const adapter = createRouteAdapterFromHistory(history);
|
|
721
|
+
let subscribers = 0;
|
|
722
|
+
return {
|
|
723
|
+
...adapter,
|
|
724
|
+
subscribe: (listener) => {
|
|
725
|
+
subscribers += 1;
|
|
726
|
+
if (subscribers === 1) {
|
|
727
|
+
void history.start();
|
|
728
|
+
}
|
|
729
|
+
const unlisten = adapter.subscribe(listener);
|
|
730
|
+
return () => {
|
|
731
|
+
unlisten();
|
|
732
|
+
subscribers = Math.max(0, subscribers - 1);
|
|
733
|
+
if (subscribers === 0) {
|
|
734
|
+
history.stop();
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
};
|
|
699
740
|
var Route2 = (props) => {
|
|
700
741
|
const hasMatcherProps = typeof props.path !== "undefined" || typeof props.exact !== "undefined" || typeof props.onParamsChange !== "undefined";
|
|
701
742
|
const nativeRuntime = {
|
|
702
743
|
platformOS: String(Platform?.OS ?? ""),
|
|
703
744
|
backHandler: BackHandler
|
|
704
745
|
};
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
}
|
|
715
|
-
return /* @__PURE__ */ jsx(
|
|
746
|
+
const shouldUseAutoAdapter = !hasMatcherProps && typeof props.adapter === "undefined";
|
|
747
|
+
const routeProps = props;
|
|
748
|
+
const adapterRef = useRef(null);
|
|
749
|
+
if (shouldUseAutoAdapter && !adapterRef.current) {
|
|
750
|
+
adapterRef.current = nativeRuntime.platformOS === "web" ? createBrowserRouteAdapter() : createNativeHistoryRouteAdapter(
|
|
751
|
+
props.initialPath,
|
|
752
|
+
props.ingress,
|
|
753
|
+
nativeRuntime.backHandler
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
return shouldUseAutoAdapter ? /* @__PURE__ */ jsx(
|
|
716
757
|
Route,
|
|
717
758
|
{
|
|
718
|
-
...
|
|
719
|
-
|
|
759
|
+
...routeProps,
|
|
760
|
+
adapter: adapterRef.current ?? void 0
|
|
720
761
|
}
|
|
721
|
-
);
|
|
722
|
-
};
|
|
723
|
-
var expandPattern = (pattern, params = {}) => {
|
|
724
|
-
const segments = getPathArray(pattern, "/", true, true, false, false);
|
|
725
|
-
return segments.map((segment) => {
|
|
726
|
-
if (segment.startsWith(":")) {
|
|
727
|
-
const key = segment.slice(1);
|
|
728
|
-
if (!(key in params)) {
|
|
729
|
-
throw new Error(`Missing param "${key}" for route pattern "${pattern}".`);
|
|
730
|
-
}
|
|
731
|
-
return params[key];
|
|
732
|
-
}
|
|
733
|
-
return segment;
|
|
734
|
-
});
|
|
735
|
-
};
|
|
736
|
-
var buildPathFromRouteChain = (routeChain, config, query) => {
|
|
737
|
-
const segments = [];
|
|
738
|
-
routeChain.forEach((route) => {
|
|
739
|
-
const pattern = config[route.name];
|
|
740
|
-
if (!pattern) {
|
|
741
|
-
throw new Error(`Missing route pattern for "${route.name}".`);
|
|
742
|
-
}
|
|
743
|
-
segments.push(...expandPattern(pattern, route.params));
|
|
744
|
-
});
|
|
745
|
-
return buildRoutePath(segments, query);
|
|
762
|
+
) : /* @__PURE__ */ jsx(Route, { ...routeProps });
|
|
746
763
|
};
|
|
747
764
|
|
|
748
|
-
export { ArrayContainer, ArrayItemWrapper, AutoField, AutoForm2 as AutoForm, AutoFormView2 as AutoFormView, Button, ErrorMessage, FieldWrapper, NativeEasyLayoutView, Route2 as Route,
|
|
765
|
+
export { ArrayContainer, ArrayItemWrapper, AutoField, AutoForm2 as AutoForm, AutoFormView2 as AutoFormView, Button, ErrorMessage, FieldWrapper, NativeEasyLayoutView, Route2 as Route, createNativeBackHandler, createNativeFormRenderer, createNativeHardwareBackHandler, createNativeHistory, createNativeRouteBackIntegration, makeNativeEasyLayout, mapNativeURLToPath, nativeAutoField, nativeSuite, registerNativeHardwareBackHandler, useNativeEasyLayout };
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @packageDocumentation
|
|
3
3
|
*
|
|
4
|
-
* Native history helpers that
|
|
4
|
+
* Native history helpers that provide the mobile equivalent of browser
|
|
5
|
+
* location/history behavior for shared app Route matching.
|
|
5
6
|
*/
|
|
6
7
|
import type { HistoryController } from "../../app/utils/History";
|
|
7
8
|
/**
|
|
8
9
|
* Adapter contract for React Native deep-link APIs.
|
|
9
10
|
*
|
|
10
|
-
* Intended for RN `Linking` wrappers
|
|
11
|
+
* Intended for RN `Linking` wrappers so native URL opens behave like browser
|
|
12
|
+
* navigations entering the shared Voltra history model.
|
|
11
13
|
*/
|
|
12
14
|
export type NativeLinkAdapter = {
|
|
13
15
|
/**
|
|
@@ -23,8 +25,19 @@ export type NativeLinkAdapter = {
|
|
|
23
25
|
* Mode for incoming URL handling.
|
|
24
26
|
*/
|
|
25
27
|
export type NativeIncomingURLMode = "push" | "replace";
|
|
28
|
+
/**
|
|
29
|
+
* BackHandler-like contract for native platform back actions.
|
|
30
|
+
*/
|
|
31
|
+
export type NativeBackHandlerLike = {
|
|
32
|
+
addEventListener: (eventName: "hardwareBackPress", listener: () => boolean) => {
|
|
33
|
+
remove?: () => void;
|
|
34
|
+
} | void;
|
|
35
|
+
removeEventListener?: (eventName: "hardwareBackPress", listener: () => boolean) => void;
|
|
36
|
+
};
|
|
26
37
|
/**
|
|
27
38
|
* Native history controller with explicit lifecycle hooks.
|
|
39
|
+
*
|
|
40
|
+
* This is the native/mobile analogue to browser-backed history in web apps.
|
|
28
41
|
*/
|
|
29
42
|
export type NativeHistoryController = HistoryController & {
|
|
30
43
|
/**
|
|
@@ -62,16 +75,25 @@ export type CreateNativeHistoryOptions = {
|
|
|
62
75
|
* Default: {@link mapNativeURLToPath}.
|
|
63
76
|
*/
|
|
64
77
|
mapURLToPath?: (url: string) => string;
|
|
78
|
+
/**
|
|
79
|
+
* Optional native platform back handler wired into this history runtime.
|
|
80
|
+
*/
|
|
81
|
+
backHandler?: NativeBackHandlerLike;
|
|
65
82
|
};
|
|
66
83
|
/**
|
|
67
84
|
* Default native URL -> path mapping.
|
|
68
85
|
*
|
|
69
|
-
* Strips scheme and host, preserves path + query + hash
|
|
86
|
+
* Strips scheme and host, preserves path + query + hash so incoming native URLs
|
|
87
|
+
* become the same route paths used on web.
|
|
70
88
|
*/
|
|
71
89
|
export declare const mapNativeURLToPath: (url: string) => string;
|
|
72
90
|
/**
|
|
73
91
|
* Create a native history controller backed by in-memory history.
|
|
74
92
|
*
|
|
93
|
+
* This is the primary native routing primitive when the environment does not
|
|
94
|
+
* provide browser history. It gives shared Route matching a stable path/history
|
|
95
|
+
* source and applies incoming deep links as navigations in that same model.
|
|
96
|
+
*
|
|
75
97
|
* Lifecycle behavior:
|
|
76
98
|
* - `start()` is idempotent.
|
|
77
99
|
* - `stop()` is idempotent.
|
package/native/utils/Route.d.ts
CHANGED
|
@@ -1,54 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @packageDocumentation
|
|
3
3
|
*
|
|
4
|
-
* Native routing helpers that
|
|
5
|
-
*/
|
|
6
|
-
import { type PropsWithChildren } from "react";
|
|
7
|
-
import type { RouteAdapter, RouteProps, RouteQuery, RouteRuntimeIntegration } from "../../app/utils/Route";
|
|
8
|
-
/**
|
|
9
|
-
* Options to adapt a navigation state container into a RouteAdapter.
|
|
10
|
-
*/
|
|
11
|
-
export type NavigationStateAdapterOptions<TState> = {
|
|
12
|
-
/** Return the current navigation state. */
|
|
13
|
-
getState: () => TState;
|
|
14
|
-
/** Subscribe to navigation state changes. */
|
|
15
|
-
subscribe: (listener: () => void) => () => void;
|
|
16
|
-
/** Convert navigation state into a path string. */
|
|
17
|
-
toPath: (state: TState) => string;
|
|
18
|
-
/** Optional navigation handler used for push-style transitions. */
|
|
19
|
-
navigate?: (path: string) => void;
|
|
20
|
-
/** Optional navigation handler used for replace-style transitions. */
|
|
21
|
-
replace?: (path: string) => void;
|
|
22
|
-
};
|
|
23
|
-
/**
|
|
24
|
-
* Contract for React Native BackHandler-like integrations.
|
|
25
|
-
*/
|
|
26
|
-
export type NativeBackHandlerLike = {
|
|
27
|
-
addEventListener: (eventName: "hardwareBackPress", listener: () => boolean) => {
|
|
28
|
-
remove?: () => void;
|
|
29
|
-
} | void;
|
|
30
|
-
removeEventListener?: (eventName: "hardwareBackPress", listener: () => boolean) => void;
|
|
31
|
-
};
|
|
32
|
-
/**
|
|
33
|
-
* Route node in a navigation chain.
|
|
34
|
-
*/
|
|
35
|
-
export type NavigationRouteNode = {
|
|
36
|
-
/** Route name as reported by the navigation library. */
|
|
37
|
-
name: string;
|
|
38
|
-
/** Optional route params used to populate path patterns. */
|
|
39
|
-
params?: Record<string, any>;
|
|
40
|
-
};
|
|
41
|
-
/**
|
|
42
|
-
* Mapping of route names to path patterns (e.g. "books/:id").
|
|
43
|
-
*/
|
|
44
|
-
export type NavigationRouteConfig = Record<string, string>;
|
|
45
|
-
/**
|
|
46
|
-
* Create a RouteAdapter from a navigation state container (e.g., react-navigation).
|
|
4
|
+
* Native routing helpers that keep shared app Route semantics intact on mobile.
|
|
47
5
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
6
|
+
* The primary native model is still Voltra path/history routing. These helpers
|
|
7
|
+
* supply the runtime selection needed by the `native` barrel:
|
|
8
|
+
* - React Native mobile uses native history + deep-link + platform back
|
|
9
|
+
* - React Native web uses the browser adapter
|
|
10
|
+
*
|
|
11
|
+
* This separation keeps `app` free of native-platform code.
|
|
50
12
|
*/
|
|
51
|
-
|
|
13
|
+
import { type PropsWithChildren } from "react";
|
|
14
|
+
import type { RouteProps, RouteRuntimeIntegration } from "../../app/utils/Route";
|
|
15
|
+
import type { RouteAdapter } from "../../app/utils/Route";
|
|
16
|
+
import { type NativeBackHandlerLike } from "./History";
|
|
52
17
|
/**
|
|
53
18
|
* Create a hardware-back listener from a route adapter.
|
|
54
19
|
*/
|
|
@@ -58,23 +23,19 @@ export declare const createNativeHardwareBackHandler: (adapter: RouteAdapter) =>
|
|
|
58
23
|
*/
|
|
59
24
|
export declare const registerNativeHardwareBackHandler: (adapter: RouteAdapter, backHandler: NativeBackHandlerLike) => () => void;
|
|
60
25
|
/**
|
|
61
|
-
*
|
|
26
|
+
* Low-level helper to build a Route runtime integration from a BackHandler.
|
|
27
|
+
*
|
|
28
|
+
* Native Route no longer uses this for the default path because platform back
|
|
29
|
+
* ownership now lives with the native adapter/history layer. This remains
|
|
30
|
+
* available for manual integrations.
|
|
62
31
|
*/
|
|
63
32
|
export declare const createNativeRouteBackIntegration: (backHandler: NativeBackHandlerLike) => RouteRuntimeIntegration;
|
|
64
33
|
/**
|
|
65
34
|
* Native Route wrapper for root/provider mode.
|
|
66
35
|
*
|
|
67
36
|
* Behavior:
|
|
68
|
-
* - On mobile
|
|
69
|
-
*
|
|
37
|
+
* - On React Native mobile runtimes, auto-injects a native-history-backed
|
|
38
|
+
* adapter so native history owns platform back actions.
|
|
39
|
+
* - On React Native web runtimes, auto-injects the browser adapter.
|
|
70
40
|
*/
|
|
71
41
|
export declare const Route: <ParamsType extends Record<string, any>>(props: PropsWithChildren<RouteProps<ParamsType>>) => import("react/jsx-runtime").JSX.Element;
|
|
72
|
-
/**
|
|
73
|
-
* Build a path from a navigation route chain and route config mapping.
|
|
74
|
-
*
|
|
75
|
-
* @param routeChain - Ordered list of routes from root to leaf.
|
|
76
|
-
* @param config - Route name to path pattern mapping.
|
|
77
|
-
* @param query - Optional query parameters appended to the path.
|
|
78
|
-
* @returns Serialized path string.
|
|
79
|
-
*/
|
|
80
|
-
export declare const buildPathFromRouteChain: (routeChain: NavigationRouteNode[], config: NavigationRouteConfig, query?: RouteQuery) => string;
|