@bloopjs/web 0.0.102 → 0.0.103
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/debugui/components/BottomBar.d.ts.map +1 -1
- package/dist/debugui/components/LoadTapeDialog.d.ts.map +1 -1
- package/dist/debugui/components/TopBar.d.ts.map +1 -1
- package/dist/debugui/components/VerticalBar.d.ts +2 -1
- package/dist/debugui/components/VerticalBar.d.ts.map +1 -1
- package/dist/debugui/state.d.ts +4 -1
- package/dist/debugui/state.d.ts.map +1 -1
- package/dist/debugui/styles.d.ts +1 -1
- package/dist/debugui/styles.d.ts.map +1 -1
- package/dist/mod.js +177 -50
- package/dist/mod.js.map +11 -11
- package/package.json +3 -3
- package/src/debugui/components/BottomBar.tsx +28 -6
- package/src/debugui/components/LoadTapeDialog.tsx +55 -29
- package/src/debugui/components/Root.tsx +14 -4
- package/src/debugui/components/TopBar.tsx +11 -1
- package/src/debugui/components/VerticalBar.tsx +17 -1
- package/src/debugui/state.ts +52 -11
- package/src/debugui/styles.ts +53 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bloopjs/web",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.103",
|
|
4
4
|
"author": "Neil Sarkar",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"typescript": "^5"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@bloopjs/bloop": "0.0.
|
|
41
|
-
"@bloopjs/engine": "0.0.
|
|
40
|
+
"@bloopjs/bloop": "0.0.103",
|
|
41
|
+
"@bloopjs/engine": "0.0.103",
|
|
42
42
|
"@preact/signals": "^1.3.1",
|
|
43
43
|
"partysocket": "^1.1.6",
|
|
44
44
|
"preact": "^10.25.4"
|
|
@@ -6,6 +6,11 @@ import { LoadTapeDialog } from "./LoadTapeDialog.tsx";
|
|
|
6
6
|
const iconProps = { width: 14, height: 14, viewBox: "0 0 24 24", fill: "currentColor" };
|
|
7
7
|
|
|
8
8
|
const Icons = {
|
|
9
|
+
record: (
|
|
10
|
+
<svg {...iconProps}>
|
|
11
|
+
<circle cx="12" cy="12" r="8" />
|
|
12
|
+
</svg>
|
|
13
|
+
),
|
|
9
14
|
jumpBack: (
|
|
10
15
|
<svg {...iconProps}>
|
|
11
16
|
<path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z" />
|
|
@@ -171,15 +176,32 @@ export function BottomBar() {
|
|
|
171
176
|
debugState.onSaveTape.value?.();
|
|
172
177
|
}, []);
|
|
173
178
|
|
|
179
|
+
const handleToggleRecording = useCallback(() => {
|
|
180
|
+
debugState.onToggleRecording.value?.();
|
|
181
|
+
}, []);
|
|
182
|
+
|
|
183
|
+
const stopPropagation = useCallback((e: { stopPropagation: () => void }) => {
|
|
184
|
+
e.stopPropagation();
|
|
185
|
+
}, []);
|
|
186
|
+
|
|
174
187
|
return (
|
|
175
|
-
<div
|
|
188
|
+
<div
|
|
189
|
+
className="bottom-bar"
|
|
190
|
+
onMouseDown={stopPropagation}
|
|
191
|
+
onMouseUp={stopPropagation}
|
|
192
|
+
onClick={stopPropagation}
|
|
193
|
+
>
|
|
176
194
|
<div className="playbar-controls">
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
195
|
+
<button
|
|
196
|
+
className={`playbar-btn record-btn ${isRecording ? "recording" : ""}`}
|
|
197
|
+
onClick={handleToggleRecording}
|
|
198
|
+
>
|
|
199
|
+
{Icons.record}
|
|
200
|
+
{isRecording && <span className="btn-label">REC</span>}
|
|
201
|
+
<span className="tooltip tooltip-left">
|
|
202
|
+
{isRecording ? "Stop recording" : "Start recording"}
|
|
181
203
|
</span>
|
|
182
|
-
|
|
204
|
+
</button>
|
|
183
205
|
{isReplaying && (
|
|
184
206
|
<span className="replay-indicator" title="Replaying tape">
|
|
185
207
|
REPLAY
|
|
@@ -7,7 +7,6 @@ export function LoadTapeDialog() {
|
|
|
7
7
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
8
8
|
|
|
9
9
|
const isOpen = debugState.isLoadDialogOpen.value;
|
|
10
|
-
const lastTapeName = debugState.lastTapeName.value;
|
|
11
10
|
|
|
12
11
|
// Sync dialog open/close with signal
|
|
13
12
|
useEffect(() => {
|
|
@@ -77,36 +76,63 @@ export function LoadTapeDialog() {
|
|
|
77
76
|
debugState.onReplayLastTape.value?.();
|
|
78
77
|
}, []);
|
|
79
78
|
|
|
79
|
+
const handleReplayLastSaved = useCallback(() => {
|
|
80
|
+
debugState.onReplayLastSaved.value?.();
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
// Close dialog when clicking on backdrop
|
|
84
|
+
const handleDialogClick = useCallback(
|
|
85
|
+
(e: { target: EventTarget | null; currentTarget: EventTarget | null }) => {
|
|
86
|
+
// If click target is the dialog itself (backdrop), close it
|
|
87
|
+
if (e.target === e.currentTarget) {
|
|
88
|
+
debugState.isLoadDialogOpen.value = false;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
[],
|
|
92
|
+
);
|
|
93
|
+
|
|
80
94
|
return (
|
|
81
|
-
<dialog
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
<dialog
|
|
96
|
+
ref={dialogRef}
|
|
97
|
+
className="load-tape-dialog"
|
|
98
|
+
onClose={handleClose}
|
|
99
|
+
onClick={handleDialogClick}
|
|
100
|
+
>
|
|
101
|
+
{isOpen && (
|
|
102
|
+
<div className="load-tape-dialog-content">
|
|
103
|
+
<h3>Load Tape</h3>
|
|
104
|
+
<div
|
|
105
|
+
className={`drop-zone ${isDragOver ? "drag-over" : ""}`}
|
|
106
|
+
onClick={handleDropZoneClick}
|
|
107
|
+
onDragOver={handleDragOver}
|
|
108
|
+
onDragLeave={handleDragLeave}
|
|
109
|
+
onDrop={handleDrop}
|
|
110
|
+
>
|
|
111
|
+
<span className="drop-zone-text">
|
|
112
|
+
Drop .bloop file here
|
|
113
|
+
<br />
|
|
114
|
+
or click to browse
|
|
115
|
+
</span>
|
|
116
|
+
</div>
|
|
117
|
+
<input
|
|
118
|
+
ref={fileInputRef}
|
|
119
|
+
type="file"
|
|
120
|
+
accept=".bloop"
|
|
121
|
+
className="hidden-file-input"
|
|
122
|
+
onChange={handleFileInputChange}
|
|
123
|
+
/>
|
|
124
|
+
{debugState.lastSavedTapeName.value && (
|
|
125
|
+
<button className="replay-last-btn" onClick={handleReplayLastSaved}>
|
|
126
|
+
Replay last saved tape
|
|
127
|
+
</button>
|
|
128
|
+
)}
|
|
129
|
+
{debugState.lastTapeName.value && (
|
|
130
|
+
<button className="replay-last-btn" onClick={handleReplayLast}>
|
|
131
|
+
Replay last loaded: {debugState.lastTapeName.value}
|
|
132
|
+
</button>
|
|
133
|
+
)}
|
|
96
134
|
</div>
|
|
97
|
-
|
|
98
|
-
ref={fileInputRef}
|
|
99
|
-
type="file"
|
|
100
|
-
accept=".bloop"
|
|
101
|
-
className="hidden-file-input"
|
|
102
|
-
onChange={handleFileInputChange}
|
|
103
|
-
/>
|
|
104
|
-
{lastTapeName && (
|
|
105
|
-
<button className="replay-last-btn" onClick={handleReplayLast}>
|
|
106
|
-
Replay last: {lastTapeName}
|
|
107
|
-
</button>
|
|
108
|
-
)}
|
|
109
|
-
</div>
|
|
135
|
+
)}
|
|
110
136
|
</dialog>
|
|
111
137
|
);
|
|
112
138
|
}
|
|
@@ -61,10 +61,10 @@ function LetterboxedLayout({ canvas }: { canvas: HTMLCanvasElement }) {
|
|
|
61
61
|
const snapshotSize = debugState.snapshotSize.value;
|
|
62
62
|
const hmrFlash = debugState.hmrFlash.value;
|
|
63
63
|
|
|
64
|
-
// Left bar: frame advantage (online) or frame time
|
|
64
|
+
// Left bar: frame advantage (online) or frame time (offline)
|
|
65
65
|
const leftValue = isOnline ? Math.abs(advantage) : frameTime;
|
|
66
66
|
const leftMax = isOnline ? 10 : 16.67; // 10 frames advantage or 16.67ms budget
|
|
67
|
-
const leftLabel = isOnline ? "
|
|
67
|
+
const leftLabel = isOnline ? "adv" : "time";
|
|
68
68
|
const leftColor = isOnline
|
|
69
69
|
? advantage >= 0
|
|
70
70
|
? "#4a9eff"
|
|
@@ -72,12 +72,20 @@ function LetterboxedLayout({ canvas }: { canvas: HTMLCanvasElement }) {
|
|
|
72
72
|
: frameTime > 16.67
|
|
73
73
|
? "#ff4a4a"
|
|
74
74
|
: "#4aff4a";
|
|
75
|
+
const leftDisplayValue = isOnline
|
|
76
|
+
? `${advantage >= 0 ? "+" : ""}${advantage} frames`
|
|
77
|
+
: `${frameTime.toFixed(1)}ms`;
|
|
75
78
|
|
|
76
79
|
// Right bar: rollback depth (online) or snapshot size (offline)
|
|
77
80
|
// For now, we don't have rollback depth exposed, so use a placeholder
|
|
78
81
|
const rightValue = isOnline ? 0 : snapshotSize;
|
|
79
|
-
const rightMax = isOnline ? 10 : 10000; //
|
|
80
|
-
const rightLabel = isOnline ? "
|
|
82
|
+
const rightMax = isOnline ? 10 : 10000; // 10 frames rollback or 10KB
|
|
83
|
+
const rightLabel = isOnline ? "rb" : "size";
|
|
84
|
+
const rightDisplayValue = isOnline
|
|
85
|
+
? "0 frames"
|
|
86
|
+
: snapshotSize >= 1000
|
|
87
|
+
? `${(snapshotSize / 1000).toFixed(1)}kb`
|
|
88
|
+
: `${snapshotSize}b`;
|
|
81
89
|
|
|
82
90
|
const gameClassName = hmrFlash ? "letterboxed-game hmr-flash" : "letterboxed-game";
|
|
83
91
|
|
|
@@ -89,6 +97,7 @@ function LetterboxedLayout({ canvas }: { canvas: HTMLCanvasElement }) {
|
|
|
89
97
|
max={leftMax}
|
|
90
98
|
side="left"
|
|
91
99
|
color={leftColor}
|
|
100
|
+
displayValue={leftDisplayValue}
|
|
92
101
|
/>
|
|
93
102
|
<div className={gameClassName}>
|
|
94
103
|
<GameCanvas canvas={canvas} />
|
|
@@ -97,6 +106,7 @@ function LetterboxedLayout({ canvas }: { canvas: HTMLCanvasElement }) {
|
|
|
97
106
|
value={rightValue}
|
|
98
107
|
max={rightMax}
|
|
99
108
|
side="right"
|
|
109
|
+
displayValue={rightDisplayValue}
|
|
100
110
|
/>
|
|
101
111
|
<BottomBar />
|
|
102
112
|
</main>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useCallback } from "preact/hooks";
|
|
1
2
|
import { debugState } from "../state.ts";
|
|
2
3
|
|
|
3
4
|
type TopBarProps = {
|
|
@@ -11,8 +12,17 @@ export function TopBar({ leftLabel, rightLabel }: TopBarProps) {
|
|
|
11
12
|
const rtt = debugState.netStatus.value.rtt;
|
|
12
13
|
const isOnline = debugState.netStatus.value.peers.length > 0;
|
|
13
14
|
|
|
15
|
+
const stopPropagation = useCallback((e: { stopPropagation: () => void }) => {
|
|
16
|
+
e.stopPropagation();
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
14
19
|
return (
|
|
15
|
-
<div
|
|
20
|
+
<div
|
|
21
|
+
className="top-bar"
|
|
22
|
+
onMouseDown={stopPropagation}
|
|
23
|
+
onMouseUp={stopPropagation}
|
|
24
|
+
onClick={stopPropagation}
|
|
25
|
+
>
|
|
16
26
|
<span className="top-bar-side-label">{leftLabel}</span>
|
|
17
27
|
<div className="top-bar-center">
|
|
18
28
|
<div className="top-bar-item">
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { useCallback } from "preact/hooks";
|
|
2
|
+
|
|
1
3
|
type VerticalBarProps = {
|
|
2
4
|
value: number;
|
|
3
5
|
max: number;
|
|
4
6
|
side: "left" | "right";
|
|
5
7
|
color?: string;
|
|
8
|
+
displayValue?: string;
|
|
6
9
|
};
|
|
7
10
|
|
|
8
11
|
export function VerticalBar({
|
|
@@ -10,16 +13,29 @@ export function VerticalBar({
|
|
|
10
13
|
max,
|
|
11
14
|
side,
|
|
12
15
|
color = "#4a9eff",
|
|
16
|
+
displayValue,
|
|
13
17
|
}: VerticalBarProps) {
|
|
14
18
|
const percentage = max > 0 ? Math.min(100, (value / max) * 100) : 0;
|
|
15
19
|
|
|
20
|
+
const stopPropagation = useCallback((e: { stopPropagation: () => void }) => {
|
|
21
|
+
e.stopPropagation();
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
16
24
|
return (
|
|
17
|
-
<div
|
|
25
|
+
<div
|
|
26
|
+
className={`${side}-bar`}
|
|
27
|
+
onMouseDown={stopPropagation}
|
|
28
|
+
onMouseUp={stopPropagation}
|
|
29
|
+
onClick={stopPropagation}
|
|
30
|
+
>
|
|
18
31
|
<div className="vertical-bar">
|
|
19
32
|
<div
|
|
20
33
|
className="vertical-bar-fill"
|
|
21
34
|
style={{ height: `${percentage}%`, background: color }}
|
|
22
35
|
/>
|
|
36
|
+
{displayValue && (
|
|
37
|
+
<span className={`vertical-bar-popover ${side}`}>{displayValue}</span>
|
|
38
|
+
)}
|
|
23
39
|
</div>
|
|
24
40
|
</div>
|
|
25
41
|
);
|
package/src/debugui/state.ts
CHANGED
|
@@ -58,9 +58,13 @@ export type DebugState = {
|
|
|
58
58
|
// Tape loading/saving
|
|
59
59
|
onLoadTape: Signal<((bytes: Uint8Array, fileName: string) => void) | null>;
|
|
60
60
|
onReplayLastTape: Signal<(() => void) | null>;
|
|
61
|
+
onReplayLastSaved: Signal<(() => void) | null>;
|
|
61
62
|
onSaveTape: Signal<(() => void) | null>;
|
|
62
63
|
lastTapeName: Signal<string | null>;
|
|
64
|
+
lastSavedTapeName: Signal<string | null>;
|
|
63
65
|
isLoadDialogOpen: Signal<boolean>;
|
|
66
|
+
// Recording toggle
|
|
67
|
+
onToggleRecording: Signal<(() => void) | null>;
|
|
64
68
|
};
|
|
65
69
|
|
|
66
70
|
const layoutMode = signal<LayoutMode>("off");
|
|
@@ -97,10 +101,15 @@ const onSeek = signal<((position: number) => void) | null>(null);
|
|
|
97
101
|
// Tape loading/saving
|
|
98
102
|
const onLoadTape = signal<((bytes: Uint8Array, fileName: string) => void) | null>(null);
|
|
99
103
|
const onReplayLastTape = signal<(() => void) | null>(null);
|
|
104
|
+
const onReplayLastSaved = signal<(() => void) | null>(null);
|
|
100
105
|
const onSaveTape = signal<(() => void) | null>(null);
|
|
101
106
|
const lastTapeName = signal<string | null>(null);
|
|
107
|
+
const lastSavedTapeName = signal<string | null>(null);
|
|
102
108
|
const isLoadDialogOpen = signal(false);
|
|
103
109
|
|
|
110
|
+
// Recording toggle
|
|
111
|
+
const onToggleRecording = signal<(() => void) | null>(null);
|
|
112
|
+
|
|
104
113
|
export const debugState: DebugState = {
|
|
105
114
|
/** Layout mode: off, letterboxed, or full */
|
|
106
115
|
layoutMode,
|
|
@@ -152,9 +161,14 @@ export const debugState: DebugState = {
|
|
|
152
161
|
/** Tape loading/saving */
|
|
153
162
|
onLoadTape,
|
|
154
163
|
onReplayLastTape,
|
|
164
|
+
onReplayLastSaved,
|
|
155
165
|
onSaveTape,
|
|
156
166
|
lastTapeName,
|
|
167
|
+
lastSavedTapeName,
|
|
157
168
|
isLoadDialogOpen,
|
|
169
|
+
|
|
170
|
+
/** Recording toggle */
|
|
171
|
+
onToggleRecording,
|
|
158
172
|
};
|
|
159
173
|
|
|
160
174
|
/** Cycle through layout modes: off -> letterboxed -> full -> off */
|
|
@@ -314,12 +328,20 @@ export function wirePlaybarHandlers(app: App): void {
|
|
|
314
328
|
};
|
|
315
329
|
debugState.onSeek.value = (ratio: number) => {
|
|
316
330
|
if (app.sim.hasHistory) {
|
|
331
|
+
app.sim.pause();
|
|
317
332
|
const startFrame = debugState.tapeStartFrame.value;
|
|
318
333
|
const frameCount = debugState.tapeFrameCount.value;
|
|
319
334
|
const targetFrame = startFrame + Math.floor(ratio * frameCount);
|
|
320
335
|
app.sim.seek(targetFrame);
|
|
321
336
|
}
|
|
322
337
|
};
|
|
338
|
+
debugState.onToggleRecording.value = () => {
|
|
339
|
+
if (app.sim.isRecording) {
|
|
340
|
+
app.sim.stopRecording();
|
|
341
|
+
} else {
|
|
342
|
+
app.sim.record();
|
|
343
|
+
}
|
|
344
|
+
};
|
|
323
345
|
}
|
|
324
346
|
|
|
325
347
|
/** Set up drag-and-drop tape loading on a canvas element */
|
|
@@ -342,7 +364,8 @@ export function wireTapeDragDrop(canvas: HTMLCanvasElement, app: App): void {
|
|
|
342
364
|
// IndexedDB helpers for tape persistence
|
|
343
365
|
const TAPE_DB_NAME = "bloop-debug";
|
|
344
366
|
const TAPE_STORE_NAME = "tapes";
|
|
345
|
-
const
|
|
367
|
+
const TAPE_KEY_LOADED = "last-loaded";
|
|
368
|
+
const TAPE_KEY_SAVED = "last-saved";
|
|
346
369
|
|
|
347
370
|
function openTapeDB(): Promise<IDBDatabase> {
|
|
348
371
|
return new Promise((resolve, reject) => {
|
|
@@ -358,17 +381,18 @@ function openTapeDB(): Promise<IDBDatabase> {
|
|
|
358
381
|
async function saveTapeToStorage(
|
|
359
382
|
bytes: Uint8Array,
|
|
360
383
|
fileName: string,
|
|
384
|
+
key: string = TAPE_KEY_LOADED,
|
|
361
385
|
): Promise<void> {
|
|
362
386
|
const db = await openTapeDB();
|
|
363
387
|
return new Promise((resolve, reject) => {
|
|
364
388
|
const tx = db.transaction(TAPE_STORE_NAME, "readwrite");
|
|
365
|
-
tx.objectStore(TAPE_STORE_NAME).put({ bytes, fileName },
|
|
389
|
+
tx.objectStore(TAPE_STORE_NAME).put({ bytes, fileName }, key);
|
|
366
390
|
tx.oncomplete = () => resolve();
|
|
367
391
|
tx.onerror = () => reject(tx.error);
|
|
368
392
|
});
|
|
369
393
|
}
|
|
370
394
|
|
|
371
|
-
async function loadTapeFromStorage(): Promise<{
|
|
395
|
+
async function loadTapeFromStorage(key: string = TAPE_KEY_LOADED): Promise<{
|
|
372
396
|
bytes: Uint8Array;
|
|
373
397
|
fileName: string;
|
|
374
398
|
} | null> {
|
|
@@ -376,7 +400,7 @@ async function loadTapeFromStorage(): Promise<{
|
|
|
376
400
|
const db = await openTapeDB();
|
|
377
401
|
return new Promise((resolve, reject) => {
|
|
378
402
|
const tx = db.transaction(TAPE_STORE_NAME, "readonly");
|
|
379
|
-
const request = tx.objectStore(TAPE_STORE_NAME).get(
|
|
403
|
+
const request = tx.objectStore(TAPE_STORE_NAME).get(key);
|
|
380
404
|
request.onsuccess = () => resolve(request.result ?? null);
|
|
381
405
|
request.onerror = () => reject(request.error);
|
|
382
406
|
});
|
|
@@ -385,37 +409,54 @@ async function loadTapeFromStorage(): Promise<{
|
|
|
385
409
|
}
|
|
386
410
|
}
|
|
387
411
|
|
|
388
|
-
/** Check for saved
|
|
412
|
+
/** Check for saved tapes and update signals */
|
|
389
413
|
export async function checkForSavedTape(): Promise<void> {
|
|
390
|
-
const saved = await
|
|
391
|
-
|
|
414
|
+
const [loaded, saved] = await Promise.all([
|
|
415
|
+
loadTapeFromStorage(TAPE_KEY_LOADED),
|
|
416
|
+
loadTapeFromStorage(TAPE_KEY_SAVED),
|
|
417
|
+
]);
|
|
418
|
+
debugState.lastTapeName.value = loaded?.fileName ?? null;
|
|
419
|
+
debugState.lastSavedTapeName.value = saved?.fileName ?? null;
|
|
392
420
|
}
|
|
393
421
|
|
|
394
422
|
/** Wire up tape loading handlers */
|
|
395
423
|
export function wireTapeLoadHandlers(app: App): void {
|
|
396
424
|
debugState.onLoadTape.value = async (bytes: Uint8Array, fileName: string) => {
|
|
397
425
|
app.loadTape(bytes);
|
|
398
|
-
await saveTapeToStorage(bytes, fileName);
|
|
426
|
+
await saveTapeToStorage(bytes, fileName, TAPE_KEY_LOADED);
|
|
399
427
|
debugState.lastTapeName.value = fileName;
|
|
400
428
|
debugState.isLoadDialogOpen.value = false;
|
|
401
429
|
};
|
|
402
430
|
|
|
403
431
|
debugState.onReplayLastTape.value = async () => {
|
|
404
|
-
const saved = await loadTapeFromStorage();
|
|
432
|
+
const saved = await loadTapeFromStorage(TAPE_KEY_LOADED);
|
|
433
|
+
if (saved) {
|
|
434
|
+
app.loadTape(saved.bytes);
|
|
435
|
+
debugState.isLoadDialogOpen.value = false;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
debugState.onReplayLastSaved.value = async () => {
|
|
440
|
+
const saved = await loadTapeFromStorage(TAPE_KEY_SAVED);
|
|
405
441
|
if (saved) {
|
|
406
442
|
app.loadTape(saved.bytes);
|
|
407
443
|
debugState.isLoadDialogOpen.value = false;
|
|
408
444
|
}
|
|
409
445
|
};
|
|
410
446
|
|
|
411
|
-
debugState.onSaveTape.value = () => {
|
|
447
|
+
debugState.onSaveTape.value = async () => {
|
|
412
448
|
if (!app.sim.hasHistory) return;
|
|
413
449
|
const tape = app.sim.saveTape();
|
|
450
|
+
const fileName = `tape-${Date.now()}.bloop`;
|
|
451
|
+
// Persist to IndexedDB for later replay
|
|
452
|
+
await saveTapeToStorage(tape, fileName, TAPE_KEY_SAVED);
|
|
453
|
+
debugState.lastSavedTapeName.value = fileName;
|
|
454
|
+
// Download
|
|
414
455
|
const blob = new Blob([tape], { type: "application/octet-stream" });
|
|
415
456
|
const url = URL.createObjectURL(blob);
|
|
416
457
|
const a = document.createElement("a");
|
|
417
458
|
a.href = url;
|
|
418
|
-
a.download =
|
|
459
|
+
a.download = fileName;
|
|
419
460
|
a.click();
|
|
420
461
|
URL.revokeObjectURL(url);
|
|
421
462
|
};
|
package/src/debugui/styles.ts
CHANGED
|
@@ -160,6 +160,7 @@ export const styles = /*css*/ `
|
|
|
160
160
|
font-family: monospace;
|
|
161
161
|
font-size: 12px;
|
|
162
162
|
padding: 0;
|
|
163
|
+
user-select: none;
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
.top-bar-side-label {
|
|
@@ -213,6 +214,7 @@ export const styles = /*css*/ `
|
|
|
213
214
|
justify-content: flex-end;
|
|
214
215
|
background: #111;
|
|
215
216
|
padding: 4px 0;
|
|
217
|
+
user-select: none;
|
|
216
218
|
}
|
|
217
219
|
|
|
218
220
|
.right-bar {
|
|
@@ -223,6 +225,7 @@ export const styles = /*css*/ `
|
|
|
223
225
|
justify-content: flex-end;
|
|
224
226
|
background: #111;
|
|
225
227
|
padding: 4px 0;
|
|
228
|
+
user-select: none;
|
|
226
229
|
}
|
|
227
230
|
|
|
228
231
|
.vertical-bar {
|
|
@@ -231,7 +234,6 @@ export const styles = /*css*/ `
|
|
|
231
234
|
background: #333;
|
|
232
235
|
border-radius: 2px;
|
|
233
236
|
position: relative;
|
|
234
|
-
overflow: hidden;
|
|
235
237
|
}
|
|
236
238
|
|
|
237
239
|
.vertical-bar-fill {
|
|
@@ -244,6 +246,37 @@ export const styles = /*css*/ `
|
|
|
244
246
|
transition: height 0.1s ease-out;
|
|
245
247
|
}
|
|
246
248
|
|
|
249
|
+
.vertical-bar-popover {
|
|
250
|
+
position: absolute;
|
|
251
|
+
top: 50%;
|
|
252
|
+
transform: translateY(-50%);
|
|
253
|
+
background: #222;
|
|
254
|
+
color: #ccc;
|
|
255
|
+
padding: 4px 8px;
|
|
256
|
+
border-radius: 4px;
|
|
257
|
+
font-size: 10px;
|
|
258
|
+
font-family: monospace;
|
|
259
|
+
white-space: nowrap;
|
|
260
|
+
opacity: 0;
|
|
261
|
+
visibility: hidden;
|
|
262
|
+
transition: opacity 0.15s;
|
|
263
|
+
pointer-events: none;
|
|
264
|
+
z-index: 10;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.vertical-bar-popover.left {
|
|
268
|
+
left: calc(100% + 8px);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.vertical-bar-popover.right {
|
|
272
|
+
right: calc(100% + 8px);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.vertical-bar:hover .vertical-bar-popover {
|
|
276
|
+
opacity: 1;
|
|
277
|
+
visibility: visible;
|
|
278
|
+
}
|
|
279
|
+
|
|
247
280
|
|
|
248
281
|
.bottom-bar {
|
|
249
282
|
grid-area: bottom-bar;
|
|
@@ -253,6 +286,7 @@ export const styles = /*css*/ `
|
|
|
253
286
|
/* Mobile-first: more padding */
|
|
254
287
|
padding: 0 16px;
|
|
255
288
|
gap: 12px;
|
|
289
|
+
user-select: none;
|
|
256
290
|
}
|
|
257
291
|
|
|
258
292
|
/* Desktop: tighter padding */
|
|
@@ -270,22 +304,16 @@ export const styles = /*css*/ `
|
|
|
270
304
|
flex-shrink: 0;
|
|
271
305
|
}
|
|
272
306
|
|
|
273
|
-
/*
|
|
274
|
-
.
|
|
275
|
-
|
|
276
|
-
align-items: center;
|
|
277
|
-
margin-right: 4px;
|
|
307
|
+
/* Record button */
|
|
308
|
+
.record-btn {
|
|
309
|
+
color: #666;
|
|
278
310
|
}
|
|
279
311
|
|
|
280
|
-
.
|
|
281
|
-
|
|
312
|
+
.record-btn.recording {
|
|
313
|
+
color: #ff4444;
|
|
282
314
|
}
|
|
283
315
|
|
|
284
|
-
.recording
|
|
285
|
-
width: 10px;
|
|
286
|
-
height: 10px;
|
|
287
|
-
background: #ff4444;
|
|
288
|
-
border-radius: 50%;
|
|
316
|
+
.record-btn.recording svg {
|
|
289
317
|
animation: recording-pulse 1s ease-in-out infinite;
|
|
290
318
|
}
|
|
291
319
|
|
|
@@ -294,6 +322,17 @@ export const styles = /*css*/ `
|
|
|
294
322
|
50% { opacity: 0.4; }
|
|
295
323
|
}
|
|
296
324
|
|
|
325
|
+
/* Desktop: show REC label when recording */
|
|
326
|
+
@media (min-width: 769px) {
|
|
327
|
+
.record-btn.recording {
|
|
328
|
+
width: auto;
|
|
329
|
+
padding: 0 6px;
|
|
330
|
+
gap: 4px;
|
|
331
|
+
background: rgba(255, 68, 68, 0.15);
|
|
332
|
+
border-radius: 3px;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
297
336
|
/* Replay indicator - mobile: hidden */
|
|
298
337
|
.replay-indicator {
|
|
299
338
|
display: none;
|
|
@@ -455,6 +494,7 @@ export const styles = /*css*/ `
|
|
|
455
494
|
position: relative;
|
|
456
495
|
cursor: pointer;
|
|
457
496
|
overflow: hidden;
|
|
497
|
+
user-select: none;
|
|
458
498
|
}
|
|
459
499
|
|
|
460
500
|
/* Desktop: smaller seek bar */
|