@siax/mobile-ui (0.1.1)
Installation
@siax:registry=https://git.cloud.siax.io/api/packages/sax3l/npm/npm install @siax/mobile-ui@0.1.1"@siax/mobile-ui": "0.1.1"About this package
@siax/mobile-ui
React Native components built on @siax/ui's token VALUES (MOB-05).
This package defines no colours of its own.
Status: implemented and tested. 175 tests, 172 passing, 3 skipped (see Known gaps). No build step — the package ships TypeScript source and is bundled by the consuming app's Metro.
The problem this package solves
@siax/ui expresses every SIAX colour in OKLCH (oklch(0.65 0.25 285)).
A browser resolves that natively. React Native cannot: it has no CSS custom
properties and no oklch() function.
The tempting workarounds — eyeballing hex equivalents, or copying a conversion table from somewhere — both produce a second design system that drifts from the first. MOB-05 forbids it and MOB-06 forbids inventing tokens.
So this package is a token bridge: a correct OKLCH → sRGB implementation (Oklab → linear sRGB → gamma, with hue-preserving gamut mapping) applied to a verbatim mirror of the upstream token sheet.
What is actually built
Colour maths — src/color/oklch.ts
Dependency-free, both directions, unit-tested against published landmarks:
| Check | Result |
|---|---|
oklch(1 0 0) |
#ffffff |
oklch(0 0 0) |
#000000 |
oklch(0.5 0 0) |
#636363 |
#ff0000 → OKLCh |
L 0.62796, C 0.25768, H 29.234 |
#00ff00 → OKLCh |
L 0.86644, C 0.29483, H 142.495 |
#0000ff → OKLCh |
L 0.45201, C 0.31321, H 264.052 |
| hex → OKLCh → hex | exact for all 4096 sampled colours |
Out-of-gamut colours are mapped by reducing chroma at constant lightness and hue (CSS Color 4 §13.2), not by clipping RGB channels — clipping rotates the hue, and a hue-shifted SIAX purple is the wrong purple.
Handles percentages, none, all four angle units, and / alpha. Malformed
tokens throw rather than silently painting black.
Token mirror — src/tokens/source.ts
All 30 semantic roles × light/dark, plus the seven data-universe accent sets,
copied character-for-character from @siax/ui/src/globals.css, with
SIAX_TOKEN_PROVENANCE recording the origin commit.
Verified against the real upstream (out-of-band, against a DES1GN
checkout, 2026-08-09): 30/30 roles identical in both schemes against
globals.css, identical against the @siax/tokens JS export, and all 7
universes identical in both schemes. Token diff is empty.
Bridge, theme and derivations
src/tokens/bridge.ts— the only place a colour string is produced. Fails loudly and by role name on a bad token.diffColorSourcesis the token-diff the M2 acceptance criterion asks for.src/theme/primitives.ts— CSS → RN conversion rules, not a table of numbers:clamp()minimum (the narrow-viewport end is the phone),rem/em→ points,0.25s→250,cubic-bezier(...)→ four control points, font stack → the single family RN accepts.src/theme/theme.ts—createTheme(),darkTheme,lightTheme. Everything derived at module load, so an upstream token change moves the mobile theme.
Derived type scale (clamp minima): hero 28 · lg 20 · md 16 · sm 14 · body 14 · caption 10. Inter for text, JetBrains Mono for code.
Components — @siax/mobile-ui/components
ThemeProvider, useTheme, useThemeColors, Text, Button, Card,
Input, Badge. Variant vocabularies mirror @siax/ui's button.tsx and
badge.tsx 1:1.
Accessibility (UI-10):
- Tappable area ≥ 44pt for every button size — asserted per size in tests.
minHeight, neverheight, so Dynamic Type can grow controls. Font size is not pre-scaled (RN already scales<Text>; doing it twice is quadratic).- Title variants report
accessibilityRole="header". Buttonrequires a label (warns in__DEV__when icon-only and unlabelled), announcesdisabled/busy, and blocks presses while loading.Inputtakes a real persistent label, announces required/invalid, and renders errors as text — colour never carries meaning alone.Badgeis always announced;statusLabelseparates spoken from visible text.
DESIGN_GUARD (UI-06)
| Rule | How it is enforced |
|---|---|
radius: 0 always |
Whole scale pinned to the base token; asserted on every rendered surface |
| Hairline edges | StyleSheet.hairlineWidth, injected by the provider |
| Ring focus, no blur shadow | Border-colour swap; source scan rejects any shadow*/elevation: |
| Inter + JetBrains Mono | Derived from the font-stack tokens, asserted |
| Dark default | createTheme() with no arguments is dark |
test/design-guard.test.mjs greps the source for hex/rgb()/hsl()/named
colours, stray oklch() values, shadows, non-token radii, secrets, hardcoded
SIAX URLs, and core-bus imports. Behavioural tests cannot catch someone typing
a hex value into a screen next year; a source scan can.
Note on
radius-xl:@siax/config's preset derives--radius-xl: calc(var(--radius) + 4px), which renders a 4px corner on the web today despite the preset comment claiming all steps resolve to 0. Mobile does not inherit that offset — DESIGN_GUARD is absolute here. Worth raising upstream.
Usage
import { ThemeProvider, Button, Card, Text, Badge } from '@siax/mobile-ui/components';
export default function App() {
return (
<ThemeProvider>
<Card accessibilityLabel="Tenant status">
<Text variant="titleMd">ST0RE</Text>
<Badge variant="success">OK</Badge>
<Button title="Open" onPress={openStore} />
</Card>
</ThemeProvider>
);
}
Product universe, system appearance, tenant accent:
<ThemeProvider universe="bilhandlare" followSystem />
<ThemeProvider colorOverrides={{ accent: 'oklch(0.7 0.2 140)' }} />
Fonts are not bundled — load Inter and JetBrains Mono in the app
(expo-font) under exactly those family names, or RN falls back to the system
face and the type stops being SIAX.
Two entry points, and why
| Entry | Contains | Imports react-native? |
|---|---|---|
@siax/mobile-ui |
colour maths, tokens, bridge, theme, style builders | no |
@siax/mobile-ui/components |
the RN components + provider | yes |
The root stays RN-free so the token bridge can be used by the K6 app generator,
by CI token-diff jobs, and by node --test without a bundler or simulator.
That is what makes "zero own colours" a tested fact rather than a promise.
Tests
node --test "packages/mobile-ui/test/*.test.mjs" # 175 tests, 172 pass, 3 skip
Node 20+ (verified on 24.14.0). No npm install, no test framework — node:test
node:assertonly, with the.tssources loaded via native type stripping. Explicit.ts/.tsxextensions in relative imports are what make that work; Metro resolves them identically.
node --test packages/mobile-ui(a bare directory) also exits 0, but in Node 24 that resolves the directory topackage.json#mainand runs it as a single trivial test — it does not discovertest/. Use the glob above, ornpm testfrom the package directory. This is a Node behaviour, not a config choice.
Known gaps
Deliberately not built in this round:
- The 3 skipped tests are the live token-diff.
test/token-drift.test.mjsdiffs the mirror against the real@siax/ui/@siax/tokensand skips when they are not installed — which they are not, since this repo has nonode_modules. A skip is not a passing diff. To satisfy the M2 criterion in CI, install from Gitea's scoped registry (@siax:registry, never a global one — ADR-003) and re-run. The diff was verified manually against a DES1GN checkout (result above); that verification is not automated. tsc --noEmitcannot run here.reactandreact-nativeare peer dependencies and are not installed, so the.tsxfiles have no types to check against.tsconfig.jsonis written and strict; the check runs in a consuming app or once dev deps are added.- No example app and no Storybook. M2's acceptance criterion "varje paket har … en exempelapp som kör i CI" is not met by this package.
- No visual regression / rendered-component tests. Component styles are
tested exhaustively; the
.tsxwrappers are not rendered anywhere, so JSX typos would only surface in an app. This is the largest remaining risk. - No contrast assertions. Nothing verifies the token pairs meet WCAG AA on
mobile. The colour maths needed to check it is present (
hexToOklch), the check is not written. - Only 5 primitives. No list, sheet, tabs, dialog, toast, skeleton or form layer yet.
@siax/uiis declared as a dependency but nothing imports it. It is kept for provenance and for the token diff; none of its React DOM components are (or can be) used on RN. Installing it drags in the full Radix tree for no runtime benefit — it arguably belongs indevDependencies, but the dependency block was specified as-is and was left alone.- Not published.
npm view @siax/mobile-ui --registry=…does not yet answer.
Core-bus boundary (MOB-02)
This package is a surface. It renders. It holds no identity (ID0), no
objects (ST0RE), no queue (N0D), no AI (INF0), no audit (AUD0), no push
(N0TIFY), no secrets (Infisical), no flags (Unleash). It makes no network calls
at all and hardcodes no SIAX URL — both asserted by design-guard.test.mjs.
Dependencies
Development Dependencies
| ID | Version |
|---|---|
| @siax/tokens | ^0.1.0 |
| @siax/ui | ^0.1.0 |
Peer Dependencies
| ID | Version |
|---|---|
| react | >=18 |
| react-native | >=0.74 |