@tricrobotics/ui-native
v0.1.2
Published
TRIC React Native UI library — the map + liquid-glass panel shell, kit primitives, design tokens, sheet detents and layout hooks shared by the TRIC Expo apps
Readme
@tricrobotics/ui-native
The UI package for building TRIC's domain-specific apps on React Native +
react-native-web. Native (SwiftUI via @expo/ui) when possible, a web
fallback (RNW/DOM) always, behind one import — the app passes no platform
code.
It powers the three "map + liquid-glass panel" Expo apps
(client-dashboard-mobile, flex-monitoring-mobile, ops-dashboard-mobile):
a full-screen Mapbox background with a draggable glass panel, segmented panel
nav, a user menu, and the dashboard primitive kit those panels are built from.
Why it exists
These apps target iPhone, iPad and web from one codebase. We want native
chrome where Apple gives it to us — the native BottomSheet on iPhone, SwiftUI
glassEffect on iPad — without hand-writing a Platform.OS branch at every
call site, and without maintaining a parallel .web.tsx copy of every component
inside each app. This package owns that split once, so:
- iPhone → native
@expo/uiBottomSheet. - iPad → custom Liquid Glass (
glassEffect, iOS 26+) drag panel. - web →
backdrop-filterglass drag panel.
…all selected by the bundler from a single <MapPanel> import.
The convention (how a platform variant is chosen)
A component that has a native SwiftUI form ships three files; the consumer imports one name:
| File | Role |
| --- | --- |
| Component.types.ts | the single prop contract — shared by both impls and what tsc checks consumers against |
| Component.tsx | base = react-native-web / DOM implementation (web + Android + the type the compiler sees) |
| Component.ios.tsx | SwiftUI implementation via @expo/ui/swift-ui (Metro picks it on iOS only) |
Metro resolves .ios.tsx on iOS and .tsx everywhere else. tsc ignores
.ios.tsx, so both implementations must satisfy Component.types.ts.
@expo/ui is imported only inside .ios.tsx files (it does not resolve on
web), which is why it stays an optional peer dependency.
Pure-RN components with no SwiftUI form (Row, Txt, Pill, Button, …)
render identically through react-native-web and stay single-file.
graph TD
imp["import { MapPanel } from '@tricrobotics/ui-native'"]
metro{Metro platform resolution}
ios["MapPanel.ios.tsx (SwiftUI): iPhone BottomSheet / iPad glassEffect"]
web["MapPanel.tsx (RNW base): backdrop-filter drag panel"]
ts["tsc -> MapPanel.types.ts (one API)"]
imp --> metro
metro -->|"platform = ios"| ios
metro -->|"platform = web / android"| web
imp -.types.-> tsComponent catalog
Shell — ./panel (and the barrel)
| Component | iOS (SwiftUI) | web (RNW/DOM) |
| --- | --- | --- |
| MapPanel | iPhone native BottomSheet, iPad Liquid Glass drag panel | backdrop-filter drag panel |
| Glass | glassEffect (iOS 26+) / BlurView fallback | backdrop-filter surface |
| PanelNav | segmented Picker | RN segmented control |
| UserMenu | Menu | RN modal sheet |
| LayerControl | floating glass Menu + Toggle | floating dropdown of checkboxes |
MapPanel is generic over the view key and takes the app's
PanelViewDefinition[], selectedView/onViewChange,
detent/onDetentChange (SheetDetentKey), children (the body — RN or
SwiftUI), and userEmail/onLogout/avatarUri. Pass iosBody="swiftui" when
the body is composed from this package's SwiftUI primitives (client/flex);
leave it as the default "rn" for a react-native body hosted via RNHostView
(ops).
Primitive kit — ./primitives (and the barrel)
SwiftUI/web pairs: SectionCard, StatTile, KeyValueRow, SectionLabel,
StatusBadge, IconBadge, ViewScaffold, Icon.
Pure-RN (single-file): Row, Spacer, Txt, Card, Divider, Pill,
Button, ProgressRing, EmptyState, LoadingState.
StatusBadgeis presentational (label/tone/icon). The package ships no status vocabulary — each app maps its own domain states (robot states, review states, …) to those props locally. Same idea forViewScaffold: it accepts anofflineSlotnode instead of importing any app-specific banner.
Other entry points
./sheet— detents (peek/medium/large),detentToHeight,nearestDetentKey,PEEK_PANEL_HEIGHT, …./hooks—useLayout(phone | tabletPortrait | tabletLandscape | web),computeMapPadding../lib—supportsLiquidGlass()(iOS 26+ glass capability check)../tokens— the "field glass"COLORSpalette,CARD, RNSHADOW,TYPOGRAPHY, plus the web-onlyGLASS/WEB_SHADOW/FONT_FAMILYsurfaces used by the base.tsxfiles, andSPACING/RADIUSre-exported from@tricrobotics/tokens.
Icons
SF Symbols render natively on iOS via @expo/ui. On web, Icon.tsx maps the
same systemName to an SVG from @tricrobotics/icons (a regular
dependency). If you use a new SF Symbol on web, add it to the SF_SYMBOL_MAP in
src/primitives/Icon.tsx (unmapped names fall back to a dashed circle).
Peer dependencies
Provided by the consuming Expo app (not bundled):
react,react-nativereact-native-reanimated,react-native-gesture-handler,react-native-safe-area-contextexpo-blur@expo/ui— optional, only needed for the SwiftUI (.ios.tsx) variants.
Regular dependencies: @tricrobotics/tokens (value tokens) and
@tricrobotics/icons (web SF-Symbol SVGs).
Metro consumption (ships source, not a bundle)
This package intentionally ships TypeScript source (src/) rather than a
Vite/webpack bundle, so Metro transforms it with the app's Babel config and
Reanimated worklets + platform resolution work correctly. The exports map
exposes the react-native condition pointing at source.
Local-link development (no publish required)
Each app installs the package via a file: dependency and watches the source:
// app package.json
"@tricrobotics/icons": "^1.0.7",
"@tricrobotics/tokens": "file:../tric-tokens",
"@tricrobotics/ui-native": "file:../tric-ui-native"// app metro.config.js — see the apps for the full version
const path = require('path');
const { getDefaultConfig } = require('expo/metro-config');
const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, '..');
const config = getDefaultConfig(projectRoot);
config.watchFolders = [workspaceRoot];
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, 'node_modules'),
path.resolve(workspaceRoot, 'node_modules'),
];
module.exports = config;After publishing, swap the file: ranges for pinned ^x.y.z versions before
shipping to devices.
Validation harness — the gallery
example/ is the ui-native-gallery app: it renders every component so you
can compare the SwiftUI (iOS dev build) and RNW (Expo web) renderings
side-by-side. After adding or changing a .ios/base pair, add a <XxxDemo/> to
example/src/app/index.tsx and run both. SwiftUI primitives must be hosted, so
demos wrap them in the example's PrimitiveStage (Host on iOS, a plain View
on web). See the ui-native-gallery agent skill.
Type-check both surfaces from the example:
cd example
npm run type-check # base (.tsx / web) surface
npm run type-check:ios # the .ios.tsx (SwiftUI) surface
npm run type-check:pkg # both
@expo/uiSwiftUI is not in Expo Go — the gallery's iOS run needs a dev build (expo run:ios).
Adding a SwiftUI/web component pair
- Write
Component.types.ts— the one prop contract. - Write
Component.tsx— the RNW/DOM base (model it on the existing web bases; use./primitives/webDOM stacks +Icon+GLASStokens). - Write
Component.ios.tsx— SwiftUI via@expo/ui/swift-ui. - Export all three from the relevant barrel (
primitives/index.tsorpanel/index.ts). - Add a
<ComponentDemo/>to the gallery and verify iOS-vs-web parity.
Golden rule
A change here lands for all three apps at once (shell sizing, detents, drag
gestures, glass styling, segmented nav, primitives). Verify on iPhone + iPad +
web for each app — use the ui-native-gallery, map-panel-apps, and
rn-simulator skills — before finishing.
