create-octane-xplat 0.7.3 → 0.10.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 (30) hide show
  1. package/README.md +28 -8
  2. package/index.mjs +112 -14
  3. package/package.json +15 -8
  4. package/scaffold.mjs +531 -0
  5. package/template/.agents/skills/xplat/SKILL.md +109 -40
  6. package/template/.agents/skills/xplat/references/components.md +149 -96
  7. package/template/.agents/skills/xplat/references/navigation.md +81 -31
  8. package/template/.agents/skills/xplat/references/platform.md +74 -35
  9. package/template/.agents/skills/xplat/references/styling.md +80 -27
  10. package/template/AGENTS.md +1 -1
  11. package/template/package.json +11 -9
  12. package/template/pnpm-workspace.yaml +66 -33
  13. package/template/src/App.tsrx +17 -8
  14. package/template/src/process.d.ts +7 -0
  15. package/template/src/style.css +24 -6
  16. package/template/tsconfig.json +1 -1
  17. package/template/vite.config.native.mts +3 -2
  18. package/template/vite.config.ts +5 -3
  19. package/template/App_Resources/iOS/src/OctaneShaders.metal +0 -128
  20. package/template/App_Resources/iOS/src/ShaderEffectRegistration.swift +0 -31
  21. package/template/patches/@nativescript-community__octane@0.2.1.patch +0 -16
  22. package/template/patches/@nativescript-community__vite-octane@0.2.1.patch +0 -41
  23. package/template/patches/@nativescript__core@9.1.2.patch +0 -794
  24. package/template/patches/@nativescript__core@9.1.3-next.2.patch +0 -428
  25. package/template/patches/@nativescript__vite@8.0.11.patch +0 -569
  26. package/template/patches/@nativescript__vite@8.0.13.patch +0 -233
  27. package/template/patches/@tsrx__typescript-plugin@0.4.11.patch +0 -11
  28. package/template/patches/css@3.0.0.patch +0 -52
  29. package/template/patches/octane@0.6.3.patch +0 -1149
  30. /package/template/src/{index.ts → main.ts} +0 -0
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: xplat
3
- description: Build and review code in an Octane xplat app — one TypeScript codebase targeting web (DOM) and iOS/Android (NativeScript). Use when writing components, styles, routes, platform leaves, or device-service calls, and when a feature works on one target but not another.
3
+ description: Build and review code in an Octane Xplat app — one TypeScript codebase targeting web (DOM) and iOS/Android (NativeScript). Use when writing components, styles, routes, platform leaves, or device-service calls, and when a feature works on one target but not another.
4
4
  ---
5
5
 
6
- # Octane xplat app
6
+ # Octane Xplat app
7
7
 
8
8
  Every `src/*.tsrx` file compiles twice: once to the DOM renderer, once to
9
9
  NativeScript views. The common failure mode is **silent divergence** — code
@@ -12,53 +12,122 @@ below prevent that; `pnpm lint` enforces most of them.
12
12
 
13
13
  ## Non-negotiables
14
14
 
