create-proto 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.
- package/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +104 -0
- package/dist/cli.js.map +1 -0
- package/dist/copy-template.d.ts +8 -0
- package/dist/copy-template.d.ts.map +1 -0
- package/dist/copy-template.js +46 -0
- package/dist/copy-template.js.map +1 -0
- package/dist/detect-pm.d.ts +3 -0
- package/dist/detect-pm.d.ts.map +1 -0
- package/dist/detect-pm.js +12 -0
- package/dist/detect-pm.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/install-deps.d.ts +16 -0
- package/dist/install-deps.d.ts.map +1 -0
- package/dist/install-deps.js +37 -0
- package/dist/install-deps.js.map +1 -0
- package/dist/messages.d.ts +17 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +16 -0
- package/dist/messages.js.map +1 -0
- package/dist/render-qr.d.ts +2 -0
- package/dist/render-qr.d.ts.map +1 -0
- package/dist/render-qr.js +9 -0
- package/dist/render-qr.js.map +1 -0
- package/dist/validate-name.d.ts +9 -0
- package/dist/validate-name.d.ts.map +1 -0
- package/dist/validate-name.js +33 -0
- package/dist/validate-name.js.map +1 -0
- package/package.json +64 -0
- package/template/.proto/app/(proto)/[...screen].tsx +24 -0
- package/template/.proto/app/_layout.tsx +5 -0
- package/template/.proto/expo-config/app.json +17 -0
- package/template/CLAUDE.md +71 -0
- package/template/DESIGN.md +53 -0
- package/template/README.md +6 -0
- package/template/app/index.tsx +5 -0
- package/template/app.config.js +1 -0
- package/template/assets/.gitkeep +0 -0
- package/template/assets/icon.png +0 -0
- package/template/assets/splash.png +0 -0
- package/template/babel.config.js +7 -0
- package/template/components/proto/.gitkeep +0 -0
- package/template/components/proto/Button.tsx +61 -0
- package/template/components/proto/Card.tsx +63 -0
- package/template/components/proto/Divider.tsx +15 -0
- package/template/components/proto/Modal.tsx +43 -0
- package/template/components/proto/Row.tsx +28 -0
- package/template/components/proto/Screen.tsx +38 -0
- package/template/components/proto/Stack.tsx +17 -0
- package/template/components/proto/Text.tsx +37 -0
- package/template/components/proto/Toggle.tsx +40 -0
- package/template/components/proto/index.ts +11 -0
- package/template/components/proto/tokens/liquidGlass.ts +38 -0
- package/template/components/proto/tokens/materialYou.ts +38 -0
- package/template/components/proto/types.ts +55 -0
- package/template/components/proto/useTheme.ts +35 -0
- package/template/components/shared/.gitkeep +0 -0
- package/template/metro.config.js +5 -0
- package/template/package.json +35 -0
- package/template/pnpm-workspace.yaml +1 -0
- package/template/proto.config.js +6 -0
- package/template/screens/.gitkeep +0 -0
- package/template/screens/Home.tsx +60 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Proto Project — Claude Code Instructions
|
|
2
|
+
|
|
3
|
+
You are the design tool inside a Proto project. The iOS Simulator is the canvas. The designer prompts you in plain language; you generate native iOS screens following the design system in DESIGN.md. Designers never touch files.
|
|
4
|
+
|
|
5
|
+
## Three rules
|
|
6
|
+
|
|
7
|
+
**1. Native iOS first.** Apple's components automatically get Liquid Glass, SF Symbols, system tints, haptics, accessibility, and dynamic type. Always prefer them when they exist:
|
|
8
|
+
|
|
9
|
+
| Need | Use |
|
|
10
|
+
|---|---|
|
|
11
|
+
| Tab bar | `expo-router/unstable-native-tabs` (`NativeTabs` + `Icon sf={{default, selected}}` in `app/_layout.tsx`) |
|
|
12
|
+
| SF Symbol icon | `expo-symbols` `SymbolView` |
|
|
13
|
+
| System button | `@expo/ui/swift-ui` `Button` |
|
|
14
|
+
| System toggle | `@expo/ui/swift-ui` `Toggle` |
|
|
15
|
+
| Form / settings list | `@expo/ui/swift-ui` `Form` + `Section` |
|
|
16
|
+
| Liquid Glass surface | `expo-glass-effect` `GlassView` directly |
|
|
17
|
+
|
|
18
|
+
Never wrap a native component just to add Proto branding. Native > custom.
|
|
19
|
+
|
|
20
|
+
**2. Proto primitives are fallbacks for what native doesn't ship.** Use `/components/proto` for layout helpers, generic surfaces, themed text — never to rebuild Apple-native UI.
|
|
21
|
+
|
|
22
|
+
**3. DESIGN.md is the only source of design tokens.** Read it before every change. Never hardcode colour, spacing, radius, or typography. If the designer asks to change tokens, update DESIGN.md.
|
|
23
|
+
|
|
24
|
+
If DESIGN.md's "Component Library" section names a third-party library (Tamagui, Gluestack, etc.), use that library's components first, Proto primitives as fallback. Otherwise use Proto primitives only.
|
|
25
|
+
|
|
26
|
+
## File layout
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
/app/<route>.tsx thin re-export of a screen (one-line wrapper)
|
|
30
|
+
/app/_layout.tsx NativeTabs root layout (when app has tabs)
|
|
31
|
+
/screens/<Name>.tsx actual screen component (PascalCase, default export)
|
|
32
|
+
/components/shared/ designer-created shared components
|
|
33
|
+
/components/proto/ Proto primitives — read-only, do not edit
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For a new screen `screens/Settings.tsx`, add a matching `app/settings.tsx`:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import Settings from '../screens/Settings';
|
|
40
|
+
export default function SettingsRoute() { return <Settings />; }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Route filenames are lowercase kebab-case. Then add a one-line description to the Screens section of DESIGN.md.
|
|
44
|
+
|
|
45
|
+
## Proto primitives (`/components/proto`)
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Screen title?, scrollable? SafeAreaView + ScrollView wrapper
|
|
49
|
+
Stack gap?, padding? vertical flex
|
|
50
|
+
Row gap?, align? horizontal flex
|
|
51
|
+
Text size, color, style? themed RN Text (sizes: title/headline/body/caption/label)
|
|
52
|
+
Card glass?, padding? surface; glass={true} = real Liquid Glass on iOS 26+
|
|
53
|
+
Button label, variant?, onPress custom animated button — for iOS system style use @expo/ui Button
|
|
54
|
+
Toggle label, value, onChange themed RN Switch — for iOS system look use @expo/ui Toggle
|
|
55
|
+
Divider — 1px separator
|
|
56
|
+
Modal title?, visible, ... RN Modal wrapper
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## When modifying a screen
|
|
60
|
+
|
|
61
|
+
Read the file first, then rewrite the full file. Never partial edits or diffs.
|
|
62
|
+
|
|
63
|
+
## Never
|
|
64
|
+
|
|
65
|
+
- Never import from `react-native` directly — use a library, native iOS, or Proto fallback
|
|
66
|
+
- Never build a custom tab bar — always `expo-router/unstable-native-tabs`
|
|
67
|
+
- Never put logic in `/app/` files — they re-export a screen, nothing else
|
|
68
|
+
- Never use SF Symbol private-use Unicode codepoints (like `''`) as text — they don't render in plain Text. Use `expo-symbols` `SymbolView` or native components that take SF symbol names
|
|
69
|
+
- Never edit `/components/proto/`, `.proto/`, `app.config.js`, `babel.config.js`, `metro.config.js`
|
|
70
|
+
- Never add a build step, dependency, or visual editor
|
|
71
|
+
- Never tell the designer to open or edit a file manually
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# DESIGN.md
|
|
2
|
+
> Source of truth for {{APP_NAME}}'s design system.
|
|
3
|
+
> Update by prompting Claude Code: "update DESIGN.md, [what to change]"
|
|
4
|
+
> Last updated: {{DATE}}
|
|
5
|
+
|
|
6
|
+
## App
|
|
7
|
+
- Name: {{APP_NAME}}
|
|
8
|
+
- Theme: liquidGlass
|
|
9
|
+
- Platform: iOS
|
|
10
|
+
|
|
11
|
+
## Component Library
|
|
12
|
+
- Package: proto (built-in)
|
|
13
|
+
- Import from: ../components/proto
|
|
14
|
+
- Fallback: proto
|
|
15
|
+
|
|
16
|
+
## Colour
|
|
17
|
+
- Accent: #007AFF
|
|
18
|
+
- Surface primary: rgba(255,255,255,0.72)
|
|
19
|
+
- Surface secondary: rgba(255,255,255,0.48)
|
|
20
|
+
- Surface card: rgba(255,255,255,0.60)
|
|
21
|
+
- Surface nav: rgba(255,255,255,0.82)
|
|
22
|
+
- Text primary: #000000
|
|
23
|
+
- Text secondary: rgba(0,0,0,0.5)
|
|
24
|
+
- Text tertiary: rgba(0,0,0,0.3)
|
|
25
|
+
- Destructive: #FF3B30
|
|
26
|
+
|
|
27
|
+
## Typography
|
|
28
|
+
- Title: 34px / bold / tracking -0.4
|
|
29
|
+
- Headline: 22px / semibold / tracking -0.4
|
|
30
|
+
- Body: 17px / regular
|
|
31
|
+
- Caption: 12px / regular / text-secondary
|
|
32
|
+
- Label: 13px / medium
|
|
33
|
+
|
|
34
|
+
## Spacing
|
|
35
|
+
- xs: 4 / sm: 8 / md: 16 / lg: 24 / xl: 32
|
|
36
|
+
|
|
37
|
+
## Shape
|
|
38
|
+
- Card radius: 22
|
|
39
|
+
- Button radius: 14
|
|
40
|
+
- Modal radius: 44
|
|
41
|
+
- Input radius: 12
|
|
42
|
+
|
|
43
|
+
## Effects
|
|
44
|
+
- Card blur: 20
|
|
45
|
+
- Nav blur: 40
|
|
46
|
+
- Modal blur: 60
|
|
47
|
+
- Border: rgba(255,255,255,0.4)
|
|
48
|
+
|
|
49
|
+
## Components in use
|
|
50
|
+
- Screen, Stack, Row, Text, Card, Button, Toggle, Nav, Modal, Divider
|
|
51
|
+
|
|
52
|
+
## Screens
|
|
53
|
+
- Home (initial) — starter screen
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./.proto/expo-config/app.json');
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Pressable, type ViewStyle } from 'react-native';
|
|
2
|
+
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated';
|
|
3
|
+
import * as Haptics from 'expo-haptics';
|
|
4
|
+
import { useTheme, useAccent } from './useTheme';
|
|
5
|
+
import { Text } from './Text';
|
|
6
|
+
|
|
7
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'destructive';
|
|
8
|
+
|
|
9
|
+
export type ButtonProps = {
|
|
10
|
+
label: string;
|
|
11
|
+
variant?: ButtonVariant;
|
|
12
|
+
onPress?: () => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function Button({ label, variant = 'primary', onPress }: ButtonProps) {
|
|
16
|
+
const theme = useTheme();
|
|
17
|
+
const accent = useAccent();
|
|
18
|
+
const scale = useSharedValue(1);
|
|
19
|
+
|
|
20
|
+
const animated = useAnimatedStyle(() => ({
|
|
21
|
+
transform: [{ scale: scale.value }],
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const handlePressIn = () => {
|
|
25
|
+
scale.value = withTiming(0.96, { duration: 80 });
|
|
26
|
+
};
|
|
27
|
+
const handlePressOut = () => {
|
|
28
|
+
scale.value = withTiming(1, { duration: 120 });
|
|
29
|
+
};
|
|
30
|
+
const handlePress = () => {
|
|
31
|
+
Haptics.selectionAsync().catch(() => {});
|
|
32
|
+
onPress?.();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const palette: Record<ButtonVariant, { bg: string; fg: string }> = {
|
|
36
|
+
primary: { bg: accent, fg: '#FFFFFF' },
|
|
37
|
+
secondary: { bg: theme.surface.secondary, fg: theme.text.primary },
|
|
38
|
+
ghost: { bg: 'transparent', fg: accent },
|
|
39
|
+
destructive: { bg: theme.text.destructive, fg: '#FFFFFF' },
|
|
40
|
+
};
|
|
41
|
+
const { bg, fg } = palette[variant];
|
|
42
|
+
|
|
43
|
+
const baseStyle: ViewStyle = {
|
|
44
|
+
backgroundColor: bg,
|
|
45
|
+
borderRadius: theme.radius.button,
|
|
46
|
+
paddingVertical: theme.space.sm + 4,
|
|
47
|
+
paddingHorizontal: theme.space.md,
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
justifyContent: 'center',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
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>
|
|
58
|
+
</Animated.View>
|
|
59
|
+
</Pressable>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { View } from 'react-native';
|
|
2
|
+
import { BlurView } from 'expo-blur';
|
|
3
|
+
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
import { useTheme } from './useTheme';
|
|
6
|
+
|
|
7
|
+
export type CardProps = {
|
|
8
|
+
glass?: boolean;
|
|
9
|
+
padding?: number;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function Card({ glass = false, padding, children }: CardProps) {
|
|
14
|
+
const theme = useTheme();
|
|
15
|
+
const pad = padding ?? theme.space.md;
|
|
16
|
+
|
|
17
|
+
if (glass) {
|
|
18
|
+
if (isLiquidGlassAvailable()) {
|
|
19
|
+
return (
|
|
20
|
+
<GlassView
|
|
21
|
+
style={{
|
|
22
|
+
borderRadius: theme.radius.card,
|
|
23
|
+
borderWidth: 1,
|
|
24
|
+
borderColor: theme.border.default,
|
|
25
|
+
padding: pad,
|
|
26
|
+
overflow: 'hidden',
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
{children}
|
|
30
|
+
</GlassView>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return (
|
|
34
|
+
<View
|
|
35
|
+
style={{
|
|
36
|
+
borderRadius: theme.radius.card,
|
|
37
|
+
overflow: 'hidden',
|
|
38
|
+
borderWidth: 1,
|
|
39
|
+
borderColor: theme.border.default,
|
|
40
|
+
backgroundColor: theme.surface.card,
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<BlurView intensity={theme.blur.card} tint="light" style={{ padding: pad }}>
|
|
44
|
+
{children}
|
|
45
|
+
</BlurView>
|
|
46
|
+
</View>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<View
|
|
52
|
+
style={{
|
|
53
|
+
backgroundColor: theme.surface.card,
|
|
54
|
+
borderRadius: theme.radius.card,
|
|
55
|
+
borderWidth: 1,
|
|
56
|
+
borderColor: theme.border.default,
|
|
57
|
+
padding: pad,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{children}
|
|
61
|
+
</View>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { View } from 'react-native';
|
|
2
|
+
import { useTheme } from './useTheme';
|
|
3
|
+
|
|
4
|
+
export function Divider() {
|
|
5
|
+
const theme = useTheme();
|
|
6
|
+
return (
|
|
7
|
+
<View
|
|
8
|
+
style={{
|
|
9
|
+
height: 1,
|
|
10
|
+
width: '100%',
|
|
11
|
+
backgroundColor: theme.border.default,
|
|
12
|
+
}}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Modal as RNModal, Pressable, View } from 'react-native';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { useTheme } from './useTheme';
|
|
4
|
+
import { Text } from './Text';
|
|
5
|
+
|
|
6
|
+
export type ModalProps = {
|
|
7
|
+
title: string;
|
|
8
|
+
visible: boolean;
|
|
9
|
+
onClose?: () => void;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function Modal({ title, visible, onClose, children }: ModalProps) {
|
|
14
|
+
const theme = useTheme();
|
|
15
|
+
return (
|
|
16
|
+
<RNModal
|
|
17
|
+
visible={visible}
|
|
18
|
+
transparent
|
|
19
|
+
animationType="slide"
|
|
20
|
+
onRequestClose={onClose}
|
|
21
|
+
>
|
|
22
|
+
<Pressable
|
|
23
|
+
onPress={onClose}
|
|
24
|
+
style={{ flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.4)' }}
|
|
25
|
+
>
|
|
26
|
+
<Pressable
|
|
27
|
+
onPress={() => {}}
|
|
28
|
+
style={{
|
|
29
|
+
marginTop: 'auto',
|
|
30
|
+
backgroundColor: theme.surface.primary,
|
|
31
|
+
borderTopLeftRadius: theme.radius.modal,
|
|
32
|
+
borderTopRightRadius: theme.radius.modal,
|
|
33
|
+
padding: theme.space.lg,
|
|
34
|
+
gap: theme.space.md,
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
<Text size="headline">{title}</Text>
|
|
38
|
+
{children}
|
|
39
|
+
</Pressable>
|
|
40
|
+
</Pressable>
|
|
41
|
+
</RNModal>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { View, type ViewProps } from 'react-native';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
export type RowProps = {
|
|
5
|
+
gap?: number;
|
|
6
|
+
align?: 'start' | 'center' | 'end';
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
style?: ViewProps['style'];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const alignMap = {
|
|
12
|
+
start: 'flex-start',
|
|
13
|
+
center: 'center',
|
|
14
|
+
end: 'flex-end',
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export function Row({ gap = 0, align = 'start', style, children }: RowProps) {
|
|
18
|
+
return (
|
|
19
|
+
<View
|
|
20
|
+
style={[
|
|
21
|
+
{ flexDirection: 'row', alignItems: alignMap[align], gap },
|
|
22
|
+
style,
|
|
23
|
+
]}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</View>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ScrollView, View } from 'react-native';
|
|
2
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { useTheme } from './useTheme';
|
|
5
|
+
import { Text } from './Text';
|
|
6
|
+
|
|
7
|
+
export type ScreenProps = {
|
|
8
|
+
title?: string;
|
|
9
|
+
scrollable?: boolean;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function Screen({ title, scrollable = true, children }: ScreenProps) {
|
|
14
|
+
const theme = useTheme();
|
|
15
|
+
const Body = scrollable ? ScrollView : View;
|
|
16
|
+
return (
|
|
17
|
+
<SafeAreaView
|
|
18
|
+
style={{ flex: 1, backgroundColor: theme.surface.primary }}
|
|
19
|
+
edges={['top', 'left', 'right']}
|
|
20
|
+
>
|
|
21
|
+
{title ? (
|
|
22
|
+
<View style={{ paddingHorizontal: theme.space.md, paddingTop: theme.space.md, paddingBottom: theme.space.sm }}>
|
|
23
|
+
<Text size="title">{title}</Text>
|
|
24
|
+
</View>
|
|
25
|
+
) : null}
|
|
26
|
+
<Body
|
|
27
|
+
style={{ flex: 1 }}
|
|
28
|
+
contentContainerStyle={
|
|
29
|
+
scrollable
|
|
30
|
+
? { padding: theme.space.md, gap: theme.space.md }
|
|
31
|
+
: undefined
|
|
32
|
+
}
|
|
33
|
+
>
|
|
34
|
+
{scrollable ? children : <View style={{ flex: 1, padding: theme.space.md, gap: theme.space.md }}>{children}</View>}
|
|
35
|
+
</Body>
|
|
36
|
+
</SafeAreaView>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { View, type ViewProps } from 'react-native';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
export type StackProps = {
|
|
5
|
+
gap?: number;
|
|
6
|
+
padding?: number;
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
style?: ViewProps['style'];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function Stack({ gap = 0, padding = 0, style, children }: StackProps) {
|
|
12
|
+
return (
|
|
13
|
+
<View style={[{ flexDirection: 'column', gap, padding }, style]}>
|
|
14
|
+
{children}
|
|
15
|
+
</View>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Text as RNText, type TextProps as RNTextProps, type TextStyle } from 'react-native';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { useTheme, useAccent } from './useTheme';
|
|
4
|
+
|
|
5
|
+
export type TextSize = 'title' | 'headline' | 'body' | 'caption' | 'label';
|
|
6
|
+
export type TextColor = 'primary' | 'secondary' | 'accent' | 'destructive';
|
|
7
|
+
|
|
8
|
+
export type TextProps = {
|
|
9
|
+
size?: TextSize;
|
|
10
|
+
color?: TextColor;
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
style?: RNTextProps['style'];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const sizeMap: Record<TextSize, { fontSize: number; fontWeight: TextStyle['fontWeight'] }> = {
|
|
16
|
+
title: { fontSize: 34, fontWeight: '700' },
|
|
17
|
+
headline: { fontSize: 22, fontWeight: '600' },
|
|
18
|
+
body: { fontSize: 17, fontWeight: '400' },
|
|
19
|
+
caption: { fontSize: 13, fontWeight: '400' },
|
|
20
|
+
label: { fontSize: 13, fontWeight: '600' },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function Text({ size = 'body', color = 'primary', style, children }: TextProps) {
|
|
24
|
+
const theme = useTheme();
|
|
25
|
+
const accent = useAccent();
|
|
26
|
+
const palette: Record<TextColor, string> = {
|
|
27
|
+
primary: theme.text.primary,
|
|
28
|
+
secondary: theme.text.secondary,
|
|
29
|
+
accent,
|
|
30
|
+
destructive: theme.text.destructive,
|
|
31
|
+
};
|
|
32
|
+
return (
|
|
33
|
+
<RNText style={[sizeMap[size], { color: palette[color] }, style]}>
|
|
34
|
+
{children}
|
|
35
|
+
</RNText>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Platform, Switch as RNSwitch, View } from 'react-native';
|
|
2
|
+
import { Host, Toggle as SwiftUIToggle } from '@expo/ui/swift-ui';
|
|
3
|
+
import { useAccent, useTheme } from './useTheme';
|
|
4
|
+
import { Text } from './Text';
|
|
5
|
+
|
|
6
|
+
export type ToggleProps = {
|
|
7
|
+
label: string;
|
|
8
|
+
value: boolean;
|
|
9
|
+
onChange?: (value: boolean) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function Toggle({ label, value, onChange }: ToggleProps) {
|
|
13
|
+
const theme = useTheme();
|
|
14
|
+
const accent = useAccent();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<View
|
|
18
|
+
style={{
|
|
19
|
+
flexDirection: 'row',
|
|
20
|
+
alignItems: 'center',
|
|
21
|
+
justifyContent: 'space-between',
|
|
22
|
+
paddingVertical: theme.space.sm,
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
<Text size="body">{label}</Text>
|
|
26
|
+
{Platform.OS === 'ios' ? (
|
|
27
|
+
<Host matchContents>
|
|
28
|
+
<SwiftUIToggle isOn={value} onIsOnChange={(next) => onChange?.(next)} />
|
|
29
|
+
</Host>
|
|
30
|
+
) : (
|
|
31
|
+
<RNSwitch
|
|
32
|
+
value={value}
|
|
33
|
+
onValueChange={onChange}
|
|
34
|
+
trackColor={{ false: theme.border.default, true: accent }}
|
|
35
|
+
thumbColor="#FFFFFF"
|
|
36
|
+
/>
|
|
37
|
+
)}
|
|
38
|
+
</View>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { Screen, type ScreenProps } from './Screen';
|
|
2
|
+
export { Stack, type StackProps } from './Stack';
|
|
3
|
+
export { Row, type RowProps } from './Row';
|
|
4
|
+
export { Text, type TextProps, type TextSize, type TextColor } from './Text';
|
|
5
|
+
export { Card, type CardProps } from './Card';
|
|
6
|
+
export { Button, type ButtonProps, type ButtonVariant } from './Button';
|
|
7
|
+
export { Toggle, type ToggleProps } from './Toggle';
|
|
8
|
+
export { Divider } from './Divider';
|
|
9
|
+
export { Modal, type ModalProps } from './Modal';
|
|
10
|
+
export { useTheme, useAccent } from './useTheme';
|
|
11
|
+
export type { Theme, ThemeName, ThemeOverrides, ProtoConfig } from './types';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Theme } from '../types';
|
|
2
|
+
|
|
3
|
+
export const liquidGlass: Theme = {
|
|
4
|
+
surface: {
|
|
5
|
+
primary: 'rgba(255, 255, 255, 0.72)',
|
|
6
|
+
secondary: 'rgba(255, 255, 255, 0.48)',
|
|
7
|
+
card: 'rgba(255, 255, 255, 0.6)',
|
|
8
|
+
nav: 'rgba(255, 255, 255, 0.82)',
|
|
9
|
+
},
|
|
10
|
+
text: {
|
|
11
|
+
primary: '#000000',
|
|
12
|
+
secondary: 'rgba(0, 0, 0, 0.5)',
|
|
13
|
+
tertiary: 'rgba(0, 0, 0, 0.3)',
|
|
14
|
+
destructive: '#FF3B30',
|
|
15
|
+
},
|
|
16
|
+
blur: {
|
|
17
|
+
nav: 40,
|
|
18
|
+
card: 20,
|
|
19
|
+
modal: 60,
|
|
20
|
+
},
|
|
21
|
+
border: {
|
|
22
|
+
default: 'rgba(255, 255, 255, 0.4)',
|
|
23
|
+
strong: 'rgba(255, 255, 255, 0.7)',
|
|
24
|
+
},
|
|
25
|
+
radius: {
|
|
26
|
+
card: 22,
|
|
27
|
+
button: 14,
|
|
28
|
+
nav: 0,
|
|
29
|
+
modal: 44,
|
|
30
|
+
},
|
|
31
|
+
space: {
|
|
32
|
+
xs: 4,
|
|
33
|
+
sm: 8,
|
|
34
|
+
md: 16,
|
|
35
|
+
lg: 24,
|
|
36
|
+
xl: 32,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Theme } from '../types';
|
|
2
|
+
|
|
3
|
+
export const materialYou: Theme = {
|
|
4
|
+
surface: {
|
|
5
|
+
primary: '#FFFBFE',
|
|
6
|
+
secondary: '#E6E1E5',
|
|
7
|
+
card: '#F4EFF4',
|
|
8
|
+
nav: '#FFFBFE',
|
|
9
|
+
},
|
|
10
|
+
text: {
|
|
11
|
+
primary: '#1C1B1F',
|
|
12
|
+
secondary: '#49454F',
|
|
13
|
+
tertiary: '#79747E',
|
|
14
|
+
destructive: '#B3261E',
|
|
15
|
+
},
|
|
16
|
+
blur: {
|
|
17
|
+
nav: 0,
|
|
18
|
+
card: 0,
|
|
19
|
+
modal: 0,
|
|
20
|
+
},
|
|
21
|
+
border: {
|
|
22
|
+
default: '#CAC4D0',
|
|
23
|
+
strong: '#79747E',
|
|
24
|
+
},
|
|
25
|
+
radius: {
|
|
26
|
+
card: 12,
|
|
27
|
+
button: 20,
|
|
28
|
+
nav: 0,
|
|
29
|
+
modal: 28,
|
|
30
|
+
},
|
|
31
|
+
space: {
|
|
32
|
+
xs: 4,
|
|
33
|
+
sm: 8,
|
|
34
|
+
md: 16,
|
|
35
|
+
lg: 24,
|
|
36
|
+
xl: 32,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type ThemeName = 'liquidGlass' | 'materialYou';
|
|
2
|
+
|
|
3
|
+
export type Theme = {
|
|
4
|
+
surface: {
|
|
5
|
+
primary: string;
|
|
6
|
+
secondary: string;
|
|
7
|
+
card: string;
|
|
8
|
+
nav: string;
|
|
9
|
+
};
|
|
10
|
+
text: {
|
|
11
|
+
primary: string;
|
|
12
|
+
secondary: string;
|
|
13
|
+
tertiary: string;
|
|
14
|
+
destructive: string;
|
|
15
|
+
};
|
|
16
|
+
blur: {
|
|
17
|
+
nav: number;
|
|
18
|
+
card: number;
|
|
19
|
+
modal: number;
|
|
20
|
+
};
|
|
21
|
+
border: {
|
|
22
|
+
default: string;
|
|
23
|
+
strong: string;
|
|
24
|
+
};
|
|
25
|
+
radius: {
|
|
26
|
+
card: number;
|
|
27
|
+
button: number;
|
|
28
|
+
nav: number;
|
|
29
|
+
modal: number;
|
|
30
|
+
};
|
|
31
|
+
space: {
|
|
32
|
+
xs: number;
|
|
33
|
+
sm: number;
|
|
34
|
+
md: number;
|
|
35
|
+
lg: number;
|
|
36
|
+
xl: number;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ThemeOverrides = Partial<{
|
|
41
|
+
surface: Partial<Theme['surface']>;
|
|
42
|
+
text: Partial<Theme['text']>;
|
|
43
|
+
blur: Partial<Theme['blur']>;
|
|
44
|
+
border: Partial<Theme['border']>;
|
|
45
|
+
radius: Partial<Theme['radius']>;
|
|
46
|
+
space: Partial<Theme['space']>;
|
|
47
|
+
}>;
|
|
48
|
+
|
|
49
|
+
export type ProtoConfig = {
|
|
50
|
+
name?: string;
|
|
51
|
+
theme?: ThemeName;
|
|
52
|
+
accentColor?: string;
|
|
53
|
+
tokens?: ThemeOverrides;
|
|
54
|
+
screens?: { initial?: string };
|
|
55
|
+
};
|