@sigmela/router 0.1.2 → 0.2.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.
Files changed (52) hide show
  1. package/README.md +177 -833
  2. package/lib/module/Navigation.js +1 -10
  3. package/lib/module/NavigationStack.js +168 -19
  4. package/lib/module/Router.js +1508 -501
  5. package/lib/module/RouterContext.js +1 -1
  6. package/lib/module/ScreenStack/ScreenStack.web.js +343 -117
  7. package/lib/module/ScreenStack/ScreenStackContext.js +15 -0
  8. package/lib/module/ScreenStack/animationHelpers.js +72 -0
  9. package/lib/module/ScreenStackItem/ScreenStackItem.js +2 -1
  10. package/lib/module/ScreenStackItem/ScreenStackItem.web.js +76 -16
  11. package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.native.js +2 -1
  12. package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.web.js +1 -1
  13. package/lib/module/SplitView/RenderSplitView.native.js +85 -0
  14. package/lib/module/SplitView/RenderSplitView.web.js +79 -0
  15. package/lib/module/SplitView/SplitView.js +89 -0
  16. package/lib/module/SplitView/SplitViewContext.js +4 -0
  17. package/lib/module/SplitView/index.js +5 -0
  18. package/lib/module/SplitView/useSplitView.js +11 -0
  19. package/lib/module/StackRenderer.js +4 -2
  20. package/lib/module/TabBar/RenderTabBar.native.js +118 -33
  21. package/lib/module/TabBar/RenderTabBar.web.js +52 -47
  22. package/lib/module/TabBar/TabBar.js +117 -3
  23. package/lib/module/TabBar/index.js +4 -1
  24. package/lib/module/TabBar/useTabBarHeight.js +22 -0
  25. package/lib/module/index.js +3 -4
  26. package/lib/module/navigationNode.js +3 -0
  27. package/lib/module/styles.css +693 -28
  28. package/lib/typescript/src/NavigationStack.d.ts +25 -13
  29. package/lib/typescript/src/Router.d.ts +147 -34
  30. package/lib/typescript/src/RouterContext.d.ts +1 -1
  31. package/lib/typescript/src/ScreenStack/ScreenStack.web.d.ts +0 -2
  32. package/lib/typescript/src/ScreenStack/ScreenStackContext.d.ts +22 -0
  33. package/lib/typescript/src/ScreenStack/animationHelpers.d.ts +6 -0
  34. package/lib/typescript/src/ScreenStackItem/ScreenStackItem.types.d.ts +5 -1
  35. package/lib/typescript/src/ScreenStackItem/ScreenStackItem.web.d.ts +1 -1
  36. package/lib/typescript/src/SplitView/RenderSplitView.native.d.ts +8 -0
  37. package/lib/typescript/src/SplitView/RenderSplitView.web.d.ts +8 -0
  38. package/lib/typescript/src/SplitView/SplitView.d.ts +31 -0
  39. package/lib/typescript/src/SplitView/SplitViewContext.d.ts +3 -0
  40. package/lib/typescript/src/SplitView/index.d.ts +5 -0
  41. package/lib/typescript/src/SplitView/useSplitView.d.ts +2 -0
  42. package/lib/typescript/src/StackRenderer.d.ts +2 -1
  43. package/lib/typescript/src/TabBar/TabBar.d.ts +27 -3
  44. package/lib/typescript/src/TabBar/index.d.ts +3 -0
  45. package/lib/typescript/src/TabBar/useTabBarHeight.d.ts +18 -0
  46. package/lib/typescript/src/createController.d.ts +1 -0
  47. package/lib/typescript/src/index.d.ts +4 -3
  48. package/lib/typescript/src/navigationNode.d.ts +41 -0
  49. package/lib/typescript/src/types.d.ts +21 -32
  50. package/package.json +6 -5
  51. package/lib/module/web/TransitionStack.js +0 -227
  52. package/lib/typescript/src/web/TransitionStack.d.ts +0 -21
@@ -1,8 +1,5 @@
1
1
  "use strict";
2
2
 
3
- // import { ScreenStackItem as RNNScreenStackItem } from 'react-native-screens';
4
-
5
- import { RenderTabBar } from './TabBar/RenderTabBar';
6
3
  import { ScreenStackItem } from "./ScreenStackItem/index.js";
7
4
  import { RouterContext } from "./RouterContext.js";
8
5
  import { ScreenStack } from "./ScreenStack/index.js";
