@littlepartytime/dev-kit 1.19.1 → 1.20.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.
Files changed (52) hide show
  1. package/dist/__tests__/engine-loader.test.d.ts +2 -0
  2. package/dist/__tests__/engine-loader.test.d.ts.map +1 -0
  3. package/dist/__tests__/engine-loader.test.js +48 -0
  4. package/dist/__tests__/engine-loader.test.js.map +1 -0
  5. package/dist/__tests__/games-api.test.d.ts +2 -0
  6. package/dist/__tests__/games-api.test.d.ts.map +1 -0
  7. package/dist/__tests__/games-api.test.js +109 -0
  8. package/dist/__tests__/games-api.test.js.map +1 -0
  9. package/dist/__tests__/lan-address.test.d.ts +2 -0
  10. package/dist/__tests__/lan-address.test.d.ts.map +1 -0
  11. package/dist/__tests__/lan-address.test.js +14 -0
  12. package/dist/__tests__/lan-address.test.js.map +1 -0
  13. package/dist/__tests__/zip-manager.test.d.ts +2 -0
  14. package/dist/__tests__/zip-manager.test.d.ts.map +1 -0
  15. package/dist/__tests__/zip-manager.test.js +111 -0
  16. package/dist/__tests__/zip-manager.test.js.map +1 -0
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +19 -0
  19. package/dist/cli.js.map +1 -1
  20. package/dist/commands/dev.d.ts.map +1 -1
  21. package/dist/commands/dev.js +11 -0
  22. package/dist/commands/dev.js.map +1 -1
  23. package/dist/commands/play.d.ts +7 -0
  24. package/dist/commands/play.d.ts.map +1 -0
  25. package/dist/commands/play.js +154 -0
  26. package/dist/commands/play.js.map +1 -0
  27. package/dist/server/engine-loader.d.ts +5 -0
  28. package/dist/server/engine-loader.d.ts.map +1 -1
  29. package/dist/server/engine-loader.js +23 -0
  30. package/dist/server/engine-loader.js.map +1 -1
  31. package/dist/server/games-api.d.ts +7 -0
  32. package/dist/server/games-api.d.ts.map +1 -0
  33. package/dist/server/games-api.js +189 -0
  34. package/dist/server/games-api.js.map +1 -0
  35. package/dist/server/lan-address.d.ts +2 -0
  36. package/dist/server/lan-address.d.ts.map +1 -0
  37. package/dist/server/lan-address.js +19 -0
  38. package/dist/server/lan-address.js.map +1 -0
  39. package/dist/server/socket-server.d.ts +3 -1
  40. package/dist/server/socket-server.d.ts.map +1 -1
  41. package/dist/server/socket-server.js +32 -5
  42. package/dist/server/socket-server.js.map +1 -1
  43. package/dist/server/zip-manager.d.ts +24 -0
  44. package/dist/server/zip-manager.d.ts.map +1 -0
  45. package/dist/server/zip-manager.js +99 -0
  46. package/dist/server/zip-manager.js.map +1 -0
  47. package/dist/webapp/App.tsx +3 -1
  48. package/dist/webapp/components/GameSelector.tsx +163 -0
  49. package/dist/webapp/pages/Debug.tsx +3 -1
  50. package/dist/webapp/pages/Play.tsx +143 -94
  51. package/dist/webapp/pages/Preview.tsx +273 -219
  52. package/package.json +1 -1
