expo-interface 0.1.0 → 0.2.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.
Files changed (88) hide show
  1. package/README.md +282 -180
  2. package/package.json +39 -13
  3. package/src/alert/alert.css +49 -0
  4. package/src/alert/index.android.tsx +68 -0
  5. package/src/alert/index.ios.tsx +47 -0
  6. package/src/alert/index.tsx +60 -0
  7. package/src/alert/shared.ts +10 -0
  8. package/src/alert/types.ts +58 -0
  9. package/src/button/button.css +6 -0
  10. package/src/button/index.android.tsx +9 -2
  11. package/src/button/index.ios.tsx +8 -3
  12. package/src/button/index.tsx +19 -1
  13. package/src/button/types.ts +7 -0
  14. package/src/checkbox/checkbox.css +29 -0
  15. package/src/checkbox/index.android.tsx +54 -0
  16. package/src/checkbox/index.ios.tsx +50 -0
  17. package/src/checkbox/index.tsx +50 -0
  18. package/src/checkbox/types.ts +27 -0
  19. package/src/collapsible/collapsible.css +32 -0
  20. package/src/collapsible/index.android.tsx +34 -0
  21. package/src/collapsible/index.ios.tsx +31 -0
  22. package/src/collapsible/index.tsx +41 -0
  23. package/src/collapsible/shared.ts +21 -0
  24. package/src/collapsible/types.ts +24 -0
  25. package/src/color-picker/color-picker.css +66 -0
  26. package/src/color-picker/index.android.tsx +117 -0
  27. package/src/color-picker/index.ios.tsx +33 -0
  28. package/src/color-picker/index.tsx +62 -0
  29. package/src/color-picker/shared.ts +204 -0
  30. package/src/color-picker/sheet.tsx +432 -0
  31. package/src/color-picker/types.ts +34 -0
  32. package/src/context-menu/index.android.tsx +27 -0
  33. package/src/context-menu/index.ios.tsx +22 -0
  34. package/src/context-menu/index.tsx +56 -0
  35. package/src/date-time/index.android.tsx +3 -2
  36. package/src/divider/divider.css +16 -0
  37. package/src/divider/index.android.tsx +26 -0
  38. package/src/divider/index.ios.tsx +18 -0
  39. package/src/divider/index.tsx +21 -0
  40. package/src/divider/types.ts +20 -0
  41. package/src/field-group/field-group.css +23 -7
  42. package/src/field-group/index.android.tsx +22 -22
  43. package/src/field-group/index.web.tsx +2 -2
  44. package/src/gauge/gauge.css +200 -0
  45. package/src/gauge/index.android.tsx +212 -0
  46. package/src/gauge/index.ios.tsx +40 -0
  47. package/src/gauge/index.tsx +165 -0
  48. package/src/gauge/shared.ts +79 -0
  49. package/src/gauge/types.ts +64 -0
  50. package/src/index.ts +24 -3
  51. package/src/menu/index.android.tsx +66 -0
  52. package/src/menu/index.ios.tsx +72 -0
  53. package/src/menu/index.tsx +30 -0
  54. package/src/menu/list.tsx +98 -0
  55. package/src/menu/menu.css +83 -0
  56. package/src/menu/types.ts +60 -0
  57. package/src/picker/index.android.tsx +3 -2
  58. package/src/progress/index.android.tsx +23 -9
  59. package/src/progress/index.ios.tsx +7 -6
  60. package/src/progress/index.tsx +43 -6
  61. package/src/progress/progress.css +33 -0
  62. package/src/progress/types.ts +23 -6
  63. package/src/screen/header.tsx +1 -0
  64. package/src/segmented/index.android.tsx +67 -0
  65. package/src/segmented/index.ios.tsx +47 -0
  66. package/src/segmented/index.tsx +58 -0
  67. package/src/segmented/segmented.css +55 -0
  68. package/src/segmented/types.ts +31 -0
  69. package/src/slider/index.android.tsx +87 -0
  70. package/src/slider/index.ios.tsx +64 -0
  71. package/src/slider/index.tsx +55 -0
  72. package/src/slider/slider.css +30 -0
  73. package/src/slider/types.ts +39 -0
  74. package/src/stepper/index.android.tsx +58 -0
  75. package/src/stepper/index.ios.tsx +55 -0
  76. package/src/stepper/index.tsx +47 -0
  77. package/src/stepper/shared.ts +17 -0
  78. package/src/stepper/stepper.css +61 -0
  79. package/src/stepper/types.ts +35 -0
  80. package/src/switch/index.tsx +2 -0
  81. package/src/text-field/shared.ts +2 -0
  82. package/src/tooltip/index.android.tsx +23 -0
  83. package/src/tooltip/index.ios.tsx +17 -0
  84. package/src/tooltip/index.tsx +41 -0
  85. package/src/tooltip/tooltip.css +40 -0
  86. package/src/tooltip/types.ts +25 -0
  87. package/src/link.ts +0 -36
  88. package/src/qr/index.tsx +0 -26
