@littlepartytime/dev-kit 1.7.0 → 1.9.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.
@@ -41,7 +41,7 @@ export default function PhoneFrame({ children }: { children: React.ReactNode })
41
41
  <div
42
42
  ref={containerRef}
43
43
  className="flex items-center justify-center h-full overflow-hidden"
44
- style={{ flex: 1, minWidth: 0, minHeight: 0 }}
44
+ style={{ minWidth: 0, minHeight: 0 }}
45
45
  >
46
46
  {/* Wrapper sized to the scaled phone for correct layout flow */}
47
47
  <div style={{ width: BODY_W * scale, height: BODY_H * scale, flexShrink: 0 }}>
@@ -160,126 +160,174 @@ export default function Preview() {
160
160
  }
161
161
  }, [playerCount, playerIndex]);
162
162
 
163
+ // Resizable split panel
164
+ const [splitRatio, setSplitRatio] = useState(0.6); // left column gets 60%
165
+ const panelRef = useRef<HTMLDivElement>(null);
166
+ const dragging = useRef(false);
167
+
168
+ useEffect(() => {
169
+ const onMove = (e: MouseEvent) => {
170
+ if (!dragging.current || !panelRef.current) return;
171
+ const rect = panelRef.current.getBoundingClientRect();
172
+ const ratio = (e.clientX - rect.left) / rect.width;
173
+ setSplitRatio(Math.min(0.8, Math.max(0.2, ratio)));
174
+ };
175
+ const onUp = () => { dragging.current = false; document.body.style.cursor = ''; document.body.style.userSelect = ''; };
176
+ window.addEventListener('mousemove', onMove);
177
+ window.addEventListener('mouseup', onUp);
178
+ return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
179
+ }, []);
180
+
163
181
  return (
164
182
  <div className="flex gap-4 h-[calc(100vh-80px)]">
165
- {/* Renderer */}
166
- <PhoneFrame>
167
- {GameRenderer && platform && viewState ? (
168
- <GameRenderer platform={platform} state={viewState} />
169
- ) : (
170
- <div className="p-4 text-zinc-500">
171
- {!engine ? 'Loading engine...' : 'Initializing game...'}
172
- </div>
173
- )}
174
- </PhoneFrame>
183
+ {/* Renderer — fixed width matching the phone body */}
184
+ <div className="shrink-0 h-full" style={{ width: 420 }}>
185
+ <PhoneFrame>
186
+ {GameRenderer && platform && viewState ? (
187
+ <GameRenderer platform={platform} state={viewState} />
188
+ ) : (
189
+ <div className="p-4 text-zinc-500">
190
+ {!engine ? 'Loading engine...' : 'Initializing game...'}
191
+ </div>
192
+ )}
193
+ </PhoneFrame>
194
+ </div>
175
195
 
176
- {/* Control Panel */}
177
- <div className="w-80 flex flex-col gap-4 overflow-auto">
178
- {/* Player Count */}
179
- <div className="bg-zinc-900 rounded-lg p-3">
180
- <h3 className="text-sm font-bold text-zinc-400 mb-2">Player Count</h3>
181
- <input
182
- type="number"
183
- min={2}
184
- max={32}
185
- value={playerCount}
186
- onChange={(e) => setPlayerCount(Math.max(2, Math.min(32, Number(e.target.value))))}
187
- className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-sm"
188
- />
189
- </div>
196
+ {/* Control Panel — fills remaining width, resizable two-column */}
197
+ <div ref={panelRef} className="flex-1 min-w-0 flex h-full">
198
+ {/* Left column: Players & Controls */}
199
+ <div className="flex flex-col gap-4 overflow-auto pr-1" style={{ width: `${splitRatio * 100}%` }}>
200
+ {/* Player Count */}
201
+ <div className="bg-zinc-900 rounded-lg p-3">
202
+ <h3 className="text-sm font-bold text-zinc-400 mb-2">Player Count</h3>
203
+ <input
204
+ type="number"
205
+ min={2}
206
+ max={32}
207
+ value={playerCount}
208
+ onChange={(e) => setPlayerCount(Math.max(2, Math.min(32, Number(e.target.value))))}
209
+ className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-sm"
210
+ />
211
+ </div>
190
212
 
191
- {/* Player Switcher */}
192
- <div className="bg-zinc-900 rounded-lg p-3">
193
- <h3 className="text-sm font-bold text-zinc-400 mb-2">Current Player</h3>
194
- <div className="space-y-1">
195
- {mockPlayers.map((p, i) => {
196
- const isActive = i === playerIndex;
197
- const hue = (i * 137) % 360; // deterministic color per player
198
- return (
199
- <button
200
- key={p.id}
201
- onClick={() => setPlayerIndex(i)}
202
- className={`w-full flex items-center gap-2 px-2 py-1.5 rounded text-sm text-left transition-colors ${
203
- isActive
204
- ? 'bg-amber-600/20 ring-1 ring-amber-500 text-white'
205
- : 'bg-zinc-800 hover:bg-zinc-700 text-zinc-300'
206
- }`}
207
- >
208
- {/* Avatar */}
209
- <div
210
- className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold shrink-0"
211
- style={{ background: `hsl(${hue}, 55%, 45%)` }}
213
+ {/* Player Switcher */}
214
+ <div className="bg-zinc-900 rounded-lg p-3 flex-1 overflow-auto">
215
+ <h3 className="text-sm font-bold text-zinc-400 mb-2">Current Player</h3>
216
+ <div className="space-y-1">
217
+ {mockPlayers.map((p, i) => {
218
+ const isActive = i === playerIndex;
219
+ const hue = (i * 137) % 360; // deterministic color per player
220
+ const playerState = fullState?.players?.find((ps: any) => ps.id === p.id);
221
+ const ROLE_KEYS = ['role', 'character', 'team', 'class', 'job', 'faction', 'type'];
222
+ const roleEntry = playerState
223
+ ? Object.entries(playerState).find(([k]) => ROLE_KEYS.includes(k.toLowerCase()))
224
+ : undefined;
225
+ return (
226
+ <button
227
+ key={p.id}
228
+ onClick={() => setPlayerIndex(i)}
229
+ className={`w-full flex items-center gap-2 px-2 py-1.5 rounded text-sm text-left transition-colors ${
230
+ isActive
231
+ ? 'bg-amber-600/20 ring-1 ring-amber-500 text-white'
232
+ : 'bg-zinc-800 hover:bg-zinc-700 text-zinc-300'
233
+ }`}
212
234
  >
213
- {p.nickname[0]}
214
- </div>
215
- <span className="truncate">{p.nickname}</span>
216
- {p.isHost && (
217
- <span className="ml-auto text-[10px] text-amber-400 shrink-0">HOST</span>
218
- )}
219
- </button>
220
- );
221
- })}
235
+ {/* Avatar */}
236
+ <div
237
+ className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold shrink-0"
238
+ style={{ background: `hsl(${hue}, 55%, 45%)` }}
239
+ >
240
+ {p.nickname[0]}
241
+ </div>
242
+ <div className="min-w-0 flex-1">
243
+ <div className="flex items-center gap-1">
244
+ <span className="truncate">{p.nickname}</span>
245
+ {p.isHost && (
246
+ <span className="text-[10px] text-amber-400 shrink-0">HOST</span>
247
+ )}
248
+ </div>
249
+ {roleEntry && (
250
+ <div className="text-[10px] text-zinc-500 truncate">
251
+ {String(roleEntry[1])}
252
+ </div>
253
+ )}
254
+ </div>
255
+ </button>
256
+ );
257
+ })}
258
+ </div>
222
259
  </div>
260
+
261
+ {/* Game Result */}
262
+ {gameOver && gameResult && (
263
+ <div className="bg-zinc-900 rounded-lg p-3">
264
+ <h3 className="text-sm font-bold text-green-400 mb-2">Game Over</h3>
265
+ <pre className="text-xs font-mono bg-zinc-800 rounded p-2 overflow-auto max-h-32 whitespace-pre-wrap">
266
+ {JSON.stringify(gameResult, null, 2)}
267
+ </pre>
268
+ <button
269
+ onClick={resetGame}
270
+ className="mt-2 w-full bg-amber-600 hover:bg-amber-500 text-white px-3 py-1 rounded text-sm"
271
+ >
272
+ Reset Game
273
+ </button>
274
+ </div>
275
+ )}
276
+
277
+ {/* Reset button (when game is not over) */}
278
+ {!gameOver && engine && (
279
+ <div className="bg-zinc-900 rounded-lg p-3">
280
+ <button
281
+ onClick={resetGame}
282
+ className="w-full bg-zinc-700 hover:bg-zinc-600 text-white px-3 py-1 rounded text-sm"
283
+ >
284
+ Reset Game
285
+ </button>
286
+ </div>
287
+ )}
223
288
  </div>
224
289
 
225
- {/* Game Result */}
226
- {gameOver && gameResult && (
227
- <div className="bg-zinc-900 rounded-lg p-3">
228
- <h3 className="text-sm font-bold text-green-400 mb-2">Game Over</h3>
229
- <pre className="text-xs font-mono bg-zinc-800 rounded p-2 overflow-auto max-h-32 whitespace-pre-wrap">
230
- {JSON.stringify(gameResult, null, 2)}
231
- </pre>
232
- <button
233
- onClick={resetGame}
234
- className="mt-2 w-full bg-amber-600 hover:bg-amber-500 text-white px-3 py-1 rounded text-sm"
235
- >
236
- Reset Game
237
- </button>
238
- </div>
239
- )}
290
+ {/* Drag handle */}
291
+ <div
292
+ className="shrink-0 w-2 cursor-col-resize flex items-center justify-center group"
293
+ onMouseDown={() => { dragging.current = true; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }}
294
+ >
295
+ <div className="w-0.5 h-8 bg-zinc-700 rounded group-hover:bg-zinc-500 transition-colors" />
296
+ </div>
240
297
 
241
- {/* Reset button (when game is not over) */}
242
- {!gameOver && engine && (
243
- <div className="bg-zinc-900 rounded-lg p-3">
298
+ {/* Right column: State & Logs */}
299
+ <div className="flex flex-col gap-4 overflow-auto pl-1" style={{ width: `${(1 - splitRatio) * 100}%` }}>
300
+ {/* State Editor */}
301
+ <div className="bg-zinc-900 rounded-lg p-3 flex-1 flex flex-col min-h-0">
302
+ <h3 className="text-sm font-bold text-zinc-400 mb-2">Game State (Full)</h3>
303
+ <textarea
304
+ value={stateJson}
305
+ onChange={(e) => setStateJson(e.target.value)}
306
+ className="flex-1 bg-zinc-800 border border-zinc-700 rounded p-2 font-mono text-xs resize-none min-h-[120px]"
307
+ />
244
308
  <button
245
- onClick={resetGame}
246
- className="w-full bg-zinc-700 hover:bg-zinc-600 text-white px-3 py-1 rounded text-sm"
309
+ onClick={applyState}
310
+ className="mt-2 bg-amber-600 hover:bg-amber-500 text-white px-3 py-1 rounded text-sm"
247
311
  >
248
- Reset Game
312
+ Apply State
249
313
  </button>
250
314
  </div>
251
- )}
252
-
253
- {/* State Editor */}
254
- <div className="bg-zinc-900 rounded-lg p-3 flex-1 flex flex-col min-h-0">
255
- <h3 className="text-sm font-bold text-zinc-400 mb-2">Game State (Full)</h3>
256
- <textarea
257
- value={stateJson}
258
- onChange={(e) => setStateJson(e.target.value)}
259
- className="flex-1 bg-zinc-800 border border-zinc-700 rounded p-2 font-mono text-xs resize-none min-h-[120px]"
260
- />
261
- <button
262
- onClick={applyState}
263
- className="mt-2 bg-amber-600 hover:bg-amber-500 text-white px-3 py-1 rounded text-sm"
264
- >
265
- Apply State
266
- </button>
267
- </div>
268
315
 
269
- {/* Action Log */}
270
- <div className="bg-zinc-900 rounded-lg p-3 h-48 overflow-auto">
271
- <h3 className="text-sm font-bold text-zinc-400 mb-2">Action Log</h3>
272
- {actions.length === 0 ? (
273
- <p className="text-zinc-500 text-xs">No actions yet</p>
274
- ) : (
275
- <div className="space-y-1">
276
- {actions.map((a, i) => (
277
- <div key={i} className="text-xs font-mono bg-zinc-800 rounded p-1">
278
- <span className="text-amber-400">{a.player}</span>: {JSON.stringify(a.action)}
279
- </div>
280
- ))}
281
- </div>
282
- )}
316
+ {/* Action Log */}
317
+ <div className="bg-zinc-900 rounded-lg p-3 h-48 overflow-auto">
318
+ <h3 className="text-sm font-bold text-zinc-400 mb-2">Action Log</h3>
319
+ {actions.length === 0 ? (
320
+ <p className="text-zinc-500 text-xs">No actions yet</p>
321
+ ) : (
322
+ <div className="space-y-1">
323
+ {actions.map((a, i) => (
324
+ <div key={i} className="text-xs font-mono bg-zinc-800 rounded p-1">
325
+ <span className="text-amber-400">{a.player}</span>: {JSON.stringify(a.action)}
326
+ </div>
327
+ ))}
328
+ </div>
329
+ )}
330
+ </div>
283
331
  </div>
284
332
  </div>
285
333
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@littlepartytime/dev-kit",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Development toolkit CLI for Little Party Time game developers",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",