@@ -20,19 +17,16 @@ export const Navigation = /*#__PURE__*/memo(({
20
17
  appearance
21
18
  }) => {
22
19
  const [root, setRoot] = useState(() => ({
23
- hasTabBar: router.hasTabBar(),
24
20
  rootId: router.getRootStackId()
25
21
  }));
26
22
  useEffect(() => {
27
23
  return router.subscribeRoot(() => {
28
24
  setRoot({
29
- hasTabBar: router.hasTabBar(),
30
25
  rootId: router.getRootStackId()
31
26
  });
32
27
  });
33
28
  }, [router]);
34
29
  const {
35
- hasTabBar,
36
30
  rootId
37
31
  } = root;
38
32
  const rootTransition = router.getRootTransition();
@@ -43,10 +37,7 @@ export const Navigation = /*#__PURE__*/memo(({
43
37
  value: router,
44
38
  children: /*#__PURE__*/_jsxs(ScreenStack, {
45
39
  style: styles.flex,
46
- children: [hasTabBar && /*#__PURE__*/_jsx(RenderTabBar, {
47
- tabBar: router.tabBar,
48
- appearance: appearance
49
- }), rootItems.map(item => /*#__PURE__*/_jsx(ScreenStackItem, {
40
+ children: [rootItems.map(item => /*#__PURE__*/_jsx(ScreenStackItem, {
50
41
  stackId: rootId,
51
42
  item: item,
52
43
  stackAnimation: rootTransition,
@@ -1,43 +1,82 @@
1
1
  "use strict";
2
2
 
3
3
  import { nanoid } from 'nanoid/non-secure';
4
- import { match } from 'path-to-regexp';
4
+ import { match as pathMatchFactory } from 'path-to-regexp';
5
+ import qs from 'query-string';
5
6
  export class NavigationStack {
6
7
  routes = [];
7
-
8
- // Overloads
9
-
10
- constructor(idOrOptions, maybeOptions) {
8
+ children = [];
9
+ debugEnabled = false;
10
+ constructor(idOrOptions, maybeOptions, maybeDebug) {
11
11
  if (typeof idOrOptions === 'string') {
12
12
  this.stackId = idOrOptions ?? `stack-${nanoid()}`;
13
13
  this.defaultOptions = maybeOptions;
14
+ this.debugEnabled = maybeDebug ?? false;
14
15
  } else {
15
16
  this.stackId = `stack-${nanoid()}`;
16
17
  this.defaultOptions = idOrOptions;
18
+ this.debugEnabled = false;
19
+ }
20
+ }
21
+ log(message, data) {
22
+ if (this.debugEnabled) {
23
+ if (data !== undefined) {
24
+ console.log(`[NavigationStack] ${message}`, data);
25
+ } else {
26
+ console.log(`[NavigationStack] ${message}`);
27
+ }
17
28
  }
18
29
  }
19
30
  getId() {
20
31
  return this.stackId;
21
32
  }
22
- addScreen(path, mixedComponent, options) {
33
+ addScreen(pathPattern, mixedComponent, options) {
23
34
  const {
24
35
  component,
25
- controller
36
+ controller,
37
+ childNode
26
38
  } = this.extractComponent(mixedComponent);
39
+ const parsed = qs.parseUrl(pathPattern);
40
+ const pathnamePattern = parsed.url || '/';
41
+ const rawQuery = parsed.query || {};
42
+ const isWildcardPath = pathnamePattern === '*';
43
+ const queryPattern = this.buildQueryPattern(rawQuery);
44
+ const baseSpecificity = this.computeBaseSpecificity(pathnamePattern, isWildcardPath, queryPattern);
27
45
  const routeId = `${this.stackId}-route-${this.routes.length}`;
28
- const matcher = match(path);
29
- this.routes.push({
46
+ const pathMatcher = !isWildcardPath ? pathMatchFactory(pathnamePattern) : null;
47
+ const matchPath = p => {
48
+ if (isWildcardPath) {
49
+ return {
50
+ params: {}
51
+ };
52
+ }
53
+ const result = pathMatcher ? pathMatcher(p) : null;
54
+ return result ? {
55
+ params: result.params ?? {}
56
+ } : false;
57
+ };
58
+ const builtRoute = {
30
59
  routeId,
31
- path,
32
- match: p => {
33
- const result = matcher(p);
34
- return result ? {
35
- params: result.params ?? {}
36
- } : false;
37
- },
60
+ path: pathPattern,
61
+ pathnamePattern,
62
+ isWildcardPath,
63
+ queryPattern,
64
+ baseSpecificity,
65
+ matchPath,
38
66
  component,
39
67
  controller,
40
- options
68
+ options,
69
+ childNode
70
+ };
71
+ this.routes.push(builtRoute);
72
+ this.log('addRoute', {
73
+ stackId: this.stackId,
74
+ routeId,
75
+ path: pathPattern,
76
+ pathnamePattern,
77
+ isWildcardPath,
78
+ queryPattern,
79
+ baseSpecificity
41
80
  });
42
81
  return this;
43
82
  }
@@ -53,6 +92,28 @@ export class NavigationStack {
53
92
  stackPresentation: 'sheet'
54
93
  });
55
94
  }
95
+ addStack(prefixOrStack, maybeStack) {
96
+ const hasExplicitPrefix = typeof prefixOrStack === 'string';
97
+ const prefixRaw = hasExplicitPrefix ? prefixOrStack : '';
98
+ const stack = hasExplicitPrefix ? maybeStack : prefixOrStack;
99
+ if (!stack) {
100
+ throw new Error('NavigationStack.addStack: child stack is required');
101
+ }
102
+ const prefix = this.normalizePrefix(prefixRaw ?? '');
103
+ this.children.push({
104
+ prefix,
105
+ node: stack
106
+ });
107
+ this.log('addStack', {
108
+ stackId: this.stackId,
109
+ childId: stack.getId(),
110
+ prefix
111
+ });
112
+ return this;
113
+ }
114
+ getChildren() {
115
+ return this.children.slice();
116
+ }
56
117
  getRoutes() {
57
118
  return this.routes.slice();
58
119
  }
@@ -62,17 +123,105 @@ export class NavigationStack {
62
123
  getDefaultOptions() {
63
124
  return this.defaultOptions;
64
125
  }
126
+ getNodeRoutes() {
127
+ return this.getRoutes();
128
+ }
129
+ getNodeChildren() {
130
+ return this.children.slice();
131
+ }
132
+ getRenderer() {
133
+ return () => null;
134
+ }
135
+ seed() {
136
+ const first = this.getFirstRoute();
137
+ if (!first) return null;
138
+ return {
139
+ routeId: first.routeId,
140
+ params: {},
141
+ path: first.path,
142
+ stackId: this.stackId
143
+ };
144
+ }
65
145
  extractComponent(component) {
146
+ if (this.isNavigationNode(component)) {
147
+ return {
148
+ controller: undefined,
149
+ component: component.getRenderer(),
150
+ childNode: component
151
+ };
152
+ }
66
153
  const componentWithController = component;
67
154
  if (componentWithController?.component) {
68
155
  return {
69
156
  controller: componentWithController.controller,
70
- component: componentWithController.component
157
+ component: componentWithController.component,
158
+ childNode: componentWithController.childNode
71
159
  };
72
160
  }
73
161
  return {
74
162
  component: component,
75
- controller: undefined
163
+ controller: undefined,
164
+ childNode: undefined
76
165
  };
77
166
  }
167
+ isNavigationNode(obj) {
168
+ return obj && typeof obj === 'object' && typeof obj.getNodeRoutes === 'function' && typeof obj.getNodeChildren === 'function' && typeof obj.getRenderer === 'function';
169
+ }
170
+ buildQueryPattern(rawQuery) {
171
+ const patternEntries = [];
172
+ for (const [key, value] of Object.entries(rawQuery)) {
173
+ if (typeof value !== 'string') {
174
+ continue;
175
+ }
176
+ if (value.startsWith(':') && value.length > 1) {
177
+ patternEntries.push([key, {
178
+ type: 'param',
179
+ name: value.slice(1)
180
+ }]);
181
+ } else {
182
+ patternEntries.push([key, {
183
+ type: 'const',
184
+ value
185
+ }]);
186
+ }
187
+ }
188
+ if (!patternEntries.length) return null;
189
+ return Object.fromEntries(patternEntries);
190
+ }
191
+ computeBaseSpecificity(pathnamePattern, isWildcardPath, queryPattern) {
192
+ let pathScore = 0;
193
+ if (!isWildcardPath) {
194
+ const segments = pathnamePattern.split('/').filter(Boolean);
195
+ for (const seg of segments) {
196
+ if (seg.startsWith(':')) {
197
+ pathScore += 1;
198
+ } else {
199
+ pathScore += 2;
200
+ }
201
+ }
202
+ } else {
203
+ pathScore = 0;
204
+ }
205
+ let queryScore = 0;
206
+ if (queryPattern) {
207
+ for (const token of Object.values(queryPattern)) {
208
+ if (token.type === 'const') {
209
+ queryScore += 2;
210
+ } else {
211
+ queryScore += 1;
212
+ }
213
+ }
214
+ }
215
+ return pathScore + queryScore;
216
+ }
217
+ normalizePrefix(input) {
218
+ if (!input || input === '/') {
219
+ return '';
220
+ }
221
+ let normalized = input.startsWith('/') ? input : `/${input}`;
222
+ if (normalized.length > 1 && normalized.endsWith('/')) {
223
+ normalized = normalized.slice(0, -1);
224
+ }
225
+ return normalized;
226
+ }
78
227
  }