@sigmela/router 0.3.6 → 0.4.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 (118) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +2 -1
  3. package/README.md +108 -479
  4. package/dist/Router.d.ts +190 -0
  5. package/dist/Router.d.ts.map +1 -0
  6. package/dist/components/Modal.d.ts +56 -0
  7. package/dist/components/Modal.d.ts.map +1 -0
  8. package/dist/components/OverlayContentScope.d.ts +8 -0
  9. package/dist/components/OverlayContentScope.d.ts.map +1 -0
  10. package/dist/components/RouterCloseButton.d.ts +9 -0
  11. package/dist/components/RouterCloseButton.d.ts.map +1 -0
  12. package/dist/components/Sheet.d.ts +53 -0
  13. package/dist/components/Sheet.d.ts.map +1 -0
  14. package/dist/components/SheetSurface.d.ts +27 -0
  15. package/dist/components/SheetSurface.d.ts.map +1 -0
  16. package/dist/components/index.d.ts +4 -0
  17. package/dist/components/index.d.ts.map +1 -0
  18. package/dist/components/pageScrollLock.d.ts +3 -0
  19. package/dist/components/pageScrollLock.d.ts.map +1 -0
  20. package/dist/components/sheetDragEngine.d.ts +27 -0
  21. package/dist/components/sheetDragEngine.d.ts.map +1 -0
  22. package/dist/index.d.ts +5 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +3649 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/navigationHistory.d.ts +17 -0
  27. package/dist/navigationHistory.d.ts.map +1 -0
  28. package/dist/router.css +971 -0
  29. package/dist/types.d.ts +3 -0
  30. package/dist/types.d.ts.map +1 -0
  31. package/dist/utils/id.d.ts +2 -0
  32. package/dist/utils/id.d.ts.map +1 -0
  33. package/package.json +69 -174
  34. package/lib/module/Drawer/Drawer.js +0 -250
  35. package/lib/module/Drawer/DrawerContext.js +0 -4
  36. package/lib/module/Drawer/DrawerIcon.web.js +0 -47
  37. package/lib/module/Drawer/RenderDrawer.native.js +0 -248
  38. package/lib/module/Drawer/RenderDrawer.web.js +0 -197
  39. package/lib/module/Drawer/useDrawer.js +0 -11
  40. package/lib/module/Navigation.js +0 -79
  41. package/lib/module/NavigationStack.js +0 -254
  42. package/lib/module/Router.js +0 -2171
  43. package/lib/module/RouterContext.js +0 -33
  44. package/lib/module/ScreenStack/ScreenStack.native.js +0 -3
  45. package/lib/module/ScreenStack/ScreenStack.web.js +0 -561
  46. package/lib/module/ScreenStack/ScreenStackContext.js +0 -21
  47. package/lib/module/ScreenStack/animationHelpers.js +0 -87
  48. package/lib/module/ScreenStack/index.js +0 -3
  49. package/lib/module/ScreenStackItem/ScreenStackItem.js +0 -70
  50. package/lib/module/ScreenStackItem/ScreenStackItem.types.js +0 -3
  51. package/lib/module/ScreenStackItem/ScreenStackItem.web.js +0 -171
  52. package/lib/module/ScreenStackItem/index.js +0 -3
  53. package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.native.js +0 -70
  54. package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.web.js +0 -5
  55. package/lib/module/ScreenStackSheetItem/index.js +0 -3
  56. package/lib/module/SplitView/RenderSplitView.native.js +0 -101
  57. package/lib/module/SplitView/RenderSplitView.web.js +0 -107
  58. package/lib/module/SplitView/SplitView.js +0 -97
  59. package/lib/module/SplitView/SplitViewContext.js +0 -4
  60. package/lib/module/SplitView/index.js +0 -5
  61. package/lib/module/SplitView/useSplitView.js +0 -11
  62. package/lib/module/StackRenderer.js +0 -42
  63. package/lib/module/TabBar/RenderTabBar.native.js +0 -418
  64. package/lib/module/TabBar/RenderTabBar.web.js +0 -218
  65. package/lib/module/TabBar/TabBar.js +0 -208
  66. package/lib/module/TabBar/TabBarContext.js +0 -4
  67. package/lib/module/TabBar/TabIcon.web.js +0 -47
  68. package/lib/module/TabBar/index.js +0 -6
  69. package/lib/module/TabBar/useTabBar.js +0 -11
  70. package/lib/module/TabBar/useTabBarHeight.js +0 -22
  71. package/lib/module/createController.js +0 -5
  72. package/lib/module/index.js +0 -15
  73. package/lib/module/navigationNode.js +0 -3
  74. package/lib/module/package.json +0 -1
  75. package/lib/module/styles.css +0 -1111
  76. package/lib/module/types.js +0 -13
  77. package/lib/typescript/package.json +0 -1
  78. package/lib/typescript/src/Drawer/Drawer.d.ts +0 -100
  79. package/lib/typescript/src/Drawer/DrawerContext.d.ts +0 -3
  80. package/lib/typescript/src/Drawer/DrawerIcon.web.d.ts +0 -7
  81. package/lib/typescript/src/Drawer/RenderDrawer.native.d.ts +0 -8
  82. package/lib/typescript/src/Drawer/RenderDrawer.web.d.ts +0 -8
  83. package/lib/typescript/src/Drawer/useDrawer.d.ts +0 -2
  84. package/lib/typescript/src/Navigation.d.ts +0 -17
  85. package/lib/typescript/src/NavigationStack.d.ts +0 -46
  86. package/lib/typescript/src/Router.d.ts +0 -232
  87. package/lib/typescript/src/RouterContext.d.ts +0 -18
  88. package/lib/typescript/src/ScreenStack/ScreenStack.native.d.ts +0 -2
  89. package/lib/typescript/src/ScreenStack/ScreenStack.web.d.ts +0 -9
  90. package/lib/typescript/src/ScreenStack/ScreenStackContext.d.ts +0 -31
  91. package/lib/typescript/src/ScreenStack/animationHelpers.d.ts +0 -6
  92. package/lib/typescript/src/ScreenStack/index.d.ts +0 -2
  93. package/lib/typescript/src/ScreenStackItem/ScreenStackItem.d.ts +0 -3
  94. package/lib/typescript/src/ScreenStackItem/ScreenStackItem.types.d.ts +0 -14
  95. package/lib/typescript/src/ScreenStackItem/ScreenStackItem.web.d.ts +0 -3
  96. package/lib/typescript/src/ScreenStackItem/index.d.ts +0 -3
  97. package/lib/typescript/src/ScreenStackSheetItem/ScreenStackSheetItem.native.d.ts +0 -11
  98. package/lib/typescript/src/ScreenStackSheetItem/ScreenStackSheetItem.web.d.ts +0 -2
  99. package/lib/typescript/src/ScreenStackSheetItem/index.d.ts +0 -2
  100. package/lib/typescript/src/SplitView/RenderSplitView.native.d.ts +0 -19
  101. package/lib/typescript/src/SplitView/RenderSplitView.web.d.ts +0 -8
  102. package/lib/typescript/src/SplitView/SplitView.d.ts +0 -33
  103. package/lib/typescript/src/SplitView/SplitViewContext.d.ts +0 -3
  104. package/lib/typescript/src/SplitView/index.d.ts +0 -5
  105. package/lib/typescript/src/SplitView/useSplitView.d.ts +0 -2
  106. package/lib/typescript/src/StackRenderer.d.ts +0 -8
  107. package/lib/typescript/src/TabBar/RenderTabBar.native.d.ts +0 -8
  108. package/lib/typescript/src/TabBar/RenderTabBar.web.d.ts +0 -8
  109. package/lib/typescript/src/TabBar/TabBar.d.ts +0 -112
  110. package/lib/typescript/src/TabBar/TabBarContext.d.ts +0 -3
  111. package/lib/typescript/src/TabBar/TabIcon.web.d.ts +0 -7
  112. package/lib/typescript/src/TabBar/index.d.ts +0 -5
  113. package/lib/typescript/src/TabBar/useTabBar.d.ts +0 -2
  114. package/lib/typescript/src/TabBar/useTabBarHeight.d.ts +0 -18
  115. package/lib/typescript/src/createController.d.ts +0 -15
  116. package/lib/typescript/src/index.d.ts +0 -21
  117. package/lib/typescript/src/navigationNode.d.ts +0 -41
  118. package/lib/typescript/src/types.d.ts +0 -140