15
- 1. **One element vocabulary per file.** Shared code renders only
16
- `@octane-xplat/ui` components. Platform divergence lives in whole files
17
- — `.web` for browser code, `.mobile` for shared iOS/Android variants, OS-specific suffixes, and the unsuffixed native default —
18
- chosen by the bundler when something imports `./Foo`. Never branch on
19
- platform inside JSX (`Platform.OS`, conditional imports, `typeof
20
- document` checks). Platform-authentic widgets live behind
21
- `@octane-xplat/ui/ios`, `/android`, and `/web` — importing a subpath
22
- outside a matching suffix file fails the other platform's build (the
23
- `xplat/platform-subpath-import` lint enforces it).
24
- 2. **No DOM globals in shared code.** `document`, `window`, `localStorage`,
25
- DOM events — all web-only. Device capabilities (clipboard, storage,
26
- permissions, connectivity, geolocation, …) come from
27
- `@octane-xplat/platform`.
28
- 3. **Styles:** `className` for anything static; `style` objects only for
29
- values that change while the app runs. Shared CSS must use the portable
30
- subset — the native build warns on declarations it drops
31
- (`position: fixed`, `margin: auto`, `box-shadow`, …).
32
- 4. **Hooks and JSX live in `.tsrx` (or `.tsx`).** Plain `.ts` is for
33
- non-component code and may not import `.tsrx`. Import `.tsrx` files with
34
- the explicit extension: `import { App } from './App.tsrx'`.
35
- 5. **State = `octane/signals`.** `signal$()` for module-level shared state,
36
- `useSignal$()` for component-local, `derived$()` for computed, `query$()`
37
- for async data. Name signal variables with a `$` suffix (`count$`,
38
- `user$`) — the compiler uses the suffix to preserve reactive reads on
39
- native. Every module that reads or writes signals needs a runtime
40
- `import 'octane/signals'` (or `octane/signals/client`). `query$` reads
41
- suspend — put `.get()` under `@try`/`@pending`/`@catch`, or use
42
- `.snapshot()`/`.latest()` for non-suspending reads. Never write to a
43
- shared signal during render — screen-scoped data keyed by route params
44
- gets a component-local `query$`, not a shared selector signal.
45
- 6. **One `octane` per app.** Don't add a second renderer or duplicate the
46
- package — two copies break the reconciler without a helpful error.
15
+ **One element vocabulary per file.** Shared code renders only
16
+ `@octane-xplat/ui` components. Platform divergence lives in whole files
17
+ — `.web` for browser code, `.mobile` for shared iOS/Android variants, OS-specific suffixes, and the unsuffixed native default —
18
+ chosen by the bundler when something imports `./Foo`. Never branch on
19
+ platform inside JSX (`Platform.OS`, conditional imports, checks for browser globals). Platform-authentic widgets live behind
20
+ `@octane-xplat/ui/ios`, `/android`, and `/web` — importing a subpath
21
+ outside a matching suffix file fails the other platform's build (the
22
+ `xplat/platform-subpath-import` lint enforces it).
23
+ **No DOM globals in shared code.** `document`, `window`, `localStorage`,
24
+ DOM events — all web-only. Device capabilities (clipboard, storage,
25
+ permissions, connectivity) come from
26
+ `@octane-xplat/platform`; plugin-backed features use their leaf packages.
27
+ **Styles:** `className` for anything static; `style` objects only for
28
+ values that change while the app runs. Shared CSS must use the portable
29
+ subset — the native build warns on declarations it drops
30
+ (`position: fixed`, `margin: auto`, `box-shadow`, …).
31
+ **Hooks and JSX live in `.tsrx` (or `.tsx`).** Plain `.ts` is for
32
+ non-component code and may not import `.tsrx`. Import `.tsrx` files with
33
+ the explicit extension: `import { App } from './App.tsrx'`.
34
+ **Keep signal names suffixed with `$`.** Use `useSignal$` for state owned
35
+ by a component. Runtime signal imports let the compiler track reads.
36
+
37
+ ```tsrx
38
+ import { useSignal$ } from 'octane/signals/client'
39
+ import { Pressable, Text } from '@octane-xplat/ui'
40
+
41
+ export function Counter() {
42
+ const count$ = useSignal$(0)
43
+ return <Pressable onPress={() => count$.set((value) => value + 1)}>
44
+ <Text>{count$.get()}</Text>
45
+ </Pressable>
46
+ }
47
+ ```
48
+
49
+ Use `signal$` for shared module state and `derived$` for a computed value.
50
+ Write from event handlers, not while rendering.
51
+
52
+ ```ts
53
+ import { signal$, derived$ } from 'octane/signals'
54
+
55
+ export const travelers$ = signal$(1)
56
+ export const seatsRemaining$ = derived$(() => 4 - travelers$.get())
57
+ export function addTraveler() {
58
+ travelers$.set((count) => count + 1)
59
+ }
60
+ ```
61
+
62
+ `query$` loads asynchronous data. A screen-owned query keeps its selection
63
+ local to the screen. `.latest()` reads without suspending; use the data
64
+ guide's boundary pattern when reading with `.get()`.
65
+
66
+ ```tsrx
67
+ import { query$ } from 'octane/signals'
68
+ import { Text } from '@octane-xplat/ui'
69
+
70
+ export function TripTitle(props: { id: string; loadTitle: (id: string) => Promise<string> }) {
71
+ const title$ = query$(() => props.id, (id) => props.loadTitle(id))
72
+ return <Text>{title$.latest() ?? 'Loading trip…'}</Text>
73
+ }
74
+ ```
75
+
76
+ **One `octane` per app.** Don't add a second renderer or duplicate the
77
+ package — two copies break the reconciler without a helpful error.
78
+
79
+ For component, routing, service, and styling usage, use the references below.
80
+ They show the supported names and matching code examples.
47
81
 
