expo-interface 0.1.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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -0
  3. package/package.json +78 -0
  4. package/src/accent.tsx +62 -0
  5. package/src/button/button.css +118 -0
  6. package/src/button/index.android.tsx +114 -0
  7. package/src/button/index.ios.tsx +78 -0
  8. package/src/button/index.tsx +75 -0
  9. package/src/button/shared.ts +42 -0
  10. package/src/button/types.ts +72 -0
  11. package/src/css.d.ts +2 -0
  12. package/src/date-time/index.android.tsx +108 -0
  13. package/src/date-time/index.ios.tsx +54 -0
  14. package/src/date-time/index.tsx +119 -0
  15. package/src/date-time/shared.ts +160 -0
  16. package/src/date-time/types.ts +41 -0
  17. package/src/field-group/field-group.css +18 -0
  18. package/src/field-group/index.android.tsx +193 -0
  19. package/src/field-group/index.tsx +8 -0
  20. package/src/field-group/index.web.tsx +24 -0
  21. package/src/fill/index.android.ts +4 -0
  22. package/src/fill/index.ios.ts +9 -0
  23. package/src/fill/index.ts +10 -0
  24. package/src/global.css +17 -0
  25. package/src/icons.ts +42 -0
  26. package/src/index.ts +59 -0
  27. package/src/link.ts +36 -0
  28. package/src/list-item/index.android.tsx +52 -0
  29. package/src/list-item/index.tsx +21 -0
  30. package/src/list-item/types.ts +21 -0
  31. package/src/picker/index.android.tsx +80 -0
  32. package/src/picker/index.ios.tsx +48 -0
  33. package/src/picker/index.tsx +145 -0
  34. package/src/picker/shared.ts +70 -0
  35. package/src/picker/types.ts +48 -0
  36. package/src/progress/index.android.tsx +24 -0
  37. package/src/progress/index.ios.tsx +20 -0
  38. package/src/progress/index.tsx +30 -0
  39. package/src/progress/progress.css +27 -0
  40. package/src/progress/types.ts +19 -0
  41. package/src/qr/index.tsx +26 -0
  42. package/src/router/external-link.tsx +26 -0
  43. package/src/screen/header.tsx +65 -0
  44. package/src/screen/host-accent.android.ts +8 -0
  45. package/src/screen/host-accent.ios.ts +9 -0
  46. package/src/screen/host-accent.ts +15 -0
  47. package/src/screen/index.tsx +79 -0
  48. package/src/sheet/index.android.tsx +25 -0
  49. package/src/sheet/index.ios.tsx +20 -0
  50. package/src/sheet/index.tsx +18 -0
  51. package/src/stack-header/index.tsx +3 -0
  52. package/src/stack-header/index.web.tsx +33 -0
  53. package/src/switch/index.android.tsx +60 -0
  54. package/src/switch/index.ios.tsx +33 -0
  55. package/src/switch/index.tsx +62 -0
  56. package/src/switch/types.ts +23 -0
  57. package/src/tab-stack/index.tsx +21 -0
  58. package/src/tabs/index.tsx +34 -0
  59. package/src/tabs/index.web.tsx +114 -0
  60. package/src/tabs/types.ts +64 -0
  61. package/src/text-field/index.android.tsx +123 -0
  62. package/src/text-field/index.ios.tsx +73 -0
  63. package/src/text-field/index.tsx +78 -0
  64. package/src/text-field/shared.ts +83 -0
  65. package/src/text-field/types.ts +64 -0
  66. package/src/theme.ts +528 -0
  67. package/src/typography/index.tsx +83 -0
  68. package/src/typography/index.web.tsx +80 -0
  69. package/src/typography/types.ts +45 -0