@@ -0,0 +1,163 @@
1
+ // packages/dev-kit/src/webapp/components/GameSelector.tsx
2
+ import React, { useState, useEffect, useCallback } from 'react';
3
+
4
+ declare const __SOCKET_PORT__: number;
5
+
6
+ interface GameInfo {
7
+ id: string;
8
+ name: string;
9
+ description: string;
10
+ version: string;
11
+ minPlayers: number;
12
+ maxPlayers: number;
13
+ active: boolean;
14
+ }
15
+
16
+ interface GameSelectorProps {
17
+ onGameActivated?: () => void;
18
+ }
19
+
20
+ export default function GameSelector({ onGameActivated }: GameSelectorProps) {
21
+ const [games, setGames] = useState<GameInfo[]>([]);
22
+ const [uploading, setUploading] = useState(false);
23
+ const [error, setError] = useState<string | null>(null);
24
+
25
+ const apiBase = `http://${window.location.hostname}:${window.location.port}`;
26
+
27
+ const fetchGames = useCallback(async () => {
28
+ try {
29
+ const res = await fetch(`${apiBase}/api/games`);
30
+ const data = await res.json();
31
+ setGames(data.games);
32
+ } catch {
33
+ // ignore fetch errors
34
+ }
35
+ }, [apiBase]);
36
+
37
+ useEffect(() => {
38
+ fetchGames();
39
+ const interval = setInterval(fetchGames, 3000);
40
+ return () => clearInterval(interval);
41
+ }, [fetchGames]);
42
+
43
+ const handleUpload = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
44
+ const file = e.target.files?.[0];
45
+ if (!file) return;
46
+
47
+ setUploading(true);
48
+ setError(null);
49
+
50
+ try {
51
+ const buffer = await file.arrayBuffer();
52
+ const res = await fetch(`${apiBase}/api/games/upload`, {
53
+ method: 'POST',
54
+ headers: { 'Content-Type': 'application/octet-stream' },
55
+ body: buffer,
56
+ });
57
+
58
+ if (!res.ok) {
59
+ const data = await res.json();
60
+ throw new Error(data.error || 'Upload failed');
61
+ }
62
+
63
+ await fetchGames();
64
+ } catch (err) {
65
+ setError((err as Error).message);
66
+ } finally {
67
+ setUploading(false);
68
+ // Reset input
69
+ e.target.value = '';
70
+ }
71
+ }, [apiBase, fetchGames]);
72
+
73
+ const activate = useCallback(async (id: string) => {
74
+ try {
75
+ await fetch(`${apiBase}/api/games/${id}/activate`, { method: 'POST' });
76
+ await fetchGames();
77
+ onGameActivated?.();
78
+ } catch (err) {
79
+ setError((err as Error).message);
80
+ }
81
+ }, [apiBase, fetchGames, onGameActivated]);
82
+
83
+ const remove = useCallback(async (id: string) => {
84
+ try {
85
+ await fetch(`${apiBase}/api/games/${id}`, { method: 'DELETE' });
86
+ await fetchGames();
87
+ } catch (err) {
88
+ setError((err as Error).message);
89
+ }
90
+ }, [apiBase, fetchGames]);
91
+
92
+ if (games.length === 0 && !uploading) {
93
+ return (
94
+ <div style={{ background: '#18181b', borderRadius: 8, padding: 12, marginBottom: 16, display: 'flex', alignItems: 'center', gap: 12 }}>
95
+ <span style={{ color: '#71717a', fontSize: 13 }}>No games loaded.</span>
96
+ <label style={{ background: '#d97706', color: '#fff', padding: '4px 12px', borderRadius: 4, fontSize: 13, cursor: 'pointer', fontWeight: 600 }}>
97
+ Upload ZIP
98
+ <input type="file" accept=".zip" onChange={handleUpload} style={{ display: 'none' }} />
99
+ </label>
100
+ {error && <span style={{ color: '#ef4444', fontSize: 12 }}>{error}</span>}
101
+ </div>
102
+ );
103
+ }
104
+
105
+ return (
106
+ <div style={{ background: '#18181b', borderRadius: 8, padding: 12, marginBottom: 16 }}>
107
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
108
+ {games.map((game) => (
109
+ <div
110
+ key={game.id}
111
+ onClick={() => !game.active && activate(game.id)}
112
+ style={{
113
+ display: 'flex',
114
+ alignItems: 'center',
115
+ gap: 8,
116
+ padding: '6px 12px',
117
+ borderRadius: 6,
118
+ cursor: game.active ? 'default' : 'pointer',
119
+ border: game.active ? '2px solid #d97706' : '1px solid #3f3f46',
120
+ background: game.active ? 'rgba(217, 119, 6, 0.15)' : '#27272a',
121
+ fontSize: 13,
122
+ }}
123
+ >
124
+ <img
125
+ src={`/api/games/${game.id}/icon`}
126
+ alt=""
127
+ style={{ width: 24, height: 24, borderRadius: 4 }}
128
+ onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
129
+ />
130
+ <div>
131
+ <div style={{ color: '#e5e5e5', fontWeight: 600 }}>{game.name}</div>
132
+ <div style={{ color: '#71717a', fontSize: 10 }}>v{game.version} · {game.minPlayers}-{game.maxPlayers}p</div>
133
+ </div>
134
+ {!game.active && (
135
+ <button
136
+ onClick={(e) => { e.stopPropagation(); remove(game.id); }}
137
+ style={{ background: 'none', border: 'none', color: '#71717a', cursor: 'pointer', fontSize: 14, padding: '0 2px' }}
138
+ title="Remove"
139
+ >
140
+ ×
141
+ </button>
142
+ )}
143
+ </div>
144
+ ))}
145
+
146
+ <label style={{
147
+ background: '#3f3f46',
148
+ color: '#d4d4d8',
149
+ padding: '6px 12px',
150
+ borderRadius: 6,
151
+ fontSize: 13,
152
+ cursor: uploading ? 'default' : 'pointer',
153
+ fontWeight: 600,
154
+ opacity: uploading ? 0.5 : 1,
155
+ }}>
156
+ {uploading ? 'Uploading...' : '+ Upload'}
157
+ <input type="file" accept=".zip" onChange={handleUpload} disabled={uploading} style={{ display: 'none' }} />
158
+ </label>
159
+ </div>
160
+ {error && <div style={{ color: '#ef4444', fontSize: 12, marginTop: 8 }}>{error}</div>}
161
+ </div>
162
+ );
163
+ }
@@ -1,13 +1,15 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { io, Socket } from 'socket.io-client';
3
3
 
