create-octane-xplat 0.9.0 → 0.11.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.
@@ -1,98 +1,174 @@
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` (see note), `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
+ For images, prefer the `Image` from `@octane-xplat/image` — it has the exact
35
+ same props as `@octane-xplat/ui`'s `Image` but loads through a real image
36
+ engine on native (Glide on Android, SDWebImage on iOS) with a sized memory +
37
+ disk cache and decode-to-view-size, instead of the 5 MB cache in the core
38
+ component that evicts visible images mid-scroll. Apps scaffolded with iOS or
39
+ Android targets already have the package; a web-only app renders a plain
40
+ `<img>` either way, so `pnpm add @octane-xplat/image` is optional there. Core
41
+ `Image` remains the baseline for SVG sources, which the engine cannot decode.
42
+
43
+ ```tsrx
44
+ import { Image } from '@octane-xplat/image'
45
+
46
+ export function TripPhoto() {
47
+ return <Image
48
+ src="https://picsum.photos/seed/trail/800/450"
49
+ alt="Trailhead"
50
+ contentFit="cover"
51
+ placeholder="blurhash:LEHV6nWB2yk8pyo0adR*.7kCMdnj"
52
+ style={{ width: '100%', height: 180 }}
53
+ />
54
+ }
55
+ ```
56
+
57
+ `ScrollableArea` scrolls a bounded set of children. For a long list, use
58
+ `VirtualList` so the app does not mount every row at once.
59
+
60
+ ```tsrx
61
+ import { ScrollableArea, VirtualList, Text } from '@octane-xplat/ui'
62
+
63
+ const destinations = [{ id: 'oslo', name: 'Oslo' }, { id: 'rome', name: 'Rome' }]
64
+
65
+ export function Destinations() {
66
+ return <>
67
+ <ScrollableArea><Text>Choose your next destination</Text></ScrollableArea>
68
+ <VirtualList items={destinations} keyExtractor={(item) => item.id}
69
+ renderItem={(item) => <Text>{item.name}</Text>} />
70
+ </>
71
+ }
72
+ ```
73
+
74
+ Text fields keep the platform's keyboard, caret, selection, and input methods.
75
+ Use `onChange` to store the new value. `KeyboardAvoiding` helps keep the field
76
+ visible when the keyboard opens.
77
+
78
+ ```tsrx
79
+ import { useState } from 'octane'
80
+ import { KeyboardAvoiding, TextInput } from '@octane-xplat/ui'
81
+
82
+ export function TravelerName() {
83
+ const [name, setName] = useState('')
84
+ return <KeyboardAvoiding>
85
+ <TextInput accessibilityLabel="Traveler name" placeholder="Your name"
86
+ value={name} onChange={setName} />
87
+ </KeyboardAvoiding>
88
+ }
89
+ ```
90
+
91
+ `TextArea` supports multiline text. Its submit shortcut is Cmd/Ctrl+Enter on
92
+ web; native submits with `returnKeyType="done"` or `"send"`.
93
+
94
+ ```tsrx
95
+ import { useState } from 'octane'
96
+ import { TextArea } from '@octane-xplat/ui'
97
+
98
+ export function TravelNotes() {
99
+ const [notes, setNotes] = useState('')
100
+ return <TextArea value={notes} onChange={setNotes} returnKeyType="done"
101
+ onSubmit={() => console.log(notes)} accessibilityLabel="Travel notes" />
102
+ }
103
+ ```
104
+
105
+ `BottomSheet` opens above the current screen. Keep its open state in your
106
+ component and give it a label that explains its purpose.
107
+
108
+ ```tsrx
109
+ import { useState } from 'octane'
110
+ import { BottomSheet, Pressable, Text } from '@octane-xplat/ui'
111
+
112
+ export function TripHelp() {
113
+ const [open, setOpen] = useState(false)
114
+ return <>
115
+ <Pressable onPress={() => setOpen(true)}><Text>Trip help</Text></Pressable>
116
+ <BottomSheet label="Trip help" isOpen={open} onOpenChange={setOpen}>
117
+ <Text>You can change your destination later.</Text>
118
+ </BottomSheet>
119
+ </>
120
+ }
121
+ ```
122
+
123
+ `RichTextSpan` gives individual text runs their own style or press handler.
124
+ Native uses formatted text runs for these spans.
125
+
126
+ ```tsrx
127
+ import { RichText, RichTextSpan } from '@octane-xplat/ui'
128
+
129
+ export function TripStatus() {
130
+ return <RichText>
131
+ <RichTextSpan text="Your trip is " />
132
+ <RichTextSpan text="ready" className="status-ready"
133
+ onPress={() => console.log('Show trip')} />
134
+ </RichText>
135
+ }
136
+ ```
137
+
138
+ ## Platform widgets
139
+
140
+ Choose platform widgets when you want the OS's own controls. Import them only
141
+ in the matching platform file. This iOS example belongs in `Preference.ios.tsrx`:
142
+
143
+ ```tsrx
144
+ /** @jsxImportSource @nativescript-community/octane */
145
+ import { useState } from 'octane'
146
+ import { UISwitch } from '@octane-xplat/ui/ios'
147
+
148
+ export function Preference() {
149
+ const [checked, setChecked] = useState(false)
150
+ return <UISwitch checked={checked} onCheckedChange={setChecked} />
151
+ }
152
+ ```
153
+
154
+ The Android equivalent belongs in `Preference.android.tsrx`:
155
+
156
+ ```tsrx
157
+ /** @jsxImportSource @nativescript-community/octane */
158
+ import { useState } from 'octane'
159
+ import { MaterialSwitch } from '@octane-xplat/ui/android'
160
+
161
+ export function Preference() {
162
+ const [checked, setChecked] = useState(false)
163
+ return <MaterialSwitch checked={checked} onCheckedChange={setChecked} />
164
+ }
165
+ ```
166
+
167
+ | Platform | Additional widget names |
168
+ | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
169
+ | iOS | `UISlider`, `UIActivityIndicatorView`, `UITableView`, `UITabBar`, `UIModal`, `SideDrawer`, `LiquidGlass`, `LiquidGlassContainer` |
170
+ | Android | `SeekBar`, `CircularProgressIndicator`, `RecyclerView`, `BottomNavigationView`, `MaterialDialog`, `DrawerLayout` |
171
+
172
+ Keep platform lists outside other scrolling containers. Use a layout container
173
+ around the list; see the [components guide](https://octane-xplat.goddardai.org/components)
174
+ 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).
@@ -1,49 +1,84 @@
1
- # Platform services + writing leaves
1
+ # Platform services and platform files
2
2
 
