create-octane-xplat 0.4.0 → 0.6.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.
package/README.md CHANGED
@@ -23,3 +23,8 @@ pnpm typecheck # web + native tsconfigs
23
23
  ```
24
24
 
25
25
  Docs: [Running and checking an app](https://octane-xplat.goddardai.org/toolchain)
26
+
27
+ The scaffolded app ships `.agents/skills/xplat/` — a skill with the
28
+ invariants agents need before writing code (element vocabulary, DOM-global
29
+ rules, signal naming, compiler surprises) plus per-domain references.
30
+ Agents: [llms.txt](https://octane-xplat.goddardai.org/llms.txt) indexes the docs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-octane-xplat",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Scaffold a minimal @octane-xplat/ui app — one Octane codebase for web + iOS + Android",
5
5
  "bin": {
6
6
  "create-octane-xplat": "./index.mjs"
@@ -0,0 +1,80 @@
1
+ ---
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.
4
+ ---
5
+
6
+ # Octane xplat app
7
+
8
+ Every `src/*.tsrx` file compiles twice: once to the DOM renderer, once to
9
+ NativeScript views. The common failure mode is **silent divergence** — code
10
+ that compiles and works on web but no-ops or crashes on native. The rules
11
+ below prevent that; `pnpm lint` enforces most of them.
12
+
13
+ ## Non-negotiables
14
+
15
+ 1. **One element vocabulary per file.** Shared code renders only
16
+ `@octane-xplat/ui` components. Platform divergence lives in whole files
17
+ — `Foo.web.tsrx`, `Foo.native.tsrx`, `Foo.ios.tsrx`, `Foo.android.tsrx` —
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`).
41
+ 6. **One `octane` per app.** Don't add a second renderer or duplicate the
42
+ package — two copies break the reconciler without a helpful error.
43
+
44
+ ## Compiler rules that surprise
45
+
46
+ - `.tsrx` files can't declare `async` top-level functions — the compiler
47
+ treats them as async components and the native build fails.
48
+ - Literal `@{` in JSX text parses as a code block. Write `{'@'}{expr}` to
49
+ render a literal `@`.
50
+ - Effect dependencies are inferred from closure reads. An effect that only
51
+ writes (refs, DOM) and never reads its driving prop won't re-run —
52
+ declare deps explicitly: `useLayoutEffect(fn, [props.x])`.
53
+ - Input handlers must be idempotent — `onInput`/`onChange` can dispatch
54
+ more than once per user action on web.
55
+ - `.native.tsrx`/`.ios.tsrx`/`.android.tsrx` files containing JSX must
56
+ start with `/** @jsxImportSource @nativescript-community/octane */` on
57
+ line 1 — nothing may precede it.
58
+
59
+ ## Verify
60
+
61
+ | Command | Catches |
62
+ | ----------------------------------- | ------------------------------------------------------------------------ |
63
+ | `pnpm lint` | vocabulary / DOM-global / style violations — its output is authoritative |
64
+ | `pnpm typecheck` | both web and native TS configs |
65
+ | `pnpm dev` | web behavior |
66
+ | `pnpm dev:ios` / `pnpm dev:android` | the platform where behavior actually differs |
67
+
68
+ ## Read next (only when needed)
69
+
70
+ - `references/components.md` — the component vocabulary plus per-platform
71
+ divergences and name traps.
72
+ - `references/styling.md` — tokens, the portable CSS subset, dark mode.
73
+ - `references/navigation.md` — route table, links, params, stacks.
74
+ - `references/platform.md` — device services and writing your own leaf.
75
+ - Current limits and platform boundaries:
76
+ https://octane-xplat.goddardai.org/known-limits
77
+ - Full framework docs, one file:
78
+ https://octane-xplat.goddardai.org/llms-full.txt (index:
79
+ https://octane-xplat.goddardai.org/llms.txt). The `/notes/*` design
80
+ record explains _why_ a constraint exists — rarely needed for app work.
@@ -0,0 +1,93 @@
1
+ # Components — the vocabulary
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`/`.native.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` — native root export only (inert on web).
56
+ - `Hoverable` — `@octane-xplat/ui/web` only. Hover has no touch-platform
57
+ semantic; the old native leaf's long-press stand-in was fake parity.
58
+
59
+ ## Divergences that matter
60
+
61
+ - `ScrollBox` is a **name trap**: on native it's a plain, non-scrolling
62
+ container (a platform list inside it owns scrolling). Use `ScrollView`
63
+ when you actually want a scroller.
64
+ - A `UITableView`/`RecyclerView` must not sit inside a `ScrollView` on
65
+ native — two scrollers fight and the framework throws. Wrap it in
66
+ `ScrollBox` instead.
67
+ - `TextArea` `onSubmit`: web fires on Cmd/Ctrl+Enter; native fires only
68
+ when `returnKeyType` is `"done"` or `"send"` — otherwise every newline
69
+ reports as a submit.
70
+ - `MaterialDialog` is a centered dialog when non-fullscreen on Android;
71
+ `UIModal` is a form sheet there. Context and theme don't cross modal
72
+ roots — read them inside the modal or pass values down.
73
+ - `Sheet`/`openSheet` is a bottom-anchored sheet on both platforms; on web
74
+ it renders an in-window portal layer over the app.
75
+ - The shared `Drawer` is self-drawn: a left panel + backdrop on every
76
+ platform (backdrop fires `onDismiss`). The edge-gesture plugin widget is
77
+ `DrawerLayout`/`SideDrawer` in the platform subpaths.
78
+ - `RichTextSpan` children carry their own `className`/`style`/`onPress`;
79
+ native maps spans to `FormattedString` runs.
80
+ - `UITabBar`/`BottomNavigationView` give real per-pane navigation stacks
81
+ for `TabSpec.stack`; the shared `Tabs` keeps stack history in the route
82
+ store instead. On Android, Frame-in-TabViewItem is unreliable upstream
83
+ (NativeScript#11444) — prefer the shared `Tabs` there for stack panes.
84
+ - Accessibility props (`accessible`, label, hint, role, state, live
85
+ region) share names across targets; native translates roles such as
86
+ `heading` → `header`.
87
+
88
+ ## Things that look right but aren't
89
+
90
+ - There is no shared `Modal`/`openModal` or `List` — import the platform
91
+ names from the subpaths, or compose `Sheet`/`ScrollView`.
92
+ - Route `params` must be scalar (see navigation reference) — objects work
93
+ on native but are silently dropped on web.
@@ -0,0 +1,30 @@
1
+ # Navigation
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
+ ## Known gap
23
+
24
+ Pushing into a _named_ stack is iOS-only until
25
+ [NativeScript#11444](https://github.com/NativeScript/NativeScript/issues/11444)
26
+ lands — `pushRoute` there warns loudly on Android instead of dropping
27
+ silently. Root-stack navigation works on both platforms.
28
+
29
+ For loaders, route config, and generated route types, see the navigation
30
+ guide: https://octane-xplat.goddardai.org/navigation
@@ -0,0 +1,45 @@
1
+ # Platform services + writing leaves
2
+
3
+ Two seams cover everything shared code can't reach:
4
+
5
+ ## `@octane-xplat/platform` — device services
6
+
7
+ One import per capability; each service resolves internally to a `.web` or
8
+ `.native` implementation. Surface: `device`, `appInfo`, `locale`,
9
+ `connectivity`, `useWindowSize`, `useAppState`, `useSafeAreaInsets`, deep
10
+ links, clipboard, `secureStorage`, haptics, notifications, `announce`,
11
+ `systemBars`, geolocation, permissions, media, files, share, `openUrl`,
12
+ `openSettings`, biometrics.
13
+
14
+ Services report honestly — `biometrics` returns `unsupported` on web
15
+ rather than faking a result. Handle the tier your app can fall back to
16
+ instead of assuming success.
17
+
18
+ ## Leaf files — your own divergence
19
+
20
+ When a service doesn't exist yet or a visual behavior truly differs,
21
+ split the file, not the JSX:
22
+
23
+ ```text
24
+ src/parts/Scanner.tsrx shared part — imports './Scanner.impl'
25
+ src/parts/Scanner.impl.web.ts DOM implementation (camera via getUserMedia)
26
+ src/parts/Scanner.impl.native.ts NativeScript implementation
27
+ ```
28
+
29
+ Import the bare specifier (`./Scanner.impl`); the bundler picks the leaf
30
+ via the suffix chain `.ios` → `.android` → `.native` → shared / `.web` →
31
+ shared.
32
+
33
+ Leaf rules:
34
+
35
+ - Inside a leaf, platform APIs are _expected_: DOM globals in `.web.*`,
36
+ `@nativescript/*` imports in `.native.*`. The lint rules key off the
37
+ suffix — don't fight them with suppressions.
38
+ - Never read app-level NativeScript globals (`Application.android.*`)
39
+ at module top — read them inside the call. Top-level reads crash cold
40
+ start before the app exists.
41
+ - `.native.tsrx` files with JSX start with
42
+ `/** @jsxImportSource @nativescript-community/octane */` on line 1.
43
+ - Deep imports under `@nativescript/core/ui/*` bundle as a second module
44
+ instance — import from `@nativescript/core` only.
45
+ - `console.debug` doesn't exist on device — use `console.log`.
@@ -0,0 +1,42 @@
1
+ # Styling
2
+
3
+ Two channels, chosen by _when the value is known_:
4
+
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.
8
+
9
+ ## The portable subset
10
+
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.
16
+
17
+ NativeScript limits worth knowing before you design around them:
18
+
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.
24
+
25
+ ## Tokens and dark mode
26
+
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.
32
+
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.
36
+
37
+ ## Fonts
38
+
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
42
+ https://octane-xplat.goddardai.org/styling for the per-target table.
@@ -0,0 +1,9 @@
1
+ # AGENTS.md
2
+
3
+ Octane xplat app — one TypeScript codebase for web + iOS + Android (Octane
4
+
5
+ - NativeScript).
6
+
7
+ Before writing code, read `.agents/skills/xplat/SKILL.md` (or invoke the
8
+ `xplat` skill). Its invariants are enforced by `pnpm lint` — violations
9
+ usually still compile but silently break on one platform.
@@ -4,6 +4,13 @@ export default {
4
4
  id: 'org.nativescript.octanexplat',
5
5
  appPath: 'src',
6
6
  appResourcesPath: 'App_Resources',
7
+ // The workspace is pnpm-managed — without this the CLI defaults to npm
8
+ // and spawns the bundler with --preserve-symlinks, which breaks isolated
9
+ // node_modules resolution (a package's transitive deps never realpath
10
+ // into the .pnpm store → ERR_MODULE_NOT_FOUND).
11
+ cli: {
12
+ packageManager: 'pnpm',
13
+ },
7
14
  android: {
8
15
  v8Flags: '--expose_gc',
9
16
  markingMode: 'none',
@@ -16,11 +16,13 @@
16
16
  "typecheck": "tsrx-tsc --noEmit && tsrx-tsc --noEmit -p tsconfig.native.json"
17
17
  },
18
18
  "dependencies": {
19
+ "@nativescript-community/gesturehandler": "2.0.45",
19
20
  "@nativescript-community/octane": "0.2.1",
20
21
  "@nativescript-community/ui-canvas": "4.6.52",
22
+ "@nativescript-community/ui-drawer": "0.1.32",
21
23
  "@nativescript-community/ui-svg": "0.2.24",
22
24
  "@nativescript/core": "9.1.2",
23
- "@octane-xplat/ui": "^0.4.0",
25
+ "@octane-xplat/ui": "^0.5.0",
24
26
  "@valor/nativescript-websockets": "2.0.3",
25
27
  "octane": "0.4.0"
26
28
  },
@@ -30,12 +32,12 @@
30
32
  "@nativescript/ios": "9.1.0",
31
33
  "@nativescript/types": "9.0.0",
32
34
  "@nativescript/vite": "8.0.11",
33
- "@octane-xplat/cli": "^0.4.0",
35
+ "@octane-xplat/cli": "^0.5.0",
34
36
  "@octane-xplat/lint": "^0.1.0",
35
37
  "@octanejs/vite-plugin": "0.1.58",
36
38
  "@tsrx/typescript-plugin": "0.4.7",
37
- "oxlint": "^1.74.0",
38
39
  "nativescript": "9.1.1",
40
+ "oxlint": "^1.74.0",
39
41
  "typescript": "5.9.3",
40
42
  "vite": "8.3.0"
41
43
  }
@@ -1,5 +1,5 @@
1
1
  import { useState } from 'octane';
2
- import { View, Row, Text, Pressable, List, useColorScheme } from '@octane-xplat/ui';
2
+ import { View, Row, Text, Pressable, ScrollView, useColorScheme } from '@octane-xplat/ui';
3
3
 
4
4
  const FEATURES = [
5
5
  'shared components',
@@ -28,7 +28,9 @@ export function App() @{
28
28
  <Pressable className="btn" onPress={() => setCount(count + 1)}><Text>+</Text></Pressable>
29
29
  </Row>
30
30
 
31
- <List items={FEATURES} renderItem={(f) => <Text className="chip">{f}</Text>} />
31
+ <ScrollView>
32
+ {FEATURES.map((f) => <Text className="chip">{f}</Text>)}
33
+ </ScrollView>
32
34
 
33
35
  <Pressable className="link" onPress={() => setOverride(!dark)}>
34
36
  <Text>{dark ? 'light' : 'dark'} mode (system: {scheme})</Text>
@@ -65,7 +65,7 @@ body {
65
65
 
66
66
  .btn {
67
67
  background-color: var(--color-primary);
68
- color: var(--color-on-primary);
68
+ color: var(--color-onprimary);
69
69
  padding: 10px 20px;
70
70
  border-radius: 10px;
71
71
  corner-shape: squircle;
@@ -74,7 +74,7 @@ body {
74
74
 
75
75
  .chip {
76
76
  background-color: var(--color-primary);
77
- color: var(--color-on-primary);
77
+ color: var(--color-onprimary);
78
78
  padding: 6px 14px;
79
79
  border-radius: 14px;
80
80
  corner-shape: squircle;