4
+ declare const __SOCKET_PORT__: number;
5
+
4
6
  export default function Debug() {
5
7
  const [socket, setSocket] = useState<Socket | null>(null);
6
8
  const [room, setRoom] = useState<any>({ players: [], phase: 'lobby' });
7
9
  const [fullState, setFullState] = useState<any>(null);
8
10
 
9
11
  useEffect(() => {
10
- const sock = io('http://localhost:4001', { query: { nickname: '__debug__' } });
12
+ const sock = io(`http://${window.location.hostname}:${__SOCKET_PORT__}`, { query: { nickname: '__debug__' } });
11
13
 
12
14
  sock.on('room:update', setRoom);
13
15
  sock.on('game:state', setFullState);
@@ -2,6 +2,10 @@ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'
2
2
  import { io, Socket } from 'socket.io-client';
3
3
  import PhoneFrame from '../components/PhoneFrame';
4
4
  import PlatformTakeover from '../components/PlatformTakeover';
5
+ import GameSelector from '../components/GameSelector';
6
+
7
+ declare const __SOCKET_PORT__: number;
8
+ declare const __DEV_KIT_MODE__: string;
5
9
 
6
10
  const card: React.CSSProperties = { background: '#18181b', borderRadius: 8, padding: 24 };
7
11
  const inputStyle: React.CSSProperties = { width: '100%', background: '#27272a', border: '1px solid #3f3f46', borderRadius: 4, padding: '8px 12px', marginBottom: 16, color: '#e5e5e5', fontSize: 14 };
@@ -16,19 +20,42 @@ export default function Play() {
16
20
  const [myId, setMyId] = useState<string | null>(null);
17
21
  const [GameRenderer, setGameRenderer] = useState<React.ComponentType<any> | null>(null);
18
22
  const [gameResult, setGameResult] = useState<any>(null);
23
+ const [activeGameId, setActiveGameId] = useState<string | null>(null);
19
24
 
20
25
  const isAutoMode = useMemo(() => new URLSearchParams(window.location.search).get('auto') === 'true', []);
21
26
  const [myPlayerId, setMyPlayerId] = useState<string | null>(null);
22
27
  // Ref survives React Fast Refresh (HMR) but not new tabs — perfect for reconnect identity
23
28
  const assignedNicknameRef = useRef<string | null>(null);
24
29
 
25
- // Load renderer
30
+ // Fetch active game ID on mount (play mode only)
26
31
  useEffect(() => {
27
- import('/src/renderer.tsx').then((mod) => {
28
- setGameRenderer(() => mod.default || mod.Renderer);
29
- }).catch(console.error);
32
+ if (__DEV_KIT_MODE__ !== 'play') return;
33
+ const fetchActive = async () => {
34
+ try {
35
+ const res = await fetch(`http://${window.location.hostname}:${window.location.port}/api/games`);
36
+ const data = await res.json();
37
+ if (data.activeGameId) setActiveGameId(data.activeGameId);
38
+ } catch { /* ignore */ }
39
+ };
40
+ fetchActive();
30
41
  }, []);
31
42
 
43
+ // Load renderer
44
+ useEffect(() => {
45
+ if (__DEV_KIT_MODE__ === 'play') {
46
+ if (!activeGameId) return;
47
+ import(/* @vite-ignore */ `virtual:active-game?id=${activeGameId}&t=${Date.now()}`)
48
+ .then((mod) => {
49
+ setGameRenderer(() => mod.Renderer || mod.default);
50
+ })
51
+ .catch(console.error);
52
+ } else {
53
+ import('/src/renderer.tsx').then((mod) => {
54
+ setGameRenderer(() => mod.default || mod.Renderer);
55
+ }).catch(console.error);
56
+ }
57
+ }, [activeGameId]);
58
+
32
59
  // Auto-join: connect immediately with server-assigned name
33
60
  // assignedNicknameRef persists across HMR (React Refresh keeps refs) but resets per new tab
34
61
  useEffect(() => {
@@ -38,7 +65,7 @@ export default function Play() {
38
65
  ? { nickname: assignedNicknameRef.current }
39
66
  : { auto: 'true' };
40
67
 
41
- const sock = io('http://localhost:4001', { query });
68
+ const sock = io(`http://${window.location.hostname}:${__SOCKET_PORT__}`, { query });
42
69
 
43
70
  sock.on('connect', () => {
44
71
  setMyId(sock.id);
@@ -72,7 +99,7 @@ export default function Play() {
72
99
  const join = useCallback(() => {
73
100
  if (!nickname.trim()) return;
74
101
 
75
- const sock = io('http://localhost:4001', { query: { nickname } });
102
+ const sock = io(`http://${window.location.hostname}:${__SOCKET_PORT__}`, { query: { nickname } });
76
103
 
77
104
  sock.on('connect', () => {
78
105
  setMyId(sock.id);
@@ -100,6 +127,16 @@ export default function Play() {
100
127
  setSocket(sock);
101
128
  }, [nickname]);
102
129
 
130
+ const handleGameActivated = useCallback(async () => {
131
+ setGameRenderer(null);
132
+ setGameState(null);
133
+ try {
134
+ const res = await fetch(`http://${window.location.hostname}:${window.location.port}/api/games`);
135
+ const data = await res.json();
136
+ if (data.activeGameId) setActiveGameId(data.activeGameId);
137
+ } catch { /* ignore */ }
138
+ }, []);
139
+
103
140
  const me = room.players.find((p: any) => myPlayerId && p.id === myPlayerId)
104
141
  || room.players.find((p: any) => p.nickname === nickname);
105
142
  const isHost = me?.isHost;
@@ -133,7 +170,10 @@ export default function Play() {
133
170
  if (event === 'stateUpdate') socket.off('game:state', handler as any);
134
171
  },
135
172
  reportResult: () => {},
136
- getAssetUrl: (assetPath: string) => `/assets/${assetPath}`,
173
+ getAssetUrl: (assetPath: string) =>
174
+ __DEV_KIT_MODE__ === 'play'
175
+ ? `/api/games/active/assets/${assetPath}`
176
+ : `/assets/${assetPath}`,
137
177
  getDeviceCapabilities: () => ({ haptics: false, motion: false }),
138
178
  haptic: () => {},
139
179
  onShake: () => () => {},
@@ -143,25 +183,28 @@ export default function Play() {
143
183
 
144
184
  if (!joined && !isAutoMode) {
145
185
  return (
146
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60vh' }}>
147
- <div style={{ ...card, width: 320 }}>
148
- <h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 16 }}>Join Game</h2>
149
- <input
150
- type="text"
151
- placeholder="Your nickname"
152
- value={nickname}
153
- onChange={(e) => setNickname(e.target.value)}
154
- onKeyDown={(e) => e.key === 'Enter' && join()}
155
- className="dk-input"
156
- style={inputStyle}
157
- />
158
- <button
159
- onClick={join}
160
- className="dk-btn-amber"
161
- style={btnAmber}
162
- >
163
- Join
164
- </button>
186
+ <div>
187
+ {__DEV_KIT_MODE__ === 'play' && <GameSelector />}
188
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60vh' }}>
189
+ <div style={{ ...card, width: 320 }}>
190
+ <h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 16 }}>Join Game</h2>
191
+ <input
192
+ type="text"
193
+ placeholder="Your nickname"
194
+ value={nickname}
195
+ onChange={(e) => setNickname(e.target.value)}
196
+ onKeyDown={(e) => e.key === 'Enter' && join()}
197
+ className="dk-input"
198
+ style={inputStyle}
199
+ />
200
+ <button
201
+ onClick={join}
202
+ className="dk-btn-amber"
203
+ style={btnAmber}
204
+ >
205
+ Join
206
+ </button>
207
+ </div>
165
208
  </div>
166
209
  </div>
167
210
  );
@@ -169,48 +212,51 @@ export default function Play() {
169
212
 
170
213
  if (room.phase === 'lobby' || room.phase === 'ready') {
171
214
  return (
172
- <div style={{ maxWidth: 448, margin: '32px auto 0' }}>
173
- <div style={card}>
174
- <h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 16 }}>Lobby</h2>
175
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 24 }}>
176
- {room.players.map((p: any) => (
177
- <div key={p.id} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', background: '#27272a', borderRadius: 4, padding: '8px 12px' }}>
178
- <span>{p.nickname} {p.isHost && '(Host)'}</span>
179
- <span style={{ color: p.ready ? '#4ade80' : '#71717a' }}>
180
- {p.ready ? 'Ready' : 'Not Ready'}
181
- </span>
182
- </div>
183
- ))}
184
- </div>
185
- <div style={{ display: 'flex', gap: 8 }}>
186
- <button
187
- onClick={() => socket?.emit('player:ready', !isReady)}
188
- className={isReady ? 'dk-btn-zinc' : 'dk-btn-green'}
189
- style={{
190
- flex: 1,
191
- padding: '8px 0',
192
- borderRadius: 4,
193
- fontWeight: 600,
194
- border: 'none',
195
- cursor: 'pointer',
196
- fontSize: 14,
197
- ...(isReady
198
- ? { background: '#3f3f46', color: '#d4d4d8' }
199
- : { background: '#16a34a', color: '#fff' }),
200
- }}
201
- >
202
- {isReady ? 'Cancel Ready' : 'Ready'}
203
- </button>
204
- {isHost && (
215
+ <div>
216
+ {__DEV_KIT_MODE__ === 'play' && <GameSelector onGameActivated={handleGameActivated} />}
217
+ <div style={{ maxWidth: 448, margin: '32px auto 0' }}>
218
+ <div style={card}>
219
+ <h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 16 }}>Lobby</h2>
220
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 24 }}>
221
+ {room.players.map((p: any) => (
222
+ <div key={p.id} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', background: '#27272a', borderRadius: 4, padding: '8px 12px' }}>
223
+ <span>{p.nickname} {p.isHost && '(Host)'}</span>
224
+ <span style={{ color: p.ready ? '#4ade80' : '#71717a' }}>
225
+ {p.ready ? 'Ready' : 'Not Ready'}
226
+ </span>
227
+ </div>
228
+ ))}
229
+ </div>
230
+ <div style={{ display: 'flex', gap: 8 }}>
205
231
  <button
206
- onClick={() => socket?.emit('game:start')}
207
- disabled={!room.players.every((p: any) => p.ready) || room.players.length < 2}
208
- className="dk-btn-amber"
209
- style={{ ...btnAmber, flex: 1, width: 'auto' }}
232
+ onClick={() => socket?.emit('player:ready', !isReady)}
233
+ className={isReady ? 'dk-btn-zinc' : 'dk-btn-green'}
234
+ style={{
235
+ flex: 1,
236
+ padding: '8px 0',
237
+ borderRadius: 4,
238
+ fontWeight: 600,
239
+ border: 'none',
240
+ cursor: 'pointer',
241
+ fontSize: 14,
242
+ ...(isReady
243
+ ? { background: '#3f3f46', color: '#d4d4d8' }
244
+ : { background: '#16a34a', color: '#fff' }),
245
+ }}
210
246
  >
211
- Start Game
247
+ {isReady ? 'Cancel Ready' : 'Ready'}
212
248
  </button>
213
- )}
249
+ {isHost && (
250
+ <button
251
+ onClick={() => socket?.emit('game:start')}
252
+ disabled={!room.players.every((p: any) => p.ready) || room.players.length < 2}
253
+ className="dk-btn-amber"
254
+ style={{ ...btnAmber, flex: 1, width: 'auto' }}
255
+ >
256
+ Start Game
257
+ </button>
258
+ )}
259
+ </div>
214
260
  </div>
215
261
  </div>
216
262
  </div>
@@ -219,36 +265,39 @@ export default function Play() {
219
265
 
220
266
  // Playing or ended
221
267
  return (
222
- <div style={{ height: 'calc(100vh - 80px)', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 24 }}>
223
- <PhoneFrame>
224
- {gameOver ? (
225
- <PlatformTakeover
226
- result={gameResult}
227
- players={room.players.map((p: any) => ({ id: p.id, nickname: p.nickname }))}
228
- onReturn={handleReturn}
229
- />
230
- ) : GameRenderer && platform && gameState ? (
231
- <GameRenderer platform={platform} state={gameState} />
232
- ) : (
233
- <div style={{ padding: 16, color: '#71717a' }}>Loading game...</div>
268
+ <div>
269
+ {__DEV_KIT_MODE__ === 'play' && <GameSelector onGameActivated={handleGameActivated} />}
270
+ <div style={{ height: __DEV_KIT_MODE__ === 'play' ? 'calc(100vh - 140px)' : 'calc(100vh - 80px)', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 24 }}>
271
+ <PhoneFrame>
272
+ {gameOver ? (
273
+ <PlatformTakeover
274
+ result={gameResult}
275
+ players={room.players.map((p: any) => ({ id: p.id, nickname: p.nickname }))}
276
+ onReturn={handleReturn}
277
+ />
278
+ ) : GameRenderer && platform && gameState ? (
279
+ <GameRenderer platform={platform} state={gameState} />
280
+ ) : (
281
+ <div style={{ padding: 16, color: '#71717a' }}>Loading game...</div>
282
+ )}
283
+ </PhoneFrame>
284
+ {isHost && (
285
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 10, width: 160 }}>
286
+ <button
287
+ onClick={() => socket?.emit('game:forceReset')}
288
+ style={{ width: '100%', background: '#d97706', color: '#fff', border: 'none', padding: '8px 0', borderRadius: 6, fontWeight: 600, cursor: 'pointer', fontSize: 13 }}
289
+ >
290
+ Reset Game
291
+ </button>
292
+ <button
293
+ onClick={() => socket?.emit('room:kickAll')}
294
+ style={{ width: '100%', background: '#dc2626', color: '#fff', border: 'none', padding: '8px 0', borderRadius: 6, fontWeight: 600, cursor: 'pointer', fontSize: 13 }}
295
+ >
296
+ Kick All Players
297
+ </button>
298
+ </div>
234
299
  )}
235
- </PhoneFrame>
236
- {isHost && (
237
- <div style={{ display: 'flex', flexDirection: 'column', gap: 10, width: 160 }}>
238
- <button
239
- onClick={() => socket?.emit('game:forceReset')}
240
- style={{ width: '100%', background: '#d97706', color: '#fff', border: 'none', padding: '8px 0', borderRadius: 6, fontWeight: 600, cursor: 'pointer', fontSize: 13 }}
241
- >
242
- Reset Game
243
- </button>
244
- <button
245
- onClick={() => socket?.emit('room:kickAll')}
246
- style={{ width: '100%', background: '#dc2626', color: '#fff', border: 'none', padding: '8px 0', borderRadius: 6, fontWeight: 600, cursor: 'pointer', fontSize: 13 }}
247
- >
248
- Kick All Players
249
- </button>
250
- </div>
251
- )}
300
+ </div>
252
301
  </div>
253
302
  );
254
303
  }