3
- ## `@octane-xplat/platform` — device services
3
+ Use `@octane-xplat/platform` for common device services such as storage,
4
+ clipboard, permissions, connectivity, app lifecycle, URLs, and screen size.
5
+ Clipboard operations can fail or be unavailable; show a useful result.
4
6
 
5
- Import a capability through the package barrel. The unsuffixed module is its
6
- native default, `.web` is the browser implementation, and `.mobile` or an OS
7
- suffix narrows behavior when needed. Surface: `device`, `appInfo`, `locale`,
8
- `connectivity`, `useWindowSize`, `useAppState`, `useSafeAreaInsets`, deep
9
- links, clipboard, `secureStorage`, haptics, notifications, `announce`,
10
- `systemBars`, permissions, `openUrl`, `openSettings`. Services that need a
11
- NativeScript plugin ship as leaf packages: `@octane-xplat/share`,
12
- `@octane-xplat/files`, `@octane-xplat/media`, `@octane-xplat/biometrics`,
13
- `@octane-xplat/geolocation`, `@octane-xplat/notifications`,
14
- `@octane-xplat/secure-storage`, and `haptics` in `@octane-xplat/haptics` —
15
- each leaf owns its plugin as a real dependency.
7
+ ```tsrx
8
+ import { useState } from 'octane'
9
+ import { clipboard } from '@octane-xplat/platform'
10
+ import { Pressable, Text } from '@octane-xplat/ui'
16
11
 
17
- Services report honestly — `biometrics` returns `unsupported` on web rather
18
- than faking a result. Handle the tier your app can fall back to instead of
19
- assuming success.
12
+ export function CopyTrip() {
13
+ const [message, setMessage] = useState('Copy trip link')
14
+ return <Pressable onPress={async () => {
15
+ const copied = await clipboard.writeText('https://example.com/trips/42')
16
+ setMessage(copied ? 'Copied' : 'Copy unavailable')
17
+ }}><Text>{message}</Text></Pressable>
18
+ }
19
+ ```
20
+
21
+ Features with additional native dependencies live in separate packages:
22
+ `@octane-xplat/share`, `files`, `media`, `biometrics`, `geolocation`,
23
+ `notifications`, `secure-storage`, and `haptics`. Install the package for the
24
+ feature you need. Biometrics, for example, reports `unsupported` on web;
25
+ check permission and availability before trying to verify someone.
20
26
 
