@bloopjs/web 0.0.91 → 0.0.93

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bloopjs/web",
3
- "version": "0.0.91",
3
+ "version": "0.0.93",
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.91",
37
- "@bloopjs/engine": "0.0.91",
36
+ "@bloopjs/bloop": "0.0.93",
37
+ "@bloopjs/engine": "0.0.93",
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>
@@ -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
  }
@@ -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
- display: grid;
29
- grid-template-areas:
30
- "game stats"
31
- "logs logs";
32
- grid-template-columns: calc(50% - 0.5rem) calc(50% - 0.5rem);
33
- grid-template-rows: calc(50% - 0.5rem) calc(50% - 0.5rem);
34
- gap: 1rem;
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: 2vw 1fr 2vw;
48
- grid-template-rows: 2vh 1fr 2vh;
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: 2vw;
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
- padding: 0 8px;
146
- gap: 8px;
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
- width: 1.5vh;
158
- height: 1.5vh;
159
- min-width: 18px;
160
- min-height: 18px;
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: 10px;
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
- height: 16px;
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;