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.
- package/README.md +282 -180
- package/package.json +39 -13
- package/src/alert/alert.css +49 -0
- package/src/alert/index.android.tsx +68 -0
- package/src/alert/index.ios.tsx +47 -0
- package/src/alert/index.tsx +60 -0
- package/src/alert/shared.ts +10 -0
- package/src/alert/types.ts +58 -0
- package/src/button/button.css +6 -0
- package/src/button/index.android.tsx +9 -2
- package/src/button/index.ios.tsx +8 -3
- package/src/button/index.tsx +19 -1
- package/src/button/types.ts +7 -0
- package/src/checkbox/checkbox.css +29 -0
- package/src/checkbox/index.android.tsx +54 -0
- package/src/checkbox/index.ios.tsx +50 -0
- package/src/checkbox/index.tsx +50 -0
- package/src/checkbox/types.ts +27 -0
- package/src/collapsible/collapsible.css +32 -0
- package/src/collapsible/index.android.tsx +34 -0
- package/src/collapsible/index.ios.tsx +31 -0
- package/src/collapsible/index.tsx +41 -0
- package/src/collapsible/shared.ts +21 -0
- package/src/collapsible/types.ts +24 -0
- package/src/color-picker/color-picker.css +66 -0
- package/src/color-picker/index.android.tsx +117 -0
- package/src/color-picker/index.ios.tsx +33 -0
- package/src/color-picker/index.tsx +62 -0
- package/src/color-picker/shared.ts +204 -0
- package/src/color-picker/sheet.tsx +432 -0
- package/src/color-picker/types.ts +34 -0
- package/src/context-menu/index.android.tsx +27 -0
- package/src/context-menu/index.ios.tsx +22 -0
- package/src/context-menu/index.tsx +56 -0
- package/src/date-time/index.android.tsx +3 -2
- package/src/divider/divider.css +16 -0
- package/src/divider/index.android.tsx +26 -0
- package/src/divider/index.ios.tsx +18 -0
- package/src/divider/index.tsx +21 -0
- package/src/divider/types.ts +20 -0
- package/src/field-group/field-group.css +23 -7
- package/src/field-group/index.android.tsx +22 -22
- package/src/field-group/index.web.tsx +2 -2
- package/src/gauge/gauge.css +200 -0
- package/src/gauge/index.android.tsx +212 -0
- package/src/gauge/index.ios.tsx +40 -0
- package/src/gauge/index.tsx +165 -0
- package/src/gauge/shared.ts +79 -0
- package/src/gauge/types.ts +64 -0
- package/src/index.ts +24 -3
- package/src/menu/index.android.tsx +66 -0
- package/src/menu/index.ios.tsx +72 -0
- package/src/menu/index.tsx +30 -0
- package/src/menu/list.tsx +98 -0
- package/src/menu/menu.css +83 -0
- package/src/menu/types.ts +60 -0
- package/src/picker/index.android.tsx +3 -2
- package/src/progress/index.android.tsx +23 -9
- package/src/progress/index.ios.tsx +7 -6
- package/src/progress/index.tsx +43 -6
- package/src/progress/progress.css +33 -0
- package/src/progress/types.ts +23 -6
- package/src/screen/header.tsx +1 -0
- package/src/segmented/index.android.tsx +67 -0
- package/src/segmented/index.ios.tsx +47 -0
- package/src/segmented/index.tsx +58 -0
- package/src/segmented/segmented.css +55 -0
- package/src/segmented/types.ts +31 -0
- package/src/slider/index.android.tsx +87 -0
- package/src/slider/index.ios.tsx +64 -0
- package/src/slider/index.tsx +55 -0
- package/src/slider/slider.css +30 -0
- package/src/slider/types.ts +39 -0
- package/src/stepper/index.android.tsx +58 -0
- package/src/stepper/index.ios.tsx +55 -0
- package/src/stepper/index.tsx +47 -0
- package/src/stepper/shared.ts +17 -0
- package/src/stepper/stepper.css +61 -0
- package/src/stepper/types.ts +35 -0
- package/src/switch/index.tsx +2 -0
- package/src/text-field/shared.ts +2 -0
- package/src/tooltip/index.android.tsx +23 -0
- package/src/tooltip/index.ios.tsx +17 -0
- package/src/tooltip/index.tsx +41 -0
- package/src/tooltip/tooltip.css +40 -0
- package/src/tooltip/types.ts +25 -0
- package/src/link.ts +0 -36
- package/src/qr/index.tsx +0 -26
package/README.md
CHANGED
|
@@ -1,227 +1,329 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Expo Interface
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[`@expo/ui`](https://docs.expo.dev/versions/
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
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-
|
|
13
|
+
npx expo install expo-interface @expo/ui @expo/material-symbols
|
|
26
14
|
```
|
|
27
15
|
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
|
137
|
-
|
|
|
138
|
-
|
|
|
139
|
-
|
|
|
140
|
-
|
|
|
141
|
-
|
|
|
142
|
-
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
|
147
|
-
|
|
|
148
|
-
|
|
|
149
|
-
|
|
|
150
|
-
|
|
|
151
|
-
|
|
|
152
|
-
|
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
|
|
|
156
|
-
|
|
|
157
|
-
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
scale
|
|
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
|
-
|
|
170
|
-
`{ios, android, web}` map
|
|
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
|
|
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
|
-
###
|
|
148
|
+
### Colors
|
|
191
149
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
203
|
-
`backgroundElement`, `backgroundSelected`, `separator`, `tint`, `onTint`,
|
|
204
|
-
`pillBackground`, `switchTrack`, `switchOn`, `destructive`, `onDestructive`.
|
|
154
|
+
#### `useColor`
|
|
205
155
|
|
|
206
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
51
|
-
"
|
|
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
|
-
"expo": "^
|
|
69
|
+
"@expo/ui": "^57.0.0",
|
|
70
|
+
"expo": "^57.0.0",
|
|
57
71
|
"expo-constants": "*",
|
|
58
72
|
"expo-image": "*",
|
|
59
|
-
"expo-router": "^
|
|
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
|
-
"
|
|
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.
|
|
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
|
+
}
|