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,41 @@
1
+ {
2
+ "name": "{{PROJECT_NAME_KEBAB}}",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "Pika mini-game: {{PROJECT_NAME_PASCAL}}",
6
+ "main": "dev/index.js",
7
+ "scripts": {
8
+ "dev": "expo start",
9
+ "dev:ios": "expo run:ios",
10
+ "dev:android": "expo run:android",
11
+ "build:ios": "npx react-native webpack-bundle --platform ios --entry-file index.js --dev false --bundle-output build/outputs/ios/local/index.ios.bundle --assets-dest build/outputs/ios/local",
12
+ "build:android": "npx react-native webpack-bundle --platform android --entry-file index.js --dev false --bundle-output build/outputs/android/local/index.android.bundle --assets-dest build/outputs/android/local",
13
+ "serve:ios": "npx serve build/outputs/ios/remotes -l 9000 --cors",
14
+ "serve:android": "npx serve build/outputs/android/remotes -l 9000 --cors",
15
+ "deploy": "echo 'Configure your deployment in package.json scripts'",
16
+ "lint": "eslint src/ dev/",
17
+ "typecheck": "tsc --noEmit"
18
+ },
19
+ "dependencies": {
20
+ "expo": "~52.0.0",
21
+ "expo-status-bar": "~2.0.0",
22
+ "react": "18.3.1",
23
+ "react-native": "0.77.0",
24
+ "react-native-gesture-handler": "~2.20.0",
25
+ "react-native-reanimated": "~3.16.0",
26
+ "react-native-safe-area-context": "4.12.0",
27
+ "react-native-screens": "~4.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@babel/core": "^7.25.0",
31
+ "@callstack/repack": "^5.0.0",
32
+ "@callstack/repack-plugin-reanimated": "^1.0.0",
33
+ "@types/react": "~18.3.0",
34
+ "typescript": "~5.3.0"
35
+ },
36
+ "peerDependencies": {
37
+ "react": "18.3.1",
38
+ "react-native": "0.77.0",
39
+ "react-native-reanimated": "~3.16.0"
40
+ }
41
+ }
@@ -0,0 +1,81 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import * as Repack from '@callstack/repack';
4
+ import { ReanimatedPlugin } from '@callstack/repack-plugin-reanimated';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ /**
10
+ * Rspack config for Module Federation remote.
11
+ *
12
+ * This bundles your mini-game as a remote that can be loaded
13
+ * by the Pika host app at runtime.
14
+ */
15
+ export default Repack.defineRspackConfig((env) => {
16
+ const { mode, platform } = env;
17
+
18
+ return {
19
+ mode,
20
+ context: __dirname,
21
+ entry: './index.js',
22
+ output: {
23
+ path: `${__dirname}/build/outputs/${platform}/remotes`,
24
+ uniqueName: '{{PROJECT_NAME_SNAKE}}',
25
+ // Set this to your CDN URL for production builds:
26
+ // MF_PUBLIC_PATH=https://your-cdn.com/games/{{PROJECT_NAME_KEBAB}}/
27
+ publicPath: process.env.MF_PUBLIC_PATH || 'http://localhost:9000/',
28
+ },
29
+ resolve: {
30
+ ...Repack.getResolveOptions({ enablePackageExports: true }),
31
+ },
32
+ module: {
33
+ rules: [
34
+ {
35
+ test: /\.[cm]?[jt]sx?$/,
36
+ type: 'javascript/auto',
37
+ use: {
38
+ loader: '@callstack/repack/babel-swc-loader',
39
+ parallel: true,
40
+ options: {},
41
+ },
42
+ },
43
+ ...Repack.getAssetTransformRules({ svg: 'svgr' }),
44
+ ],
45
+ },
46
+ plugins: [
47
+ new Repack.RepackPlugin({
48
+ extraChunks: [
49
+ {
50
+ include: /.*/,
51
+ type: 'remote',
52
+ outputPath: `build/outputs/${platform}/remotes`,
53
+ },
54
+ ],
55
+ }),
56
+ new ReanimatedPlugin({ unstable_disableTransform: true }),
57
+ new Repack.plugins.ModuleFederationPluginV2({
58
+ name: '{{PROJECT_NAME_SNAKE}}',
59
+ filename: '{{PROJECT_NAME_SNAKE}}.container.js.bundle',
60
+ dts: false,
61
+ exposes: {
62
+ './App': './src/App.tsx',
63
+ },
64
+ shared: {
65
+ react: { singleton: true, eager: false, requiredVersion: '18.3.1' },
66
+ 'react-native': { singleton: true, eager: false, requiredVersion: '0.77.0' },
67
+ 'react-native-reanimated': { singleton: true, eager: false },
68
+ 'react-native-gesture-handler': { singleton: true, eager: false },
69
+ 'react-native-safe-area-context': { singleton: true, eager: false },
70
+ 'react-native-screens': { singleton: true, eager: false },
71
+ 'react-native-svg': { singleton: true, eager: false },
72
+ },
73
+ }),
74
+ ],
75
+ ignoreWarnings: [
76
+ (warning) =>
77
+ /Critical dependency: require function is used/.test(warning.message) &&
78
+ /react-native-reanimated/.test(warning.module?.resource ?? ''),
79
+ ],
80
+ };
81
+ });
@@ -0,0 +1,215 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import { View, Text, StyleSheet } from 'react-native';
3
+ import { TopBar, Button, Card } from './components';
4
+ import { semantic, space, typography, color, radius } from './tokens';
5
+ import { GameProps, Screen } from './types';
6
+
7
+ /**
8
+ * {{PROJECT_NAME_PASCAL}} - Main App Component
9
+ *
10
+ * This is your mini-game entry point. The host app loads this component
11
+ * and passes the `host` bridge for native capabilities.
12
+ *
13
+ * In dev mode (yarn dev), the host is mocked so you can test standalone.
14
+ */
15
+ const App: React.FC<GameProps> = ({ onExit, onGameOver, host }) => {
16
+ const [screen, setScreen] = useState<Screen>('home');
17
+ const [score, setScore] = useState(0);
18
+
19
+ const handleStartGame = useCallback(() => {
20
+ setScreen('game');
21
+ setScore(0);
22
+ }, []);
23
+
24
+ const handleGameComplete = useCallback((finalScore: number) => {
25
+ setScore(finalScore);
26
+ setScreen('result');
27
+ onGameOver?.({ score: finalScore });
28
+ }, [onGameOver]);
29
+
30
+ const handlePlayAgain = useCallback(() => {
31
+ setScreen('home');
32
+ setScore(0);
33
+ }, []);
34
+
35
+ // Home Screen
36
+ if (screen === 'home') {
37
+ return (
38
+ <View style={styles.container}>
39
+ <TopBar
40
+ left="close"
41
+ onLeftPress={onExit}
42
+ right={<Text style={styles.badge}>{{PROJECT_NAME_PASCAL}}</Text>}
43
+ />
44
+ <View style={styles.body}>
45
+ <Text style={styles.title}>Welcome!</Text>
46
+ <Text style={styles.subtitle}>
47
+ This is your mini-game template. Edit App.tsx to build your game.
48
+ </Text>
49
+
50
+ <Card style={styles.infoCard}>
51
+ <View style={styles.cardContent}>
52
+ <Text style={styles.cardTitle}>Getting Started</Text>
53
+ <Text style={styles.cardText}>
54
+ 1. Edit this file to create your game{'\n'}
55
+ 2. Use tokens.ts for consistent styling{'\n'}
56
+ 3. Use components/ for UI elements{'\n'}
57
+ 4. Host bridge in types.ts for native features
58
+ </Text>
59
+ </View>
60
+ </Card>
61
+
62
+ <Button
63
+ label="Start Game"
64
+ variant="primary"
65
+ size="lg"
66
+ fullWidth
67
+ onPress={handleStartGame}
68
+ style={styles.startButton}
69
+ />
70
+ </View>
71
+ </View>
72
+ );
73
+ }
74
+
75
+ // Game Screen (placeholder - build your game logic here)
76
+ if (screen === 'game') {
77
+ return (
78
+ <View style={styles.container}>
79
+ <TopBar
80
+ left="back"
81
+ onLeftPress={() => setScreen('home')}
82
+ title="Playing..."
83
+ />
84
+ <View style={styles.body}>
85
+ <Text style={styles.title}>Game Screen</Text>
86
+ <Text style={styles.subtitle}>
87
+ Build your game mechanics here!
88
+ </Text>
89
+
90
+ <View style={styles.gameArea}>
91
+ <Text style={styles.emoji}>🎮</Text>
92
+ <Text style={styles.gameText}>
93
+ Use host.pronunciation for voice features,{'\n'}
94
+ or add your own game logic.
95
+ </Text>
96
+ </View>
97
+
98
+ <Button
99
+ label="Complete Game (Score: 85)"
100
+ variant="primary"
101
+ size="lg"
102
+ fullWidth
103
+ onPress={() => handleGameComplete(85)}
104
+ style={styles.startButton}
105
+ />
106
+ </View>
107
+ </View>
108
+ );
109
+ }
110
+
111
+ // Result Screen
112
+ return (
113
+ <View style={styles.container}>
114
+ <TopBar title="Results" />
115
+ <View style={styles.body}>
116
+ <Text style={styles.emoji}>🎉</Text>
117
+ <Text style={styles.title}>Great Job!</Text>
118
+ <Text style={styles.scoreText}>{score}</Text>
119
+ <Text style={styles.subtitle}>points</Text>
120
+
121
+ <Button
122
+ label="Play Again"
123
+ variant="primary"
124
+ size="lg"
125
+ fullWidth
126
+ onPress={handlePlayAgain}
127
+ style={styles.startButton}
128
+ />
129
+ <Button
130
+ label="Exit"
131
+ variant="secondary"
132
+ size="md"
133
+ fullWidth
134
+ onPress={onExit}
135
+ style={styles.exitButton}
136
+ />
137
+ </View>
138
+ </View>
139
+ );
140
+ };
141
+
142
+ const styles = StyleSheet.create({
143
+ container: {
144
+ flex: 1,
145
+ backgroundColor: semantic.bg,
146
+ },
147
+ badge: {
148
+ color: semantic.textSecondary,
149
+ fontSize: typography.tag.size,
150
+ },
151
+ body: {
152
+ flex: 1,
153
+ paddingHorizontal: space[5],
154
+ paddingTop: space[8],
155
+ alignItems: 'center',
156
+ },
157
+ title: {
158
+ color: semantic.text,
159
+ fontSize: typography.title1.size,
160
+ fontWeight: typography.title1.fontWeight,
161
+ marginBottom: space[2],
162
+ textAlign: 'center',
163
+ },
164
+ subtitle: {
165
+ color: semantic.textSecondary,
166
+ fontSize: typography.body.size,
167
+ textAlign: 'center',
168
+ marginBottom: space[6],
169
+ },
170
+ infoCard: {
171
+ width: '100%',
172
+ marginBottom: space[8],
173
+ },
174
+ cardContent: {
175
+ padding: space[4],
176
+ },
177
+ cardTitle: {
178
+ color: semantic.text,
179
+ fontSize: typography.title2.size,
180
+ fontWeight: typography.title2.fontWeight,
181
+ marginBottom: space[3],
182
+ },
183
+ cardText: {
184
+ color: semantic.textSecondary,
185
+ fontSize: typography.caption.size,
186
+ lineHeight: 22,
187
+ },
188
+ startButton: {
189
+ marginTop: space[4],
190
+ },
191
+ exitButton: {
192
+ marginTop: space[3],
193
+ },
194
+ gameArea: {
195
+ flex: 1,
196
+ justifyContent: 'center',
197
+ alignItems: 'center',
198
+ },
199
+ emoji: {
200
+ fontSize: 64,
201
+ marginBottom: space[4],
202
+ },
203
+ gameText: {
204
+ color: semantic.textSecondary,
205
+ fontSize: typography.body.size,
206
+ textAlign: 'center',
207
+ },
208
+ scoreText: {
209
+ color: color.cyan[400],
210
+ fontSize: typography.display.size,
211
+ fontWeight: typography.display.fontWeight,
212
+ },
213
+ });
214
+
215
+ export default App;
@@ -0,0 +1,117 @@
1
+ import React, { useRef } from 'react';
2
+ import {
3
+ Animated,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ ActivityIndicator,
9
+ StyleProp,
10
+ ViewStyle,
11
+ } from 'react-native';
12
+ import { color, semantic, space, radius } from '../tokens';
13
+
14
+ export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
15
+ export type ButtonSize = 'sm' | 'md' | 'lg';
16
+
17
+ export interface ButtonProps {
18
+ label: string;
19
+ variant?: ButtonVariant;
20
+ size?: ButtonSize;
21
+ loading?: boolean;
22
+ disabled?: boolean;
23
+ fullWidth?: boolean;
24
+ leftIcon?: React.ReactNode;
25
+ rightIcon?: React.ReactNode;
26
+ onPress?: () => void;
27
+ style?: StyleProp<ViewStyle>;
28
+ }
29
+
30
+ const VARIANT_STYLE = {
31
+ primary: { bg: color.cyan[400], text: color.white },
32
+ secondary: { bg: semantic.surfaceElevated, text: color.white },
33
+ danger: { bg: color.red[400], text: color.white },
34
+ ghost: { bg: 'transparent', text: color.cyan[400], border: color.cyan[400] },
35
+ };
36
+
37
+ const SIZE_STYLE = {
38
+ sm: { height: 40, paddingH: space[4], fontSize: 13 },
39
+ md: { height: 50, paddingH: space[6], fontSize: 15 },
40
+ lg: { height: 60, paddingH: space[8], fontSize: 17 },
41
+ };
42
+
43
+ export function Button({
44
+ label,
45
+ variant = 'primary',
46
+ size = 'md',
47
+ loading = false,
48
+ disabled = false,
49
+ fullWidth = false,
50
+ leftIcon,
51
+ rightIcon,
52
+ onPress,
53
+ style,
54
+ }: ButtonProps) {
55
+ const v = VARIANT_STYLE[variant];
56
+ const s = SIZE_STYLE[size];
57
+ const inactive = disabled || loading;
58
+ const scale = useRef(new Animated.Value(1)).current;
59
+
60
+ const pressIn = () => {
61
+ if (inactive) return;
62
+ Animated.timing(scale, { toValue: 0.96, duration: 80, useNativeDriver: true }).start();
63
+ };
64
+
65
+ const pressOut = () => {
66
+ if (inactive) return;
67
+ Animated.spring(scale, { toValue: 1, friction: 8, tension: 200, useNativeDriver: true }).start();
68
+ };
69
+
70
+ return (
71
+ <Pressable
72
+ onPress={inactive ? undefined : onPress}
73
+ onPressIn={pressIn}
74
+ onPressOut={pressOut}
75
+ style={[styles.pressable, fullWidth && styles.fullWidth, style]}
76
+ >
77
+ <Animated.View
78
+ style={[
79
+ styles.face,
80
+ {
81
+ height: s.height,
82
+ paddingHorizontal: s.paddingH,
83
+ backgroundColor: v.bg,
84
+ borderRadius: radius.xl,
85
+ opacity: inactive ? 0.5 : 1,
86
+ transform: [{ scale }],
87
+ },
88
+ 'border' in v && { borderWidth: 1, borderColor: v.border },
89
+ ]}
90
+ >
91
+ {loading ? (
92
+ <ActivityIndicator size="small" color={v.text} />
93
+ ) : (
94
+ <View style={styles.content}>
95
+ {leftIcon && <View style={styles.iconLeft}>{leftIcon}</View>}
96
+ <Text style={[styles.label, { fontSize: s.fontSize, color: v.text }]}>
97
+ {label.toUpperCase()}
98
+ </Text>
99
+ {rightIcon && <View style={styles.iconRight}>{rightIcon}</View>}
100
+ </View>
101
+ )}
102
+ </Animated.View>
103
+ </Pressable>
104
+ );
105
+ }
106
+
107
+ const styles = StyleSheet.create({
108
+ pressable: { alignSelf: 'flex-start' },
109
+ fullWidth: { alignSelf: 'stretch' },
110
+ face: { alignItems: 'center', justifyContent: 'center' },
111
+ content: { flexDirection: 'row', alignItems: 'center', gap: space[2] },
112
+ label: { fontWeight: '800', letterSpacing: 0.8 },
113
+ iconLeft: {},
114
+ iconRight: {},
115
+ });
116
+
117
+ export default Button;
@@ -0,0 +1,86 @@
1
+ import React, { useRef } from 'react';
2
+ import {
3
+ Animated,
4
+ Pressable,
5
+ StyleSheet,
6
+ View,
7
+ StyleProp,
8
+ ViewStyle,
9
+ } from 'react-native';
10
+ import { semantic, space, radius } from '../tokens';
11
+
12
+ export type CardVariant = 'default' | 'elevated';
13
+ export type CardPadding = 'default' | 'none';
14
+
15
+ export interface CardProps {
16
+ variant?: CardVariant;
17
+ padding?: CardPadding;
18
+ onPress?: () => void;
19
+ disabled?: boolean;
20
+ children: React.ReactNode;
21
+ style?: StyleProp<ViewStyle>;
22
+ }
23
+
24
+ const PADDING_MAP = { default: space[4], none: 0 };
25
+
26
+ export function Card({
27
+ variant = 'default',
28
+ padding: paddingProp = 'default',
29
+ onPress,
30
+ disabled = false,
31
+ children,
32
+ style,
33
+ }: CardProps) {
34
+ const cardPadding = PADDING_MAP[paddingProp];
35
+ const interactive = !!onPress && !disabled;
36
+ const scale = useRef(new Animated.Value(1)).current;
37
+
38
+ const pressIn = () => {
39
+ if (!interactive) return;
40
+ Animated.timing(scale, { toValue: 0.98, duration: 100, useNativeDriver: true }).start();
41
+ };
42
+
43
+ const pressOut = () => {
44
+ if (!interactive) return;
45
+ Animated.spring(scale, { toValue: 1, friction: 8, tension: 200, useNativeDriver: true }).start();
46
+ };
47
+
48
+ const cardContent = (
49
+ <Animated.View
50
+ style={[
51
+ styles.face,
52
+ {
53
+ backgroundColor: variant === 'elevated' ? semantic.surfaceElevated : semantic.surface,
54
+ borderRadius: radius.lg,
55
+ padding: cardPadding,
56
+ opacity: disabled ? 0.5 : 1,
57
+ transform: [{ scale }],
58
+ },
59
+ style,
60
+ ]}
61
+ >
62
+ {children}
63
+ </Animated.View>
64
+ );
65
+
66
+ if (onPress) {
67
+ return (
68
+ <Pressable
69
+ onPress={disabled ? undefined : onPress}
70
+ onPressIn={pressIn}
71
+ onPressOut={pressOut}
72
+ disabled={disabled}
73
+ >
74
+ {cardContent}
75
+ </Pressable>
76
+ );
77
+ }
78
+
79
+ return cardContent;
80
+ }
81
+
82
+ const styles = StyleSheet.create({
83
+ face: { overflow: 'hidden' },
84
+ });
85
+
86
+ export default Card;
@@ -0,0 +1,68 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Animated, Easing, StyleSheet, View, StyleProp, ViewStyle } from 'react-native';
3
+ import { color } from '../tokens';
4
+
5
+ export type SpinnerSize = 'sm' | 'md' | 'lg' | 'xl';
6
+
7
+ export interface SpinnerProps {
8
+ size?: SpinnerSize;
9
+ color?: string;
10
+ style?: StyleProp<ViewStyle>;
11
+ }
12
+
13
+ const SIZE_STYLE = {
14
+ sm: { diameter: 16, stroke: 2 },
15
+ md: { diameter: 24, stroke: 3 },
16
+ lg: { diameter: 40, stroke: 4 },
17
+ xl: { diameter: 80, stroke: 5 },
18
+ };
19
+
20
+ export function Spinner({ size = 'md', color: spinnerColor = color.cyan[400], style }: SpinnerProps) {
21
+ const s = SIZE_STYLE[size];
22
+ const rotation = useRef(new Animated.Value(0)).current;
23
+
24
+ useEffect(() => {
25
+ Animated.loop(
26
+ Animated.timing(rotation, { toValue: 1, duration: 800, easing: Easing.linear, useNativeDriver: true }),
27
+ ).start();
28
+ return () => rotation.stopAnimation();
29
+ }, [rotation]);
30
+
31
+ const spin = rotation.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '360deg'] });
32
+
33
+ return (
34
+ <Animated.View
35
+ style={[{ width: s.diameter, height: s.diameter, transform: [{ rotate: spin }] }, style]}
36
+ accessibilityLabel="Loading"
37
+ >
38
+ <View
39
+ style={[
40
+ styles.circle,
41
+ { width: s.diameter, height: s.diameter, borderRadius: s.diameter / 2, borderWidth: s.stroke, borderColor: 'rgba(255,255,255,0.1)' },
42
+ ]}
43
+ />
44
+ <View
45
+ style={[
46
+ styles.arc,
47
+ {
48
+ width: s.diameter,
49
+ height: s.diameter,
50
+ borderRadius: s.diameter / 2,
51
+ borderWidth: s.stroke,
52
+ borderTopColor: spinnerColor,
53
+ borderRightColor: spinnerColor,
54
+ borderBottomColor: 'transparent',
55
+ borderLeftColor: 'transparent',
56
+ },
57
+ ]}
58
+ />
59
+ </Animated.View>
60
+ );
61
+ }
62
+
63
+ const styles = StyleSheet.create({
64
+ circle: { position: 'absolute' },
65
+ arc: { position: 'absolute' },
66
+ });
67
+
68
+ export default Spinner;