create-proto 0.4.5 → 0.5.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/package.json +2 -2
- package/template/CLAUDE.md +57 -2
- package/template/components/proto/a11y.ts +12 -0
- package/template/components/proto/gestures/index.ts +4 -0
- package/template/components/proto/index.ts +2 -0
- package/template/components/proto/mock.ts +13 -0
- package/template/components/proto/svg/index.ts +23 -0
- package/template/components/proto/tokens/base.ts +75 -0
- package/template/components/proto/tokens/liquidGlass.ts +37 -0
- package/template/components/proto/tokens/materialYou.ts +37 -0
- package/template/components/proto/types.ts +3 -1
- package/template/components/proto/useTheme.ts +22 -6
- package/template/metro.config.js +6 -0
- package/template/package.json +16 -1
- package/template/screens/Home.tsx +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-proto",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Scaffold a new Proto prototype: `npm create proto@latest myapp`. Designer-first native iOS prototyping environment that pairs with Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@clack/prompts": "^0.7.0",
|
|
45
|
-
"@sherizan/proto-cli": "^0.
|
|
45
|
+
"@sherizan/proto-cli": "^0.5.0",
|
|
46
46
|
"fs-extra": "^11.2.0",
|
|
47
47
|
"qrcode-terminal": "^0.12.0",
|
|
48
48
|
"validate-npm-package-name": "^5.0.1"
|
package/template/CLAUDE.md
CHANGED
|
@@ -24,11 +24,16 @@ You're the design tool inside a Prototo project. The designer prompts you in pla
|
|
|
24
24
|
- `../components/proto/gestures` — `AnimatedView`, `useSharedValue`, `useAnimatedStyle`, `withSpring`, `Gesture`, `GestureDetector`, etc. Use **only** when the animation must read gesture state, scroll position, or interpolate continuously: "drag this card", "swipe to delete", "parallax this header". Driven by `react-native-reanimated` + `react-native-gesture-handler`.
|
|
25
25
|
- `../components/proto/lottie` — `Lottie` component. Plays `.json` files dropped into `/assets/lottie/`. The designer brings the animation file (LottieFiles / After Effects export); you wire it: `<Lottie source={require('../assets/lottie/<name>.json')} />`. Defaults to `autoPlay` and `loop`. Driven by `lottie-react-native`.
|
|
26
26
|
- `../components/proto/canvas` — `Canvas`, `Path`, `Circle`, `Rect`, `LinearGradient`, etc. For custom drawing that doesn't fit RN's box model: confetti bursts, custom charts, badge shapes. Driven by `@shopify/react-native-skia`.
|
|
27
|
+
- `../components/proto/svg` — `Svg`, `Path`, `Circle`, `Rect`, `G`, `LinearGradient`, etc. For vector icons, logos, and illustrations. You can also import an SVG file directly: `import Logo from '../assets/logo.svg'` then `<Logo width={120} height={40} />`. Driven by `react-native-svg`. Use this for static vector art; reach for `canvas` only when you need to animate or compute the drawing.
|
|
27
28
|
|
|
28
|
-
Never import `react-native-ease`, `react-native-reanimated`, `lottie-react-native`,
|
|
29
|
+
Never import `react-native-ease`, `react-native-reanimated`, `lottie-react-native`, `@shopify/react-native-skia`, or `react-native-svg` directly in a screen — always route through the `../components/proto/<subpath>` module above. If `motion` can't express what's needed, fall back to `gestures`.
|
|
29
30
|
|
|
30
31
|
**Custom** — when none of the above fit, write the component you need with React Native. Put shared ones in `/components/shared/`. The designer's vision wins; primitives are starting points, not constraints.
|
|
31
32
|
|
|
33
|
+
## Adding a library
|
|
34
|
+
|
|
35
|
+
To add any npm package (a font, an icon set, a utility), run `proto add <package>` — never `npm install` / `pnpm add` directly. `proto add` installs through `expo install`, which picks the version that matches this project and resolves dependencies cleanly, so the project doesn't break. If the package needs native code this Prototo doesn't bundle, `proto add` will say so — that feature won't appear on the device until the Proto team ships an updated Prototo.
|
|
36
|
+
|
|
32
37
|
## File layout
|
|
33
38
|
|
|
34
39
|
```
|
|
@@ -51,9 +56,59 @@ When the designer asks to change colors, typography, spacing, shape, accent, or
|
|
|
51
56
|
|
|
52
57
|
When you add a new screen, add a one-line entry to `DESIGN.md`'s Screens section.
|
|
53
58
|
|
|
59
|
+
## One palette, one place
|
|
60
|
+
|
|
61
|
+
The theme colors live in `DESIGN.md` and the proto tokens — read them with `useTheme()` from `../components/proto`. When a design needs custom brand colors, fonts, or constants beyond the theme (e.g. a specific gradient or accent set), define them **once** in a single shared module (`/components/shared/theme.ts`) and import it everywhere that needs them. Never paste the same color/font constants inline into more than one screen — duplicated palettes drift out of sync. If you find a palette already inlined in a screen, lift it into the shared module and import it back.
|
|
62
|
+
|
|
63
|
+
## Light, dark, and accessibility
|
|
64
|
+
|
|
65
|
+
- **Dark mode is automatic.** `useTheme()` returns the right palette for the device's light or dark setting and re-renders when it flips. So use theme colors (`theme.surface.*`, `theme.text.*`, `theme.border.*`) instead of hardcoded hex/rgba, and the screen adapts for free. Custom brand colors in the shared theme module won't auto-adapt — if a design needs a dark variant of a brand color, define both and pick with the same light/dark signal. To pin a scheme for a prototype, set `colorScheme: 'light' | 'dark'` in `proto.config.js`.
|
|
66
|
+
- **Text already scales** with the device's text-size setting (iOS Dynamic Type). Don't disable it. Lay out so text can grow a couple of steps without clipping — avoid fixed heights on text containers.
|
|
67
|
+
- **Accessibility floors** live in `a11y` from `../components/proto`: tap targets ≥ `a11y.minTapTarget` (44pt), text contrast ≥ `a11y.minTextContrast`. After visual changes, the `proto shot` check (below) is where you confirm contrast holds — in both light and dark.
|
|
68
|
+
|
|
69
|
+
## Check your work visually
|
|
70
|
+
|
|
71
|
+
You can't see the Simulator by default, so after any visual change, look at it:
|
|
72
|
+
|
|
73
|
+
1. Run `proto shot` — it captures the running Simulator to `.proto/last-shot.png`.
|
|
74
|
+
2. Read that image and inspect it for real defects: overlapping elements, low contrast / unreadable text, clipping, cramped or uneven spacing, off-center layout, wrong colors.
|
|
75
|
+
3. If something's off, fix it and capture again. Iterate until it looks right — don't make the designer be your QA.
|
|
76
|
+
|
|
77
|
+
Do this especially after layout, color, typography, or spacing changes. If `proto shot` reports no preview is running, the designer needs to run `proto start` first.
|
|
78
|
+
|
|
79
|
+
## Mock vs real data
|
|
80
|
+
|
|
81
|
+
When a screen shows placeholder numbers that aren't yet wired to a real source, wrap them in `mock()` from `../components/proto` — `const conditions = mock({ wave: '0.8m' })`. It returns the value unchanged, so nothing breaks; it just makes stubbed data obvious and greppable so fake numbers never ship believing they're real. When you wire the value to a live source (a `fetch`), drop the `mock()` wrapper. (Don't use code comments to mark mock data — generated screens stay comment-free.)
|
|
82
|
+
|
|
83
|
+
### Making it real
|
|
84
|
+
|
|
85
|
+
When the designer says "use real data", follow this shape so every screen handles loading and failure the same way:
|
|
86
|
+
|
|
87
|
+
1. Keep the `mock()` value as the **starting state** — it's what shows before the fetch resolves and if the network fails. Drop the `mock()` wrapper from the live value once wired.
|
|
88
|
+
2. Fetch in an effect, guarding against the screen unmounting:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
const [data, setData] = useState(FALLBACK);
|
|
92
|
+
const [loading, setLoading] = useState(true);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
let alive = true;
|
|
95
|
+
fetch(URL)
|
|
96
|
+
.then((r) => r.json())
|
|
97
|
+
.then((json) => { if (alive) setData(shape(json)); })
|
|
98
|
+
.catch(() => {})
|
|
99
|
+
.finally(() => { if (alive) setLoading(false); });
|
|
100
|
+
return () => { alive = false; };
|
|
101
|
+
}, []);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
3. While `loading`, show a skeleton or the fallback — never a blank screen. On error, keep the fallback (the `.catch` above already does this); don't surface a raw error to the designer.
|
|
105
|
+
4. Put fetch + shaping logic in `/components/shared/<name>Data.ts`, not inline in the screen.
|
|
106
|
+
|
|
107
|
+
**Keyless APIs** (no key, CORS-open, good for prototypes): Open-Meteo (weather/marine/air), REST Countries, Open Library, PokéAPI, Art Institute of Chicago, TheMealDB, Wikipedia REST. Prefer these so "make it real" stays a one-prompt step. If a source needs a key, tell the designer that key goes in `proto.config.js`, nowhere else.
|
|
108
|
+
|
|
54
109
|
## When modifying
|
|
55
110
|
|
|
56
|
-
Read the file first,
|
|
111
|
+
Read the file first, then make precise, targeted edits to the parts that change. Keep edits scoped — don't rewrite a whole file when a few lines change.
|
|
57
112
|
|
|
58
113
|
## Avoid
|
|
59
114
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility floors for native iOS prototypes. Small, fixed values that don't
|
|
3
|
+
* belong to a theme — they hold across light/dark and every palette.
|
|
4
|
+
*/
|
|
5
|
+
export const a11y = {
|
|
6
|
+
/** Apple's Human Interface minimum for a comfortable tap target, in points. */
|
|
7
|
+
minTapTarget: 44,
|
|
8
|
+
/** WCAG AA contrast floor for normal-size text (foreground vs background). */
|
|
9
|
+
minTextContrast: 4.5,
|
|
10
|
+
/** WCAG AA contrast floor for large text (≥ 24pt, or 19pt bold). */
|
|
11
|
+
minLargeTextContrast: 3,
|
|
12
|
+
} as const;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export {
|
|
2
|
+
default as Animated,
|
|
2
3
|
default as AnimatedView,
|
|
3
4
|
useSharedValue,
|
|
4
5
|
useAnimatedStyle,
|
|
@@ -12,10 +13,13 @@ export {
|
|
|
12
13
|
withRepeat,
|
|
13
14
|
cancelAnimation,
|
|
14
15
|
interpolate,
|
|
16
|
+
interpolateColor,
|
|
15
17
|
Extrapolation,
|
|
16
18
|
Easing,
|
|
17
19
|
runOnJS,
|
|
18
20
|
runOnUI,
|
|
19
21
|
} from 'react-native-reanimated';
|
|
20
22
|
|
|
23
|
+
export type { SharedValue } from 'react-native-reanimated';
|
|
24
|
+
|
|
21
25
|
export { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
@@ -8,4 +8,6 @@ export { Toggle, type ToggleProps } from './Toggle';
|
|
|
8
8
|
export { Divider } from './Divider';
|
|
9
9
|
export { Modal, type ModalProps } from './Modal';
|
|
10
10
|
export { useTheme, useAccent } from './useTheme';
|
|
11
|
+
export { mock } from './mock';
|
|
12
|
+
export { a11y } from './a11y';
|
|
11
13
|
export type { Theme, ThemeName, ThemeOverrides, ProtoConfig } from './types';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marks a value as placeholder/mock data — not yet wired to a real source.
|
|
3
|
+
*
|
|
4
|
+
* It's an identity function: it returns exactly what you pass in, so wrapping
|
|
5
|
+
* a value changes nothing at runtime. Its job is to make stubbed data obvious
|
|
6
|
+
* at a glance (and greppable) so it's never shipped believing it's real. When
|
|
7
|
+
* the value gets wired to a live source, drop the `mock()` wrapper.
|
|
8
|
+
*
|
|
9
|
+
* const conditions = mock({ wave: '0.8m', wind: '8 km/h' });
|
|
10
|
+
*/
|
|
11
|
+
export function mock<T>(value: T): T {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export {
|
|
2
|
+
default as Svg,
|
|
3
|
+
Circle,
|
|
4
|
+
Ellipse,
|
|
5
|
+
G,
|
|
6
|
+
Line,
|
|
7
|
+
Path,
|
|
8
|
+
Polygon,
|
|
9
|
+
Polyline,
|
|
10
|
+
Rect,
|
|
11
|
+
Use,
|
|
12
|
+
Defs,
|
|
13
|
+
LinearGradient,
|
|
14
|
+
RadialGradient,
|
|
15
|
+
Stop,
|
|
16
|
+
ClipPath,
|
|
17
|
+
Mask,
|
|
18
|
+
Pattern,
|
|
19
|
+
Text as SvgText,
|
|
20
|
+
TSpan,
|
|
21
|
+
TextPath,
|
|
22
|
+
Image as SvgImage,
|
|
23
|
+
} from 'react-native-svg';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Theme } from '../types';
|
|
2
|
+
|
|
3
|
+
export const base: Theme = {
|
|
4
|
+
surface: {
|
|
5
|
+
primary: '#FFFFFF',
|
|
6
|
+
secondary: '#F2F2F7',
|
|
7
|
+
card: '#FFFFFF',
|
|
8
|
+
nav: '#FFFFFF',
|
|
9
|
+
},
|
|
10
|
+
text: {
|
|
11
|
+
primary: '#000000',
|
|
12
|
+
secondary: 'rgba(0, 0, 0, 0.6)',
|
|
13
|
+
tertiary: 'rgba(0, 0, 0, 0.4)',
|
|
14
|
+
destructive: '#D70015',
|
|
15
|
+
},
|
|
16
|
+
blur: {
|
|
17
|
+
nav: 0,
|
|
18
|
+
card: 0,
|
|
19
|
+
modal: 0,
|
|
20
|
+
},
|
|
21
|
+
border: {
|
|
22
|
+
default: 'rgba(0, 0, 0, 0.1)',
|
|
23
|
+
strong: 'rgba(0, 0, 0, 0.2)',
|
|
24
|
+
},
|
|
25
|
+
radius: {
|
|
26
|
+
card: 12,
|
|
27
|
+
button: 10,
|
|
28
|
+
nav: 0,
|
|
29
|
+
modal: 24,
|
|
30
|
+
},
|
|
31
|
+
space: {
|
|
32
|
+
xs: 4,
|
|
33
|
+
sm: 8,
|
|
34
|
+
md: 16,
|
|
35
|
+
lg: 24,
|
|
36
|
+
xl: 32,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const baseDark: Theme = {
|
|
41
|
+
surface: {
|
|
42
|
+
primary: '#000000',
|
|
43
|
+
secondary: '#1C1C1E',
|
|
44
|
+
card: '#1C1C1E',
|
|
45
|
+
nav: '#000000',
|
|
46
|
+
},
|
|
47
|
+
text: {
|
|
48
|
+
primary: '#FFFFFF',
|
|
49
|
+
secondary: 'rgba(255, 255, 255, 0.6)',
|
|
50
|
+
tertiary: 'rgba(255, 255, 255, 0.4)',
|
|
51
|
+
destructive: '#FF453A',
|
|
52
|
+
},
|
|
53
|
+
blur: {
|
|
54
|
+
nav: 0,
|
|
55
|
+
card: 0,
|
|
56
|
+
modal: 0,
|
|
57
|
+
},
|
|
58
|
+
border: {
|
|
59
|
+
default: 'rgba(255, 255, 255, 0.15)',
|
|
60
|
+
strong: 'rgba(255, 255, 255, 0.3)',
|
|
61
|
+
},
|
|
62
|
+
radius: {
|
|
63
|
+
card: 12,
|
|
64
|
+
button: 10,
|
|
65
|
+
nav: 0,
|
|
66
|
+
modal: 24,
|
|
67
|
+
},
|
|
68
|
+
space: {
|
|
69
|
+
xs: 4,
|
|
70
|
+
sm: 8,
|
|
71
|
+
md: 16,
|
|
72
|
+
lg: 24,
|
|
73
|
+
xl: 32,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -36,3 +36,40 @@ export const liquidGlass: Theme = {
|
|
|
36
36
|
xl: 32,
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
|
+
|
|
40
|
+
export const liquidGlassDark: Theme = {
|
|
41
|
+
surface: {
|
|
42
|
+
primary: 'rgba(28, 28, 30, 0.72)',
|
|
43
|
+
secondary: 'rgba(28, 28, 30, 0.48)',
|
|
44
|
+
card: '#1c1c1e',
|
|
45
|
+
nav: 'rgba(28, 28, 30, 0.82)',
|
|
46
|
+
},
|
|
47
|
+
text: {
|
|
48
|
+
primary: '#FFFFFF',
|
|
49
|
+
secondary: 'rgba(255, 255, 255, 0.6)',
|
|
50
|
+
tertiary: 'rgba(255, 255, 255, 0.4)',
|
|
51
|
+
destructive: '#FF453A',
|
|
52
|
+
},
|
|
53
|
+
blur: {
|
|
54
|
+
nav: 40,
|
|
55
|
+
card: 20,
|
|
56
|
+
modal: 60,
|
|
57
|
+
},
|
|
58
|
+
border: {
|
|
59
|
+
default: 'rgba(255, 255, 255, 0.12)',
|
|
60
|
+
strong: 'rgba(255, 255, 255, 0.24)',
|
|
61
|
+
},
|
|
62
|
+
radius: {
|
|
63
|
+
card: 22,
|
|
64
|
+
button: 14,
|
|
65
|
+
nav: 0,
|
|
66
|
+
modal: 44,
|
|
67
|
+
},
|
|
68
|
+
space: {
|
|
69
|
+
xs: 4,
|
|
70
|
+
sm: 8,
|
|
71
|
+
md: 16,
|
|
72
|
+
lg: 24,
|
|
73
|
+
xl: 32,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -36,3 +36,40 @@ export const materialYou: Theme = {
|
|
|
36
36
|
xl: 32,
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
|
+
|
|
40
|
+
export const materialYouDark: Theme = {
|
|
41
|
+
surface: {
|
|
42
|
+
primary: '#1C1B1F',
|
|
43
|
+
secondary: '#2B2930',
|
|
44
|
+
card: '#211F26',
|
|
45
|
+
nav: '#1C1B1F',
|
|
46
|
+
},
|
|
47
|
+
text: {
|
|
48
|
+
primary: '#E6E1E5',
|
|
49
|
+
secondary: '#CAC4D0',
|
|
50
|
+
tertiary: '#938F99',
|
|
51
|
+
destructive: '#F2B8B5',
|
|
52
|
+
},
|
|
53
|
+
blur: {
|
|
54
|
+
nav: 0,
|
|
55
|
+
card: 0,
|
|
56
|
+
modal: 0,
|
|
57
|
+
},
|
|
58
|
+
border: {
|
|
59
|
+
default: '#49454F',
|
|
60
|
+
strong: '#938F99',
|
|
61
|
+
},
|
|
62
|
+
radius: {
|
|
63
|
+
card: 12,
|
|
64
|
+
button: 20,
|
|
65
|
+
nav: 0,
|
|
66
|
+
modal: 28,
|
|
67
|
+
},
|
|
68
|
+
space: {
|
|
69
|
+
xs: 4,
|
|
70
|
+
sm: 8,
|
|
71
|
+
md: 16,
|
|
72
|
+
lg: 24,
|
|
73
|
+
xl: 32,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ThemeName = 'liquidGlass' | 'materialYou';
|
|
1
|
+
export type ThemeName = 'liquidGlass' | 'materialYou' | 'base';
|
|
2
2
|
|
|
3
3
|
export type Theme = {
|
|
4
4
|
surface: {
|
|
@@ -49,6 +49,8 @@ export type ThemeOverrides = Partial<{
|
|
|
49
49
|
export type ProtoConfig = {
|
|
50
50
|
name?: string;
|
|
51
51
|
theme?: ThemeName;
|
|
52
|
+
// 'system' (default) follows the device; 'light' / 'dark' pin the scheme.
|
|
53
|
+
colorScheme?: 'system' | 'light' | 'dark';
|
|
52
54
|
accentColor?: string;
|
|
53
55
|
tokens?: ThemeOverrides;
|
|
54
56
|
screens?: { initial?: string };
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
+
import { useColorScheme } from 'react-native';
|
|
1
2
|
import config from '../../proto.config.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { base, baseDark } from './tokens/base';
|
|
4
|
+
import { liquidGlass, liquidGlassDark } from './tokens/liquidGlass';
|
|
5
|
+
import { materialYou, materialYouDark } from './tokens/materialYou';
|
|
4
6
|
import type { ProtoConfig, Theme, ThemeName, ThemeOverrides } from './types';
|
|
5
7
|
|
|
6
|
-
const
|
|
8
|
+
const lightThemes: Record<ThemeName, Theme> = {
|
|
7
9
|
liquidGlass,
|
|
8
10
|
materialYou,
|
|
11
|
+
base,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const darkThemes: Record<ThemeName, Theme> = {
|
|
15
|
+
liquidGlass: liquidGlassDark,
|
|
16
|
+
materialYou: materialYouDark,
|
|
17
|
+
base: baseDark,
|
|
9
18
|
};
|
|
10
19
|
|
|
11
20
|
function mergeTheme(base: Theme, overrides?: ThemeOverrides): Theme {
|
|
@@ -22,14 +31,21 @@ function mergeTheme(base: Theme, overrides?: ThemeOverrides): Theme {
|
|
|
22
31
|
|
|
23
32
|
const cfg = config as ProtoConfig;
|
|
24
33
|
|
|
25
|
-
//
|
|
34
|
+
// A hook — it reads the system colour scheme so screens re-render when the device
|
|
35
|
+
// switches between light and dark. Call it during render, like any hook.
|
|
36
|
+
// `proto.config.js` can pin a scheme with `colorScheme: 'light' | 'dark'`; the
|
|
37
|
+
// default ('system') follows the device.
|
|
26
38
|
export function useTheme(): Theme {
|
|
39
|
+
const systemScheme = useColorScheme();
|
|
40
|
+
const preference = cfg.colorScheme ?? 'system';
|
|
41
|
+
const isDark = preference === 'dark' || (preference === 'system' && systemScheme === 'dark');
|
|
42
|
+
|
|
27
43
|
const name: ThemeName = cfg.theme ?? 'liquidGlass';
|
|
28
|
-
const
|
|
44
|
+
const set = isDark ? darkThemes : lightThemes;
|
|
45
|
+
const base = set[name] ?? set.liquidGlass;
|
|
29
46
|
return mergeTheme(base, cfg.tokens);
|
|
30
47
|
}
|
|
31
48
|
|
|
32
|
-
// Named `use*` for ergonomics but reads module-level config; safe to call outside React.
|
|
33
49
|
export function useAccent(): string {
|
|
34
50
|
return cfg.accentColor ?? '#007AFF';
|
|
35
51
|
}
|
package/template/metro.config.js
CHANGED
|
@@ -2,4 +2,10 @@ const { getDefaultConfig } = require('expo/metro-config');
|
|
|
2
2
|
|
|
3
3
|
const config = getDefaultConfig(__dirname);
|
|
4
4
|
|
|
5
|
+
// Let screens import .svg files directly (import Logo from './logo.svg').
|
|
6
|
+
// Managed by Proto — don't edit.
|
|
7
|
+
config.transformer.babelTransformerPath = require.resolve('react-native-svg-transformer/expo');
|
|
8
|
+
config.resolver.assetExts = config.resolver.assetExts.filter((ext) => ext !== 'svg');
|
|
9
|
+
config.resolver.sourceExts = [...config.resolver.sourceExts, 'svg'];
|
|
10
|
+
|
|
5
11
|
module.exports = config;
|
package/template/package.json
CHANGED
|
@@ -10,30 +10,45 @@
|
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@expo/ui": "55.0.17",
|
|
13
|
+
"@react-native-async-storage/async-storage": "2.2.0",
|
|
14
|
+
"@shopify/flash-list": "2.0.2",
|
|
13
15
|
"@shopify/react-native-skia": "2.4.18",
|
|
14
16
|
"expo": "~55.0.26",
|
|
17
|
+
"expo-audio": "~55.0.14",
|
|
18
|
+
"expo-camera": "~55.0.18",
|
|
15
19
|
"expo-clipboard": "~55.0.13",
|
|
16
20
|
"expo-dev-client": "~55.0.35",
|
|
21
|
+
"expo-font": "~55.0.8",
|
|
17
22
|
"expo-glass-effect": "~55.0.11",
|
|
18
23
|
"expo-haptics": "~55.0.14",
|
|
24
|
+
"expo-image": "~55.0.11",
|
|
25
|
+
"expo-image-picker": "~55.0.20",
|
|
26
|
+
"expo-linear-gradient": "~55.0.14",
|
|
27
|
+
"expo-location": "~55.1.10",
|
|
28
|
+
"expo-maps": "~55.0.19",
|
|
19
29
|
"expo-router": "~55.0.16",
|
|
20
30
|
"expo-status-bar": "~55.0.6",
|
|
21
31
|
"expo-symbols": "~55.0.9",
|
|
32
|
+
"expo-video": "~55.0.17",
|
|
22
33
|
"lottie-react-native": "~7.3.4",
|
|
23
34
|
"react": "19.2.0",
|
|
35
|
+
"react-dom": "19.2.0",
|
|
24
36
|
"react-native": "0.83.6",
|
|
25
37
|
"react-native-ease": "^0.7.2",
|
|
26
38
|
"react-native-gesture-handler": "~2.30.1",
|
|
27
39
|
"react-native-reanimated": "4.2.1",
|
|
28
40
|
"react-native-safe-area-context": "5.6.2",
|
|
29
41
|
"react-native-screens": "4.23.0",
|
|
42
|
+
"react-native-svg": "15.15.3",
|
|
43
|
+
"react-native-webview": "13.16.0",
|
|
30
44
|
"react-native-worklets": "0.7.4"
|
|
31
45
|
},
|
|
32
46
|
"devDependencies": {
|
|
33
47
|
"@babel/core": "^7.24.0",
|
|
34
|
-
"@sherizan/proto-cli": "^0.
|
|
48
|
+
"@sherizan/proto-cli": "^0.5.0",
|
|
35
49
|
"@types/react": "~19.2.15",
|
|
36
50
|
"babel-preset-expo": "~55.0.22",
|
|
51
|
+
"react-native-svg-transformer": "^1.5.0",
|
|
37
52
|
"typescript": "^5.9.2"
|
|
38
53
|
}
|
|
39
54
|
}
|
|
@@ -2,12 +2,13 @@ import { useEffect, useState } from 'react';
|
|
|
2
2
|
import { Pressable } from 'react-native';
|
|
3
3
|
import * as Clipboard from 'expo-clipboard';
|
|
4
4
|
import * as Haptics from 'expo-haptics';
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
Animated,
|
|
6
7
|
useSharedValue,
|
|
7
8
|
useAnimatedStyle,
|
|
8
9
|
withTiming,
|
|
9
10
|
Easing,
|
|
10
|
-
} from '
|
|
11
|
+
} from '../components/proto/gestures';
|
|
11
12
|
import { Screen, Stack, Row, Text, Card, Divider } from '../components/proto';
|
|
12
13
|
|
|
13
14
|
const STEPS = [
|