expo-interface 0.1.0 → 0.1.1
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 +260 -180
- package/package.json +41 -10
- 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 +61 -0
- package/src/alert/shared.ts +10 -0
- package/src/alert/types.ts +58 -0
- package/src/button/index.tsx +17 -1
- 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/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/index.ts +20 -2
- 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 +24 -0
- package/src/progress/types.ts +23 -6
- package/src/qr/index.tsx +2 -2
- 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 +56 -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/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/README.md
CHANGED
|
@@ -1,227 +1,307 @@
|
|
|
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 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
|
+
| [Slider](src/slider/types.ts) | Thumb dragged along a continuous or stepped range | ✓ | ✓ | ✓ |
|
|
111
|
+
| [Stepper](src/stepper/types.ts) | Number adjusted with increment and decrement buttons | ✓ | ✓ | ✓ |
|
|
112
|
+
| [Picker](src/picker/types.ts) | Dropdown that selects one option from a list | ✓ | ✓ | ✓ |
|
|
113
|
+
| [SegmentedControl](src/segmented/types.ts) | Row of segments that selects one option | ✓ | ✓ | ✓ |
|
|
114
|
+
| [DateTimePicker](src/date-time/types.ts) | Picks a date, a time or both, with optional bounds | ✓ | ✓ | ✓ |
|
|
115
|
+
| [Progress](src/progress/types.ts) | Linear bar or circular ring, determinate or indeterminate | ✓ | ✓ | ✓ |
|
|
116
|
+
| [Menu](src/menu/types.ts) | Dropdown menu of actions opened from a button | ✓ | ✓ | ✓ |
|
|
117
|
+
| [ContextMenu](src/menu/types.ts) | Menu of actions opened by long-pressing (or right-clicking) its content | ✓ | ✓ | ✓ |
|
|
118
|
+
| [Tooltip](src/tooltip/types.ts) | Short hint shown on hover, focus or long-press; an accessibility hint on iOS | | ✓ | ✓ |
|
|
119
|
+
| [Alert](src/alert/types.ts) | Modal dialog or action sheet with a title, message and actions | ✓ | ✓ | ✓ |
|
|
120
|
+
| [QRCode](src/qr/index.tsx) | Renders a value as a QR code image | ✓ | ✓ | ✓ |
|
|
121
|
+
| [ExternalLink](src/router/external-link.tsx) | Link that opens in an in-app browser on native and a new tab on web | ✓ | ✓ | ✓ |
|
|
122
|
+
| [Typography](src/typography/types.ts) | Text in the platform type scale, with `Title`, `Body`, `Caption` and other variants as shortcuts | ✓ | ✓ | ✓ |
|
|
166
123
|
|
|
167
124
|
### Icons
|
|
168
125
|
|
|
169
|
-
|
|
170
|
-
`{ios, android, web}` map
|
|
126
|
+
Icon props take an `IconToken`: an `expo-symbols` name, or a
|
|
127
|
+
`{ios, android, web}` map, plus an optional Android drawable. Keep drawables in
|
|
128
|
+
an `.android.ts` file so the XML is only bundled there.
|
|
171
129
|
|
|
172
130
|
```ts
|
|
173
131
|
// icons.drawables.android.ts
|
|
174
132
|
import share from '@expo/material-symbols/share.xml';
|
|
175
133
|
export const drawables = {share};
|
|
176
134
|
|
|
177
|
-
// icons.drawables.ts
|
|
135
|
+
// icons.drawables.ts
|
|
178
136
|
export const drawables: Record<string, ImageSourcePropType | undefined> = {};
|
|
179
137
|
|
|
180
138
|
// icons.ts
|
|
181
139
|
import {icon} from 'expo-interface';
|
|
182
140
|
import {drawables} from './icons.drawables';
|
|
183
|
-
|
|
184
141
|
export const share = icon(
|
|
185
142
|
{ios: 'square.and.arrow.up', android: 'share', web: 'share'},
|
|
186
143
|
drawables.share,
|
|
187
144
|
);
|
|
188
145
|
```
|
|
189
146
|
|
|
190
|
-
###
|
|
147
|
+
### Colors
|
|
148
|
+
|
|
149
|
+
- Follow the system's light or dark scheme and take a color tint from the `AccentProvider` seed.
|
|
150
|
+
- A screen looks native on each platform without any per-platform styling.
|
|
151
|
+
- There are two ways to read colors: `theme` in styles, and `useColor` everywhere else.
|
|
152
|
+
|
|
153
|
+
#### `useColor`
|
|
154
|
+
|
|
155
|
+
Returns a color token as a plain string that tracks the active scheme and accent.
|
|
156
|
+
Use it for props that won't accept a platform color object from `theme`, such as
|
|
157
|
+
symbol tints or `@expo/ui` components:
|
|
158
|
+
```tsx
|
|
159
|
+
import {SymbolView} from 'expo-symbols';
|
|
160
|
+
import {useColor} from 'expo-interface';
|
|
161
|
+
|
|
162
|
+
// Inside a component...
|
|
163
|
+
const tint = useColor('tint');
|
|
164
|
+
|
|
165
|
+
// Inside component return...
|
|
166
|
+
<SymbolView name="star" tintColor={tint}/>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### `theme`
|
|
170
|
+
|
|
171
|
+
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).
|
|
172
|
+
|
|
173
|
+
The OS resolves the actual value and updates it when the scheme changes, with no re-render required:
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
import {theme} from 'expo-interface';
|
|
177
|
+
|
|
178
|
+
// Inside a component...
|
|
179
|
+
<View style={{
|
|
180
|
+
backgroundColor: theme.backgroundElement,
|
|
181
|
+
borderColor: theme.separator,
|
|
182
|
+
}}/>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### Color Tokens
|
|
186
|
+
|
|
187
|
+
| ID | Description |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| `label` | Primary text: titles, body copy and row labels |
|
|
190
|
+
| `secondaryLabel` | Secondary text: subtitles, captions and supporting text |
|
|
191
|
+
| `tertiaryLabel` | Placeholders, disabled hints and decorative glyphs such as chevrons |
|
|
192
|
+
| `background` | Screen background behind all content |
|
|
193
|
+
| `backgroundElement` | Raised or inset surfaces: cards, sheets and grouped list rows |
|
|
194
|
+
| `backgroundSelected` | Background of a selected or pressed element |
|
|
195
|
+
| `separator` | Hairline separators between rows and borders around controls |
|
|
196
|
+
| `pillBackground` | Track behind pill-shaped controls: pickers, segments, steppers |
|
|
197
|
+
| `tint` | Interactive elements such as buttons, switches and links; the accent seed |
|
|
198
|
+
| `onTint` | Text and icons drawn on top of `tint`, for example a filled button label |
|
|
199
|
+
| `switchTrack` | Track of a switch in the off position |
|
|
200
|
+
| `switchOn` | Success states such as a completed upload |
|
|
201
|
+
| `destructive` | Delete buttons, failed states and other destructive actions |
|
|
202
|
+
| `onDestructive` | Text and icons drawn on top of `destructive` |
|
|
203
|
+
|
|
204
|
+
#### Other exports
|
|
205
|
+
|
|
206
|
+
| Export | Purpose |
|
|
207
|
+
| --- | --- |
|
|
208
|
+
| `useNavTheme()` | React Navigation theme built from the palette and accent |
|
|
209
|
+
| `getThemeCSS()` | Palette as CSS variables, for `+html.tsx` |
|
|
210
|
+
| `colors` | Raw light and dark palettes |
|
|
211
|
+
| `spacing`, `bound`, `inset` | Layout constants |
|
|
212
|
+
| `fonts`, `fontWeights`, `variants` | Type constants |
|
|
213
|
+
|
|
214
|
+
## Install details
|
|
215
|
+
|
|
216
|
+
The components are built on standard Expo modules, which are peer
|
|
217
|
+
dependencies.
|
|
218
|
+
|
|
219
|
+
Most Expo Router apps already have these dependencies, but incase any are missing:
|
|
191
220
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
221
|
+
```sh
|
|
222
|
+
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
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Jetpack Compose draws icons from XML vector drawables, which
|
|
226
|
+
`@expo/material-symbols` provides. Register the extension in
|
|
227
|
+
`metro.config.js`:
|
|
228
|
+
|
|
229
|
+
```js
|
|
230
|
+
const {getDefaultConfig} = require('expo/metro-config');
|
|
231
|
+
const config = getDefaultConfig(__dirname);
|
|
232
|
+
config.resolver.assetExts.push('xml');
|
|
233
|
+
module.exports = config;
|
|
234
|
+
```
|
|
201
235
|
|
|
202
|
-
|
|
203
|
-
`backgroundElement`, `backgroundSelected`, `separator`, `tint`, `onTint`,
|
|
204
|
-
`pillBackground`, `switchTrack`, `switchOn`, `destructive`, `onDestructive`.
|
|
236
|
+
## Development
|
|
205
237
|
|
|
206
|
-
|
|
238
|
+
The [example](./example) app, dropfiles, uses every component and imports the
|
|
239
|
+
package from `../src`, so it doubles as the development harness.
|
|
207
240
|
|
|
208
|
-
|
|
209
|
-
|
|
241
|
+
```sh
|
|
242
|
+
bun install # bun >= 1.4
|
|
243
|
+
bun run web # or ios, android
|
|
244
|
+
bun run typecheck # package, example and storybook
|
|
245
|
+
bun run lint # eslint (eslint-config-expo)
|
|
246
|
+
bun run test # vitest, once per platform (ios, android, web)
|
|
247
|
+
bun run test:ui # vitest watch mode with the browser UI
|
|
248
|
+
```
|
|
210
249
|
|
|
211
|
-
|
|
250
|
+
### Storybook
|
|
212
251
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
252
|
+
Stories live next to each component (`src/<name>/<name>.stories.tsx`) and run
|
|
253
|
+
in the [storybook](./storybook) app, an Expo project built on
|
|
254
|
+
`@storybook/react-native`, so every story renders the real SwiftUI, Compose
|
|
255
|
+
or DOM control through Metro.
|
|
216
256
|
|
|
217
257
|
```sh
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
npm run android # expo run:android
|
|
222
|
-
npm run typecheck # package + example
|
|
258
|
+
bun run storybook:web # storybook in the browser
|
|
259
|
+
bun run storybook:ios # or storybook:android — on-device storybook
|
|
260
|
+
bun run storybook:build # static web export in storybook/dist
|
|
223
261
|
```
|
|
224
262
|
|
|
263
|
+
The global decorator (`storybook/.rnstorybook/preview.tsx`) wraps stories in
|
|
264
|
+
`AccentProvider` and an accent-seeded `@expo/ui` `Host`. Stories built from
|
|
265
|
+
plain React Native views opt out with `parameters: {native: false}`.
|
|
266
|
+
|
|
267
|
+
### Tests
|
|
268
|
+
|
|
269
|
+
Vitest (`vitest-expo`) runs the suite three times — an ios, android and web
|
|
270
|
+
project — so each `index.ios.tsx` / `index.android.tsx` / `index.web.tsx`
|
|
271
|
+
implementation is exercised (`vitest.config.mts`; the web pipeline lives in
|
|
272
|
+
`vitest.config.web.mts`). The file name picks the platforms:
|
|
273
|
+
|
|
274
|
+
| Pattern | Platforms |
|
|
275
|
+
| --- | --- |
|
|
276
|
+
| `*.test.ts(x)` | ios, android, web |
|
|
277
|
+
| `*.native.test.tsx` | ios, android |
|
|
278
|
+
| `*.ios.test.tsx` / `*.android.test.tsx` | one platform |
|
|
279
|
+
| `*.web.test.tsx` | web |
|
|
280
|
+
|
|
281
|
+
Web tests use `@testing-library/react` against the real DOM (jsdom +
|
|
282
|
+
react-native-web). Native tests run real React Native and use
|
|
283
|
+
`@testing-library/react-native`; `@expo/ui` controls render as host views whose
|
|
284
|
+
props are the payload sent to SwiftUI/Compose, and `src/__tests__/native.ts`
|
|
285
|
+
has helpers to assert on them. `bun run test:coverage` writes an interactive
|
|
286
|
+
HTML test report to `test-report/` and coverage to `coverage/`.
|
|
287
|
+
|
|
288
|
+
### CI
|
|
289
|
+
|
|
290
|
+
GitHub Actions ([.github/workflows](./.github/workflows)):
|
|
291
|
+
|
|
292
|
+
- **CI** (`ci.yml`) — on every push and pull request: typecheck, lint, tests
|
|
293
|
+
(HTML report and coverage as artifacts), a Metro export of the example app
|
|
294
|
+
for ios, android and web (proves every platform file and
|
|
295
|
+
`@expo/material-symbols` asset resolves without Xcode or Gradle), and a web
|
|
296
|
+
Storybook build uploaded as an artifact.
|
|
297
|
+
- **Storybook** (`storybook.yml`) — on push to `master`: publishes the web
|
|
298
|
+
Storybook to GitHub Pages, with the Vitest HTML report at `/tests` and
|
|
299
|
+
coverage at `/coverage`. Enable Pages with the "GitHub Actions" source in
|
|
300
|
+
the repository settings.
|
|
301
|
+
- **Release** (`release.yml`) — on a `v*` tag matching `package.json`:
|
|
302
|
+
re-runs the checks, publishes to npm with provenance (needs an `NPM_TOKEN`
|
|
303
|
+
secret) and creates a GitHub release with generated notes.
|
|
304
|
+
|
|
225
305
|
## License
|
|
226
306
|
|
|
227
307
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-interface",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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,46 @@
|
|
|
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": "eslint . --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"
|
|
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
|
+
"test:ui": "vitest --ui"
|
|
49
65
|
},
|
|
50
66
|
"dependencies": {
|
|
51
67
|
"qrcode-generator": "^2.0.4"
|
|
52
68
|
},
|
|
53
69
|
"peerDependencies": {
|
|
54
70
|
"@expo/material-symbols": "*",
|
|
55
|
-
"@expo/ui": "^
|
|
56
|
-
"expo": "^
|
|
71
|
+
"@expo/ui": "^57.0.0",
|
|
72
|
+
"expo": "^57.0.0",
|
|
57
73
|
"expo-constants": "*",
|
|
58
74
|
"expo-image": "*",
|
|
59
|
-
"expo-router": "^
|
|
75
|
+
"expo-router": "^57.0.0",
|
|
60
76
|
"expo-status-bar": "*",
|
|
61
77
|
"expo-symbols": "*",
|
|
62
78
|
"expo-system-ui": "*",
|
|
@@ -71,8 +87,23 @@
|
|
|
71
87
|
}
|
|
72
88
|
},
|
|
73
89
|
"devDependencies": {
|
|
90
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
91
|
+
"@testing-library/react": "^16.3.3",
|
|
92
|
+
"@testing-library/react-native": "^14.0.1",
|
|
74
93
|
"@types/react": "~19.2.2",
|
|
75
|
-
"typescript": "
|
|
94
|
+
"@typescript-eslint/parser": "8.59.0",
|
|
95
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
96
|
+
"@vitest/ui": "^4.1.11",
|
|
97
|
+
"eslint": "^9.39.5",
|
|
98
|
+
"eslint-config-expo": "~57.0.2",
|
|
99
|
+
"globals": "^16.0.0",
|
|
100
|
+
"jsdom": "^30.0.1",
|
|
101
|
+
"test-renderer": "^1.2.0",
|
|
102
|
+
"typescript": "~6.0.3",
|
|
103
|
+
"vite": "^8.2.2",
|
|
104
|
+
"vitest": "^4.1.11",
|
|
105
|
+
"vitest-expo": "^57.1.0",
|
|
106
|
+
"vitest-native": "^0.13.0"
|
|
76
107
|
},
|
|
77
|
-
"packageManager": "bun@1.
|
|
108
|
+
"packageManager": "bun@1.4.0"
|
|
78
109
|
}
|
|
@@ -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: center;
|
|
27
|
+
gap: 4px;
|
|
28
|
+
text-align: center;
|
|
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
|
+
}
|