create-pika-minigame 1.0.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.
@@ -0,0 +1,109 @@
1
+ import React, { useRef } from 'react';
2
+ import {
3
+ Animated,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ StyleProp,
9
+ ViewStyle,
10
+ } from 'react-native';
11
+ import { SafeAreaView } from 'react-native-safe-area-context';
12
+ import { semantic, space, typography } from '../tokens';
13
+
14
+ export type LeftPreset = 'back' | 'close';
15
+
16
+ export interface TopBarProps {
17
+ left?: LeftPreset | React.ReactNode;
18
+ onLeftPress?: () => void;
19
+ title?: string;
20
+ center?: React.ReactNode;
21
+ right?: React.ReactNode;
22
+ style?: StyleProp<ViewStyle>;
23
+ }
24
+
25
+ const SIDE_SLOT = 40;
26
+ const BAR_HEIGHT = 56;
27
+
28
+ function PressableSlot({
29
+ onPress,
30
+ accessibilityLabel,
31
+ children,
32
+ }: {
33
+ onPress?: () => void;
34
+ accessibilityLabel: string;
35
+ children: React.ReactNode;
36
+ }) {
37
+ const scale = useRef(new Animated.Value(1)).current;
38
+
39
+ return (
40
+ <Pressable
41
+ onPress={onPress}
42
+ onPressIn={() => Animated.timing(scale, { toValue: 0.92, duration: 80, useNativeDriver: true }).start()}
43
+ onPressOut={() => Animated.spring(scale, { toValue: 1, friction: 8, tension: 200, useNativeDriver: true }).start()}
44
+ accessibilityLabel={accessibilityLabel}
45
+ hitSlop={{ top: 8, bottom: 8, left: 4, right: 4 }}
46
+ style={styles.sideSlot}
47
+ >
48
+ <Animated.View style={{ transform: [{ scale }] }}>{children}</Animated.View>
49
+ </Pressable>
50
+ );
51
+ }
52
+
53
+ export function TopBar({ left, onLeftPress, title, center, right, style }: TopBarProps) {
54
+ const leftNode = (() => {
55
+ if (!left) return <View style={styles.sideSlot} />;
56
+ if (left === 'back') {
57
+ return (
58
+ <PressableSlot onPress={onLeftPress} accessibilityLabel="Back">
59
+ <Text style={styles.backIcon}>←</Text>
60
+ </PressableSlot>
61
+ );
62
+ }
63
+ if (left === 'close') {
64
+ return (
65
+ <PressableSlot onPress={onLeftPress} accessibilityLabel="Close">
66
+ <Text style={styles.closeIcon}>✕</Text>
67
+ </PressableSlot>
68
+ );
69
+ }
70
+ return <View style={styles.sideSlot}>{left}</View>;
71
+ })();
72
+
73
+ const centerNode = (() => {
74
+ if (center) return <View style={styles.center}>{center}</View>;
75
+ if (title) {
76
+ return (
77
+ <View style={styles.center}>
78
+ <Text style={styles.title} numberOfLines={1}>{title}</Text>
79
+ </View>
80
+ );
81
+ }
82
+ return <View style={styles.center} />;
83
+ })();
84
+
85
+ const rightNode = right ? <View style={styles.rightSlot}>{right}</View> : <View style={styles.sideSlot} />;
86
+
87
+ return (
88
+ <SafeAreaView edges={['top']} style={[styles.safeArea, style]}>
89
+ <View style={styles.row}>
90
+ {leftNode}
91
+ {centerNode}
92
+ {rightNode}
93
+ </View>
94
+ </SafeAreaView>
95
+ );
96
+ }
97
+
98
+ const styles = StyleSheet.create({
99
+ safeArea: { backgroundColor: semantic.bg },
100
+ row: { height: BAR_HEIGHT, flexDirection: 'row', alignItems: 'center', paddingHorizontal: space[3] },
101
+ sideSlot: { width: SIDE_SLOT, height: SIDE_SLOT, alignItems: 'center', justifyContent: 'center' },
102
+ rightSlot: { minWidth: SIDE_SLOT, height: SIDE_SLOT, alignItems: 'center', justifyContent: 'flex-end', flexDirection: 'row' },
103
+ center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: space[2] },
104
+ title: { color: semantic.text, fontSize: typography.title2.size, fontWeight: typography.title2.fontWeight, textAlign: 'center' },
105
+ backIcon: { color: semantic.textSecondary, fontSize: 20 },
106
+ closeIcon: { color: semantic.textSecondary, fontSize: 16 },
107
+ });
108
+
109
+ export default TopBar;
@@ -0,0 +1,11 @@
1
+ export { Button } from './Button';
2
+ export type { ButtonProps, ButtonVariant, ButtonSize } from './Button';
3
+
4
+ export { Card } from './Card';
5
+ export type { CardProps, CardVariant, CardPadding } from './Card';
6
+
7
+ export { TopBar } from './TopBar';
8
+ export type { TopBarProps, LeftPreset } from './TopBar';
9
+
10
+ export { Spinner } from './Spinner';
11
+ export type { SpinnerProps, SpinnerSize } from './Spinner';
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Mini-game design tokens — aligned with Pika Design System.
3
+ * Dark theme variant for immersive game experience.
4
+ */
5
+
6
+ export const color = {
7
+ // Pika brand primary
8
+ cyan: {
9
+ 100: '#A5E1EB',
10
+ 200: '#78D2E1',
11
+ 400: '#46C4D7',
12
+ 500: '#3AABBC',
13
+ 600: '#2E95A4',
14
+ },
15
+ // Error / destructive / recording state
16
+ red: {
17
+ 100: '#FFDFE0',
18
+ 400: '#FF4B4B',
19
+ 500: '#EA2B2B',
20
+ light: '#FF8A80',
21
+ },
22
+ // Success / good score
23
+ green: {
24
+ 400: '#2E7D32',
25
+ 500: '#1B5E20',
26
+ },
27
+ // Medium score
28
+ orange: {
29
+ 400: '#FF9600',
30
+ 500: '#B26A00',
31
+ },
32
+ // Dark theme neutrals
33
+ neutral: {
34
+ 900: '#0B0B0B',
35
+ 800: '#1A1A1A',
36
+ 700: '#2A2A2A',
37
+ 600: '#3A3A3A',
38
+ 500: 'rgba(255,255,255,0.6)',
39
+ 400: 'rgba(255,255,255,0.4)',
40
+ 200: 'rgba(255,255,255,0.14)',
41
+ 100: 'rgba(255,255,255,0.08)',
42
+ },
43
+ white: '#FFFFFF',
44
+ } as const;
45
+
46
+ export const semantic = {
47
+ primary: color.cyan[400],
48
+ primaryDark: color.cyan[600],
49
+ error: color.red[400],
50
+ errorLight: color.red.light,
51
+ success: color.green[400],
52
+ warning: color.orange[500],
53
+ recording: color.red[400],
54
+ bg: color.neutral[900],
55
+ surface: color.neutral[100],
56
+ surfaceElevated: color.neutral[200],
57
+ text: color.white,
58
+ textSecondary: color.neutral[500],
59
+ textDisabled: color.neutral[400],
60
+ } as const;
61
+
62
+ export const space = {
63
+ 1: 4,
64
+ 2: 8,
65
+ 3: 12,
66
+ 4: 16,
67
+ 5: 20,
68
+ 6: 24,
69
+ 7: 28,
70
+ 8: 32,
71
+ 10: 40,
72
+ 12: 48,
73
+ 14: 56,
74
+ 16: 64,
75
+ 24: 96,
76
+ } as const;
77
+
78
+ export const radius = {
79
+ xs: 4,
80
+ sm: 8,
81
+ md: 12,
82
+ lg: 16,
83
+ xl: 20,
84
+ '2xl': 24,
85
+ '3xl': 32,
86
+ pill: 9999,
87
+ } as const;
88
+
89
+ export const typography = {
90
+ display: { size: 64, fontWeight: '900' as const },
91
+ wordDisplay: { size: 52, fontWeight: '800' as const },
92
+ title1: { size: 28, fontWeight: '800' as const },
93
+ title2: { size: 17, fontWeight: '700' as const },
94
+ body: { size: 16, fontWeight: '400' as const },
95
+ bodyBold: { size: 15, fontWeight: '700' as const },
96
+ caption: { size: 14, fontWeight: '600' as const },
97
+ label: { size: 13, lineHeight: 18 },
98
+ tag: { size: 12, fontWeight: '400' as const },
99
+ icon: { lg: 44, md: 28, sm: 24 },
100
+ } as const;
101
+
102
+ export const sizing = {
103
+ micButton: 96,
104
+ gameIcon: 56,
105
+ minTouch: 44,
106
+ } as const;
107
+
108
+ export const scoreColor = (score: number): string =>
109
+ score >= 80 ? color.green[400] : score >= 50 ? color.orange[500] : color.red[400];
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Type definitions for Pika mini-game host bridge.
3
+ * These types define the contract between mini-game and host app.
4
+ */
5
+
6
+ export interface LetterScore {
7
+ letter: string;
8
+ score: number;
9
+ }
10
+
11
+ export interface PronunciationResult {
12
+ totalScore: number;
13
+ words: { word: string; score: number; letters: LetterScore[] }[];
14
+ }
15
+
16
+ /**
17
+ * Host bridge interface - provided by the main app.
18
+ * In dev mode, this is mocked by MockHost.
19
+ */
20
+ export interface HostBridge {
21
+ pronunciation: {
22
+ startRecording: () => Promise<void>;
23
+ stopAndCheck: (target: string) => Promise<PronunciationResult>;
24
+ cancel: () => Promise<void>;
25
+ };
26
+ // Add more capabilities here as needed:
27
+ // audio: { play: (url: string) => Promise<void>; stop: () => void; };
28
+ // haptics: { impact: (style: 'light' | 'medium' | 'heavy') => void; };
29
+ // analytics: { track: (event: string, props?: object) => void; };
30
+ }
31
+
32
+ /**
33
+ * Props passed to the mini-game App component.
34
+ */
35
+ export interface GameProps {
36
+ /** Called when user wants to exit the game */
37
+ onExit?: () => void;
38
+ /** Called when game ends with a result */
39
+ onGameOver?: (result: { score: number }) => void;
40
+ /** Host bridge for native capabilities (undefined in standalone dev mode uses mock) */
41
+ host?: HostBridge;
42
+ }
43
+
44
+ /**
45
+ * Navigation screens in your mini-game.
46
+ * Add more screens as your game grows.
47
+ */
48
+ export type Screen = 'home' | 'game' | 'result';
49
+
50
+ /**
51
+ * Game phase states.
52
+ */
53
+ export type Phase = 'idle' | 'playing' | 'checking' | 'result' | 'error';
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "expo/tsconfig.base",
3
+ "compilerOptions": {
4
+ "strict": true,
5
+ "baseUrl": ".",
6
+ "paths": {
7
+ "@/*": ["src/*"],
8
+ "@components/*": ["src/components/*"]
9
+ }
10
+ },
11
+ "include": ["**/*.ts", "**/*.tsx"],
12
+ "exclude": ["node_modules", "build"]
13
+ }