@littlepartytime/dev-kit 1.17.0 → 1.18.1
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/dist/webapp/App.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import Preview from './pages/Preview';
|
|
3
3
|
import Play from './pages/Play';
|
|
4
4
|
import Debug from './pages/Debug';
|
|
5
|
+
import { captureScreen } from './utils/captureScreen';
|
|
5
6
|
|
|
6
7
|
type Page = 'preview' | 'play' | 'debug';
|
|
7
8
|
|
|
@@ -13,6 +14,13 @@ export default function App() {
|
|
|
13
14
|
return 'preview';
|
|
14
15
|
});
|
|
15
16
|
|
|
17
|
+
// Expose capture API for LLM/Playwright callers:
|
|
18
|
+
// await page.evaluate(() => window.__devkit__.captureScreen())
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
(window as any).__devkit__ = { captureScreen };
|
|
21
|
+
return () => { delete (window as any).__devkit__; };
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
16
24
|
return (
|
|
17
25
|
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
18
26
|
{/* Nav */}
|
|
@@ -84,6 +84,8 @@ export default function PhoneFrame({ children }: { children: React.ReactNode })
|
|
|
84
84
|
contain:paint makes this the containing block for
|
|
85
85
|
position:fixed elements inside the game. */}
|
|
86
86
|
<div
|
|
87
|
+
id="devkit-game-screen"
|
|
88
|
+
data-testid="game-screen"
|
|
87
89
|
style={{
|
|
88
90
|
position: 'absolute',
|
|
89
91
|
overflow: 'hidden',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
2
2
|
import PhoneFrame from '../components/PhoneFrame';
|
|
3
3
|
import PlatformTakeover from '../components/PlatformTakeover';
|
|
4
|
+
import { captureScreen, downloadScreenshot } from '../utils/captureScreen';
|
|
4
5
|
|
|
5
6
|
const PLAYER_NAMES = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve', 'Frank', 'Grace', 'Heidi'];
|
|
6
7
|
|
|
@@ -23,6 +24,7 @@ export default function Preview() {
|
|
|
23
24
|
const [gameOver, setGameOver] = useState(false);
|
|
24
25
|
const [gameResult, setGameResult] = useState<any>(null);
|
|
25
26
|
const [stateJson, setStateJson] = useState('');
|
|
27
|
+
const [capturing, setCapturing] = useState(false);
|
|
26
28
|
|
|
27
29
|
// Refs to avoid recreating platform on every state change
|
|
28
30
|
const fullStateRef = useRef(fullState);
|
|
@@ -202,10 +204,23 @@ export default function Preview() {
|
|
|
202
204
|
return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
|
|
203
205
|
}, []);
|
|
204
206
|
|
|
207
|
+
const handleScreenshot = useCallback(async () => {
|
|
208
|
+
if (capturing) return;
|
|
209
|
+
setCapturing(true);
|
|
210
|
+
try {
|
|
211
|
+
const dataUrl = await captureScreen();
|
|
212
|
+
downloadScreenshot(dataUrl);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.error('[DevKit] Screenshot failed:', err);
|
|
215
|
+
} finally {
|
|
216
|
+
setCapturing(false);
|
|
217
|
+
}
|
|
218
|
+
}, [capturing]);
|
|
219
|
+
|
|
205
220
|
return (
|
|
206
221
|
<div style={{ display: 'flex', gap: 16, height: 'calc(100vh - 80px)' }}>
|
|
207
222
|
{/* Renderer — half the screen width */}
|
|
208
|
-
<div style={{ width: '50%', height: '100%' }}>
|
|
223
|
+
<div style={{ width: '50%', height: '100%', position: 'relative' }}>
|
|
209
224
|
<PhoneFrame>
|
|
210
225
|
{gameOver && gameResult ? (
|
|
211
226
|
<PlatformTakeover result={gameResult} players={mockPlayers} onReturn={resetGame} />
|
|
@@ -217,6 +232,36 @@ export default function Preview() {
|
|
|
217
232
|
</div>
|
|
218
233
|
)}
|
|
219
234
|
</PhoneFrame>
|
|
235
|
+
{/* Screenshot button — floats below the centered phone */}
|
|
236
|
+
<button
|
|
237
|
+
onClick={handleScreenshot}
|
|
238
|
+
disabled={capturing}
|
|
239
|
+
title="Capture game screen (without phone frame)"
|
|
240
|
+
style={{
|
|
241
|
+
position: 'absolute',
|
|
242
|
+
bottom: 12,
|
|
243
|
+
left: '50%',
|
|
244
|
+
transform: 'translateX(-50%)',
|
|
245
|
+
display: 'flex',
|
|
246
|
+
alignItems: 'center',
|
|
247
|
+
gap: 6,
|
|
248
|
+
padding: '5px 14px',
|
|
249
|
+
borderRadius: 6,
|
|
250
|
+
border: '1px solid #3f3f46',
|
|
251
|
+
background: capturing ? '#27272a' : '#18181b',
|
|
252
|
+
color: capturing ? '#71717a' : '#a1a1aa',
|
|
253
|
+
fontSize: 13,
|
|
254
|
+
cursor: capturing ? 'default' : 'pointer',
|
|
255
|
+
transition: 'background 0.15s, color 0.15s',
|
|
256
|
+
zIndex: 10,
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
259
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
260
|
+
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
|
|
261
|
+
<circle cx="12" cy="13" r="4"/>
|
|
262
|
+
</svg>
|
|
263
|
+
{capturing ? 'Capturing...' : 'Screenshot'}
|
|
264
|
+
</button>
|
|
220
265
|
</div>
|
|
221
266
|
|
|
222
267
|
{/* Control Panel — fills remaining width, resizable two-column */}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import html2canvas from 'html2canvas';
|
|
2
|
+
|
|
3
|
+
// Must match PhoneFrame constants: SCREEN_W=390, SAFE_AREA_TOP=59, SAFE_AREA_BOTTOM=34, SCREEN_H=844
|
|
4
|
+
const CAPTURE_W = 390;
|
|
5
|
+
const CAPTURE_H = 751; // 844 - 59 - 34
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Captures the game content area (safe area only, no phone bezel).
|
|
9
|
+
* Returns a base64-encoded PNG data URL.
|
|
10
|
+
*
|
|
11
|
+
* Also exposed on window.__devkit__.captureScreen() for LLM/Playwright callers:
|
|
12
|
+
* await page.evaluate(() => window.__devkit__.captureScreen())
|
|
13
|
+
*
|
|
14
|
+
* ## Why the style normalization?
|
|
15
|
+
* The PhoneFrame has two CSS properties that break html2canvas:
|
|
16
|
+
*
|
|
17
|
+
* 1. `contain: paint` on Screen div and safe-area div — html2canvas uses
|
|
18
|
+
* getBoundingClientRect() to determine clip rects. With a scaled ancestor,
|
|
19
|
+
* getBoundingClientRect() returns the *visual* (post-transform) size. Combined
|
|
20
|
+
* with contain:paint, html2canvas clips the rendered output to the visual height
|
|
21
|
+
* (e.g. 375px at 0.5× scale) even though the canvas is 751px tall, leaving the
|
|
22
|
+
* bottom half black.
|
|
23
|
+
*
|
|
24
|
+
* 2. `transform: scale(x)` on the phone body — causes the same getBoundingClientRect
|
|
25
|
+
* mismatch; the element's intrinsic size is 390×751 but its visual rect is smaller.
|
|
26
|
+
*
|
|
27
|
+
* Fix: temporarily set both to neutral values, wait two rAF ticks for the browser to
|
|
28
|
+
* apply the changes, run html2canvas, then restore. An invisible overlay prevents the
|
|
29
|
+
* user from seeing the brief layout shift.
|
|
30
|
+
*/
|
|
31
|
+
export async function captureScreen(): Promise<string> {
|
|
32
|
+
const el = document.getElementById('devkit-game-screen');
|
|
33
|
+
if (!el) throw new Error('[devkit] #devkit-game-screen not found — is the Preview page active?');
|
|
34
|
+
|
|
35
|
+
// Invisible overlay so the user never sees the brief style normalization flash.
|
|
36
|
+
const overlay = document.createElement('div');
|
|
37
|
+
overlay.style.cssText = 'position:fixed;inset:0;z-index:99999;pointer-events:none;';
|
|
38
|
+
document.body.appendChild(overlay);
|
|
39
|
+
|
|
40
|
+
// Wait for overlay to paint before touching any styles.
|
|
41
|
+
await new Promise<void>(r => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
42
|
+
|
|
43
|
+
// Walk up the DOM and neutralise contain:paint and transform:scale on all ancestors.
|
|
44
|
+
const saved: Array<[HTMLElement, 'contain' | 'transform', string]> = [];
|
|
45
|
+
let node: HTMLElement | null = el;
|
|
46
|
+
while (node && node !== document.body) {
|
|
47
|
+
for (const prop of ['contain', 'transform'] as const) {
|
|
48
|
+
if (node.style[prop]) {
|
|
49
|
+
saved.push([node, prop, node.style[prop]]);
|
|
50
|
+
node.style[prop] = prop === 'transform' ? 'none' : '';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
node = node.parentElement;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Wait for the browser to reflow/repaint with the normalised styles.
|
|
57
|
+
await new Promise<void>(r => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const canvas = await html2canvas(el, {
|
|
61
|
+
width: CAPTURE_W,
|
|
62
|
+
height: CAPTURE_H,
|
|
63
|
+
scale: 2,
|
|
64
|
+
logging: false,
|
|
65
|
+
backgroundColor: null,
|
|
66
|
+
useCORS: true,
|
|
67
|
+
allowTaint: true,
|
|
68
|
+
});
|
|
69
|
+
return canvas.toDataURL('image/png');
|
|
70
|
+
} finally {
|
|
71
|
+
// Restore styles and remove overlay regardless of success/failure.
|
|
72
|
+
for (const [node, prop, val] of saved) node.style[prop] = val;
|
|
73
|
+
document.body.removeChild(overlay);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function downloadScreenshot(dataUrl: string): void {
|
|
78
|
+
const a = document.createElement('a');
|
|
79
|
+
a.href = dataUrl;
|
|
80
|
+
a.download = `game-screenshot-${Date.now()}.png`;
|
|
81
|
+
document.body.appendChild(a);
|
|
82
|
+
a.click();
|
|
83
|
+
document.body.removeChild(a);
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@littlepartytime/dev-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.1",
|
|
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",
|
|
@@ -23,8 +23,14 @@
|
|
|
23
23
|
"test": "vitest run",
|
|
24
24
|
"test:watch": "vitest"
|
|
25
25
|
},
|
|
26
|
-
"files": [
|
|
27
|
-
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"keywords": [
|
|
30
|
+
"boardgame",
|
|
31
|
+
"dev-tools",
|
|
32
|
+
"littlepartytime"
|
|
33
|
+
],
|
|
28
34
|
"license": "MIT",
|
|
29
35
|
"repository": {
|
|
30
36
|
"type": "git",
|
|
@@ -44,6 +50,7 @@
|
|
|
44
50
|
"archiver": "^7",
|
|
45
51
|
"chokidar": "^4",
|
|
46
52
|
"express": "^5",
|
|
53
|
+
"html2canvas": "^1.4.1",
|
|
47
54
|
"react": "^19",
|
|
48
55
|
"react-dom": "^19",
|
|
49
56
|
"socket.io": "^4",
|