SIAX Technology (sax3l)

@siax/mobile-ui (0.1.1)

Published 2026-08-10 11:59:10 +00:00 by admin

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. diffColorSources is 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.25s250, cubic-bezier(...) → four control points, font stack → the single family RN accepts.
  • src/theme/theme.tscreateTheme(), 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, never height, 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".
  • Button requires a label (warns in __DEV__ when icon-only and unlabelled), announces disabled/busy, and blocks presses while loading.
  • Input takes a real persistent label, announces required/invalid, and renders errors as text — colour never carries meaning alone.
  • Badge is always announced; statusLabel separates 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:assert only, with the .ts sources loaded via native type stripping. Explicit .ts/.tsx extensions 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 to package.json#main and runs it as a single trivial test — it does not discover test/. Use the glob above, or npm test from 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.mjs diffs the mirror against the real @siax/ui/@siax/tokens and skips when they are not installed — which they are not, since this repo has no node_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 --noEmit cannot run here. react and react-native are peer dependencies and are not installed, so the .tsx files have no types to check against. tsconfig.json is 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 .tsx wrappers 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/ui is 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 in devDependencies, 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
Details
npm
2026-08-10 11:59:10 +00:00
58
UNLICENSED
latest
34 KiB
Assets (1)
Versions (2) View all
0.1.1 2026-08-10
0.1.0 2026-08-10