package/src/theme.ts ADDED
@@ -0,0 +1,528 @@
1
+ import './global.css';
2
+ import type {CSSProperties} from 'react';
3
+ import type {ColorValue, TextStyle} from 'react-native';
4
+
5
+ import {DefaultTheme} from 'expo-router';
6
+ import {Platform, PlatformColor, useColorScheme} from 'react-native';
7
+ import {TypographyVariant, TypographyStyle} from './typography/types';
8
+ import {ACCENT_SEED, onAccent, useAccentSeed} from './accent';
9
+
10
+ export type VariantMap = Record<TypographyVariant, TypographyStyle>;
11
+ export type ColorTokens = keyof typeof colors[keyof typeof colors];
12
+ export type ColorValues = typeof colors[keyof typeof colors];
13
+ export type ColorNative = ColorValue | (() => ColorValue);
14
+
15
+ export const VALID_STYLES = [
16
+ // Position
17
+ 'flexGrow',
18
+ 'flexShrink',
19
+ // Dimension
20
+ 'width',
21
+ 'minWidth',
22
+ 'maxWidth',
23
+ // Spacing
24
+ 'margin',
25
+ 'marginTop',
26
+ 'marginLeft',
27
+ 'marginRight',
28
+ 'marginBottom',
29
+ 'padding',
30
+ 'paddingTop',
31
+ 'paddingLeft',
32
+ 'paddingRight',
33
+ 'paddingBottom',
34
+ ] as const;
35
+
36
+ export const EXPAND_KEYS = {
37
+ marginVertical: ['marginTop', 'marginBottom'],
38
+ marginHorizontal: ['marginLeft', 'marginRight'],
39
+ paddingVertical: ['paddingTop', 'paddingBottom'],
40
+ paddingHorizontal: ['paddingLeft', 'paddingRight'],
41
+ } as const;
42
+
43
+ export const bound = {
44
+ contentMaxWidth: 800,
45
+ } as const;
46
+
47
+ export const spacing = {
48
+ half: 2,
49
+ one: 4,
50
+ two: 8,
51
+ three: 16,
52
+ four: 24,
53
+ five: 32,
54
+ six: 64,
55
+ } as const;
56
+
57
+ export const inset = {
58
+ topBar: Platform.select({
59
+ default: 0,
60
+ web: 80,
61
+ }),
62
+ bottomTab: Platform.select({
63
+ default: 0,
64
+ android: 80,
65
+ ios: 50,
66
+ }),
67
+ } as const;
68
+
69
+ export const fonts = Platform.select({
70
+ web: {
71
+ sans: 'var(--font-display)',
72
+ serif: 'var(--font-serif)',
73
+ mono: 'var(--font-mono)',
74
+ rounded: 'var(--font-rounded)',
75
+ },
76
+ ios: {
77
+ sans: 'system-ui',
78
+ serif: 'ui-serif',
79
+ mono: 'ui-monospace',
80
+ rounded: 'ui-rounded',
81
+ },
82
+ default: {
83
+ sans: 'normal',
84
+ serif: 'serif',
85
+ rounded: 'normal',
86
+ mono: 'monospace',
87
+ },
88
+ });
89
+
90
+ export const fontWeights = {
91
+ normal: '400',
92
+ medium: '500',
93
+ semibold: '600',
94
+ bold: '700',
95
+ heavy: '900',
96
+ } as const;
97
+
98
+ /**
99
+ * Concrete scheme palette. These are the values `useColor` resolves on iOS
100
+ * and Android (visual parity with web), the `default` of every `theme`
101
+ * token, and the values emitted as CSS custom properties by `getThemeCSS`
102
+ * on web. Native Compose controls on Android (text fields, switches,
103
+ * pickers, dialogs) are themed separately by the accent-seeded Material 3
104
+ * Host palette (`seedColor` on the Screen `Host`).
105
+ *
106
+ * `tint`/`onTint` derive from the accent seed (see `accent.tsx`) — a single
107
+ * color for both schemes, like a single-color iOS AccentColor asset.
108
+ */
109
+ export const colors = {
110
+ light: {
111
+ label: '#000000',
112
+ secondaryLabel: '#60646C',
113
+ tertiaryLabel: '#9094A0',
114
+ background: '#ffffff',
115
+ backgroundElement: '#F0F0F3',
116
+ backgroundSelected: '#E0E1E6',
117
+ separator: 'rgba(60, 60, 67, 0.29)',
118
+ tint: ACCENT_SEED,
119
+ onTint: onAccent(ACCENT_SEED),
120
+ pillBackground: 'rgba(118, 118, 128, 0.12)',
121
+ switchTrack: '#e9e9ea',
122
+ switchOn: '#34C759',
123
+ destructive: '#FF3B30',
124
+ onDestructive: '#FFFFFF',
125
+ },
126
+ dark: {
127
+ label: '#ffffff',
128
+ secondaryLabel: '#B0B4BA',
129
+ tertiaryLabel: '#6E7378',
130
+ background: '#000000',
131
+ backgroundElement: '#212225',
132
+ backgroundSelected: '#2E3135',
133
+ separator: 'rgba(84, 84, 88, 0.6)',
134
+ tint: ACCENT_SEED,
135
+ onTint: onAccent(ACCENT_SEED),
136
+ pillBackground: 'rgba(118, 118, 128, 0.24)',
137
+ switchTrack: '#39393d',
138
+ switchOn: '#30D158',
139
+ destructive: '#FF453A',
140
+ onDestructive: '#FFFFFF',
141
+ },
142
+ } as const;
143
+
144
+ /**
145
+ * Static, scheme-adaptive color tokens. Three resolution paths exist:
146
+ *
147
+ * 1. `theme.*` (this object) — opaque platform values: iOS `PlatformColor`
148
+ * (resolved by UIKit per scheme), Android theme attributes, web
149
+ * `var(--color-*)` custom properties. Use in styles that the platform
150
+ * resolves natively.
151
+ * 2. `useColor(token)` — a concrete string usable anywhere: on native the
152
+ * scheme palette hex (tint/onTint from the live accent seed — identical
153
+ * on iOS and Android for web parity), on web the CSS var (stays reactive
154
+ * in CSS). Compose-native controls are instead themed by the seeded
155
+ * Android Host (`hostAccentProps`).
156
+ * 3. `getThemeCSS()` — emits the palette as `--color-*` vars on web;
157
+ * `AccentProvider` overlays user-supplied tints as inline vars.
158
+ *
159
+ * The accent seed (`accent.tsx`) is the single source for all tints. Note
160
+ * the static iOS/Android entries below cannot react to a runtime
161
+ * user-supplied seed — only `useColor`/CSS vars do.
162
+ */
163
+ export const theme = {
164
+ /**
165
+ * Primary text.
166
+ * - iOS: `PlatformColor('label')` — #000 light / #fff dark.
167
+ * - Android: `?android:attr/textColorPrimary` here; palette via `useColor`.
168
+ * - Web: `var(--color-label)`.
169
+ * - Fallback: #000000 light / #ffffff dark.
170
+ */
171
+ label: getPlatformToken({
172
+ ios: () => PlatformColor('label'),
173
+ android: () => PlatformColor('?android:attr/textColorPrimary'),
174
+ web: 'var(--color-label)',
175
+ default: colors.light.label,
176
+ }),
177
+ /**
178
+ * Secondary text (subtitles, captions).
179
+ * - iOS: `PlatformColor('secondaryLabel')`.
180
+ * - Android: `?android:attr/textColorSecondary` here; palette via `useColor`.
181
+ * - Web: `var(--color-secondary-label)`.
182
+ * - Fallback: #60646C light / #B0B4BA dark.
183
+ */
184
+ secondaryLabel: getPlatformToken({
185
+ ios: () => PlatformColor('secondaryLabel'),
186
+ android: () => PlatformColor('?android:attr/textColorSecondary'),
187
+ web: 'var(--color-secondary-label)',
188
+ default: colors.light.secondaryLabel,
189
+ }),
190
+ /**
191
+ * Tertiary text (placeholders, disabled hints, decorative glyphs).
192
+ * - iOS: `PlatformColor('tertiaryLabel')`.
193
+ * - Android: `?android:attr/textColorTertiary` here; palette via `useColor`.
194
+ * - Web: `var(--color-tertiary-label)`.
195
+ * - Fallback: #9094A0 light / #6E7378 dark.
196
+ */
197
+ tertiaryLabel: getPlatformToken({
198
+ ios: () => PlatformColor('tertiaryLabel'),
199
+ android: () => PlatformColor('?android:attr/textColorTertiary'),
200
+ web: 'var(--color-tertiary-label)',
201
+ default: colors.light.tertiaryLabel,
202
+ }),
203
+ /**
204
+ * Screen background.
205
+ * - iOS: `PlatformColor('systemBackground')`.
206
+ * - Android: `?android:attr/colorBackground` here; palette via `useColor`.
207
+ * - Web: `var(--color-background)`.
208
+ * - Fallback: #ffffff light / #000000 dark.
209
+ */
210
+ background: getPlatformToken({
211
+ ios: () => PlatformColor('systemBackground'),
212
+ android: () => PlatformColor('?android:attr/colorBackground'),
213
+ web: 'var(--color-background)',
214
+ default: colors.light.background,
215
+ }),
216
+ /**
217
+ * Raised/inset element background (cards, list rows).
218
+ * - iOS: `PlatformColor('secondarySystemBackground')`.
219
+ * - Android: `?android:attr/colorBackgroundFloating` here; palette via `useColor`.
220
+ * - Web: `var(--color-background-element)`.
221
+ * - Fallback: #F0F0F3 light / #212225 dark.
222
+ */
223
+ backgroundElement: getPlatformToken({
224
+ ios: () => PlatformColor('secondarySystemBackground'),
225
+ android: () => PlatformColor('?android:attr/colorBackgroundFloating'),
226
+ web: 'var(--color-background-element)',
227
+ default: colors.light.backgroundElement,
228
+ }),
229
+ /**
230
+ * Selected/pressed element background.
231
+ * - iOS: `PlatformColor('tertiarySystemBackground')`.
232
+ * - Android: `?android:attr/colorControlHighlight` here; palette via `useColor`.
233
+ * - Web: `var(--color-background-selected)`.
234
+ * - Fallback: #E0E1E6 light / #2E3135 dark.
235
+ */
236
+ backgroundSelected: getPlatformToken({
237
+ ios: () => PlatformColor('tertiarySystemBackground'),
238
+ android: () => PlatformColor('?android:attr/colorControlHighlight'),
239
+ web: 'var(--color-background-selected)',
240
+ default: colors.light.backgroundSelected,
241
+ }),
242
+ /**
243
+ * Hairline separators and borders.
244
+ * - iOS: `PlatformColor('separator')`.
245
+ * - Android: `?android:attr/colorControlHighlight` here; palette via `useColor`.
246
+ * - Web: `var(--color-separator)`.
247
+ * - Fallback: rgba(60,60,67,0.29) light / rgba(84,84,88,0.6) dark.
248
+ */
249
+ separator: getPlatformToken({
250
+ ios: () => PlatformColor('separator'),
251
+ android: () => PlatformColor('?android:attr/colorControlHighlight'),
252
+ web: 'var(--color-separator)',
253
+ default: colors.light.separator,
254
+ }),
255
+ /**
256
+ * Accent color for interactive elements, derived from the accent seed.
257
+ * - iOS: the hardcoded seed here; SwiftUI children get it via the Host-level
258
+ * `tint()` cascade, and `useColor('tint')` returns the live seed.
259
+ * - Android: `?attr/colorPrimary` here; `useColor('tint')` returns the
260
+ * live seed (web parity). Compose-native controls derive their own M3
261
+ * `primary` from the Host `seedColor`.
262
+ * - Web: `var(--color-tint)` — default emitted from the seed, user-supplied
263
+ * seeds applied as inline vars by `AccentProvider`.
264
+ * - Fallback: ACCENT_SEED (#007AFF) both schemes.
265
+ */
266
+ tint: getPlatformToken({
267
+ ios: ACCENT_SEED,
268
+ android: () => PlatformColor('?attr/colorPrimary'),
269
+ web: 'var(--color-tint)',
270
+ default: colors.light.tint,
271
+ }),
272
+ /**
273
+ * Content rendered on top of the accent (filled-button labels/icons).
274
+ * - iOS/Android: luminance contrast of the live seed via `useColor`
275
+ * (white for #007AFF).
276
+ * - Web: `var(--color-on-tint)`; follows the seed via `AccentProvider`.
277
+ * - Fallback: contrast of ACCENT_SEED (#FFFFFF) both schemes.
278
+ */
279
+ onTint: getPlatformToken({
280
+ ios: onAccent(ACCENT_SEED),
281
+ android: onAccent(ACCENT_SEED),
282
+ web: 'var(--color-on-tint)',
283
+ default: colors.light.onTint,
284
+ }),
285
+ /**
286
+ * Pill/segmented control background.
287
+ * - iOS: `PlatformColor('secondarySystemFill')`.
288
+ * - Android: `?android:attr/colorControlHighlight` here; palette via `useColor`.
289
+ * - Web: `var(--color-pill-background)`.
290
+ * - Fallback: rgba(118,118,128,0.12) light / rgba(118,118,128,0.24) dark.
291
+ */
292
+ pillBackground: getPlatformToken({
293
+ ios: () => PlatformColor('secondarySystemFill'),
294
+ android: () => PlatformColor('?android:attr/colorControlHighlight'),
295
+ web: 'var(--color-pill-background)',
296
+ default: colors.light.pillBackground,
297
+ }),
298
+ /**
299
+ * Switch "off" track.
300
+ * - iOS: `PlatformColor('systemGray5')`.
301
+ * - Android: `?android:attr/colorControlHighlight` here; palette via `useColor`.
302
+ * (The Compose Switch is themed natively by the seeded Host.)
303
+ * - Web: `var(--color-switch-track)`.
304
+ * - Fallback: #e9e9ea light / #39393d dark.
305
+ */
306
+ switchTrack: getPlatformToken({
307
+ ios: () => PlatformColor('systemGray5'),
308
+ android: () => PlatformColor('?android:attr/colorControlHighlight'),
309
+ web: 'var(--color-switch-track)',
310
+ default: colors.light.switchTrack,
311
+ }),
312
+ /**
313
+ * Success green (iOS systemGreen), e.g. completed states. Note switches
314
+ * themselves follow the accent `tint` on every platform (the iOS Host
315
+ * `tint` cascade colors the SwiftUI `Toggle` with the seed).
316
+ * - iOS: `PlatformColor('systemGreen')` — #34C759 light / #30D158 dark.
317
+ * - Android: `?attr/colorPrimary` here; palette via `useColor`.
318
+ * - Web: `var(--color-switch-on)`.
319
+ * - Fallback: #34C759 light / #30D158 dark.
320
+ */
321
+ switchOn: getPlatformToken({
322
+ ios: () => PlatformColor('systemGreen'),
323
+ android: () => PlatformColor('?attr/colorPrimary'),
324
+ web: 'var(--color-switch-on)',
325
+ default: colors.light.switchOn,
326
+ }),
327
+ /**
328
+ * Destructive actions (delete buttons, error states).
329
+ * - iOS: `PlatformColor('systemRed')` — #FF3B30 light / #FF453A dark.
330
+ * - Android: `?attr/colorError` here; palette via `useColor` (web parity).
331
+ * - Web: `var(--color-destructive)`.
332
+ * - Fallback: #FF3B30 light / #FF453A dark.
333
+ */
334
+ destructive: getPlatformToken({
335
+ ios: () => PlatformColor('systemRed'),
336
+ android: () => PlatformColor('?attr/colorError'),
337
+ web: 'var(--color-destructive)',
338
+ default: colors.light.destructive,
339
+ }),
340
+ /**
341
+ * Content rendered on top of the destructive color.
342
+ * - iOS/Android: white via `useColor` (matches labels on filled
343
+ * systemRed/destructive buttons).
344
+ * - Web: `var(--color-on-destructive)`.
345
+ * - Fallback: #FFFFFF both schemes.
346
+ */
347
+ onDestructive: getPlatformToken({
348
+ ios: colors.light.onDestructive,
349
+ android: colors.light.onDestructive,
350
+ web: 'var(--color-on-destructive)',
351
+ default: colors.light.onDestructive,
352
+ }),
353
+ } as const;
354
+
355
+ export const nav = {
356
+ ...DefaultTheme,
357
+ colors: {
358
+ ...DefaultTheme.colors,
359
+ primary: theme.tint,
360
+ background: theme.background,
361
+ card: theme.backgroundElement,
362
+ text: theme.label,
363
+ border: theme.separator,
364
+ },
365
+ fonts: {
366
+ regular: {fontFamily: fonts.sans, fontWeight: fontWeights.normal},
367
+ medium: {fontFamily: fonts.sans, fontWeight: fontWeights.medium},
368
+ bold: {fontFamily: fonts.sans, fontWeight: fontWeights.bold},
369
+ heavy: {fontFamily: fonts.sans, fontWeight: fontWeights.heavy},
370
+ },
371
+ } as const;
372
+
373
+ /**
374
+ * React Navigation does not resolve PlatformColor tokens on native headers,
375
+ * so resolve concrete palette colors (with the live accent seed as primary).
376
+ */
377
+ export function useNavTheme() {
378
+ if (Platform.OS === 'web') return nav;
379
+ /* eslint-disable react-hooks/rules-of-hooks -- Platform.OS is a runtime constant. */
380
+ const seed = useAccentSeed();
381
+ const palette = colors[useColorScheme() === 'dark' ? 'dark' : 'light'];
382
+ /* eslint-enable react-hooks/rules-of-hooks */
383
+ return {
384
+ ...nav,
385
+ colors: {
386
+ ...nav.colors,
387
+ primary: seed,
388
+ background: palette.background,
389
+ card: palette.backgroundElement,
390
+ text: palette.label,
391
+ border: palette.separator,
392
+ },
393
+ };
394
+ }
395
+
396
+ const iosVars: VariantMap = {
397
+ largeTitle: {fontSize: 34, fontWeight: 'bold', lineHeight: 41, letterSpacing: 0.37},
398
+ title: {fontSize: 28, fontWeight: 'bold', lineHeight: 34, letterSpacing: 0.36},
399
+ title2: {fontSize: 22, fontWeight: 'bold', lineHeight: 28, letterSpacing: 0.35},
400
+ title3: {fontSize: 20, fontWeight: 'semibold', lineHeight: 25, letterSpacing: 0.38},
401
+ headline: {fontSize: 17, fontWeight: 'semibold', lineHeight: 22, letterSpacing: -0.41},
402
+ body: {fontSize: 17, fontWeight: 'normal', lineHeight: 22, letterSpacing: -0.41},
403
+ callout: {fontSize: 16, fontWeight: 'normal', lineHeight: 21, letterSpacing: -0.32},
404
+ subheadline: {fontSize: 15, fontWeight: 'normal', lineHeight: 20, letterSpacing: -0.24},
405
+ footnote: {fontSize: 13, fontWeight: 'normal', lineHeight: 18, letterSpacing: -0.08},
406
+ caption: {fontSize: 12, fontWeight: 'normal', lineHeight: 16},
407
+ label: {fontSize: 14, fontWeight: 'medium', lineHeight: 18},
408
+ };
409
+
410
+ const androidVars: VariantMap = {
411
+ largeTitle: {fontSize: 32, fontWeight: 'normal', lineHeight: 40},
412
+ title: {fontSize: 28, fontWeight: 'normal', lineHeight: 36},
413
+ title2: {fontSize: 22, fontWeight: 'normal', lineHeight: 28},
414
+ title3: {fontSize: 20, fontWeight: 'medium', lineHeight: 26},
415
+ headline: {fontSize: 16, fontWeight: 'medium', lineHeight: 24},
416
+ body: {fontSize: 16, fontWeight: 'normal', lineHeight: 24},
417
+ callout: {fontSize: 16, fontWeight: 'normal', lineHeight: 24},
418
+ subheadline: {fontSize: 14, fontWeight: 'normal', lineHeight: 20},
419
+ footnote: {fontSize: 13, fontWeight: 'normal', lineHeight: 18},
420
+ caption: {fontSize: 12, fontWeight: 'normal', lineHeight: 16},
421
+ label: {fontSize: 12, fontWeight: 'medium', lineHeight: 16},
422
+ };
423
+
424
+ export const variants = Platform.select({
425
+ default: iosVars,
426
+ android: androidVars,
427
+ ios: iosVars,
428
+ web: iosVars,
429
+ });
430
+
431
+ /**
432
+ * Resolves a color token to a usable string value.
433
+ *
434
+ * - Web: returns the `var(--color-*)` custom property so theming stays fully
435
+ * reactive through CSS (driven by `getThemeCSS` + `AccentProvider`), never
436
+ * recomputed in JS.
437
+ * - Android: resolves the Material 3 role from the palette seeded by the
438
+ * accent color (scheme-aware, every API level).
439
+ * - iOS: PlatformColor values are opaque objects that can't be consumed
440
+ * everywhere (they stringify to "[object Object]" and aren't honored by
441
+ * expo-symbols glyphs), so resolve a concrete palette color that reacts to
442
+ * the active light/dark scheme; `tint`/`onTint` follow the live accent seed.
443
+ */
444
+ export function useColor(token: ColorTokens): string {
445
+ if (Platform.OS === 'web') return theme[token] as string;
446
+ /* eslint-disable react-hooks/rules-of-hooks -- Platform.OS is a runtime constant. */
447
+ const seed = useAccentSeed();
448
+ const scheme = useColorScheme() === 'dark' ? 'dark' : 'light';
449
+ /* eslint-enable react-hooks/rules-of-hooks */
450
+ if (token === 'tint') return seed;
451
+ if (token === 'onTint') return onAccent(seed);
452
+ return colors[scheme][token];
453
+ }
454
+
455
+ export function getPlatformToken(specifics: {
456
+ default: ColorValue;
457
+ android: ColorNative;
458
+ ios: ColorNative;
459
+ web: ColorValue;
460
+ }): ColorValue {
461
+ const get = (c: ColorNative) => typeof c === 'function' ? c() : c;
462
+ switch (Platform.OS) {
463
+ case 'ios':
464
+ return get(specifics.ios);
465
+ case 'android':
466
+ return get(specifics.android);
467
+ case 'web':
468
+ return specifics.web;
469
+ default:
470
+ return specifics.default;
471
+ }
472
+ }
473
+
474
+ export function getThemeCSS(): string {
475
+ const format = (s: string) => s.replace(/[A-Z]/g, v => `-${v.toLowerCase()}`);
476
+ const render = (o: ColorValues) => Object.entries(o).map(([k,v]) =>
477
+ `\t\t${`--color-${format(k)}`}: ${v};`).join('\n');
478
+ return `
479
+ :root {
480
+ color-scheme: light dark;
481
+ ${render(colors.light)}
482
+ }
483
+ @media (prefers-color-scheme: dark) {
484
+ :root {
485
+ ${render(colors.dark)}
486
+ }
487
+ }
488
+ `;
489
+ }
490
+
491
+ export function flatten(style?: TextStyle): CSSProperties {
492
+ if (!style) return {};
493
+ const css: CSSProperties = {};
494
+ if (typeof style.opacity === 'number')
495
+ css.opacity = style.opacity;
496
+ if (typeof style.color === 'string')
497
+ css.color = style.color;
498
+ for (const key of VALID_STYLES) {
499
+ if (style[key] != null)
500
+ css[key] = style[key] as never;
501
+ }
502
+ for (const [k,v] of Object.entries(EXPAND_KEYS)) {
503
+ const x = style[k as keyof TextStyle];
504
+ if (x == null) continue;
505
+ for (const s of v)
506
+ css[s] = x as never;
507
+ }
508
+ return css;
509
+ }
510
+
511
+ export function clamp(numberOfLines?: number): CSSProperties {
512
+ if (numberOfLines === 1) {
513
+ return {
514
+ overflow: 'hidden',
515
+ whiteSpace: 'nowrap',
516
+ textOverflow: 'ellipsis',
517
+ };
518
+ }
519
+ if (numberOfLines != null) {
520
+ return {
521
+ display: '-webkit-box',
522
+ overflow: 'hidden',
523
+ WebkitBoxOrient: 'vertical',
524
+ WebkitLineClamp: numberOfLines,
525
+ };
526
+ }
527
+ return {};
528
+ }
@@ -0,0 +1,83 @@
1
+ import type {TypographyProps} from './types';
2
+ import type {TextProps} from 'react-native';
3
+ import {createElement} from 'react';
4
+ import {useColor, variants, fonts, fontWeights} from '../theme';
5
+
6
+ const Text = (props: TextProps) => createElement('RCTText', props);
7
+
8
+ export function Typography({
9
+ children,
10
+ variant = 'body',
11
+ color = 'label',
12
+ weight,
13
+ align,
14
+ style,
15
+ numberOfLines,
16
+ testID,
17
+ }: TypographyProps) {
18
+ const v = variants[variant];
19
+ const c = useColor(color);
20
+ return (
21
+ <Text
22
+ numberOfLines={numberOfLines}
23
+ testID={testID}
24
+ style={[
25
+ {
26
+ textAlign: align,
27
+ letterSpacing: v.letterSpacing,
28
+ lineHeight: v.lineHeight,
29
+ fontSize: v.fontSize,
30
+ fontWeight: fontWeights[weight ?? v.fontWeight],
31
+ fontFamily: fonts.sans,
32
+ color: c,
33
+ },
34
+ style,
35
+ ]}>
36
+ {children}
37
+ </Text>
38
+ );
39
+ }
40
+
41
+ export function LargeTitle(props: Omit<TypographyProps, 'variant'>) {
42
+ return <Typography variant="largeTitle" {...props}/>;
43
+ }
44
+
45
+ export function Title(props: Omit<TypographyProps, 'variant'>) {
46
+ return <Typography variant="title" {...props}/>;
47
+ }
48
+
49
+ export function Title2(props: Omit<TypographyProps, 'variant'>) {
50
+ return <Typography variant="title2" {...props}/>;
51
+ }
52
+
53
+ export function Title3(props: Omit<TypographyProps, 'variant'>) {
54
+ return <Typography variant="title3" {...props}/>;
55
+ }
56
+
57
+ export function Headline(props: Omit<TypographyProps, 'variant'>) {
58
+ return <Typography variant="headline" {...props}/>;
59
+ }
60
+
61
+ export function Body(props: Omit<TypographyProps, 'variant'>) {
62
+ return <Typography variant="body" {...props}/>;
63
+ }
64
+
65
+ export function Callout(props: Omit<TypographyProps, 'variant'>) {
66
+ return <Typography variant="callout" {...props}/>;
67
+ }
68
+
69
+ export function Subheadline(props: Omit<TypographyProps, 'variant'>) {
70
+ return <Typography variant="subheadline" {...props}/>;
71
+ }
72
+
73
+ export function Footnote(props: Omit<TypographyProps, 'variant'>) {
74
+ return <Typography variant="footnote" {...props}/>;
75
+ }
76
+
77
+ export function Caption(props: Omit<TypographyProps, 'variant'>) {
78
+ return <Typography variant="caption" {...props}/>;
79
+ }
80
+
81
+ export function Label(props: Omit<TypographyProps, 'variant'>) {
82
+ return <Typography variant="label" {...props}/>;
83
+ }
@@ -0,0 +1,80 @@
1
+ import type {TypographyProps} from './types';
2
+ import {StyleSheet} from 'react-native';
3
+ import {variants, fonts, fontWeights, useColor, flatten, clamp} from '../theme';
4
+
5
+ export function Typography({
6
+ children,
7
+ variant = 'body',
8
+ color = 'label',
9
+ weight,
10
+ align,
11
+ numberOfLines,
12
+ style,
13
+ testID,
14
+ }: TypographyProps) {
15
+ const flat = StyleSheet.flatten(style);
16
+ const vars = variants[variant];
17
+ const c = useColor(color);
18
+ return (
19
+ <span
20
+ data-testid={testID}
21
+ style={{
22
+ textAlign: align,
23
+ letterSpacing: vars.letterSpacing,
24
+ lineHeight: `${vars.lineHeight}px`,
25
+ fontSize: vars.fontSize,
26
+ fontWeight: fontWeights[weight ?? vars.fontWeight],
27
+ fontFamily: fonts?.sans,
28
+ display: flat?.flexShrink != null ? 'block' : undefined,
29
+ color: c,
30
+ ...flatten(flat),
31
+ ...clamp(numberOfLines),
32
+ }}>
33
+ {children}
34
+ </span>
35
+ );
36
+ }
37
+
38
+ export function LargeTitle(props: Omit<TypographyProps, 'variant'>) {
39
+ return <Typography variant="largeTitle" {...props}/>;
40
+ }
41
+
42
+ export function Title(props: Omit<TypographyProps, 'variant'>) {
43
+ return <Typography variant="title" {...props}/>;
44
+ }
45
+
46
+ export function Title2(props: Omit<TypographyProps, 'variant'>) {
47
+ return <Typography variant="title2" {...props}/>;
48
+ }
49
+
50
+ export function Title3(props: Omit<TypographyProps, 'variant'>) {
51
+ return <Typography variant="title3" {...props}/>;
52
+ }
53
+
54
+ export function Headline(props: Omit<TypographyProps, 'variant'>) {
55
+ return <Typography variant="headline" {...props}/>;
56
+ }
57
+
58
+ export function Body(props: Omit<TypographyProps, 'variant'>) {
59
+ return <Typography variant="body" {...props}/>;
60
+ }
61
+
62
+ export function Callout(props: Omit<TypographyProps, 'variant'>) {
63
+ return <Typography variant="callout" {...props}/>;
64
+ }
65
+
66
+ export function Subheadline(props: Omit<TypographyProps, 'variant'>) {
67
+ return <Typography variant="subheadline" {...props}/>;
68
+ }
69
+
70
+ export function Footnote(props: Omit<TypographyProps, 'variant'>) {
71
+ return <Typography variant="footnote" {...props}/>;
72
+ }
73
+
74
+ export function Caption(props: Omit<TypographyProps, 'variant'>) {
75
+ return <Typography variant="caption" {...props}/>;
76
+ }
77
+
78
+ export function Label(props: Omit<TypographyProps, 'variant'>) {
79
+ return <Typography variant="label" {...props}/>;
80
+ }