create-proto 0.7.2 → 0.7.4
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 +1 -1
- package/template/.proto/expo-config/app.json +1 -1
- package/template/CLAUDE.md +7 -3
- package/template/components/proto/Button.tsx +26 -7
- package/template/components/proto/Card.tsx +0 -2
- package/template/components/proto/Divider.tsx +21 -8
- package/template/components/proto/Input.tsx +27 -0
- package/template/components/proto/Lottie.tsx +12 -0
- package/template/components/proto/Screen.tsx +8 -3
- package/template/components/proto/Slider.tsx +55 -0
- package/template/components/proto/Stepper.tsx +52 -0
- package/template/components/proto/index.ts +5 -1
- package/template/components/proto/tokens/liquidGlass.ts +1 -1
- package/template/package.json +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-proto",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Scaffold a new Proto prototype: `npm create proto@latest myapp`. Describe a screen, watch your prototype run natively on iPhone — designer-first, paired with Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/template/CLAUDE.md
CHANGED
|
@@ -16,7 +16,7 @@ You're the design tool inside a Prototo project. The designer prompts you in pla
|
|
|
16
16
|
- `@expo/ui/swift-ui` — `Button`, `Toggle`, `Form`, `Section`, etc.
|
|
17
17
|
- `expo-glass-effect` `GlassView` — Liquid Glass surfaces
|
|
18
18
|
|
|
19
|
-
**Prototo primitives** in `/components/proto` — small set of themed fallbacks: `Screen`, `Stack`, `Row`, `Text`, `Card`, `Button`, `Toggle`, `Divider`, `Modal`. Read the file when you need the API. Card's `glass={true}` uses `expo-glass-effect`'s native iOS 26 material; on older iOS it falls back to a plain View.
|
|
19
|
+
**Prototo primitives** in `/components/proto` — small set of themed fallbacks: `Screen`, `Stack`, `Row`, `Text`, `Card`, `Button`, `Toggle`, `Slider`, `Stepper`, `Divider`, `Modal`. Read the file when you need the API. `Toggle`, `Slider`, and `Stepper` render real native SwiftUI (`@expo/ui`) on iOS, tinted with the app accent. Card's `glass={true}` uses `expo-glass-effect`'s native iOS 26 material; on older iOS it falls back to a plain View.
|
|
20
20
|
|
|
21
21
|
**Prototo motion + graphics** — four subpath modules in `/components/proto` cover animation and drawing. Pick by what the prompt actually asks for:
|
|
22
22
|
|
|
@@ -78,14 +78,18 @@ Do this especially after layout, color, typography, or spacing changes. If `prot
|
|
|
78
78
|
|
|
79
79
|
## Proto MCP tools
|
|
80
80
|
|
|
81
|
-
When the designer runs `proto start`, a local MCP server (`prototo`) connects automatically — no setup. It gives you
|
|
81
|
+
When the designer runs `proto start`, a local MCP server (`prototo`) connects automatically — no setup. It gives you three tools that close the feedback loop, so you can see what you built instead of asking the designer to relay it.
|
|
82
|
+
|
|
83
|
+
**At the start of any fix session** (the designer says something is broken, red, or not working):
|
|
84
|
+
|
|
85
|
+
1. Call `get_metro_errors` first — it returns the current build failures and runtime crashes from the running prototype, with the raw error text and the screen involved. Don't ask the designer to paste or describe an error before checking it.
|
|
82
86
|
|
|
83
87
|
**After every screen write:**
|
|
84
88
|
|
|
85
89
|
1. Call `compile_check` with the screen name — it type-checks the project and reports any problems in plain language. Fix anything it surfaces before moving on.
|
|
86
90
|
2. Call `get_simulator_screenshot` — it returns what the prototype actually renders right now. Inspect it for the same defects as above.
|
|
87
91
|
|
|
88
|
-
Never assume a screen rendered correctly — check the screenshot. Never ask the designer to describe an error you can catch with `compile_check`. If a tool says the Simulator isn't running, the designer needs to run `proto start` first.
|
|
92
|
+
Never assume a screen rendered correctly — check the screenshot. Never ask the designer to describe an error you can catch with `get_metro_errors` or `compile_check`. If a tool says the Simulator isn't running (or Metro reports clean while the designer still sees an error), the designer needs to run `proto start` first.
|
|
89
93
|
|
|
90
94
|
(`get_simulator_screenshot` is the automated form of the `proto shot` loop above — prefer the tool when it's available.)
|
|
91
95
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Pressable, type ViewStyle } from 'react-native';
|
|
1
|
+
import { Pressable, View, type TextStyle, type ViewStyle } from 'react-native';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
2
3
|
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated';
|
|
3
4
|
import * as Haptics from 'expo-haptics';
|
|
4
5
|
import { useTheme, useAccent } from './useTheme';
|
|
@@ -10,9 +11,21 @@ export type ButtonProps = {
|
|
|
10
11
|
label: string;
|
|
11
12
|
variant?: ButtonVariant;
|
|
12
13
|
onPress?: () => void;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
icon?: ReactNode;
|
|
16
|
+
style?: ViewStyle;
|
|
17
|
+
textStyle?: TextStyle;
|
|
13
18
|
};
|
|
14
19
|
|
|
15
|
-
export function Button({
|
|
20
|
+
export function Button({
|
|
21
|
+
label,
|
|
22
|
+
variant = 'primary',
|
|
23
|
+
onPress,
|
|
24
|
+
disabled = false,
|
|
25
|
+
icon,
|
|
26
|
+
style,
|
|
27
|
+
textStyle,
|
|
28
|
+
}: ButtonProps) {
|
|
16
29
|
const theme = useTheme();
|
|
17
30
|
const accent = useAccent();
|
|
18
31
|
const scale = useSharedValue(1);
|
|
@@ -22,9 +35,11 @@ export function Button({ label, variant = 'primary', onPress }: ButtonProps) {
|
|
|
22
35
|
}));
|
|
23
36
|
|
|
24
37
|
const handlePressIn = () => {
|
|
38
|
+
if (disabled) return;
|
|
25
39
|
scale.value = withTiming(0.96, { duration: 80 });
|
|
26
40
|
};
|
|
27
41
|
const handlePressOut = () => {
|
|
42
|
+
if (disabled) return;
|
|
28
43
|
scale.value = withTiming(1, { duration: 120 });
|
|
29
44
|
};
|
|
30
45
|
const handlePress = () => {
|
|
@@ -47,14 +62,18 @@ export function Button({ label, variant = 'primary', onPress }: ButtonProps) {
|
|
|
47
62
|
paddingHorizontal: theme.space.md,
|
|
48
63
|
alignItems: 'center',
|
|
49
64
|
justifyContent: 'center',
|
|
65
|
+
opacity: disabled ? 0.5 : 1,
|
|
50
66
|
};
|
|
51
67
|
|
|
52
68
|
return (
|
|
53
|
-
<Pressable onPressIn={handlePressIn} onPressOut={handlePressOut} onPress={handlePress}>
|
|
54
|
-
<Animated.View style={[baseStyle, animated]}>
|
|
55
|
-
<
|
|
56
|
-
{
|
|
57
|
-
|
|
69
|
+
<Pressable disabled={disabled} onPressIn={handlePressIn} onPressOut={handlePressOut} onPress={handlePress}>
|
|
70
|
+
<Animated.View style={[baseStyle, animated, style]}>
|
|
71
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: theme.space.sm }}>
|
|
72
|
+
{icon}
|
|
73
|
+
<Text size="label" style={[{ color: fg }, textStyle]}>
|
|
74
|
+
{label}
|
|
75
|
+
</Text>
|
|
76
|
+
</View>
|
|
58
77
|
</Animated.View>
|
|
59
78
|
</Pressable>
|
|
60
79
|
);
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { View } from 'react-native';
|
|
2
2
|
import { useTheme } from './useTheme';
|
|
3
|
+
import { Text } from './Text';
|
|
3
4
|
|
|
4
|
-
export
|
|
5
|
+
export type DividerProps = {
|
|
6
|
+
label?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function Divider({ label }: DividerProps) {
|
|
5
10
|
const theme = useTheme();
|
|
11
|
+
|
|
12
|
+
if (!label) {
|
|
13
|
+
return (
|
|
14
|
+
<View style={{ height: 1, width: '100%', backgroundColor: theme.border.default }} />
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const line = <View style={{ flex: 1, height: 1, backgroundColor: theme.border.default }} />;
|
|
6
19
|
return (
|
|
7
|
-
<View
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
20
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: theme.space.sm }}>
|
|
21
|
+
{line}
|
|
22
|
+
<Text size="caption" color="secondary">
|
|
23
|
+
{label}
|
|
24
|
+
</Text>
|
|
25
|
+
{line}
|
|
26
|
+
</View>
|
|
14
27
|
);
|
|
15
28
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { TextInput, type TextInputProps } from 'react-native';
|
|
2
|
+
import { useTheme } from './useTheme';
|
|
3
|
+
|
|
4
|
+
export type InputProps = TextInputProps;
|
|
5
|
+
|
|
6
|
+
export function Input({ style, ...props }: InputProps) {
|
|
7
|
+
const theme = useTheme();
|
|
8
|
+
return (
|
|
9
|
+
<TextInput
|
|
10
|
+
placeholderTextColor={theme.text.tertiary}
|
|
11
|
+
style={[
|
|
12
|
+
{
|
|
13
|
+
backgroundColor: theme.surface.secondary,
|
|
14
|
+
color: theme.text.primary,
|
|
15
|
+
borderColor: theme.border.default,
|
|
16
|
+
borderWidth: 1,
|
|
17
|
+
borderRadius: theme.radius.button,
|
|
18
|
+
paddingHorizontal: theme.space.md,
|
|
19
|
+
height: 52,
|
|
20
|
+
fontSize: 17,
|
|
21
|
+
},
|
|
22
|
+
style,
|
|
23
|
+
]}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import LottieView, { type LottieViewProps } from 'lottie-react-native';
|
|
2
|
+
|
|
3
|
+
export type LottieProps = {
|
|
4
|
+
source: LottieViewProps['source'];
|
|
5
|
+
autoPlay?: boolean;
|
|
6
|
+
loop?: boolean;
|
|
7
|
+
style?: LottieViewProps['style'];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function Lottie({ source, autoPlay = true, loop = true, style }: LottieProps) {
|
|
11
|
+
return <LottieView source={source} autoPlay={autoPlay} loop={loop} style={style} />;
|
|
12
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ScrollView, View } from 'react-native';
|
|
1
|
+
import { KeyboardAvoidingView, Platform, ScrollView, View } from 'react-native';
|
|
2
2
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
3
3
|
import type { ReactNode } from 'react';
|
|
4
4
|
import { useTheme } from './useTheme';
|
|
@@ -41,9 +41,14 @@ export function Screen({ scrollable = true, children }: ScreenProps) {
|
|
|
41
41
|
return (
|
|
42
42
|
<SafeAreaView
|
|
43
43
|
style={{ flex: 1, backgroundColor: theme.surface.primary }}
|
|
44
|
-
edges={['bottom', 'left', 'right']}
|
|
44
|
+
edges={['top', 'bottom', 'left', 'right']}
|
|
45
45
|
>
|
|
46
|
-
<
|
|
46
|
+
<KeyboardAvoidingView
|
|
47
|
+
style={{ flex: 1 }}
|
|
48
|
+
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
49
|
+
>
|
|
50
|
+
<View style={{ flex: 1, padding, gap: padding }}>{children}</View>
|
|
51
|
+
</KeyboardAvoidingView>
|
|
47
52
|
</SafeAreaView>
|
|
48
53
|
);
|
|
49
54
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Platform, View } from 'react-native';
|
|
2
|
+
import { Host, Slider as SwiftUISlider } from '@expo/ui/swift-ui';
|
|
3
|
+
import { tint } from '@expo/ui/swift-ui/modifiers';
|
|
4
|
+
import { useAccent, useTheme } from './useTheme';
|
|
5
|
+
import { Text } from './Text';
|
|
6
|
+
|
|
7
|
+
export type SliderProps = {
|
|
8
|
+
value: number;
|
|
9
|
+
onChange?: (value: number) => void;
|
|
10
|
+
min?: number;
|
|
11
|
+
max?: number;
|
|
12
|
+
step?: number;
|
|
13
|
+
label?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function Slider({ value, onChange, min = 0, max = 1, step, label }: SliderProps) {
|
|
17
|
+
const theme = useTheme();
|
|
18
|
+
const accent = useAccent();
|
|
19
|
+
|
|
20
|
+
const control =
|
|
21
|
+
Platform.OS === 'ios' ? (
|
|
22
|
+
<Host style={{ width: '100%', height: 44 }}>
|
|
23
|
+
<SwiftUISlider
|
|
24
|
+
value={value}
|
|
25
|
+
min={min}
|
|
26
|
+
max={max}
|
|
27
|
+
step={step}
|
|
28
|
+
onValueChange={(next) => onChange?.(next)}
|
|
29
|
+
modifiers={[tint(accent)]}
|
|
30
|
+
/>
|
|
31
|
+
</Host>
|
|
32
|
+
) : (
|
|
33
|
+
// ponytail: iOS-first product; Android renders a read-only track (no core RN slider, and
|
|
34
|
+
// adding @react-native-community/slider for a path that never ships isn't worth it).
|
|
35
|
+
<View style={{ height: 4, borderRadius: 2, backgroundColor: theme.border.default }}>
|
|
36
|
+
<View
|
|
37
|
+
style={{
|
|
38
|
+
height: 4,
|
|
39
|
+
borderRadius: 2,
|
|
40
|
+
backgroundColor: accent,
|
|
41
|
+
width: `${((value - min) / (max - min)) * 100}%`,
|
|
42
|
+
}}
|
|
43
|
+
/>
|
|
44
|
+
</View>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (!label) return control;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<View style={{ gap: theme.space.xs }}>
|
|
51
|
+
<Text size="label">{label}</Text>
|
|
52
|
+
{control}
|
|
53
|
+
</View>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Platform, Pressable, View } from 'react-native';
|
|
2
|
+
import { Host, Stepper as SwiftUIStepper } from '@expo/ui/swift-ui';
|
|
3
|
+
import { useTheme } from './useTheme';
|
|
4
|
+
import { Text } from './Text';
|
|
5
|
+
|
|
6
|
+
export type StepperProps = {
|
|
7
|
+
label: string;
|
|
8
|
+
value: number;
|
|
9
|
+
onChange: (value: number) => void;
|
|
10
|
+
min?: number;
|
|
11
|
+
max?: number;
|
|
12
|
+
step?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function Stepper({ label, value, onChange, min, max, step = 1 }: StepperProps) {
|
|
16
|
+
const theme = useTheme();
|
|
17
|
+
|
|
18
|
+
if (Platform.OS === 'ios') {
|
|
19
|
+
return (
|
|
20
|
+
<Host matchContents>
|
|
21
|
+
<SwiftUIStepper
|
|
22
|
+
label={label}
|
|
23
|
+
value={value}
|
|
24
|
+
min={min}
|
|
25
|
+
max={max}
|
|
26
|
+
step={step}
|
|
27
|
+
onValueChange={(next) => onChange(next)}
|
|
28
|
+
/>
|
|
29
|
+
</Host>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ponytail: iOS-first product; minimal functional RN fallback for Android (no core RN stepper).
|
|
34
|
+
const clamp = (n: number) => {
|
|
35
|
+
if (min != null && n < min) return min;
|
|
36
|
+
if (max != null && n > max) return max;
|
|
37
|
+
return n;
|
|
38
|
+
};
|
|
39
|
+
return (
|
|
40
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
41
|
+
<Text size="body">{label}</Text>
|
|
42
|
+
<View style={{ flexDirection: 'row', gap: theme.space.md }}>
|
|
43
|
+
<Pressable onPress={() => onChange(clamp(value - step))}>
|
|
44
|
+
<Text size="headline">−</Text>
|
|
45
|
+
</Pressable>
|
|
46
|
+
<Pressable onPress={() => onChange(clamp(value + step))}>
|
|
47
|
+
<Text size="headline">+</Text>
|
|
48
|
+
</Pressable>
|
|
49
|
+
</View>
|
|
50
|
+
</View>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -5,7 +5,11 @@ export { Text, type TextProps, type TextSize, type TextColor } from './Text';
|
|
|
5
5
|
export { Card, type CardProps } from './Card';
|
|
6
6
|
export { Button, type ButtonProps, type ButtonVariant } from './Button';
|
|
7
7
|
export { Toggle, type ToggleProps } from './Toggle';
|
|
8
|
-
export {
|
|
8
|
+
export { Slider, type SliderProps } from './Slider';
|
|
9
|
+
export { Stepper, type StepperProps } from './Stepper';
|
|
10
|
+
export { Divider, type DividerProps } from './Divider';
|
|
11
|
+
export { Input, type InputProps } from './Input';
|
|
12
|
+
export { Lottie, type LottieProps } from './Lottie';
|
|
9
13
|
export { Modal, type ModalProps } from './Modal';
|
|
10
14
|
export { useTheme, useAccent } from './useTheme';
|
|
11
15
|
export { ProtoConfigProvider, useProtoConfig } from './ProtoConfigContext';
|