21
- ## Leaf files — your own divergence
27
+ ```tsrx
28
+ import { useState } from 'octane'
29
+ import { biometrics } from '@octane-xplat/biometrics'
30
+ import { Pressable, Text } from '@octane-xplat/ui'
31
+
32
+ export function UnlockTrip() {
33
+ const [message, setMessage] = useState('Unlock trip')
34
+ return <Pressable onPress={async () => {
35
+ const status = await biometrics.ensure()
36
+ if (status !== 'granted' || !biometrics.impl) {
37
+ setMessage('Use your password instead')
38
+ return
39
+ }
40
+ const verified = await biometrics.impl.verify('Unlock your trip')
41
+ setMessage(verified ? 'Unlocked' : 'Try again')
42
+ }}><Text>{message}</Text></Pressable>
43
+ }
44
+ ```
22
45
 
23
- When a service doesn't exist yet or a visual behavior truly differs, split the
24
- file, not the JSX:
46
+ ## Write a platform file when behavior differs
47
+
48
+ Split the file when a feature needs browser or OS APIs. Import the component
49
+ without a suffix; the bundler selects the matching implementation.
25
50
 
26
51
  ```text
27
- src/parts/Scanner.tsrx native default
28
- src/parts/Scanner.web.tsrx browser implementation (camera via getUserMedia)
52
+ src/parts/Scanner.web.tsrx browser implementation
29
53
  src/parts/Scanner.mobile.tsrx shared iOS/Android implementation
30
- src/parts/Scanner.ios.tsrx iOS-only override
54
+ src/parts/Scanner.ios.tsrx iOS override
55
+ src/parts/Scanner.tsrx native default
56
+ ```
57
+
58
+ ```tsrx
59
+ import { Scanner } from './parts/Scanner'
60
+
61
+ export function ScanScreen() { return <Scanner /> }
62
+ ```
63
+
64
+ Native JSX files start with the NativeScript pragma on line 1. Import native
65
+ APIs from `@nativescript/core`, and read app lifecycle state inside the call
66
+ rather than during module initialization. This example belongs in
67
+ `Status.mobile.tsrx`:
68
+
69
+ ```tsrx
70
+ /** @jsxImportSource @nativescript-community/octane */
71
+ import { Application } from '@nativescript/core'
72
+ import { Pressable, Text } from '@octane-xplat/ui'
73
+
74
+ export function Status() {
75
+ return <Pressable onPress={() => console.log(Application.hasLaunched())}>
76
+ <Text>Check app status</Text>
77
+ </Pressable>
78
+ }
31
79
  ```
32
80
 
