@bloopjs/web 0.0.91 → 0.0.92
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/state.d.ts +1 -0
- 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 +214 -26
- package/dist/mod.js.map +7 -7
- package/package.json +3 -3
- package/src/debugui/components/BottomBar.tsx +15 -5
- package/src/debugui/state.ts +18 -3
- package/src/debugui/styles.ts +173 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bloopjs/web",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.92",
|
|
4
4
|
"author": "Neil Sarkar",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"typescript": "^5"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@bloopjs/bloop": "0.0.
|
|
37
|
-
"@bloopjs/engine": "0.0.
|
|
36
|
+
"@bloopjs/bloop": "0.0.92",
|
|
37
|
+
"@bloopjs/engine": "0.0.92",
|
|
38
38
|
"@preact/signals": "^1.3.1",
|
|
39
39
|
"partysocket": "^1.1.6",
|
|
40
40
|
"preact": "^10.25.4"
|
|
@@ -119,39 +119,49 @@ export function BottomBar() {
|
|
|
119
119
|
debugState.isLoadDialogOpen.value = true;
|
|
120
120
|
}, []);
|
|
121
121
|
|
|
122
|
+
const handleSaveTapeClick = useCallback(() => {
|
|
123
|
+
debugState.onSaveTape.value?.();
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
122
126
|
return (
|
|
123
127
|
<div className="bottom-bar">
|
|
124
128
|
<div className="playbar-controls">
|
|
125
|
-
<button className="playbar-btn" {...jumpBackRepeat}>
|
|
129
|
+
<button className="playbar-btn jump-back" {...jumpBackRepeat}>
|
|
126
130
|
{"<<"}
|
|
127
131
|
<span className="tooltip tooltip-left">
|
|
128
132
|
Jump back <kbd>4</kbd>
|
|
129
133
|
</span>
|
|
130
134
|
</button>
|
|
131
|
-
<button className="playbar-btn" {...stepBackRepeat}>
|
|
135
|
+
<button className="playbar-btn step-back" {...stepBackRepeat}>
|
|
132
136
|
{"<"}
|
|
133
137
|
<span className="tooltip">
|
|
134
138
|
Step back <kbd>5</kbd>
|
|
135
139
|
</span>
|
|
136
140
|
</button>
|
|
137
|
-
<button className="playbar-btn" onClick={handlePlayPause}>
|
|
141
|
+
<button className="playbar-btn play-pause" onClick={handlePlayPause}>
|
|
138
142
|
{isPlaying ? "||" : ">"}
|
|
139
143
|
<span className="tooltip">
|
|
140
144
|
{isPlaying ? "Pause" : "Play"} <kbd>6</kbd>
|
|
141
145
|
</span>
|
|
142
146
|
</button>
|
|
143
|
-
<button className="playbar-btn" {...stepForwardRepeat}>
|
|
147
|
+
<button className="playbar-btn step-forward" {...stepForwardRepeat}>
|
|
144
148
|
{">"}
|
|
145
149
|
<span className="tooltip">
|
|
146
150
|
Step forward <kbd>7</kbd>
|
|
147
151
|
</span>
|
|
148
152
|
</button>
|
|
149
|
-
<button className="playbar-btn" {...jumpForwardRepeat}>
|
|
153
|
+
<button className="playbar-btn jump-forward" {...jumpForwardRepeat}>
|
|
150
154
|
{">>"}
|
|
151
155
|
<span className="tooltip">
|
|
152
156
|
Jump forward <kbd>8</kbd>
|
|
153
157
|
</span>
|
|
154
158
|
</button>
|
|
159
|
+
<button className="playbar-btn save-tape-btn" onClick={handleSaveTapeClick}>
|
|
160
|
+
Save
|
|
161
|
+
<span className="tooltip">
|
|
162
|
+
Save tape <kbd>Cmd+S</kbd>
|
|
163
|
+
</span>
|
|
164
|
+
</button>
|
|
155
165
|
<button className="playbar-btn load-tape-btn" onClick={handleLoadTapeClick}>
|
|
156
166
|
Load
|
|
157
167
|
<span className="tooltip">Load tape</span>
|
package/src/debugui/state.ts
CHANGED
|
@@ -53,9 +53,10 @@ export type DebugState = {
|
|
|
53
53
|
onStepForward: Signal<(() => void) | null>;
|
|
54
54
|
onJumpForward: Signal<(() => void) | null>;
|
|
55
55
|
onSeek: Signal<((position: number) => void) | null>;
|
|
56
|
-
// Tape loading
|
|
56
|
+
// Tape loading/saving
|
|
57
57
|
onLoadTape: Signal<((bytes: Uint8Array, fileName: string) => void) | null>;
|
|
58
58
|
onReplayLastTape: Signal<(() => void) | null>;
|
|
59
|
+
onSaveTape: Signal<(() => void) | null>;
|
|
59
60
|
lastTapeName: Signal<string | null>;
|
|
60
61
|
isLoadDialogOpen: Signal<boolean>;
|
|
61
62
|
};
|
|
@@ -89,9 +90,10 @@ const onStepForward = signal<(() => void) | null>(null);
|
|
|
89
90
|
const onJumpForward = signal<(() => void) | null>(null);
|
|
90
91
|
const onSeek = signal<((position: number) => void) | null>(null);
|
|
91
92
|
|
|
92
|
-
// Tape loading
|
|
93
|
+
// Tape loading/saving
|
|
93
94
|
const onLoadTape = signal<((bytes: Uint8Array, fileName: string) => void) | null>(null);
|
|
94
95
|
const onReplayLastTape = signal<(() => void) | null>(null);
|
|
96
|
+
const onSaveTape = signal<(() => void) | null>(null);
|
|
95
97
|
const lastTapeName = signal<string | null>(null);
|
|
96
98
|
const isLoadDialogOpen = signal(false);
|
|
97
99
|
|
|
@@ -141,9 +143,10 @@ export const debugState: DebugState = {
|
|
|
141
143
|
onJumpForward,
|
|
142
144
|
onSeek,
|
|
143
145
|
|
|
144
|
-
/** Tape loading */
|
|
146
|
+
/** Tape loading/saving */
|
|
145
147
|
onLoadTape,
|
|
146
148
|
onReplayLastTape,
|
|
149
|
+
onSaveTape,
|
|
147
150
|
lastTapeName,
|
|
148
151
|
isLoadDialogOpen,
|
|
149
152
|
};
|
|
@@ -397,6 +400,18 @@ export function wireTapeLoadHandlers(app: App): void {
|
|
|
397
400
|
}
|
|
398
401
|
};
|
|
399
402
|
|
|
403
|
+
debugState.onSaveTape.value = () => {
|
|
404
|
+
if (!app.sim.hasHistory) return;
|
|
405
|
+
const tape = app.sim.saveTape();
|
|
406
|
+
const blob = new Blob([tape], { type: "application/octet-stream" });
|
|
407
|
+
const url = URL.createObjectURL(blob);
|
|
408
|
+
const a = document.createElement("a");
|
|
409
|
+
a.href = url;
|
|
410
|
+
a.download = `tape-${Date.now()}.bloop`;
|
|
411
|
+
a.click();
|
|
412
|
+
URL.revokeObjectURL(url);
|
|
413
|
+
};
|
|
414
|
+
|
|
400
415
|
// Check for saved tape on init
|
|
401
416
|
checkForSavedTape();
|
|
402
417
|
}
|
package/src/debugui/styles.ts
CHANGED
|
@@ -4,10 +4,26 @@ export const styles = /*css*/ `
|
|
|
4
4
|
box-sizing: border-box;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
/* Mobile-first CSS variables */
|
|
8
|
+
:host {
|
|
9
|
+
--bar-size: 10vw;
|
|
10
|
+
--bar-size-h: 10vh;
|
|
11
|
+
--bar-size-h: 10dvh;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Desktop overrides */
|
|
15
|
+
@media (min-width: 769px) {
|
|
16
|
+
:host {
|
|
17
|
+
--bar-size: 2vw;
|
|
18
|
+
--bar-size-h: 2vh;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
7
22
|
/* Layout */
|
|
8
23
|
.fullscreen {
|
|
9
24
|
width: 100vw;
|
|
10
25
|
height: 100vh;
|
|
26
|
+
height: 100dvh;
|
|
11
27
|
margin: 0;
|
|
12
28
|
padding: 0;
|
|
13
29
|
overflow: hidden;
|
|
@@ -24,17 +40,96 @@ export const styles = /*css*/ `
|
|
|
24
40
|
display: block;
|
|
25
41
|
}
|
|
26
42
|
|
|
43
|
+
/* Mobile-first: vertical scroll layout */
|
|
27
44
|
.layout {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
/* Use fixed position on mobile to escape parent overflow:hidden */
|
|
46
|
+
position: fixed;
|
|
47
|
+
top: 0;
|
|
48
|
+
left: 0;
|
|
49
|
+
right: 0;
|
|
50
|
+
bottom: 0;
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-direction: column;
|
|
53
|
+
overflow-y: auto;
|
|
54
|
+
overflow-x: hidden;
|
|
55
|
+
-webkit-overflow-scrolling: touch;
|
|
56
|
+
overscroll-behavior-y: contain;
|
|
57
|
+
padding: 0;
|
|
58
|
+
gap: 0;
|
|
59
|
+
background: #1a1a1a;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.layout .game {
|
|
63
|
+
/* Use dvh with vh fallback for mobile Safari address bar */
|
|
64
|
+
height: 100vh;
|
|
65
|
+
height: 100dvh;
|
|
66
|
+
width: 100%;
|
|
67
|
+
flex-shrink: 0;
|
|
68
|
+
/* Mobile: no border radius, fullscreen game */
|
|
69
|
+
border-radius: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Mobile: stretch canvas to fill game area */
|
|
73
|
+
.layout .game .canvas-container {
|
|
35
74
|
width: 100%;
|
|
36
75
|
height: 100%;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.layout .game .canvas-container canvas {
|
|
79
|
+
width: 100%;
|
|
80
|
+
height: 100%;
|
|
81
|
+
max-width: none;
|
|
82
|
+
max-height: none;
|
|
83
|
+
display: block;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.layout .stats,
|
|
87
|
+
.layout .logs {
|
|
88
|
+
width: 100%;
|
|
89
|
+
min-height: 50vh;
|
|
90
|
+
min-height: 50dvh;
|
|
37
91
|
padding: 1rem;
|
|
92
|
+
flex-shrink: 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Desktop: 2x2 grid layout */
|
|
96
|
+
@media (min-width: 769px) {
|
|
97
|
+
.layout {
|
|
98
|
+
position: static;
|
|
99
|
+
display: grid;
|
|
100
|
+
grid-template-areas:
|
|
101
|
+
"game stats"
|
|
102
|
+
"logs logs";
|
|
103
|
+
grid-template-columns: calc(50% - 0.5rem) calc(50% - 0.5rem);
|
|
104
|
+
grid-template-rows: calc(50% - 0.5rem) calc(50% - 0.5rem);
|
|
105
|
+
gap: 1rem;
|
|
106
|
+
padding: 1rem;
|
|
107
|
+
height: 100%;
|
|
108
|
+
overflow: hidden;
|
|
109
|
+
-webkit-overflow-scrolling: auto;
|
|
110
|
+
overscroll-behavior-y: auto;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.layout .game {
|
|
114
|
+
height: auto;
|
|
115
|
+
flex-shrink: initial;
|
|
116
|
+
border-radius: 8px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Desktop: restore centered canvas with constraints */
|
|
120
|
+
.layout .game .canvas-container canvas {
|
|
121
|
+
width: auto;
|
|
122
|
+
height: auto;
|
|
123
|
+
max-width: 100%;
|
|
124
|
+
max-height: 100%;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.layout .stats,
|
|
128
|
+
.layout .logs {
|
|
129
|
+
min-height: auto;
|
|
130
|
+
padding: 1rem;
|
|
131
|
+
flex-shrink: initial;
|
|
132
|
+
}
|
|
38
133
|
}
|
|
39
134
|
|
|
40
135
|
/* Letterboxed layout - using equal vw/vh percentages keeps game at viewport aspect ratio */
|
|
@@ -44,10 +139,12 @@ export const styles = /*css*/ `
|
|
|
44
139
|
"top-bar top-bar top-bar"
|
|
45
140
|
"left-bar game right-bar"
|
|
46
141
|
"bottom-bar bottom-bar bottom-bar";
|
|
47
|
-
grid-template-columns:
|
|
48
|
-
grid-template-rows:
|
|
142
|
+
grid-template-columns: var(--bar-size) 1fr var(--bar-size);
|
|
143
|
+
grid-template-rows: var(--bar-size-h) 1fr var(--bar-size-h);
|
|
49
144
|
width: 100vw;
|
|
145
|
+
/* Use dvh with vh fallback for mobile Safari address bar */
|
|
50
146
|
height: 100vh;
|
|
147
|
+
height: 100dvh;
|
|
51
148
|
background: #1a1a1a;
|
|
52
149
|
overflow: hidden;
|
|
53
150
|
overscroll-behavior: none;
|
|
@@ -66,7 +163,7 @@ export const styles = /*css*/ `
|
|
|
66
163
|
}
|
|
67
164
|
|
|
68
165
|
.top-bar-side-label {
|
|
69
|
-
width:
|
|
166
|
+
width: var(--bar-size);
|
|
70
167
|
text-align: center;
|
|
71
168
|
font-size: 9px;
|
|
72
169
|
text-transform: uppercase;
|
|
@@ -74,6 +171,17 @@ export const styles = /*css*/ `
|
|
|
74
171
|
color: #666;
|
|
75
172
|
}
|
|
76
173
|
|
|
174
|
+
/* Mobile: larger top bar text */
|
|
175
|
+
@media (max-width: 768px) {
|
|
176
|
+
.top-bar-side-label {
|
|
177
|
+
font-size: 12px;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.top-bar {
|
|
181
|
+
font-size: 14px;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
77
185
|
.top-bar-center {
|
|
78
186
|
display: flex;
|
|
79
187
|
align-items: center;
|
|
@@ -142,8 +250,17 @@ export const styles = /*css*/ `
|
|
|
142
250
|
display: flex;
|
|
143
251
|
align-items: center;
|
|
144
252
|
background: #111;
|
|
145
|
-
|
|
146
|
-
|
|
253
|
+
/* Mobile-first: more padding */
|
|
254
|
+
padding: 0 16px;
|
|
255
|
+
gap: 12px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* Desktop: tighter padding */
|
|
259
|
+
@media (min-width: 769px) {
|
|
260
|
+
.bottom-bar {
|
|
261
|
+
padding: 0 8px;
|
|
262
|
+
gap: 8px;
|
|
263
|
+
}
|
|
147
264
|
}
|
|
148
265
|
|
|
149
266
|
.playbar-controls {
|
|
@@ -153,16 +270,35 @@ export const styles = /*css*/ `
|
|
|
153
270
|
flex-shrink: 0;
|
|
154
271
|
}
|
|
155
272
|
|
|
273
|
+
/* Mobile-first: hide step/jump buttons */
|
|
274
|
+
.playbar-btn.jump-back,
|
|
275
|
+
.playbar-btn.step-back,
|
|
276
|
+
.playbar-btn.step-forward,
|
|
277
|
+
.playbar-btn.jump-forward {
|
|
278
|
+
display: none;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* Desktop: show all controls */
|
|
282
|
+
@media (min-width: 769px) {
|
|
283
|
+
.playbar-btn.jump-back,
|
|
284
|
+
.playbar-btn.step-back,
|
|
285
|
+
.playbar-btn.step-forward,
|
|
286
|
+
.playbar-btn.jump-forward {
|
|
287
|
+
display: flex;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
156
291
|
.playbar-btn {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
min-
|
|
292
|
+
/* Mobile-first: larger buttons */
|
|
293
|
+
width: 4vh;
|
|
294
|
+
height: 4vh;
|
|
295
|
+
min-width: 32px;
|
|
296
|
+
min-height: 32px;
|
|
161
297
|
border: none;
|
|
162
298
|
outline: none;
|
|
163
299
|
background: transparent;
|
|
164
300
|
color: #888;
|
|
165
|
-
font-size:
|
|
301
|
+
font-size: 16px;
|
|
166
302
|
cursor: pointer;
|
|
167
303
|
border-radius: 2px;
|
|
168
304
|
display: flex;
|
|
@@ -172,6 +308,17 @@ export const styles = /*css*/ `
|
|
|
172
308
|
position: relative;
|
|
173
309
|
}
|
|
174
310
|
|
|
311
|
+
/* Desktop: smaller buttons */
|
|
312
|
+
@media (min-width: 769px) {
|
|
313
|
+
.playbar-btn {
|
|
314
|
+
width: 1.5vh;
|
|
315
|
+
height: 1.5vh;
|
|
316
|
+
min-width: 18px;
|
|
317
|
+
min-height: 18px;
|
|
318
|
+
font-size: 10px;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
175
322
|
.playbar-btn:hover {
|
|
176
323
|
background: #333;
|
|
177
324
|
color: #fff;
|
|
@@ -215,7 +362,8 @@ export const styles = /*css*/ `
|
|
|
215
362
|
|
|
216
363
|
.seek-bar {
|
|
217
364
|
flex: 1;
|
|
218
|
-
|
|
365
|
+
/* Mobile-first: larger seek bar */
|
|
366
|
+
height: 32px;
|
|
219
367
|
background: #222;
|
|
220
368
|
border-radius: 4px;
|
|
221
369
|
position: relative;
|
|
@@ -223,6 +371,13 @@ export const styles = /*css*/ `
|
|
|
223
371
|
overflow: hidden;
|
|
224
372
|
}
|
|
225
373
|
|
|
374
|
+
/* Desktop: smaller seek bar */
|
|
375
|
+
@media (min-width: 769px) {
|
|
376
|
+
.seek-bar {
|
|
377
|
+
height: 16px;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
226
381
|
.seek-bar-fill {
|
|
227
382
|
position: absolute;
|
|
228
383
|
top: 0;
|