48
82
  ## Compiler rules that surprise
49
83
 
50
84
  - `.tsrx` files can't declare `async` top-level functions — the compiler
51
85
  treats them as async components and the native build fails.
52
- - Literal `@{` in JSX text parses as a code block. Write `{'@'}{expr}` to
53
- render a literal `@`.
86
+ - Literal `@{` in JSX text parses as a code block. Render the sign separately:
87
+
88
+ ```tsrx
89
+ import { Text } from '@octane-xplat/ui'
90
+
91
+ export function Handle() { return <Text>{'@'}{'traveler'}</Text> }
92
+ ```
93
+
54
94
  - Effect dependencies are inferred from closure reads. An effect that only
55
95
  writes (refs, DOM) and never reads its driving prop won't re-run —
56
- declare deps explicitly: `useLayoutEffect(fn, [props.x])`.
57
- - Input handlers must be idempotent — `onInput`/`onChange` can dispatch
58
- more than once per user action on web.
96
+ declare dependencies explicitly when needed:
97
+
98
+ ```tsrx
99
+ import { useLayoutEffect } from 'octane'
100
+ import { Text } from '@octane-xplat/ui'
101
+
102
+ export function Progress(props: { percent: number }) {
103
+ useLayoutEffect(() => { console.log('Progress changed') }, [props.percent])
104
+ return <Text>{props.percent}%</Text>
105
+ }
106
+ ```
107
+
108
+ - Input handlers must be idempotent: store the supplied value rather than
109
+ incrementing a counter for each input event. Web may dispatch more than once.
110
+
111
+ ```tsrx
112
+ import { useState } from 'octane'
113
+ import { TextInput } from '@octane-xplat/ui'
114
+
115
+ export function NameField() {
116
+ const [name, setName] = useState('')
117
+ return <TextInput value={name} onChange={setName} accessibilityLabel="Name" />
118
+ }
119
+ ```
120
+
59
121
  - `.mobile.tsrx`/`.ios.tsrx`/`.android.tsrx` files and unsuffixed native-default files containing JSX must
60
122
  start with `/** @jsxImportSource @nativescript-community/octane */` on
61
- line 1 — nothing may precede it.
123
+ line 1 — nothing may precede it:
124
+
125
+ ```tsrx
126
+ /** @jsxImportSource @nativescript-community/octane */
127
+ import { Text } from '@octane-xplat/ui'
128
+
129
+ export function NativeGreeting() { return <Text>Hello</Text> }
130
+ ```
62
131
 
63
132
  ## Verify
64
133
 
@@ -1,98 +1,151 @@
1
1
  # Components — the vocabulary
2
2
 