package/README.md CHANGED
@@ -1,227 +1,329 @@
1
- # expo-interface
1
+ # Expo Interface
2
2
 
3
- Native-first, universal UI kit for [Expo](https://expo.dev), built on
4
- [`@expo/ui`](https://docs.expo.dev/versions/v56.0.0/sdk/ui/).
3
+ > A cross-platform UI kit for [Expo](https://expo.dev) built on
4
+ [`@expo/ui`](https://docs.expo.dev/versions/v57.0.0/sdk/ui/):
5
5
 
6
- Every component renders the platform's real control:
7
-
8
- | Platform | Renders with |
9
- | -------- | ------------ |
10
- | iOS | SwiftUI (`@expo/ui/swift-ui`) |
11
- | Android | Jetpack Compose / Material 3 (`@expo/ui/jetpack-compose`) |
12
- | Web | Plain DOM elements styled with CSS custom properties |
13
-
14
- One accent seed themes all three: it seeds a Material 3 palette on Android,
15
- cascades as a SwiftUI `tint` on iOS, and is emitted as `--color-*` variables on
16
- web. The prop surface of each component is the intersection of what the three
17
- platforms support, so the same JSX renders a native-feeling control everywhere.
18
-
19
- > **Status:** early. The API follows `@expo/ui`, which is itself still
20
- > evolving, so expect breaking changes between minor versions.
6
+ - Every component renders the platform's own control.
7
+ - SwiftUI on iOS, Jetpack Compose (Material 3) on Android, plain DOM on web.
8
+ - A single accent color seeds the theme everywhere, on all platforms.
21
9
 
22
10
  ## Install
23
11
 
24
12
  ```sh
25
- npx expo install expo-interface @expo/ui expo-router expo-symbols expo-image expo-constants expo-status-bar expo-system-ui expo-web-browser react-native-safe-area-context
13
+ npx expo install expo-interface @expo/ui @expo/material-symbols
26
14
  ```
27
15
 
28
- `expo-interface` ships TypeScript source (`src/index.ts`) and relies on Metro to
29
- compile it, exactly like the rest of your app — no build step, and the
30
- `.ios.tsx` / `.android.tsx` / `.web.tsx` platform files resolve as usual.
31
-
32
- ### Android drawables (optional)
33
-
34
- Jetpack Compose controls render icons from drawables rather than symbol glyphs.
35
- To use Material Symbols as drawables, install `@expo/material-symbols` and
36
- register `xml` as an asset extension in `metro.config.js`:
37
-
38
- ```js
39
- const {getDefaultConfig} = require('expo/metro-config');
40
-
41
- const config = getDefaultConfig(__dirname);
42
-
43
- if (!config.resolver.assetExts.includes('xml')) {
44
- config.resolver.assetExts.push('xml');
45
- }
46
-
47
- module.exports = config;
48
- ```
16
+ Peer dependencies and Android icon setup are covered in
17
+ [install details](#install-details).
49
18
 
50
19
  ## Setup
51
20
 
52
- ### 1. Provide the accent
53
-
54
- Wrap the app in `AccentProvider`. Omit `seed` for the default (iOS systemBlue,
55
- `#007AFF`) or pass a hex color to re-theme every platform at once.
56
-
57
- ```tsx
58
- // app/_layout.tsx
59
- import {ThemeProvider, Stack} from 'expo-router';
60
- import {AccentProvider, useNavTheme} from 'expo-interface';
61
-
62
- function Navigation() {
63
- // Reads the accent seed, so it must render inside AccentProvider.
64
- return (
65
- <ThemeProvider value={useNavTheme()}>
66
- <Stack screenOptions={{headerShown: false}}/>
67
- </ThemeProvider>
68
- );
69
- }
70
-
71
- export default function Layout() {
72
- return (
73
- <AccentProvider seed="#8959EA">
74
- <Navigation/>
75
- </AccentProvider>
76
- );
77
- }
78
- ```
79
-
80
- ### 2. Emit the palette on web
81
-
82
- `getThemeCSS()` renders the light/dark palette as `--color-*` custom
83
- properties. Add it to the root HTML so CSS consumers (buttons, typography,
84
- `theme.*` tokens) resolve without JavaScript.
85
-
86
- ```tsx
87
- // app/+html.tsx
88
- import {ScrollViewStyleReset} from 'expo-router/html';
89
- import {getThemeCSS} from 'expo-interface';
90
-
91
- export default function Root({children}: React.PropsWithChildren) {
92
- return (
93
- <html lang="en">
94
- <head>
95
- <meta charSet="utf-8"/>
96
- <meta name="viewport" content="width=device-width, initial-scale=1"/>
97
- <style dangerouslySetInnerHTML={{__html: getThemeCSS()}}/>
98
- <ScrollViewStyleReset/>
99
- </head>
100
- <body>{children}</body>
101
- </html>
102
- );
103
- }
104
- ```
105
-
106
- ### 3. Build screens
107
-
108
- `Screen` handles safe areas, the status bar, the system background color, and
109
- the content max-width. With `native`, children are mounted inside an
110
- accent-seeded `@expo/ui` `Host`, so you can compose SwiftUI / Compose
111
- primitives directly.
112
-
113
- ```tsx
114
- import {Screen, Button, Title, Body} from 'expo-interface';
115
-
116
- export default function Home() {
117
- return (
118
- <Screen gutter>
119
- <Title>Hello</Title>
120
- <Body color="secondaryLabel">Rendered with the platform's own text.</Body>
121
- <Button label="Continue" onPress={() => {}}/>
122
- </Screen>
123
- );
124
- }
125
- ```
21
+ 1. Wrap the app in `AccentProvider`. `seed` is any hex color; the default is `#007AFF`.
22
+ ```tsx
23
+ // app/_layout.tsx
24
+ import {ThemeProvider, Stack} from 'expo-router';
25
+ import {AccentProvider, useNavTheme} from 'expo-interface';
26
+
27
+ function Navigation() {
28
+ return (
29
+ <ThemeProvider value={useNavTheme()}>
30
+ <Stack screenOptions={{headerShown: false}}/>
31
+ </ThemeProvider>
32
+ );
33
+ }
34
+
35
+ export default function Layout() {
36
+ return (
37
+ <AccentProvider seed="#8959EA">
38
+ <Navigation/>
39
+ </AccentProvider>
40
+ );
41
+ }
42
+ ```
43
+
44
+ 2. On web, emit the palette as CSS variables in the root HTML.
45
+
46
+ ```tsx
47
+ // app/+html.tsx
48
+ import {ScrollViewStyleReset} from 'expo-router/html';
49
+ import {getThemeCSS} from 'expo-interface';
50
+
51
+ export default function Root({children}: React.PropsWithChildren) {
52
+ return (
53
+ <html lang="en">
54
+ <head>
55
+ <meta charSet="utf-8"/>
56
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
57
+ <style dangerouslySetInnerHTML={{__html: getThemeCSS()}}/>
58
+ <ScrollViewStyleReset/>
59
+ </head>
60
+ <body>{children}</body>
61
+ </html>
62
+ );
63
+ }
64
+ ```
65
+
66
+ 3. Build screens.
67
+
68
+ ```tsx
69
+ import {Screen, FieldGroup, Switch, Button} from 'expo-interface';
70
+
71
+ export default function Home() {
72
+ return (
73
+ <Screen native>
74
+ <FieldGroup>
75
+ <FieldGroup.Section title="Sync">
76
+ <Switch label="Notifications" value={on} onValueChange={setOn}/>
77
+ <Button label="Continue" onPress={save}/>
78
+ </FieldGroup.Section>
79
+ </FieldGroup>
80
+ </Screen>
81
+ );
82
+ }
83
+ ```
126
84
 
127
85
  ## Components
128
86
 
129
- All exports come from the package root: `import {…} from 'expo-interface'`.
130
-
131
- ### Layout
132
-
133
- | Export | Description |
134
- | ------ | ----------- |
135
- | `Screen` | Safe-area screen container. Props: `native` (mount an accent-seeded `Host`), `header` (sits under a stack header), `gutter` (horizontal padding). |
136
- | `ScreenHeader` | Simple header bar with optional back button and trailing slot. |
137
- | `ConstrainedStackHeader` | `Stack` `header` implementation for web that matches the content max-width (renders nothing on native). |
138
- | `TabStack` | Pre-configured `expo-router` `Stack` for a tab's root screen, themed from `useNavTheme()`. |
139
- | `Tabs` | Tab bar: `NativeTabs` on iOS/Android, a floating top bar on web. Props: `routes`, `webLogo` (`'icon-only'`, `'text-only'`, `'icon-and-text'` or a `ReactNode`), `webIcon`. |
140
- | `Sheet` | Accent-aware `@expo/ui` `BottomSheet`. |
141
- | `hostAccentProps(seed)` | Extra `Host` props that apply the accent (`seedColor` on Android, `tint` modifier on iOS) use when mounting your own `Host`. |
142
- | `fillWidth` | Modifiers that make a universal `Column`/`Row` span its parent's width. |
143
-
144
- ### Controls
145
-
146
- | Export | Description |
147
- | ------ | ----------- |
148
- | `Button` | `label`, `onPress`, `variant` (`filled` / `outlined` / `text`), `role` (`default` / `destructive`), `size`, `shape` (`rounded` / `pill` / `circle`), `color`, `prefixIcon`, `suffixIcon`, `hideLabel`, `disabled`. |
149
- | `TextField` | `value`, `onChangeText`, `onSubmit`, `placeholder`, `keyboardType`, `autoCapitalize`, `secureTextEntry`, `multiline`, `maxLength`, `accentColor`, |
150
- | `Switch` | `value`, `onValueChange`, `label`, `disabled`, `accentColor`. |
151
- | `Picker` | `selectedValue`, `onValueChange`, `label`, with `{label, value}` option children. |
152
- | `DateTimePicker` | `value`, `onChange`, `mode` (`date` / `time` / `datetime`), `minimumDate`, `maximumDate`, `label`. |
153
- | `Progress` | Linear progress: `value` (0–1, indeterminate when omitted), `color`, `trackColor`. |
154
- | `ListItem` | Row with `leading`, `trailing`, `supporting` text and `onPress`. |
155
- | `FieldGroup` | Re-export of `@expo/ui`'s grouped form container (styled on web). |
156
- | `QRCode` | Renders `value` as a QR image (`size`). |
157
- | `ExternalLink` | `expo-router` `Link` that opens in an in-app browser on native. |
158
-
159
- ### Typography
160
-
161
- `Typography` plus one component per variant: `LargeTitle`, `Title`, `Title2`,
162
- `Title3`, `Headline`, `Body`, `Callout`, `Subheadline`, `Footnote`, `Caption`,
163
- `Label`. Props: `color` (a theme token), `weight`, `align`, `numberOfLines`,
164
- `style`. Metrics follow the iOS text styles on iOS/web and the Material type
165
- scale on Android.
87
+ Everything is exported from `expo-interface`. Component names link to their
88
+ props. Value controls are controlled: pair `value` with `onValueChange`.
89
+
90
+ Each component renders the native control of its platform (SwiftUI, Material 3
91
+ Compose, or a DOM element). Web overlays use the Popover API, CSS anchor
92
+ positioning and `<dialog>`.
93
+
94
+ | Component | Description | iOS | Android | Web |
95
+ | --- | --- | :-: | :-: | :-: |
96
+ | [Screen](src/screen/index.tsx) | Screen container that handles safe areas, status bar, background and content width, optionally hosting native content | ✓ | ✓ | ✓ |
97
+ | [ScreenHeader](src/screen/header.tsx) | Simple header bar with an optional back button and a trailing slot | | | |
98
+ | [Tabs](src/tabs/types.ts) | Tab bar for `expo-router`: native tabs on iOS and Android, a floating top bar with a logo on web | ✓ | ✓ | ✓ |
99
+ | [TabStack](src/tab-stack/index.tsx) | Preconfigured `expo-router` stack for the root screen of a tab | | | |
100
+ | [ConstrainedStackHeader](src/stack-header/index.tsx) | Stack header that matches the content max-width on web | | | ✓ |
101
+ | [Sheet](src/sheet/index.tsx) | Bottom sheet that inherits the accent color | ✓ | ✓ | ✓ |
102
+ | [FieldGroup](src/field-group/index.tsx) | Scrollable settings form made of titled sections of rows | ✓ | ✓ | ✓ |
103
+ | [ListItem](src/list-item/types.ts) | Tappable row with leading, trailing and supporting text slots | ✓ | ✓ | ✓ |
104
+ | [Collapsible](src/collapsible/types.ts) | Row that expands and collapses its content | ✓ | ✓ | ✓ |
105
+ | [Divider](src/divider/types.ts) | Horizontal or vertical hairline separator | ✓ | ✓ | ✓ |
106
+ | [Button](src/button/types.ts) | Filled, outlined or text button with optional icons, sizes, shapes and a destructive role | | | |
107
+ | [TextField](src/text-field/types.ts) | Single or multiline text input with keyboard type, capitalization and secure entry | ✓ | ✓ | ✓ |
108
+ | [Switch](src/switch/types.ts) | On/off toggle with a leading label | | ✓ | ✓ |
109
+ | [Checkbox](src/checkbox/types.ts) | Checked/unchecked box with a leading label | | | ✓ |
110
+ | [ColorPicker](src/color-picker/types.ts) | Label with a rainbow-ringed color well that opens the iOS-style color picker (Grid, Spectrum, Sliders, opacity) | ✓ | ✓ | ✓ |
111
+ | [Slider](src/slider/types.ts) | Thumb dragged along a continuous or stepped range | | ✓ | ✓ |
112
+ | [Stepper](src/stepper/types.ts) | Number adjusted with increment and decrement buttons | | ✓ | ✓ |
113
+ | [Picker](src/picker/types.ts) | Dropdown that selects one option from a list | | ✓ | ✓ |
114
+ | [SegmentedControl](src/segmented/types.ts) | Row of segments that selects one option | ✓ | ✓ | ✓ |
115
+ | [DateTimePicker](src/date-time/types.ts) | Picks a date, a time or both, with optional bounds | ✓ | ✓ | ✓ |
116
+ | [Progress](src/progress/types.ts) | Linear bar or circular ring, determinate or indeterminate | ✓ | ✓ | ✓ |
117
+ | [Gauge](src/gauge/types.ts) | Value within a range in the SwiftUI gauge styles: capacity bars, marker bar, open or closed ring | ✓ | ✓ | ✓ |
118
+ | [Menu](src/menu/types.ts) | Dropdown menu of actions opened from a button | ✓ | ✓ | ✓ |
119
+ | [ContextMenu](src/menu/types.ts) | Menu of actions opened by long-pressing (or right-clicking) its content | ✓ | ✓ | ✓ |
120
+ | [Tooltip](src/tooltip/types.ts) | Short hint shown on hover, focus or long-press; an accessibility hint on iOS | | ✓ | ✓ |
121
+ | [Alert](src/alert/types.ts) | Modal dialog or action sheet with a title, message and actions | ✓ | ✓ | ✓ |
122
+ | [ExternalLink](src/router/external-link.tsx) | Link that opens in an in-app browser on native and a new tab on web | ✓ | ✓ | ✓ |
123
+ | [Typography](src/typography/types.ts) | Text in the platform type scale, with `Title`, `Body`, `Caption` and other variants as shortcuts | ✓ | ✓ | ✓ |
166
124
 
167
125
  ### Icons
168
126
 
169
- Components take an `IconToken` an `expo-symbols` name (single string or
170
- `{ios, android, web}` map) plus an optional Android drawable:
127
+ Icon props take an `IconToken`: an `expo-symbols` name, or a
128
+ `{ios, android, web}` map, plus an optional Android drawable. Keep drawables in
129
+ an `.android.ts` file so the XML is only bundled there.
171
130
 
172
131
  ```ts
173
132
  // icons.drawables.android.ts
174
133
  import share from '@expo/material-symbols/share.xml';
175
134
  export const drawables = {share};
176
135
 
177
- // icons.drawables.ts (iOS/web stub)
136
+ // icons.drawables.ts
178
137
  export const drawables: Record<string, ImageSourcePropType | undefined> = {};
179
138
 
180
139
  // icons.ts
181
140
  import {icon} from 'expo-interface';
182
141
  import {drawables} from './icons.drawables';
183
-
184
142
  export const share = icon(
185
143
  {ios: 'square.and.arrow.up', android: 'share', web: 'share'},
186
144
  drawables.share,
187
145
  );
188
146
  ```
189
147
 
190
- ### Theme
148
+ ### Colors
191
149
 
192
- | Export | Description |
193
- | ------ | ----------- |
194
- | `theme` | Scheme-adaptive color tokens as opaque platform values (`PlatformColor` on iOS, theme attrs on Android, `var(--color-*)` on web) for styles the platform resolves natively. |
195
- | `useColor(token)` | Hook returning a concrete color string for the active scheme and accent — use for anything that can't consume a `PlatformColor` (symbols, `@expo/ui` props). |
196
- | `colors` | The raw light/dark palette. |
197
- | `spacing`, `bound`, `inset`, `fonts`, `fontWeights`, `variants` | Layout and type constants. |
198
- | `useNavTheme()` | React Navigation theme resolved from the palette and live accent. |
199
- | `getThemeCSS()` | Palette as `--color-*` CSS for `+html.tsx`. |
200
- | `AccentProvider`, `useAccentSeed`, `onAccent`, `ACCENT_SEED` | Accent seed context and helpers. |
150
+ - Follow the system's light or dark scheme and take a color tint from the `AccentProvider` seed.
151
+ - A screen looks native on each platform without any per-platform styling.
152
+ - There are two ways to read colors: `theme` in styles, and `useColor` everywhere else.
201
153
 
202
- Tokens: `label`, `secondaryLabel`, `tertiaryLabel`, `background`,
203
- `backgroundElement`, `backgroundSelected`, `separator`, `tint`, `onTint`,
204
- `pillBackground`, `switchTrack`, `switchOn`, `destructive`, `onDestructive`.
154
+ #### `useColor`
205
155
 
206
- ### Utilities
156
+ Returns a color token as a plain string that tracks the active scheme and accent.
157
+ Use it for props that won't accept a platform color object from `theme`, such as
158
+ symbol tints or `@expo/ui` components:
159
+ ```tsx
160
+ import {SymbolView} from 'expo-symbols';
161
+ import {useColor} from 'expo-interface';
162
+
163
+ // Inside a component...
164
+ const tint = useColor('tint');
165
+
166
+ // Inside component return...
167
+ <SymbolView name="star" tintColor={tint}/>
168
+ ```
169
+
170
+ #### `theme`
171
+
172
+ A static object with one entry per color token. Each entry is a platform color reference (`PlatformColor` on iOS, a theme attribute on Android, a CSS variable on web).
173
+
174
+ The OS resolves the actual value and updates it when the scheme changes, with no re-render required:
175
+
176
+ ```tsx
177
+ import {theme} from 'expo-interface';
178
+
179
+ // Inside a component...
180
+ <View style={{
181
+ backgroundColor: theme.backgroundElement,
182
+ borderColor: theme.separator,
183
+ }}/>
184
+ ```
185
+
186
+ #### Color Tokens
187
+
188
+ | ID | Description |
189
+ | --- | --- |
190
+ | `label` | Primary text: titles, body copy and row labels |
191
+ | `secondaryLabel` | Secondary text: subtitles, captions and supporting text |
192
+ | `tertiaryLabel` | Placeholders, disabled hints and decorative glyphs such as chevrons |
193
+ | `background` | Screen background behind all content |
194
+ | `backgroundElement` | Raised or inset surfaces: cards, sheets and grouped list rows |
195
+ | `backgroundSelected` | Background of a selected or pressed element |
196
+ | `separator` | Hairline separators between rows and borders around controls |
197
+ | `pillBackground` | Track behind pill-shaped controls: pickers, segments, steppers |
198
+ | `tint` | Interactive elements such as buttons, switches and links; the accent seed |
199
+ | `onTint` | Text and icons drawn on top of `tint`, for example a filled button label |
200
+ | `switchTrack` | Track of a switch in the off position |
201
+ | `switchOn` | Success states such as a completed upload |
202
+ | `destructive` | Delete buttons, failed states and other destructive actions |
203
+ | `onDestructive` | Text and icons drawn on top of `destructive` |
204
+
205
+ #### Other exports
206
+
207
+ | Export | Purpose |
208
+ | --- | --- |
209
+ | `useNavTheme()` | React Navigation theme built from the palette and accent |
210
+ | `getThemeCSS()` | Palette as CSS variables, for `+html.tsx` |
211
+ | `colors` | Raw light and dark palettes |
212
+ | `spacing`, `bound`, `inset` | Layout constants |
213
+ | `fonts`, `fontWeights`, `variants` | Type constants |
214
+
215
+ ## Install details
216
+
217
+ The components are built on standard Expo modules, which are peer
218
+ dependencies.
219
+
220
+ Most Expo Router apps already have these dependencies, but incase any are missing:
221
+
222
+ ```sh
223
+ npx expo install expo-router expo-symbols expo-image expo-constants expo-status-bar expo-system-ui expo-web-browser react-native-safe-area-context
224
+ ```
225
+
226
+ Jetpack Compose draws icons from XML vector drawables, which
227
+ `@expo/material-symbols` provides. Register the extension in
228
+ `metro.config.js`:
229
+
230
+ ```js
231
+ const {getDefaultConfig} = require('expo/metro-config');
232
+ const config = getDefaultConfig(__dirname);
233
+ config.resolver.assetExts.push('xml');
234
+ module.exports = config;
235
+ ```
236
+
237
+ ## Development
207
238
 
208
- `shareUrl(url, message?)` opens the native share sheet / Web Share API;
209
- `copyText(text)` copies on web and falls back to the share sheet on native.
239
+ The [example](./example) app, dropfiles, uses every component and imports the
240
+ package from `../src`, so it doubles as the development harness.
210
241
 
211
- ## Example app
242
+ ```sh
243
+ bun install # bun >= 1.4
244
+ bun run web # or ios, android
245
+ bun run typecheck # package, example and storybook
246
+ bun run lint # oxlint
247
+ bun run test # vitest, once per platform (ios, android, web)
248
+ bun run test:ui # vitest watch mode with the browser UI
249
+ ```
212
250
 
213
- [`example/`](./example) is **dropfiles**, a small file-drop app that exercises
214
- every component in the kit. It resolves the package straight from `../src`
215
- through the npm workspace, so it doubles as the development harness:
251
+ ### Storybook
252
+
253
+ Stories live next to each component (`src/<name>/<name>.stories.tsx`) and are
254
+ shared by two Storybooks in the [storybook](./storybook) workspace:
255
+
256
+ - **Web** (`storybook/.storybook`): `@storybook/react-native-web-vite`, so the
257
+ site has the full Storybook manager — MDX guides in `storybook/docs/`, an
258
+ autogenerated docs page with a props table per component, controls, a
259
+ black/white theme that follows the OS scheme and a toolbar to force light or
260
+ dark. This is the documentation site published to GitHub Pages.
261
+ - **On-device** (`storybook/.rnstorybook`): `@storybook/react-native` through
262
+ Metro, rendering the real SwiftUI and Compose controls on iOS and Android.
216
263
 
217
264
  ```sh
218
- npm install
219
- npm run web # expo start --web
220
- npm run ios # expo run:ios
221
- npm run android # expo run:android
222
- npm run typecheck # package + example
265
+ bun run storybook:web # web storybook + docs site (Vite)
266
+ bun run storybook:ios # or storybook:android — on-device storybook
267
+ bun run storybook:build # static web build in storybook/dist
268
+ bun run storybook:test # every story as a Vitest test in headless Chromium
223
269
  ```
224
270
 
271
+ The shared decorator (`storybook/src/frame.tsx`) wraps stories in
272
+ `AccentProvider` and an accent-seeded `@expo/ui` `Host`. Stories built from
273
+ plain React Native views opt out with `parameters: {native: false}`; a story
274
+ can pick an accent with `parameters: {accent: '#8959EA'}`. The web build runs
275
+ the kit through Vite with the same Metro-compat plugin as the Vitest web
276
+ project (`vitest/metro-compat.ts`).
277
+
278
+ ### Tests
279
+
280
+ Vitest (`vitest-expo`) runs the suite three times — an ios, android and web
281
+ project — so each `index.ios.tsx` / `index.android.tsx` / `index.web.tsx`
282
+ implementation is exercised (`vitest.config.mts`; the web pipeline lives in
283
+ `vitest.config.web.mts`). The file name picks the platforms:
284
+
285
+ | Pattern | Platforms |
286
+ | --- | --- |
287
+ | `*.test.ts(x)` | ios, android, web |
288
+ | `*.native.test.tsx` | ios, android |
289
+ | `*.ios.test.tsx` / `*.android.test.tsx` | one platform |
290
+ | `*.web.test.tsx` | web |
291
+
292
+ Web tests use `@testing-library/react` against the real DOM (jsdom +
293
+ react-native-web). Native tests run real React Native and use
294
+ `@testing-library/react-native`; `@expo/ui` controls render as host views whose
295
+ props are the payload sent to SwiftUI/Compose, and `src/__tests__/native.ts`
296
+ has helpers to assert on them. `bun run test:coverage` writes an interactive
297
+ HTML test report to `test-report/` and coverage to `coverage/`.
298
+
299
+ The stories double as tests: `@storybook/addon-vitest`
300
+ (`storybook/vitest.config.mts`) renders every web story in headless Chromium
301
+ through Vitest browser mode, so a story that throws while mounting fails the
302
+ run, and `@storybook/addon-a11y` runs axe-core on each rendered story as part
303
+ of it (`parameters.a11y.test: 'error'`; the `color-contrast` rule is off
304
+ because the palette follows the iOS system colors). `bun run storybook:test` runs them from the CLI (Playwright's Chromium
305
+ must be installed: `bunx playwright install chromium` in `storybook/`); in
306
+ `storybook:web` the testing widget at the bottom of the sidebar runs them
307
+ live.
308
+
309
+ ### CI
310
+
311
+ GitHub Actions ([.github/workflows](./.github/workflows)):
312
+
313
+ - **CI** (`ci.yml`) — on every push and pull request: typecheck, lint, tests
314
+ (HTML report and coverage as artifacts), a Metro export of the example app
315
+ for ios, android and web (proves every platform file and
316
+ `@expo/material-symbols` asset resolves without Xcode or Gradle), and a web
317
+ Storybook build uploaded as an artifact after its stories pass as Vitest
318
+ browser tests.
319
+ - **Storybook** (`storybook.yml`) — on push to `master`: publishes the web
320
+ Storybook to GitHub Pages, with the Vitest HTML report at `/tests` and
321
+ coverage at `/coverage`. Enable Pages with the "GitHub Actions" source in
322
+ the repository settings.
323
+ - **Release** (`release.yml`) — on a `v*` tag matching `package.json`:
324
+ re-runs the checks, publishes to npm with provenance (needs an `NPM_TOKEN`
325
+ secret) and creates a GitHub release with generated notes.
326
+
225
327
  ## License
226
328
 
227
329
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-interface",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Native-first universal UI kit for Expo — SwiftUI on iOS, Jetpack Compose on Android, real DOM on web — built on @expo/ui.",
5
5
  "license": "MIT",
6
6
  "author": "kat-tax",
@@ -33,30 +33,44 @@
33
33
  ],
34
34
  "files": [
35
35
  "src",
36
+ "!src/matchers.d.ts",
37
+ "!src/__stories__",
38
+ "!src/__tests__",
39
+ "!src/**/*.stories.tsx",
40
+ "!src/**/*.test.ts",
41
+ "!src/**/*.test.tsx",
36
42
  "README.md",
37
43
  "LICENSE"
38
44
  ],
39
45
  "workspaces": [
40
- "example"
46
+ "example",
47
+ "storybook"
41
48
  ],
42
49
  "scripts": {
43
- "typecheck": "tsc --noEmit && bun run --cwd example typecheck",
44
- "lint": "bun run --cwd example lint",
50
+ "typecheck": "tsc --noEmit && bun run --cwd example typecheck && bun run --cwd storybook typecheck",
51
+ "lint": "oxlint --max-warnings 0",
52
+ "test": "vitest run",
53
+ "test:watch": "vitest",
54
+ "test:coverage": "vitest run --coverage",
45
55
  "start": "bun run --cwd example start",
46
56
  "web": "bun run --cwd example web",
47
57
  "ios": "bun run --cwd example ios",
48
- "android": "bun run --cwd example android"
49
- },
50
- "dependencies": {
51
- "qrcode-generator": "^2.0.4"
58
+ "android": "bun run --cwd example android",
59
+ "storybook": "bun run --cwd storybook start",
60
+ "storybook:web": "bun run --cwd storybook web",
61
+ "storybook:ios": "bun run --cwd storybook ios",
62
+ "storybook:android": "bun run --cwd storybook android",
63
+ "storybook:build": "bun run --cwd storybook build",
64
+ "storybook:test": "bun run --cwd storybook test",
65
+ "test:ui": "vitest --ui"
52
66
  },
53
67
  "peerDependencies": {
54
68
  "@expo/material-symbols": "*",
55
- "@expo/ui": "^56.0.0",
56
- "expo": "^56.0.0",
69
+ "@expo/ui": "^57.0.0",
70
+ "expo": "^57.0.0",
57
71
  "expo-constants": "*",
58
72
  "expo-image": "*",
59
- "expo-router": "^56.0.0",
73
+ "expo-router": "^57.0.0",
60
74
  "expo-status-bar": "*",
61
75
  "expo-symbols": "*",
62
76
  "expo-system-ui": "*",
@@ -71,8 +85,20 @@
71
85
  }
72
86
  },
73
87
  "devDependencies": {
88
+ "@testing-library/jest-dom": "^7.0.1",
89
+ "@testing-library/react": "^16.3.3",
90
+ "@testing-library/react-native": "^14.0.1",
74
91
  "@types/react": "~19.2.2",
75
- "typescript": "~6.0.3"
92
+ "@vitest/coverage-v8": "^4.1.11",
93
+ "@vitest/ui": "^4.1.11",
94
+ "jsdom": "^30.0.1",
95
+ "oxlint": "^1.81.0",
96
+ "test-renderer": "^1.2.0",
97
+ "typescript": "~6.0.3",
98
+ "vite": "^8.2.2",
99
+ "vitest": "^4.1.11",
100
+ "vitest-expo": "^57.1.0",
101
+ "vitest-native": "^0.13.0"
76
102
  },
77
- "packageManager": "bun@1.3.9"
103
+ "packageManager": "bun@1.4.0"
78
104
  }
@@ -0,0 +1,49 @@
1
+ .ui-alert {
2
+ box-sizing: border-box;
3
+ width: min(100% - 32px, 320px);
4
+ margin: auto;
5
+ padding: 20px;
6
+ border: none;
7
+ border-radius: 16px;
8
+ background: var(--color-background-element);
9
+ color: var(--color-label);
10
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.25);
11
+ font-family: var(--font-display);
12
+ }
13
+
14
+ .ui-alert::backdrop {
15
+ background: rgba(0, 0, 0, 0.4);
16
+ }
17
+
18
+ .ui-alert--sheet {
19
+ width: min(100% - 16px, 600px);
20
+ margin: auto auto 8px;
21
+ }
22
+
23
+ .ui-alert__body {
24
+ display: flex;
25
+ flex-direction: column;
26
+ align-items: flex-start;
27
+ gap: 16px;
28
+ text-align: left;
29
+ }
30
+
31
+ .ui-alert__actions {
32
+ display: flex;
33
+ flex-direction: row;
34
+ justify-content: flex-end;
35
+ flex-wrap: wrap;
36
+ gap: 8px;
37
+ margin-top: 20px;
38
+ }
39
+
40
+ .ui-alert--sheet .ui-alert__actions {
41
+ flex-direction: column;
42
+ align-items: stretch;
43
+ gap: 8px;
44
+ }
45
+
46
+ .ui-alert--sheet .ui-alert__actions > * {
47
+ display: flex;
48
+ width: 100%;
49
+ }