create-proto 0.7.3 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-proto",
3
- "version": "0.7.3",
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",
@@ -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 two tools that close the feedback loop, so you can see what you built instead of asking the designer to relay it.
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({ label, variant = 'primary', onPress }: ButtonProps) {
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
- <Text size="label" style={{ color: fg }}>
56
- {label}
57
- </Text>
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
  );
@@ -44,8 +44,6 @@ export function Card({ glass = false, padding, children }: CardProps) {
44
44
  style={{
45
45
  backgroundColor: theme.surface.card,
46
46
  borderRadius: theme.radius.card,
47
- borderWidth: 1,
48
- borderColor: theme.border.default,
49
47
  padding: pad,
50
48
  }}
51
49
  >
@@ -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 function Divider() {
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
- style={{
9
- height: 1,
10
- width: '100%',
11
- backgroundColor: theme.border.default,
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
- <View style={{ flex: 1, padding, gap: padding }}>{children}</View>
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 { Divider } from './Divider';
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';
@@ -4,7 +4,7 @@ export const liquidGlass: Theme = {
4
4
  surface: {
5
5
  primary: 'rgba(255, 255, 255, 0.72)',
6
6
  secondary: 'rgba(255, 255, 255, 0.48)',
7
- card: '#f3f5f8',
7
+ card: '#F2F2F7',
8
8
  nav: 'rgba(255, 255, 255, 0.82)',
9
9
  },
10
10
  text: {