3
- Every shared component imports from `@octane-xplat/ui` and owns its pixels —
4
- same props, same pixels on every target. Platform-authentic widgets (real
5
- OS chrome) live behind `@octane-xplat/ui/ios` and `@octane-xplat/ui/android`
6
- under their official OS names — those subpaths resolve only in native
7
- builds and are the honest opt-in for "I want the platform widget." A shared
8
- `.tsrx` file that imports them fails the other platform's build on purpose;
9
- import them inside `.ios.tsrx`/`.android.tsrx`/`.mobile.tsrx` leaves.
10
-
11
- ## Shared — `@octane-xplat/ui` (self-drawn, same pixels everywhere)
12
-
13
- | Need | Component(s) |
14
- | ------------- | ----------------------------------------------------------------------------------------------- |
15
- | Layout | `View` (aliased `Column`), `Row`, `Grid`, `Stack`, `Absolute`, `Spacer`, `SafeArea` |
16
- | Text | `Text`, `Heading`, `RichText` + `RichTextSpan` |
17
- | Tap | `Pressable`, `Link`, `NavLink` |
18
- | Input | `TextInput`, `TextArea`, `Switch`, `Slider` |
19
- | Scrolling | `ScrollView`, `ScrollBox` |
20
- | Media | `Image`, `Icon` (`registerIcon`/`registerIcons` for glyphs) |
21
- | Feedback | `ActivityIndicator`, `Meter`, `showToast` |
22
- | Overlays | `Sheet`/`openSheet`/`closeSheet`, `Overlay`, `Popover`, `Drawer`, `openWindow` |
23
- | Shells | `Screen`, `Tabs` (+ `TabSpec`) |
24
- | Animation | `useAnimation` → `AnimatedValue` |
25
- | Theme/measure | `useColorScheme`/`getColorScheme`, `useSafeAreaInsets`, `useMeasure`, `styled()`, `createStore` |
26
-
27
- `Switch`, `Slider`, `ActivityIndicator`, `Tabs`, and `Drawer` are drawn by
28
- the framework — the OS widget is deliberately not what you get. For a list,
29
- compose `ScrollView` + mapped children; there is no shared recycled list.
30
-
31
- ## Normalized OS widgets (shared, unavoidable platform controls)
32
-
33
- `TextInput`, `TextArea`, `ScrollView`/`ScrollBox` render through the real
34
- OS text/scroll controls — self-drawing them would lose IME, secure entry,
35
- selection, and autofill. The framework resets their chrome (no platform
36
- underline/border/padding, normalized font/color/placeholder/focus) so the
37
- same `className`/`style` lands on the same box. What stays platform-native
38
- by design: caret and selection UI, autocorrect/IME toolbars, keyboard
39
- types, scroll physics and overscroll.
40
-
41
- ## Platform-authentic — `@octane-xplat/ui/ios`
42
-
43
- `UISwitch`, `UISlider`, `UIActivityIndicatorView`, `UITableView`,
44
- `UITabBar`, `UIModal` (+ `openModal`), `SideDrawer` (ui-drawer edge-gesture
45
- drawer), `LiquidGlass` + `LiquidGlassContainer` (iOS 26+ UIGlassEffect,
46
- inert layout below it).
47
-
48
- ## Platform-authentic — `@octane-xplat/ui/android`
49
-
50
- `MaterialSwitch`, `SeekBar`, `CircularProgressIndicator`, `RecyclerView`,
51
- `BottomNavigationView`, `MaterialDialog` (+ `openModal`), `DrawerLayout`.
52
-
53
- ## Platform-only shared exports
54
-
55
- - `KeyboardAvoiding` — shared root export; web avoids via the
56
- `visualViewport` inset, iOS/Android via OS keyboard events, macOS leaf
57
- is an inert column.
58
- - `Hoverable`, `Tooltip` — shared root exports; pointer platforms (web,
59
- macOS) mount the hint layer, touch targets render only the trigger —
60
- keep essential information out of `card`/`content`.
61
-
62
- ## Divergences that matter
63
-
64
- - `ScrollBox` is a **name trap**: on native it's a plain, non-scrolling
65
- container (a platform list inside it owns scrolling). Use `ScrollView`
66
- when you actually want a scroller.
67
- - A `UITableView`/`RecyclerView` must not sit inside a `ScrollView` on
68
- native — two scrollers fight and the framework throws. Wrap it in
69
- `ScrollBox` instead.
70
- - `TextArea` `onSubmit`: web fires on Cmd/Ctrl+Enter; native fires only
71
- when `returnKeyType` is `"done"` or `"send"` — otherwise every newline
72
- reports as a submit.
73
- - `MaterialDialog` is a centered dialog when non-fullscreen on Android;
74
- `UIModal` is a form sheet there. Context and theme don't cross modal
75
- roots — read them inside the modal or pass values down.
76
- - `Sheet`/`openSheet` is a bottom-anchored sheet on both platforms; on web
77
- it renders an in-window portal layer over the app.
78
- - The shared `Drawer` is self-drawn: a left panel + backdrop on every
79
- platform (backdrop fires `onDismiss`). The edge-gesture plugin widget is
80
- `DrawerLayout`/`SideDrawer` in the platform subpaths.
81
- - `RichTextSpan` children carry their own `className`/`style`/`onPress`;
82
- native maps spans to `FormattedString` runs.
83
- - `UITabBar`/`BottomNavigationView` give platform-authentic tab chrome for
84
- `TabSpec.stack`; pushed screens render inside the pane either way —
85
- through a real per-pane `Frame` on iOS, and through the route store on
86
- Android (Frame-in-TabViewItem bookkeeping is unreliable upstream,
87
- NativeScript#11444). The shared `Tabs` uses the same route-store pane on
88
- both.
89
- - Accessibility props (`accessible`, label, hint, role, state, live
90
- region) share names across targets; native translates roles such as
91
- `heading` → `header`.
92
-
93
- ## Things that look right but aren't
94
-
95
- - There is no shared `Modal`/`openModal` or `List` — import the platform
96
- names from the subpaths, or compose `Sheet`/`ScrollView`.
97
- - Route `params` must be scalar (see navigation reference) — objects work
98
- on native but are silently dropped on web.
3
+ Use shared components for screens that work across targets. Put these examples
4
+ in `.tsrx` files; style their classes in the app's CSS.
5
+
6
+ ```tsrx
7
+ import { VStack, HStack, Text, Pressable } from '@octane-xplat/ui'
8
+
9
+ export function Welcome() {
10
+ return <VStack className="welcome">
11
+ <Text>Plan your next trip</Text>
12
+ <HStack><Pressable onPress={() => console.log('Start planning')}>
13
+ <Text>Start planning</Text>
14
+ </Pressable></HStack>
15
+ </VStack>
16
+ }
17
+ ```
18
+
19
+ ## Shared component inventory
20
+
21
+ | Need | Component(s) |
22
+ | --------------------- | ----------------------------------------------------------------------------- |
23
+ | Layout | `View`, `VStack`, `HStack`, `Grid`, `Stack`, `Absolute`, `Spacer`, `SafeArea` |
24
+ | Text | `Text`, `Heading`, `RichText`, `RichTextSpan` |
25
+ | Tap | `Pressable`, `Button`, `Link`, `NavLink` |
26
+ | Input | `TextInput`, `TextArea`, `Switch`, `Slider` |
27
+ | Scrolling | `ScrollableArea`, `VirtualList` |
28
+ | Media | `Image`, `Icon` |
29
+ | Feedback | `Spinner`, `Meter`, `showToast` |
30
+ | Overlays | `BottomSheet`, `Overlay`, `Popover`, `Drawer`, `HoverCard`, `Tooltip` |
31
+ | Shells | `Screen`, `Tabs` |
32
+ | Theme and measurement | `useColorScheme`, `useSafeAreaInsets`, `useMeasure` |
33
+
34
+ `ScrollableArea` scrolls a bounded set of children. For a long list, use
35
+ `VirtualList` so the app does not mount every row at once.
36
+
37
+ ```tsrx
38
+ import { ScrollableArea, VirtualList, Text } from '@octane-xplat/ui'
39
+
40
+ const destinations = [{ id: 'oslo', name: 'Oslo' }, { id: 'rome', name: 'Rome' }]
41
+
42
+ export function Destinations() {
43
+ return <>
44
+ <ScrollableArea><Text>Choose your next destination</Text></ScrollableArea>
45
+ <VirtualList items={destinations} keyExtractor={(item) => item.id}
46
+ renderItem={(item) => <Text>{item.name}</Text>} />
47
+ </>
48
+ }
49
+ ```
50
+
51
+ Text fields keep the platform's keyboard, caret, selection, and input methods.
52
+ Use `onChange` to store the new value. `KeyboardAvoiding` helps keep the field
53
+ visible when the keyboard opens.
54
+
55
+ ```tsrx
56
+ import { useState } from 'octane'
57
+ import { KeyboardAvoiding, TextInput } from '@octane-xplat/ui'
58
+
59
+ export function TravelerName() {
60
+ const [name, setName] = useState('')
61
+ return <KeyboardAvoiding>
62
+ <TextInput accessibilityLabel="Traveler name" placeholder="Your name"
63
+ value={name} onChange={setName} />
64
+ </KeyboardAvoiding>
65
+ }
66
+ ```
67
+
68
+ `TextArea` supports multiline text. Its submit shortcut is Cmd/Ctrl+Enter on
69
+ web; native submits with `returnKeyType="done"` or `"send"`.
70
+
71
+ ```tsrx
72
+ import { useState } from 'octane'
73
+ import { TextArea } from '@octane-xplat/ui'
74
+
75
+ export function TravelNotes() {
76
+ const [notes, setNotes] = useState('')
77
+ return <TextArea value={notes} onChange={setNotes} returnKeyType="done"
78
+ onSubmit={() => console.log(notes)} accessibilityLabel="Travel notes" />
79
+ }
80
+ ```
81
+
82
+ `BottomSheet` opens above the current screen. Keep its open state in your
83
+ component and give it a label that explains its purpose.
84
+
85
+ ```tsrx
86
+ import { useState } from 'octane'
87
+ import { BottomSheet, Pressable, Text } from '@octane-xplat/ui'
88
+
89
+ export function TripHelp() {
90
+ const [open, setOpen] = useState(false)
91
+ return <>
92
+ <Pressable onPress={() => setOpen(true)}><Text>Trip help</Text></Pressable>
93
+ <BottomSheet label="Trip help" isOpen={open} onOpenChange={setOpen}>
94
+ <Text>You can change your destination later.</Text>
95
+ </BottomSheet>
96
+ </>
97
+ }
98
+ ```
99
+
100
+ `RichTextSpan` gives individual text runs their own style or press handler.
101
+ Native uses formatted text runs for these spans.
102
+
103
+ ```tsrx
104
+ import { RichText, RichTextSpan } from '@octane-xplat/ui'
105
+
106
+ export function TripStatus() {
107
+ return <RichText>
108
+ <RichTextSpan text="Your trip is " />
109
+ <RichTextSpan text="ready" className="status-ready"
110
+ onPress={() => console.log('Show trip')} />
111
+ </RichText>
112
+ }
113
+ ```
114
+
115
+ ## Platform widgets
116
+
117
+ Choose platform widgets when you want the OS's own controls. Import them only
118
+ in the matching platform file. This iOS example belongs in `Preference.ios.tsrx`:
119
+
120
+ ```tsrx
121
+ /** @jsxImportSource @nativescript-community/octane */
122
+ import { useState } from 'octane'
123
+ import { UISwitch } from '@octane-xplat/ui/ios'
124
+
125
+ export function Preference() {
126
+ const [checked, setChecked] = useState(false)
127
+ return <UISwitch checked={checked} onCheckedChange={setChecked} />
128
+ }
129
+ ```
130
+
131
+ The Android equivalent belongs in `Preference.android.tsrx`:
132
+
133
+ ```tsrx
134
+ /** @jsxImportSource @nativescript-community/octane */
135
+ import { useState } from 'octane'
136
+ import { MaterialSwitch } from '@octane-xplat/ui/android'
137
+
138
+ export function Preference() {
139
+ const [checked, setChecked] = useState(false)
140
+ return <MaterialSwitch checked={checked} onCheckedChange={setChecked} />
141
+ }
142
+ ```
143
+
144
+ | Platform | Additional widget names |
145
+ | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
146
+ | iOS | `UISlider`, `UIActivityIndicatorView`, `UITableView`, `UITabBar`, `UIModal`, `SideDrawer`, `LiquidGlass`, `LiquidGlassContainer` |
147
+ | Android | `SeekBar`, `CircularProgressIndicator`, `RecyclerView`, `BottomNavigationView`, `MaterialDialog`, `DrawerLayout` |
148
+
149
+ Keep platform lists outside other scrolling containers. Use a layout container
150
+ around the list; see the [components guide](https://octane-xplat.goddardai.org/components)
151
+ for overlay, hover, tab, and accessibility examples and their platform limits.
@@ -1,33 +1,83 @@
1
1
  # Navigation
2
2
 
3
- One route table serves both targets. Declare named screens and stacks;
4
- navigate with `<Link>`/`<NavLink>` in JSX or `pushRoute`/`popRoute`/
5
- `useRoute`/`useCanGoBack` imperatively. Web maps routes to URLs and
6
- history; native maps them to Frame pages and the back stack — the same
7
- call sites drive both.
8
-
9
- ## Rules
10
-
11
- - **Params are scalar.** They serialize as query strings on web — pass
12
- strings and numbers. Objects survive on native and are silently dropped
13
- on web.
14
- - **Tabs and named stacks** (`Tabs` + `TabSpec`, `registerStack`) exist on
15
- both targets. Use a named stack for overlay-style flows (sheets,
16
- sidebars) that shouldn't replace the root page.
17
- - **Deep links:** web is the URL itself; native parses incoming URLs into
18
- the same route names — keep names stable and shareable.
19
- - **Back** — browser back, hardware back (Android), and swipe-back (iOS)
20
- all map to `popRoute` on the current stack.
21
-
22
- ## Named stacks
23
-
24
- Pushing into a named stack (`{ into: 'stackName' }`) works on all three
25
- targets: web pushes into the nested outlet, iOS navigates a real `Frame`
26
- when the platform `UITabBar` registered one (and uses the route store
27
- under the shared `Tabs`), and Android always uses the router-owned
28
- swap-pane — pushed screens render inside the active tab pane.
29
- `popRoute(stack)`/`useRoute(stack)` behave the same either way, so app
30
- code never sees which mechanism ran.
31
-
32
- For loaders, route config, and generated route types, see the navigation
33
- guide: https://octane-xplat.goddardai.org/navigation
3
+ Register screens once during app startup. A route names a screen and a stack;
4
+ web turns it into a URL, while native presents the screen in the app's navigation.
5
+ Put components in `.tsrx` files.
6
+
7
+ ```tsrx
8
+ import { defineRoutes, registerRoutes, Text } from '@octane-xplat/ui'
9
+
10
+ function Home() { return <Text>Home</Text> }
11
+ function Settings() { return <Text>Settings</Text> }
12
+
13
+ registerRoutes(defineRoutes([
14
+ { path: '/', screen: Home },
15
+ { path: '/settings', screen: Settings },
16
+ ]))
17
+ ```
18
+
19
+ After registering `settings`, use `NavLink` for a screen link or `pushRoute`
20
+ in an event handler. `Link` accepts a URL, including an external website.
21
+
22
+ ```tsrx
23
+ import { HStack, Link, NavLink, Pressable, Text, pushRoute } from '@octane-xplat/ui'
24
+
25
+ export function SettingsLinks() {
26
+ const settings = { stack: 'root', name: 'settings', params: {} }
27
+ return <HStack>
28
+ <NavLink route={settings}><Text>Settings</Text></NavLink>
29
+ <Pressable onPress={() => pushRoute(settings)}><Text>Open settings</Text></Pressable>
30
+ <Link href="https://octane-xplat.goddardai.org"><Text>Documentation</Text></Link>
31
+ </HStack>
32
+ }
33
+ ```
34
+
35
+ Use `useRoute` to read the current screen and `useCanGoBack` to decide whether
36
+ to offer a back button. Pass the same stack name to `popRoute`.
37
+
38
+ ```tsrx
39
+ import { Pressable, Text, useRoute, useCanGoBack, popRoute } from '@octane-xplat/ui'
40
+
41
+ export function NavigationStatus() {
42
+ const route = useRoute('root')
43
+ const canGoBack = useCanGoBack('root')
44
+ return <>
45
+ <Text>{route?.name ?? 'Home'}</Text>
46
+ <Pressable disabled={!canGoBack} onPress={() => popRoute('root')}>
47
+ <Text>Back</Text>
48
+ </Pressable>
49
+ </>
50
+ }
51
+ ```
52
+
53
+ Prefer string IDs in route params: generated route helpers use string params,
54
+ and IDs make URLs shareable. The low-level router JSON-encodes object params
55
+ on web with a warning; do not rely on it to carry application state. With a
56
+ registered `/projects/:id` screen, navigate using the project ID:
57
+
58
+ ```ts
59
+ import { pushRoute } from '@octane-xplat/ui'
60
+
61
+ pushRoute({ stack: 'root', name: 'projects/:id', params: { id: '42' } })
62
+ ```
63
+
64
+ Named tab stacks keep each tab's navigation separate. `Tabs` renders the tab's
65
+ initial content; pushing a route into that stack opens it inside the tab pane.
66
+ The screen below assumes `settings` is already registered.
67
+
68
+ ```tsrx
69
+ import { Tabs, Text, Pressable, pushRoute } from '@octane-xplat/ui'
70
+
71
+ export function AppTabs() {
72
+ return <Tabs tabs={[
73
+ { title: 'Home', stack: 'home', render: () => <Text>Home</Text> },
74
+ { title: 'Account', stack: 'account', render: () =>
75
+ <Pressable onPress={() => pushRoute({ stack: 'account', name: 'settings', params: {} })}>
76
+ <Text>Settings</Text>
77
+ </Pressable> },
78
+ ]} />
79
+ }
80
+ ```
81
+
82
+ For loaders, deep links, route config, and generated route types, see the
83
+ [navigation guide](https://octane-xplat.goddardai.org/navigation).