package/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [semantic versioning](https://semver.org/).
6
+
7
+ ## 0.1.0
8
+
9
+ First standalone release, extracted from the Sigmela merchant web app.
10
+
11
+ ### Added
12
+
13
+ - Route primitives: `Router.Screen`, `Stack`, `Redirect`, `Modal`, `Sheet`,
14
+ `TabBar`/`TabBarItem`, `Drawer`/`DrawerGroup`/`DrawerSection`/`DrawerItem`,
15
+ `SplitView` and `Protected`.
16
+ - Hooks: `useRouter`, `useCurrentRoute`, `useParams`, `useSearchParams`,
17
+ `useRoute`, `useHistoryState`, `useCurrentModal`, `useDrawer`.
18
+ - Overlays with their own local push stack, gesture dismissal, and a shared
19
+ close path for button, backdrop, Escape and swipe.
20
+ - Content-sized modals via `<Modal presentation="content">`: the surface grows
21
+ with its content, never drops below half the viewport, and leaves the page
22
+ behind undimmed.
23
+ - Theming through `--router-*` CSS custom properties with built-in fallbacks.
package/LICENSE CHANGED
@@ -1,6 +1,7 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 https://github.com/bogoslavskiy
3
+ Copyright (c) 2026 Sigmela
4
+
4
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
5
6
  of this software and associated documentation files (the "Software"), to deal
6
7
  in the Software without restriction, including without limitation the rights
package/README.md CHANGED
@@ -1,537 +1,166 @@
1
- # Sigmela Router (`@sigmela/router`)
1
+ # @sigmela/router
2
2
 
3
- Modern, lightweight navigation for **React Native** and **Web**, built on top of [`react-native-screens`](https://github.com/software-mansion/react-native-screens).
3
+ A router for React web apps that behave like native apps: screen stacks with
4
+ animated push/pop, modals and bottom sheets with swipe-to-dismiss, tab bars,
5
+ drawers and split views — all declared as JSX routes.
4
6
 
5
- This library is **URL-first**: you navigate by **paths** (`/users/42?tab=posts`), and the router derives `params` and `query` for screens.
6
-
7
- ## Features
8
-
9
- - **Stacks**: predictable stack-based navigation
10
- - **Tabs**: `TabBar` with native + web renderers (or custom tab bar)
11
- - **Drawer**: side-panel navigation (`Drawer`)
12
- - **Split view**: master/details navigation (`SplitView`)
13
- - **Modals & sheets**: via `stackPresentation` (`modal`, `sheet`, …)
14
- - **Controllers**: async/guarded navigation (only present when ready)
15
- - **Appearance**: global styling via `NavigationAppearance` (tab bar colors, fonts, blur effects, etc.)
16
- - **Web History integration**: keeps Router state in sync with `pushState`, `replaceState`, `popstate`
17
- - **Dynamic root**: swap root navigation tree at runtime (`router.setRoot`)
18
- - **Type-safe hooks**: `useParams`, `useQueryParams`, `useRoute`, `useCurrentRoute`
19
-
20
- ## Installation
7
+ Built on [wouter](https://github.com/molefrog/wouter) for matching and
8
+ [motion](https://motion.dev) for transitions.
21
9
 
22
10
  ```bash
23
- yarn add @sigmela/router react-native-screens
24
- # optional (required only if you use sheet presentation)
25
- yarn add @sigmela/native-sheet
11
+ npm install @sigmela/router
26
12
  ```
27
13
 
28
- ### Peer dependencies
29
-
30
- - `react`
31
- - `react-native`
32
- - `react-native-screens` (>= `4.24.0`)
33
- - `@sigmela/native-sheet` (>= `0.0.1`) — only if you use sheets
34
-
35
- ### Web CSS
36
-
37
- On web you must import the bundled stylesheet **once**:
38
-
39
- ```ts
40
- import '@sigmela/router/styles.css';
41
- ```
14
+ `react` and `react-dom` (>= 19) are peer dependencies.
42
15
 
43
16
  ## Quick start
44
17
 
45
- ### Simple stack
46
-
47
- ```tsx
48
- import {
49
- Navigation,
50
- NavigationStack,
51
- Router,
52
- useParams,
53
- useQueryParams,
54
- useRouter,
55
- } from '@sigmela/router';
56
-
57
- function HomeScreen() {
58
- const router = useRouter();
59
- return (
60
- <Button
61
- title="Open details"
62
- onPress={() => router.navigate('/details/42?from=home')}
63
- />
64
- );
65
- }
66
-
67
- function DetailsScreen() {
68
- const { id } = useParams<{ id: string }>();
69
- const { from } = useQueryParams<{ from?: string }>();
70
- return <Text>Details: id={id}, from={from ?? 'n/a'}</Text>;
71
- }
72
-
73
- const rootStack = new NavigationStack()
74
- .addScreen('/', HomeScreen, { header: { title: 'Home' } })
75
- .addScreen('/details/:id', DetailsScreen, { header: { title: 'Details' } });
76
-
77
- const router = new Router({ roots: { app: rootStack }, root: 'app' });
78
-
79
- export default function App() {
80
- return <Navigation router={router} />;
81
- }
82
- ```
83
-
84
- ### Tabs
85
-
86
- ```tsx
87
- import { Navigation, NavigationStack, Router, TabBar } from '@sigmela/router';
88
-
89
- const homeStack = new NavigationStack().addScreen('/', HomeScreen);
90
-
91
- const catalogStack = new NavigationStack()
92
- .addScreen('/catalog', CatalogScreen)
93
- .addScreen('/catalog/products/:productId', ProductScreen);
94
-
95
- const tabBar = new TabBar({ initialIndex: 0 })
96
- .addTab({ key: 'home', stack: homeStack, title: 'Home' })
97
- .addTab({ key: 'catalog', stack: catalogStack, title: 'Catalog' });
98
-
99
- const router = new Router({ roots: { app: tabBar }, root: 'app' });
100
-
101
- export default function App() {
102
- return <Navigation router={router} />;
103
- }
104
- ```
105
-
106
- ### Tabs wrapped by a root stack (like `example/src/navigation/stacks.ts`)
107
-
108
- In the example app, tabs are mounted as a **screen** inside a root `NavigationStack`. This lets you keep tab navigation plus define modals/overlays at the root level.
109
-
110
- ```tsx
111
- import { NavigationStack, TabBar } from '@sigmela/router';
112
-
113
- const homeStack = new NavigationStack().addScreen('/', HomeScreen);
114
- const catalogStack = new NavigationStack().addScreen('/catalog', CatalogScreen);
115
-
116
- const tabBar = new TabBar()
117
- .addTab({
118
- key: 'home',
119
- stack: homeStack,
120
- title: 'Home',
121
- icon: require('./assets/home.png'),
122
- })
123
- .addTab({
124
- key: 'catalog',
125
- stack: catalogStack,
126
- title: 'Catalog',
127
- icon: require('./assets/catalog.png'),
128
- });
129
-
130
- // Root stack hosts the tab bar AND top-level modals/overlays.
131
- export const rootStack = new NavigationStack()
132
- .addScreen('/', tabBar)
133
- .addModal('/auth', AuthScreen, { header: { title: 'Login', hidden: true } })
134
- .addModal('*?modal=promo', PromoModal);
135
- ```
136
-
137
- ## Core concepts
138
-
139
- ### `NavigationStack`
140
-
141
- A `NavigationStack` defines a set of routes and how to match them.
142
-
143
- ```tsx
144
- const stack = new NavigationStack({ header: { largeTitle: true } })
145
- .addScreen('/feed', FeedScreen)
146
- .addScreen('/feed/:id', FeedItemScreen)
147
- .addModal('/auth', AuthScreen)
148
- .addSheet('/settings', SettingsSheet);
149
- ```
150
-
151
- Key methods:
152
- - `addScreen(pathPattern, componentOrNode, options?)`
153
- - `addModal(pathPattern, componentOrStack, options?)` (shorthand for `stackPresentation: 'modal'`)
154
- - `addSheet(pathPattern, componentOrStack, options?)` (shorthand for `stackPresentation: 'sheet'`)
155
- - `addStack(prefixOrStack, maybeStack?)` — compose nested stacks under a prefix
156
-
157
- #### Provider Context
158
-
159
- You can wrap an entire stack with a React context provider by passing a `provider` option:
160
-
161
18
  ```tsx
162
- import { ThemeProvider } from './theme';
163
-
164
- const stack = new NavigationStack({
165
- header: { largeTitle: true },
166
- provider: ThemeProvider,
167
- })
168
- .addScreen('/feed', FeedScreen)
169
- .addScreen('/feed/:id', FeedItemScreen);
170
- ```
171
-
172
- The `provider` component wraps the entire stack renderer, making the context available to all screens in the stack. This is useful for:
173
- - **Theme providers**: Apply theme context to all screens
174
- - **Auth providers**: Share authentication state across screens
175
- - **Localization**: Provide i18n context to the entire stack
176
-
177
- **Composing multiple providers:**
178
-
179
- If you need multiple providers, create a composed component:
180
-
181
- ```tsx
182
- const ComposedProvider = ({ children }) => (
183
- <ThemeProvider>
184
- <AuthProvider>
185
- <I18nProvider>
186
- {children}
187
- </I18nProvider>
188
- </AuthProvider>
189
- </ThemeProvider>
190
- );
191
-
192
- const stack = new NavigationStack({ provider: ComposedProvider })
193
- .addScreen('/', HomeScreen);
194
- ```
195
-
196
- **Important:** The provider should be a stable reference (not an inline arrow function) to avoid unnecessary re-renders.
197
-
198
- ### Modal Stacks (Stack in Stack)
199
-
200
- You can pass an entire `NavigationStack` to `addModal()` or `addSheet()` to create a multi-screen flow inside a modal:
201
-
202
- ```tsx
203
- // Define a flow with multiple screens
204
- const emailVerifyStack = new NavigationStack()
205
- .addScreen('/verify', EmailInputScreen)
206
- .addScreen('/verify/sent', EmailSentScreen);
207
-
208
- // Mount the entire stack as a modal
209
- const rootStack = new NavigationStack()
210
- .addScreen('/', HomeScreen)
211
- .addModal('/verify', emailVerifyStack);
212
- ```
213
-
214
- **How it works:**
215
- - Navigating to `/verify` opens the modal with `EmailInputScreen`
216
- - Inside the modal, `router.navigate('/verify/sent')` pushes `EmailSentScreen` within the same modal
217
- - `router.goBack()` navigates back inside the modal stack
218
- - `router.dismiss()` closes the entire modal from any depth
219
-
220
- **Example screen with navigation inside modal:**
19
+ import { Router, useRouter } from '@sigmela/router';
221
20
 
222
- ```tsx
223
- function EmailInputScreen() {
224
- const router = useRouter();
225
-
21
+ function App() {
226
22
  return (
227
- <View>
228
- <Button title="Next" onPress={() => router.navigate('/verify/sent')} />
229
- <Button title="Close" onPress={() => router.dismiss()} />
230
- </View>
23
+ <Router>
24
+ <Router.Screen path="/" element={HomeScreen} />
25
+ <Router.Stack path="/products">
26
+ <Router.Screen element={ProductListScreen} />
27
+ <Router.Screen path=":id" element={ProductScreen} />
28
+ <Router.Modal path=":id/edit" element={EditProductModal} />
29
+ <Router.Sheet path=":id/share" element={ShareSheet} />
30
+ </Router.Stack>
31
+ </Router>
231
32
  );
232
33
  }
233
34
 
234
- function EmailSentScreen() {
35
+ function ProductCard({ id }: { id: string }) {
235
36
  const router = useRouter();
236
-
237
- return (
238
- <View>
239
- <Button title="Back" onPress={() => router.goBack()} />
240
- <Button title="Done" onPress={() => router.dismiss()} />
241
- </View>
242
- );
37
+ return <button onClick={() => router.navigate(`/products/${id}`)}>Open</button>;
243
38
  }
244
39
  ```
245
40
 
246
- This pattern works recursively — you can nest stacks inside stacks to any depth.
41
+ Styles ship with the bundle: `dist/index.js` imports `router.css`, so bundlers
42
+ that handle CSS from dependencies (Vite, webpack, Next, Parcel) pick it up
43
+ automatically — no extra import needed.
247
44
 
248
- ### `Router`
249
-
250
- The `Router` holds navigation state and performs path matching.
45
+ Environments without a CSS loader — plain Node, `vitest` with the `node`
46
+ environment, some SSR pipelines — cannot resolve that import. Alias or stub
47
+ `@sigmela/router/router.css` there, and load the stylesheet explicitly on the
48
+ client:
251
49
 
252
50
  ```ts
253
- const router = new Router({
254
- roots: { app: root }, // NavigationNode (NavigationStack, TabBar, SplitView, ...)
255
- root: 'app',
256
- screenOptions, // optional defaults
257
- debug, // optional
258
- });
259
- ```
260
-
261
- Navigation:
262
- - `router.navigate(path)` — push
263
- - `router.replace(path, dedupe?)` — replace top of the active stack
264
- - `router.goBack()` — pop top of the active stack
265
- - `router.dismiss()` — close the nearest modal or sheet (including all screens in a modal stack)
266
- - `router.reset(path)` — **web-only**: rebuild Router state as if app loaded at `path`
267
- - `router.setRoot(rootKey, { transition? })` — swap root at runtime (`rootKey` from `config.roots`)
268
-
269
- State/subscriptions:
270
- - `router.getState()` → `{ history: HistoryItem[] }`
271
- - `router.getActiveRoute()` → `ActiveRoute | null`
272
- - `router.subscribe(cb)` — notify on any history change
273
- - `router.subscribeStack(stackId, cb)` — notify when a particular stack slice changes
274
- - `router.subscribeRoot(cb)` — notify when root is replaced via `setRoot`
275
- - `router.getStackHistory(stackId)` — slice of history for a stack
276
-
277
- ### `TabBar`
278
-
279
- `TabBar` is a container node that renders one tab at a time.
280
-
281
- ```tsx
282
- const tabBar = new TabBar({ component: CustomTabBar, initialIndex: 0 })
283
- .addTab({ key: 'home', stack: homeStack, title: 'Home' })
284
- .addTab({ key: 'search', screen: SearchScreen, title: 'Search' });
285
- ```
286
-
287
- Key methods:
288
- - `addTab({ key, stack?, node?, screen?, prefix?, title?, icon?, selectedIcon?, ... })`
289
- - `onIndexChange(index)` — switch active tab
290
- - `setBadge(index, badge | null)`
291
- - `setTabBarConfig(partialConfig)`
292
- - `getState()` and `subscribe(cb)`
293
-
294
- Notes:
295
- - Exactly one of `stack`, `node`, `screen` must be provided.
296
- - Use `prefix` to mount a tab's routes under a base path (e.g. `/mail`).
297
- - All `TabsScreenProps` from `react-native-screens` are forwarded to native. This includes lifecycle events (`onWillAppear`, `onDidAppear`, `onWillDisappear`, `onDidDisappear`), accessibility props (`testID`, `accessibilityLabel`, `tabBarItemTestID`, `tabBarItemAccessibilityLabel`), `orientation`, `systemItem`, `freezeContents`, `placeholder`, `scrollEdgeEffects`, badge styling, and more.
298
-
299
- #### `setTabBarConfig()`
300
-
301
- Runtime tab bar configuration:
302
-
303
- ```tsx
304
- tabBar.setTabBarConfig({
305
- bottomAccessory: (environment) => <MiniPlayer layout={environment} />, // iOS 26+
306
- experimentalControlNavigationStateInJS: true,
307
- });
308
- ```
309
-
310
- Web behavior note:
311
- - The built-in **web** tab bar renderer resets Router history on tab switch (to keep URL and Router state consistent) using `router.reset(firstRoutePath)`.
312
-
313
- ### `Drawer`
314
-
315
- `Drawer` provides side-panel navigation, similar to `TabBar` but with a slide-out panel.
316
-
317
- ```tsx
318
- import { Drawer, NavigationStack } from '@sigmela/router';
319
-
320
- const homeStack = new NavigationStack().addScreen('/', HomeScreen);
321
- const settingsStack = new NavigationStack().addScreen('/settings', SettingsScreen);
322
-
323
- const drawer = new Drawer({ width: 280 })
324
- .addTab({ key: 'home', stack: homeStack, title: 'Home' })
325
- .addTab({ key: 'settings', stack: settingsStack, title: 'Settings' });
326
- ```
327
-
328
- Key methods:
329
- - `addTab({ key, stack?, node?, screen?, prefix?, title?, icon?, ... })`
330
- - `open()`, `close()`, `toggle()` — manage drawer state
331
- - `getIsOpen()` — current open state
332
- - `subscribeOpenState(listener)` — subscribe to open/close changes
333
- - `onIndexChange(index)` — switch active tab
334
- - `setBadge(index, badge | null)`
335
-
336
- ### `SplitView`
337
-
338
- `SplitView` renders **two stacks**: `primary` and `secondary`.
339
-
340
- - On **native**, `secondary` overlays `primary` when it has at least one screen in its history.
341
- - On **web**, the layout becomes side-by-side at a fixed breakpoint (`minWidth`, default `640px`).
342
-
343
- ```tsx
344
- import { NavigationStack, SplitView, TabBar } from '@sigmela/router';
345
-
346
- const master = new NavigationStack().addScreen('/', ThreadsScreen);
347
- const detail = new NavigationStack().addScreen('/:threadId', ThreadScreen);
348
-
349
- const splitView = new SplitView({
350
- minWidth: 640,
351
- primary: master,
352
- secondary: detail,
353
- primaryMaxWidth: 390,
354
- });
355
-
356
- // Mount SplitView directly as a tab (no wrapper stack needed).
357
- const tabBar = new TabBar()
358
- .addTab({ key: 'mail', node: splitView, prefix: '/mail', title: 'Mail' })
359
- .addTab({ key: 'settings', stack: settingsStack, title: 'Settings' });
51
+ import '@sigmela/router/router.css';
360
52
  ```
361
53
 
362
- ## Controllers
363
-
364
- Controllers let you delay/guard navigation. A route can be registered as:
54
+ ## Route primitives
365
55
 
366
- ```tsx
367
- import { createController } from '@sigmela/router';
368
-
369
- const UserDetails = {
370
- component: UserDetailsScreen,
371
- controller: createController<{ userId: string }, { tab?: string }>(
372
- async ({ params, query }, present) => {
373
- const ok = await checkAuth();
374
- if (!ok) {
375
- router.replace('/login', true);
376
- return;
377
- }
378
-
379
- const user = await fetchUser(params.userId);
380
- present({ user, tab: query.tab });
381
- }
382
- ),
383
- };
384
-
385
- stack.addScreen('/users/:userId', UserDetails);
386
- ```
56
+ | Element | Purpose |
57
+ | --- | --- |
58
+ | `Router.Screen` | A routed screen inside the current stack |
59
+ | `Router.Stack` | Groups screens that push and pop with a shared animation |
60
+ | `Router.Redirect` | Declarative redirect |
61
+ | `Router.Modal` | Full-cover or content-sized overlay above the current screen |
62
+ | `Router.Sheet` | Bottom sheet, content-sized or fullscreen |
63
+ | `Router.TabBar` / `Router.TabBarItem` | Tab navigation with per-tab stacks |
64
+ | `Router.Drawer` / `Router.DrawerGroup` / `Router.DrawerSection` / `Router.DrawerItem` | Persistent side navigation |
65
+ | `Router.SplitView` | Primary/secondary panes above a width breakpoint |
66
+ | `Router.Protected` | Guarded subtree with a redirect target |
387
67
 
388
- If you never call `present()`, the screen is not pushed/replaced.
68
+ ### Overlays with their own stack
389
69
 
390
- ## Appearance
391
-
392
- Pass `NavigationAppearance` to `<Navigation>` to customize styling globally:
70
+ `Router.Modal` and `Router.Sheet` can own a local push stack. The children share
71
+ one surface and leave the browser URL on the underlying screen:
393
72
 
394
73
  ```tsx
395
- import { Navigation, type NavigationAppearance } from '@sigmela/router';
396
-
397
- const appearance: NavigationAppearance = {
398
- tabBar: {
399
- backgroundColor: '#ffffff',
400
- iconColor: '#999999',
401
- iconColorActive: '#007AFF',
402
- badgeBackgroundColor: '#FF3B30',
403
- iOSShadowColor: '#00000020',
404
- title: {
405
- fontFamily: 'Inter',
406
- fontSize: 10,
407
- color: '#999999',
408
- activeColor: '#007AFF',
409
- activeFontSize: 12, // Android: active tab title font size
410
- },
411
-
412
- // Android-specific
413
- androidActiveIndicatorEnabled: true,
414
- androidActiveIndicatorColor: '#007AFF20',
415
- androidRippleColor: '#007AFF10',
416
- labelVisibilityMode: 'labeled',
417
-
418
- // Tab bar behavior
419
- hidden: false, // hide/show the tab bar
420
- tintColor: '#007AFF', // iOS: selected tab tint + glow color
421
- controllerMode: 'automatic', // iOS 18+: 'automatic' | 'tabBar' | 'tabSidebar'
422
- minimizeBehavior: 'automatic', // iOS 26+: 'automatic' | 'never' | 'onScrollDown' | 'onScrollUp'
423
- nativeContainerBackgroundColor: '#fff', // native container background
424
- iOSBlurEffect: 'systemDefault', // iOS: tab bar blur effect
425
- },
426
- header: { /* ScreenStackHeaderConfigProps */ },
427
- sheet: {
428
- cornerRadius: 16,
429
- backgroundColor: '#ffffff',
430
- },
431
- };
432
-
433
- export default function App() {
434
- return <Navigation router={router} appearance={appearance} />;
435
- }
74
+ <Router.Modal path="/auth/email">
75
+ <Router.Screen element={AuthEmailModal} />
76
+ <Router.Screen path="verify" element={AuthEmailVerifyModal} />
77
+ <Router.Screen path="name" element={AuthEmailNameModal} />
78
+ </Router.Modal>
436
79
  ```
437
80
 
438
- ## Hooks
439
-
440
- ### `useRouter()`
441
-
442
- Access the router instance.
443
-
444
- ### `useCurrentRoute()`
81
+ `navigate()` and `replace()` resolve matching children inside the active
82
+ overlay. `goBack()` pops the local stack first, then closes the overlay from its
83
+ root. The close button, backdrop tap, Escape, swipe dismissal and
84
+ `useCurrentModal().close()` all dismiss the whole flow from any depth.
445
85
 
446
- Subscribes to `router.getActiveRoute()`.
447
-
448
- Returns `ActiveRoute | null` (shape from `src/types.ts`):
449
-
450
- ```ts
451
- type ActiveRoute = {
452
- routeId: string;
453
- stackId?: string;
454
- tabIndex?: number;
455
- path?: string;
456
- params?: Record<string, unknown>;
457
- query?: Record<string, unknown>;
458
- } | null;
459
- ```
86
+ ### Content-sized modals
460
87
 
461
- ### `useParams<T>()` / `useQueryParams<T>()`
462
-
463
- Returns params/query for the **current screen** (from route context).
464
-
465
- ### `useRoute()`
466
-
467
- Returns route-local context for the current screen:
468
-
469
- ```ts
470
- type RouteLocalContextValue = {
471
- presentation: StackPresentationTypes;
472
- params?: Record<string, unknown>;
473
- query?: Record<string, unknown>;
474
- pattern?: string;
475
- path?: string;
476
- };
477
- ```
478
-
479
- ### `useTabBar()`
480
-
481
- Returns the nearest `TabBar` from context (only inside tab screens).
88
+ A modal covers the viewport by default. Content that renders `<Modal>` with
89
+ `presentation="content"` sizes the surface to its content instead — floored at
90
+ half the viewport, capped at full height, with the page behind left undimmed:
482
91
 
483
92
  ```tsx
484
- import { useTabBar } from '@sigmela/router';
485
-
486
- function ScreenInsideTabs() {
487
- const tabBar = useTabBar();
93
+ import { Modal } from '@sigmela/router';
488
94
 
95
+ function ProductDetail({ product }) {
489
96
  return (
490
- <Button
491
- title="Go to second tab"
492
- onPress={() => tabBar.onIndexChange(1)}
493
- />
97
+ <Modal presentation={product.image ? 'fullscreen' : 'content'}>
98
+ {/* … */}
99
+ </Modal>
494
100
  );
495
101
  }
496
102
  ```
497
103
 
498
- ### `useTabBarHeight()`
104
+ ## Hooks
499
105
 
500
- Returns the tab bar height constant (`57`). Useful for bottom padding.
106
+ | Hook | Returns |
107
+ | --- | --- |
108
+ | `useRouter()` | `navigate`, `replace`, `goBack`, and overlay controls |
109
+ | `useCurrentRoute()` | The active route: `pathname`, `search`, `searchParams` |
110
+ | `useParams()` | Path parameters of the current route |
111
+ | `useSearchParams()` | Query string as `URLSearchParams` |
112
+ | `useRoute(pattern)` | Match test for an arbitrary pattern |
113
+ | `useHistoryState()` | State passed through `navigate(path, { state })` |
114
+ | `useCurrentModal()` | `close()` / `id` for the overlay a component renders in |
115
+ | `useDrawer()` | Drawer open state and controls |
501
116
 
502
- ### `useDrawer()`
117
+ ## Theming
503
118
 
504
- Returns the nearest `Drawer` from context (only inside drawer screens).
119
+ The stylesheet reads CSS custom properties, so an app can restyle every surface
120
+ without overriding selectors. Set them on `:root` or on an app shell element:
505
121
 
506
- ```tsx
507
- import { useDrawer } from '@sigmela/router';
122
+ **Base** — `--router-bg`, `--router-text`, `--router-line`, `--router-scrim`,
123
+ `--router-container`, `--router-card-shadow`, `--router-glass-bg`,
124
+ `--router-transparent`, `--router-divider`
508
125
 
509
- function ScreenInsideDrawer() {
510
- const drawer = useDrawer();
511
- return <Button title="Open menu" onPress={() => drawer.open()} />;
512
- }
513
- ```
126
+ **Overlay/footer** — `--router-footer-fade-bg`,
127
+ `--router-footer-fade-transparent`
514
128
 
515
- ### `useSplitView()`
129
+ **Drawer** — `--router-drawer-bg`, `--router-drawer-text`,
130
+ `--router-drawer-hover-bg`, `--router-drawer-active-bg`,
131
+ `--router-drawer-badge-bg`, `--router-drawer-badge-text`,
132
+ `--router-drawer-focus-ring`, `--router-drawer-overlay-color`,
133
+ `--router-drawer-overlay-bg`, `--router-drawer-overlay-open-bg`
516
134
 
517
- Returns the nearest `SplitView` from context (only inside split view screens).
135
+ **Close button** — `--router-close-button-bg`, `--router-close-button-text`,
136
+ `--router-close-button-light-text`, `--router-close-button-glass-bg`,
137
+ `--router-close-button-shadow`, `--router-close-button-shine`,
138
+ `--router-mask-fill`
518
139
 
519
- ## Web integration
140
+ **Typography/layout** — `--router-title-font-family`,
141
+ `--router-title-font-size`, `--router-title-line-height`,
142
+ `--router-title-font-weight`, `--router-safe-area-bottom`
520
143
 
521
- ### History API syncing
144
+ Every token falls back to a sensible default, so the router renders correctly
145
+ with no theming at all.
522
146
 
523
- On web, Router integrates with the browser History API using custom events:
147
+ ## Scope
524
148
 
525
- - `router.navigate('/x')` writes `history.pushState({ __srPath: ... })`
526
- - `router.replace('/x')` writes `history.replaceState({ __srPath: ... })`
527
- - Browser back/forward triggers `popstate` and Router updates its state accordingly
149
+ The router owns navigation, overlay surfaces and their gestures. It never
150
+ imports application features, i18n, data layers or design-system components —
151
+ screens and layouts are supplied by the host app as JSX.
528
152
 
529
- Important behavioral detail:
530
- - `router.goBack()` **does not call** `history.back()`. It pops Router state and updates the URL via `replaceState` (so it doesn’t grow/rewind the browser stack).
153
+ ## Development
531
154
 
532
- ### `syncWithUrl: false`
155
+ ```bash
156
+ npm install
157
+ npm run typecheck
158
+ npm test # 58 contract tests
159
+ npm run lint
160
+ npm run build # dist/index.js + dist/router.css + type declarations
161
+ ```
533
162
 
534
- If a route has `screenOptions.syncWithUrl = false`, Router stores the “real” router path in `history.state.__srPath` while keeping the visible URL unchanged.
163
+ `ROUTER_CONTRACT.md` documents the invariants the contract tests protect.
535
164
 
536
165
  ## License
537
166