33
- Import the bare specifier (`./Scanner`); the bundler selects `.web` first on
34
- web and the most specific native variant on each OS. The `.mobile` variant
35
- overrides the unsuffixed native default on iOS and Android.
36
-
37
- Leaf rules:
38
-
39
- - Inside `.web.*`, DOM APIs are expected; NativeScript imports belong in
40
- `.mobile.*`, `.ios.*`, `.android.*`, or the unsuffixed native default. If
41
- browser code imports that default, add a `.web.*` sibling.
42
- - Never read app-level NativeScript globals (`Application.android.*`)
43
- at module top — read them inside the call. Top-level reads crash cold start
44
- before the app exists.
45
- - Native-specific `.tsrx` files containing JSX start with
46
- `/** @jsxImportSource @nativescript-community/octane */` on line 1.
47
- - Deep imports under `@nativescript/core/ui/*` bundle as a second module
48
- instance — import from `@nativescript/core` only.
49
- - `console.debug` doesn't exist on device — use `console.log`.
81
+ Keep browser globals and DOM events in `.web.*` files. Do not import a second
82
+ renderer or deep-import NativeScript UI modules. Use `console.log` for device
83
+ logging. See the [platform services guide](https://octane-xplat.goddardai.org/platform-services)
84
+ when choosing a service or fallback.
@@ -1,42 +1,95 @@
1
1
  # Styling
2
2
 
3
- Two channels, chosen by _when the value is known_:
3
+ Use classes for static styling and inline style objects for values that change
4
+ while the app runs. Put the CSS in `src/style.css` and the component in a
5
+ `.tsrx` file.
4
6
 
5
- - `className` → CSS classes and design tokens. Use for anything static.
6
- - `style={{ ... }}` → inline object for runtime values: animated offsets,
7
- measured sizes, state-driven opacity. Don't put static styles here.
7
+ ```css
8
+ .trip-card {
9
+ padding: 16px;
10
+ border-radius: 12px;
11
+ }
12
+ ```
8
13
 
9
- ## The portable subset
14
+ ```tsrx
15
+ import { View, Text } from '@octane-xplat/ui'
10
16
 
11
- Shared CSS compiles for both targets. The native build rewrites `px` →
12
- `dip` and warns on declarations it can't honor — `position: fixed`/`sticky`,
13
- `margin-*: auto`, `float`, `box-shadow`, `white-space: pre-wrap`. A warning
14
- means the property does nothing on native: restructure (flex alignment,
15
- `Absolute`, transforms) instead of suppressing it.
17
+ export function TripCard(props: { opacity: number }) {
18
+ return <View className="trip-card" style={{ opacity: props.opacity }}>
19
+ <Text>Your next adventure</Text>
20
+ </View>
21
+ }
22
+ ```
16
23
 
17
- NativeScript limits worth knowing before you design around them:
24
+ ## Shared CSS
18
25
 
19
- - A scroll container can't clip children to a parent's rounded corners —
20
- inset separators/padding instead.
21
- - CSS transforms reach native views only through the `transform:`
22
- shorthand — `transform: translateX(16) scaleX(0.95)`. For motion, prefer
23
- `useAnimation`, which composes this per platform.
26
+ Shared CSS compiles for web and native. Prefer flex layouts, padding, and
27
+ supported transforms. The native build warns when it drops declarations such
28
+ as `position: fixed`, `margin: auto`, or `box-shadow`; resolve the warning
29
+ before depending on that style.
30
+
31
+ ```css
32
+ .trip-card {
33
+ display: flex;
34
+ flex-direction: column;
35
+ padding: 16px;
36
+ }
37
+ .trip-actions {
38
+ display: flex;
39
+ flex-direction: row;
40
+ justify-content: center;
41
+ }
42
+ ```
24
43
 
25
44
  ## Tokens and dark mode
26
45
 
27
- `import '@octane-xplat/ui/theme/tokens.css'` (already in `main.web.tsrx`)
28
- provides color/spacing tokens and `--font-sans`/`--font-mono` fallbacks.
29
- The shipped utility classes (`flex-1`, `items-center`, `gap-*`, `rounded`,
30
- `text-*`, …) mirror Tailwind v4 names and scales — write them as you would
31
- Tailwind utilities.
46
+ The starter imports the framework's tokens. They provide shared layout rules; the starter
47
+ defines its app palette in `src/style.css`. Use the palette tokens in your CSS.
48
+
49
+ ```ts
50
+ import '@octane-xplat/ui/theme/tokens.css'
51
+ ```
52
+
53
+ ```css
54
+ .trip-card {
55
+ background-color: var(--color-surface);
56
+ color: var(--color-text);
57
+ }
58
+ ```
59
+
60
+ Use `useColorScheme()` to follow the system theme. Apply the matching class to
61
+ the root so the token colors follow the theme; a user preference can override
62
+ this in the starter's `src/App.tsrx`.
32
63
 
33
- Dark mode: `useColorScheme()` reads the system; the app's override pattern
34
- is in `src/App.tsrx` — hold `boolean | null` state, apply a `dark`/`light`
35
- class on the root, and let tokens switch underneath.
64
+ ```tsrx
65
+ import { View, Text, useColorScheme } from '@octane-xplat/ui'
66
+
67
+ export function ThemePreview() {
68
+ const scheme = useColorScheme()
69
+ return <View className={scheme === 'dark' ? 'dark' : 'light'}>
70
+ <Text>Ready for your next trip</Text>
71
+ </View>
72
+ }
73
+ ```
36
74
 
37
75
  ## Fonts
38
76
 
39
- Family names differ per target (web face name, iOS PostScript name,
40
- Android filename). Ship the font file in the app, register per platform,
41
- and set the `--font-*` tokens in `style.css`. See the styling guide at
77
+ System fonts are the default. Add a custom font when your app needs one.
78
+
79
+ `pnpm xplat fonts add <file>` registers a font everywhere: stages a `.ttf`
80
+ into `src/fonts` (iOS/Android pick it up from there; `.woff`/`.woff2` inputs
81
+ are converted), writes the web `@font-face` into `src/fonts.css`, and wires
82
+ `--font-sans` (or another token via `--token mono|none|<name>`) in
83
+ `style.css`. Fontsource packages work too —
84
+ `pnpm xplat fonts add @fontsource-variable/inter --install` imports the
85
+ package's own CSS on web and stages its converted files natively (upright
86
+ faces only — italic files are skipped). Variable
87
+ fonts carry their weight range — `font-weight` on `Text` selects the matching
88
+ instance. Family names differ per target; the command reads them from the
89
+ file itself. See the styling guide at
42
90
  https://octane-xplat.goddardai.org/styling for the per-target table.
91
+
92
+ ```sh
93
+ pnpm xplat fonts add ./Inter.ttf
94
+ pnpm xplat fonts add @fontsource-variable/inter --install
95
+ ```