authorgravatar for aiden.bai05@gmail.comAiden Bai <aiden.bai05@gmail.com> 2026-05-03 01:06:54-07:00
committergravatar for me@theo.liTheo Browne <me@theo.li> 2026-06-13 22:18:37-07:00
logd6eb386e3d3a1cb114f7e00ab42c5b8e6b63c181
treeb6d36106baad27edd611f8f9635d4d47e0b34a8c
parentd25090cf6d2f334668bf1cba75b6ce3d78beb237

planning


2 files changed, 3429 insertions(+), 0 deletions(-)

404.html created+1600
......@@ -0,0 +1,1600 @@
1<html dir="ltr" lang="en"><head>
2 <meta charset="utf-8">
3 <meta name="color-scheme" content="light dark">
4 <meta name="theme-color" content="#fff">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0,
6 maximum-scale=1.0, user-scalable=no">
7 <title>asdasddddd.com</title>
8 <style>/* Copyright 2017 The Chromium Authors
9 * Use of this source code is governed by a BSD-style license that can be
10 * found in the LICENSE file. */
11
12a {
13 color: var(--link-color);
14}
15
16body {
17 --background-color: #fff;
18 --error-code-color: var(--google-gray-700);
19 --google-blue-50: rgb(232, 240, 254);
20 --google-blue-100: rgb(210, 227, 252);
21 --google-blue-300: rgb(138, 180, 248);
22 --google-blue-600: rgb(26, 115, 232);
23 --google-blue-700: rgb(25, 103, 210);
24 --google-gray-100: rgb(241, 243, 244);
25 --google-gray-300: rgb(218, 220, 224);
26 --google-gray-500: rgb(154, 160, 166);
27 --google-gray-50: rgb(248, 249, 250);
28 --google-gray-600: rgb(128, 134, 139);
29 --google-gray-700: rgb(95, 99, 104);
30 --google-gray-800: rgb(60, 64, 67);
31 --google-gray-900: rgb(32, 33, 36);
32 --heading-color: var(--google-gray-900);
33 --link-color: rgb(88, 88, 88);
34 --primary-button-fill-color-active: var(--google-blue-700);
35 --primary-button-fill-color: var(--google-blue-600);
36 --primary-button-text-color: #fff;
37 --quiet-background-color: rgb(247, 247, 247);
38 --secondary-button-border-color: var(--google-gray-500);
39 --secondary-button-fill-color: #fff;
40 --secondary-button-hover-border-color: var(--google-gray-600);
41 --secondary-button-hover-fill-color: var(--google-gray-50);
42 --secondary-button-text-color: var(--google-gray-700);
43 --small-link-color: var(--google-gray-700);
44 --text-color: var(--google-gray-700);
45 background: var(--background-color);
46 color: var(--text-color);
47 word-wrap: break-word;
48}
49
50.nav-wrapper .secondary-button {
51 background: var(--secondary-button-fill-color);
52 border: 1px solid var(--secondary-button-border-color);
53 color: var(--secondary-button-text-color);
54 float: none;
55 margin: 0;
56 padding: 8px 14px;
57}
58
59.hidden {
60 display: none;
61}
62
63html {
64 -webkit-text-size-adjust: 100%;
65 font-size: 125%;
66}
67
68.icon {
69 background-repeat: no-repeat;
70 background-size: 100%;
71}
72
73@media (prefers-color-scheme: dark) {
74 body {
75 --background-color: var(--google-gray-900);
76 --error-code-color: var(--google-gray-500);
77 --heading-color: var(--google-gray-500);
78 --link-color: var(--google-blue-300);
79 --primary-button-fill-color-active: rgb(129, 162, 208);
80 --primary-button-fill-color: var(--google-blue-300);
81 --primary-button-text-color: var(--google-gray-900);
82 --quiet-background-color: var(--background-color);
83 --secondary-button-border-color: var(--google-gray-700);
84 --secondary-button-fill-color: var(--google-gray-900);
85 --secondary-button-hover-fill-color: rgb(48, 51, 57);
86 --secondary-button-text-color: var(--google-blue-300);
87 --small-link-color: var(--google-blue-300);
88 --text-color: var(--google-gray-500);
89 }
90}
91</style>
92 <style>/* Copyright 2014 The Chromium Authors
93 Use of this source code is governed by a BSD-style license that can be
94 found in the LICENSE file. */
95
96button {
97 border: 0;
98 border-radius: 8px;
99 box-sizing: border-box;
100 color: var(--primary-button-text-color);
101 cursor: pointer;
102 float: right;
103 font-size: .875em;
104 margin: 0;
105 padding: 8px 14px;
106 transition: box-shadow 150ms cubic-bezier(0.4, 0, 0.2, 1);
107 user-select: none;
108}
109
110[dir='rtl'] button {
111 float: left;
112}
113
114.bad-clock button,
115.captive-portal button,
116.https-only button,
117.insecure-form button,
118.lookalike-url button,
119.main-frame-blocked button,
120.neterror button,
121.pdf button,
122.ssl button,
123.enterprise-block button,
124.enterprise-warn button,
125.managed-profile-required button,
126.safe-browsing-billing button,
127.supervised-user-verify button,
128.supervised-user-verify-subframe button {
129 background: var(--primary-button-fill-color);
130}
131
132button:active {
133 background: var(--primary-button-fill-color-active);
134 outline: 0;
135}
136
137#debugging {
138 display: inline;
139 overflow: auto;
140}
141
142.debugging-content {
143 line-height: 1em;
144 margin-bottom: 0;
145 margin-top: 1em;
146}
147
148.debugging-content-fixed-width {
149 display: block;
150 font-family: monospace;
151 font-size: 1.2em;
152 margin-top: 0.5em;
153}
154
155.debugging-title {
156 font-weight: bold;
157}
158
159#details {
160 margin: 0 0 50px;
161}
162
163#details p:not(:first-of-type) {
164 margin-top: 20px;
165}
166
167.secondary-button:active {
168 border-color: white;
169 box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .3),
170 0 2px 6px 2px rgba(60, 64, 67, .15);
171}
172
173.secondary-button:hover {
174 background: var(--secondary-button-hover-fill-color);
175 border-color: var(--secondary-button-hover-border-color);
176 text-decoration: none;
177}
178
179.error-code {
180 color: var(--error-code-color);
181 font-size: .8em;
182 margin-top: 12px;
183 text-transform: uppercase;
184}
185
186#error-debugging-info {
187 font-size: 0.8em;
188}
189
190h1 {
191 color: var(--heading-color);
192 font-size: 1.6em;
193 font-weight: normal;
194 line-height: 1.25em;
195 margin-bottom: 16px;
196}
197
198h2 {
199 font-size: 1.2em;
200 font-weight: normal;
201}
202
203.icon {
204 height: 72px;
205 margin: 0 0 40px;
206 width: 72px;
207}
208
209input[type=checkbox] {
210 opacity: 0;
211}
212
213input[type=checkbox]:focus ~ .checkbox::after {
214 outline: -webkit-focus-ring-color auto 5px;
215}
216
217.interstitial-wrapper {
218 box-sizing: border-box;
219 font-size: 1em;
220 line-height: 1.6em;
221 margin: 14vh auto 0;
222 max-width: 600px;
223 width: 100%;
224}
225
226#main-message > p {
227 display: inline;
228}
229
230#extended-reporting-opt-in {
231 font-size: .875em;
232 margin-top: 32px;
233}
234
235#extended-reporting-opt-in label {
236 display: grid;
237 grid-template-columns: 1.8em 1fr;
238 position: relative;
239}
240
241#enhanced-protection-message {
242 border-radius: 20px;
243 font-size: 1em;
244 margin-top: 32px;
245 padding: 10px 5px;
246}
247
248#enhanced-protection-message a {
249 color: var(--google-red-10);
250}
251
252#enhanced-protection-message label {
253 display: grid;
254 grid-template-columns: 2.5em 1fr;
255 position: relative;
256}
257
258#enhanced-protection-message div {
259 margin: 0.5em;
260}
261
262#enhanced-protection-message .icon {
263 height: 1.5em;
264 vertical-align: middle;
265 width: 1.5em;
266}
267
268.nav-wrapper {
269 margin-top: 51px;
270}
271
272.nav-wrapper::after {
273 clear: both;
274 content: '';
275 display: table;
276 width: 100%;
277}
278
279.small-link {
280 color: var(--small-link-color);
281 font-size: .875em;
282}
283
284.checkboxes {
285 flex: 0 0 24px;
286}
287
288.checkbox {
289 --padding: .9em;
290 background: transparent;
291 display: block;
292 height: 1em;
293 left: -1em;
294 padding-inline-start: var(--padding);
295 position: absolute;
296 right: 0;
297 top: -.5em;
298 width: 1em;
299}
300
301.checkbox::after {
302 border: 1px solid white;
303 border-radius: 2px;
304 content: '';
305 height: 1em;
306 left: var(--padding);
307 position: absolute;
308 top: var(--padding);
309 width: 1em;
310}
311
312.checkbox::before {
313 background: transparent;
314 border: 2px solid white;
315 border-inline-end-width: 0;
316 border-top-width: 0;
317 content: '';
318 height: .2em;
319 left: calc(.3em + var(--padding));
320 opacity: 0;
321 position: absolute;
322 top: calc(.3em + var(--padding));
323 transform: rotate(-45deg);
324 width: .5em;
325}
326
327input[type=checkbox]:checked ~ .checkbox::before {
328 opacity: 1;
329}
330
331@media (max-width: 700px) {
332 .interstitial-wrapper {
333 padding: 0 10%;
334 }
335
336 #error-debugging-info {
337 overflow: auto;
338 }
339}
340
341@media (max-width: 420px) {
342 button,
343 [dir='rtl'] button,
344 .small-link {
345 float: none;
346 font-size: .825em;
347 font-weight: 500;
348 margin: 0;
349 width: 100%;
350 }
351
352 button {
353 padding: 16px 24px;
354 }
355
356 #details {
357 margin: 20px 0 20px 0;
358 }
359
360 #details p:not(:first-of-type) {
361 margin-top: 10px;
362 }
363
364 .secondary-button:not(.hidden) {
365 display: block;
366 margin-top: 20px;
367 text-align: center;
368 width: 100%;
369 }
370
371 .interstitial-wrapper {
372 padding: 0 5%;
373 }
374
375 #extended-reporting-opt-in {
376 margin-top: 24px;
377 }
378
379 #enhanced-protection-message {
380 margin-top: 24px;
381 }
382
383 .nav-wrapper {
384 margin-top: 30px;
385 }
386}
387
388/**
389 * Mobile specific styling.
390 * Navigation buttons are anchored to the bottom of the screen.
391 * Details message replaces the top content in its own scrollable area.
392 */
393
394@media (max-width: 420px) {
395 .nav-wrapper .secondary-button {
396 border: 0;
397 margin: 16px 0 0;
398 margin-inline-end: 0;
399 padding-bottom: 16px;
400 padding-top: 16px;
401 }
402}
403
404/* Fixed nav. */
405@media (min-width: 240px) and (max-width: 420px) and
406 (min-height: 401px),
407 (min-width: 421px) and (min-height: 240px) and
408 (max-height: 560px) {
409 body .nav-wrapper {
410 background: var(--background-color);
411 bottom: 0;
412 box-shadow: 0 -12px 24px var(--background-color);
413 left: 0;
414 margin: 0 auto;
415 max-width: 736px;
416 padding-inline-end: 24px;
417 padding-inline-start: 24px;
418 position: fixed;
419 right: 0;
420 width: 100%;
421 z-index: 2;
422 }
423
424 .interstitial-wrapper {
425 max-width: 736px;
426 }
427
428 #details,
429 #main-content {
430 padding-bottom: 40px;
431 }
432
433 #details {
434 padding-top: 5.5vh;
435 }
436
437 button.small-link {
438 color: var(--google-blue-600);
439 }
440}
441
442@media (max-width: 420px) and (orientation: portrait),
443 (max-height: 560px) {
444 body {
445 margin: 0 auto;
446 }
447
448 button,
449 [dir='rtl'] button,
450 button.small-link,
451 .nav-wrapper .secondary-button {
452 font-family: Roboto-Regular,Helvetica;
453 font-size: .933em;
454 margin: 6px 0;
455 transform: translatez(0);
456 }
457
458 .nav-wrapper {
459 box-sizing: border-box;
460 padding-bottom: 8px;
461 width: 100%;
462 }
463
464 #details {
465 box-sizing: border-box;
466 height: auto;
467 margin: 0;
468 opacity: 1;
469 transition: opacity 250ms cubic-bezier(0.4, 0, 0.2, 1);
470 }
471
472 #details.hidden,
473 #main-content.hidden {
474 height: 0;
475 opacity: 0;
476 overflow: hidden;
477 padding-bottom: 0;
478 transition: none;
479 }
480
481 h1 {
482 font-size: 1.5em;
483 margin-bottom: 8px;
484 }
485
486 .icon {
487 margin-bottom: 5.69vh;
488 }
489
490 .interstitial-wrapper {
491 box-sizing: border-box;
492 margin: 7vh auto 12px;
493 padding: 0 24px;
494 position: relative;
495 }
496
497 .interstitial-wrapper p {
498 font-size: .95em;
499 line-height: 1.61em;
500 margin-top: 8px;
501 }
502
503 #main-content {
504 margin: 0;
505 transition: opacity 100ms cubic-bezier(0.4, 0, 0.2, 1);
506 }
507
508 .small-link {
509 border: 0;
510 }
511
512 .suggested-left > #control-buttons,
513 .suggested-right > #control-buttons {
514 float: none;
515 margin: 0;
516 }
517}
518
519@media (min-width: 421px) and (min-height: 500px) and (max-height: 560px) {
520 .interstitial-wrapper {
521 margin-top: 10vh;
522 }
523}
524
525@media (min-height: 400px) and (orientation: portrait) {
526 .interstitial-wrapper {
527 margin-bottom: 145px;
528 }
529}
530
531@media (min-height: 299px) {
532 .nav-wrapper {
533 padding-bottom: 16px;
534 }
535}
536
537@media (max-height: 560px) and (min-height: 240px) and (orientation: landscape) {
538 .extended-reporting-has-checkbox #details {
539 padding-bottom: 80px;
540 }
541}
542
543@media (min-height: 500px) and (max-height: 650px) and (max-width: 414px) and
544 (orientation: portrait) {
545 .interstitial-wrapper {
546 margin-top: 7vh;
547 }
548}
549
550@media (min-height: 650px) and (max-width: 414px) and (orientation: portrait) {
551 .interstitial-wrapper {
552 margin-top: 10vh;
553 }
554}
555
556/* Small mobile screens. No fixed nav. */
557@media (max-height: 400px) and (orientation: portrait),
558 (max-height: 239px) and (orientation: landscape),
559 (max-width: 419px) and (max-height: 399px) {
560 .interstitial-wrapper {
561 display: flex;
562 flex-direction: column;
563 margin-bottom: 0;
564 }
565
566 #details {
567 flex: 1 1 auto;
568 order: 0;
569 }
570
571 #main-content {
572 flex: 1 1 auto;
573 order: 0;
574 }
575
576 .nav-wrapper {
577 flex: 0 1 auto;
578 margin-top: 8px;
579 order: 1;
580 padding-inline-end: 0;
581 padding-inline-start: 0;
582 position: relative;
583 width: 100%;
584 }
585
586 button,
587 .nav-wrapper .secondary-button {
588 padding: 16px 24px;
589 }
590
591 button.small-link {
592 color: var(--google-blue-600);
593 }
594}
595
596@media (max-width: 239px) and (orientation: portrait) {
597 .nav-wrapper {
598 padding-inline-end: 0;
599 padding-inline-start: 0;
600 }
601}
602</style>
603 <style>/* Copyright 2013 The Chromium Authors
604 * Use of this source code is governed by a BSD-style license that can be
605 * found in the LICENSE file. */
606
607/* Don't use the main frame div when the error is in a subframe. */
608html[subframe] #main-frame-error {
609 display: none;
610}
611
612/* Don't use the subframe error div when the error is in a main frame. */
613html:not([subframe]) #sub-frame-error {
614 display: none;
615}
616
617h1 {
618 margin-top: 0;
619 word-wrap: break-word;
620}
621
622h1 span {
623 font-weight: 500;
624}
625
626a {
627 text-decoration: none;
628}
629
630.icon {
631 -webkit-user-select: none;
632 display: inline-block;
633}
634
635.icon-generic {
636 /* Can't access chrome://theme/IDR_ERROR_NETWORK_GENERIC from an untrusted
637 * renderer process, so embed the resource manually. */
638 content: image-set(
639 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABIAQMAAABvIyEEAAAABlBMVEUAAABTU1OoaSf/AAAAAXRSTlMAQObYZgAAAENJREFUeF7tzbEJACEQRNGBLeAasBCza2lLEGx0CxFGG9hBMDDxRy/72O9FMnIFapGylsu1fgoBdkXfUHLrQgdfrlJN1BdYBjQQm3UAAAAASUVORK5CYII=) 1x,
640 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQAQMAAADdiHD7AAAABlBMVEUAAABTU1OoaSf/AAAAAXRSTlMAQObYZgAAAFJJREFUeF7t0cENgDAMQ9FwYgxG6WjpaIzCCAxQxVggFuDiCvlLOeRdHR9yzjncHVoq3npu+wQUrUuJHylSTmBaespJyJQoObUeyxDQb3bEm5Au81c0pSCD8HYAAAAASUVORK5CYII=) 2x);
641}
642
643.icon-info {
644 content: image-set(
645 url(data:image/png;base64,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) 1x,
646 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQCAYAAADnRuK4AAAAAXNSR0IArs4c6QAAEp1JREFUeAHtnVuMFkUWx2dgRlBhvUxQSZTsw25wAUPiNQTRgFkv8YIbZhBcB8hK2NVkXnxRY0xMDFFffJkHsyxskBFRGIJ4iWjioLJqdL3EENFZ35AELxnRHZFFBtjff+gePsbv0qe6+vv6+6Y66XR39alT5/zPv6urq6q7m5rCEhAICAQEAgIBgYBAQCAgEBAICAQEAgIBgYBAQCAgEBAICAQEAgIBgYBAQCAgEBBoTASaG9Ot8l6tWLFi4sGDB3+P1HStx44d0/a85ubmyWwnHz9+fHgbHTdxPEj6IMfD2+j423HjxvWTPryeeeaZX65fv/5/HI+pZUwQ6I477vjD0NDQAgiwgOBfynYa23E+I43OY+jcy/Zjtn0tLS19zz///Oc+y8ijroYkUEdHxxSCuBDAF7DOZ/+CWoAPmb6m3J2sfexv37Jly3e1sCPLMhuGQF1dXRP2799/G2TpBLCbWFuyBM5B9xB5XoVIPVOnTn2xu7v7sIOO3GWpewJR21xJG+ZukF3MenbuEC5u0A8kb6YNtY5a6YPiIvWRWrcEWrx48XyI8xA1znX1AXVxK6mR3oBIqzdv3qxbXd0tdUcgapybIY2IM6fu0C5jMER6j3U1NdIrZcRyd6puCARx5kCabtbLcoeiR4Mg0UesXRDpPY9qM1OVewItW7asjT6bJ0DgL6y5t9dTpI6j55/0Ld2/YcOGAU86M1GT24BQ0zS3t7evxOvHWNsy8T7/SkWeB3t7e9dSK4lUuVtySSBuV9NoID8LWnNzh1htDHqHhvad3Nb21qb40qV67Y0tXUzyMzxd3Urt8wk5AnlOwjZXmAibk0n52MtNDbRq1arWgYGBx4HlvmpAwy3hJ8rpJzD98ZgW+1+RPjh+/PjB0047bfDQoUMa+2o6/fTTJ//yyy+Tjx49OjxOhsxFJA+PobE/PJ5G3kmSrcLyZFtb2wNr1qw5UoWyKhaRCwItWbLkIsaqthCEqypa7CggwqD/bbZ9bPsuueSSTx955JFjjupOyYaecbt3756Nbo21acztGraZEQr97zPW1vHcc899dYohNTioOYFo78ygvfMavl+Ygf8aQe+lhumZMWPGLgKt4YTMF8pp2bNnzzz86oRI7RSo0X3fyz78uoF20R7fii36akqgqG/nZUA+12J0JVlI8zrr08htA+BDleSzPM+t+YwDBw7cjo/LWa/3WRY+fs96Sy37jGpGIMhzM1foZgA9wweoAKnb0VbaL6uZRvGpD52+dTCtZDbtqIfQuwgy+XqA+ZmaaDEkqkkPdk0IRP/OnwFwPUCmHjGPiPNMa2vrY5s2bfrCd9Cz0Ld06dKLjxw58iC67/JEpCFItBwSqeujqkvVCRTVPC/gpQ/yfEgA7tm6deuHVUXNU2GLFi26nAvgKXy43INKkej2atdEvqrRRP6rzRPdtlKRB9APANa9s2bNuqpeySPAZLt8kC/yKRGIpYVahK0wLi3i/0zVaiAcm8GVtos1VYMZoHfQL7O8p6fnW/9w1E5jZ2fnefQ7PQ0+N6axAnzUsJ5HTVSVp7OqEEj9PNzz3wWYNI/qqqIfZt7MEwCUy3GhNIFXXsjTTG/z/dQkj3KYppbeN3HixDkbN27cl9amSvkzv4Wph1mdhBiShjzq85jPVfV4o5JHgZJv8lG+cpgm+BcePny4V9hLb5ZL5gTS8ARXVpoe5k8B9AqA/VeWQORJt3yVz9jk3B0hzKOhoUxdy/QWpsE/+j1edPWAK/It1oUA+qOrjnrOR7vxLIiwnfVaVz/oF7uN2/5Lrvkr5cusBsL5adzL11cyoNR5iLNt0qRJN45V8ggX+S4MhEUpnCqlKwaKRSU51/OZEIgrphnDn2Xr9MQlwFg7xuKbnqMDKQyEhSuJFIMoFpncbTIhUDST0Gk+D0C9xVWnyVNHR4M5Vo+FhTARNo4YzI1i4pi9dDbvrIzmMPdTpMs0VDWYrx3Lt63SoWpqUpuI2kQkml1OrsS5AeZYT/c9x9p7DRRNgHchjx7Vx3Sbp0TgR5J1YQkjElwe8eOXE0b0+djxWgNxhWio4h0Ms+pVJ6H6eWr2qM64lKlzkmEIq48+4jWsA5yvBuedHLQYlR4H57ng7O2VIa81EA22bhwyA4tTD9eSPMYg1FxcWAkzB0Oaoxg5ZC2exRuBuCr0xuhlxYspnUrDcIeGJ0pLhDPFEIiGdHYUO1cuTTFSrMrJWM55IxCGaaKUaYE8BzQwytZ0+zAV0qDCwizCzjyK7xKrUjB6IRA9zvoGj3kaASA81Gij6qWAziJd2AlDq27FSjGz5ism74VANOjMTuD4hzNnzvx7MaNCWnIEhKGwTJ7jhKRLzIqVkZpA3E+vhNGmT6zgsD4Hd4+v12qKOTZW0oShsBSmFp8VM8XOkqeYbGoCYcjKYoorpD1TzzMJK/hW9dMRls9YC3aM3SnFpCKQPiuHER2naKxwoCtFE+AriIXTRgSEqUMt1KEYGos6RTwVgfRNQrRZPyu3tV7enjgFqZwfRJhuNZp5dhRDY7aT4qkIhJplJ1Ul29N7W8kkg5QVARdsuYPoo6TOizOBaIDpU7qmCeBUsa/n9aU/ZwRzlFHYCmOjSTcplsY8I+LWsZSRjJBnIQem/Dj39IiCnO3UcmzLJxTCmNhYXqFuiWK51sUO5xqIwhYYCxxE3nlmnbGssSwujIW1ZbHGckR3GgKZejK5MnoZBKzphw5GvG7gHWEsrI0ummJZqNuJQNwz9ZKg6fcBjB73FBYc9rNDwIq1Yqn/ibhY5EQgusFNjOWK+Enf53ExMOSxIyCshbklp35GY5GPZZ0IhHGmwmD429X6uFPs2FjeCmthbsHAGtNYtxOBMO7SWEGSLcb1JZELMv4QsGJujWlsqZlA+lkbxpneM8K4QKAY8SptrZgrpoqt1TwzgfSnP4xLnA/DftIHLa2GBfl0CAhzYZ9Ui2Ia/cUxaZZhucREKNCqz9palv4wbcMClx/ZCHO9XmVZrLFtypxAMNvqhMXhIFsGAQfssycQj/CmQuiTCAQqE+QsT1mxt8ZWtpvGspSB++r5MFu7SZe6IFA9vReWFHjkTNgrtgbdw6IutzDTR7Mh21dWo4K8HwQcsDfFVla6EMj0CX9YbR3Y84Ne0KK7hRV7U2ydCASrTSxlkpPViRB6TwhYsbfG1olAZDIRSH+98YRHUGNEwAF7U2xljvkWRrVoKiT+ZZLR9yDuAQEr9tbYykQzgTz4FVQ0EAJmAnGfNN2S9LO2BsKrrlyxYm+NrcAwE4g8JgLpT391hXoDGeuAvSm2gspMIOujoX4T2UAxqStXrNhbY+tEIDKZWOryaFhXUcqxsQ7Ym2LrSqDEUwRUAKzWD2rDUgMErNhXpQ1EId8YsTANvhp1B/HyCFixN/8BydwGqsYIb3lMwtmkCFhH162xlR1mApHHOsJrvQqS4hPkKiDALcyKvSm2Kj5zAlHGdGbHuZRTAZ5wuhwCEeb5IxBfO/8SZh8rZ3zhOdpMk3bv3j27MC3sZ4+AMBf2SUtSTBXbpPKxnLlm0M8/MGxvrCDJFuMWJJELMv4QsGKumLr83MZMILmIcR9bXMW4QCALYB5krZhbYxqb6EQgjDO954Vx13BPNk+fjY0MWxsCwlqYW3JZYxrrdiJQS0uLiUAYN2nPnj3z4kLDNlsEhLUwt5RijWms24lAfAnrcxj+dawkyZY+iVSfUktSRpA5gYAVa8VSMXXBz4lAUUH6W0zihSuinc/CnJ44QxB0QkAYC2tjZlMsC3WnIZDpNkahGpX/U2HhYT8TBISxdQaENZYjhjsTiGpvO1qGRjQl2OHKWJ5ALIikQACMVxizD0WxNGY7Ie5MID6l9h0qXrWUinPX8yWs0KloAc0gK2zB+I+GLBJ9NYqlMdsJcWcCKTvMNX+2jklO5h+zOHk2BjO5YOsSw0JoUxFo6tSpL6Lsh0KFCfYXLV269OIEckHEgECE6SJDFon+EMXQmO2keCoCdXd3H0bV5pPqKu9RxY47cuTIg5Ulg4QFAWEqbC15kN0cxdCY7aS4tcCTOaM95pCs+1Vi5YS7+JjB5ZXFgkQSBCIs70oiWyjjGLtCFU7TOU5RQAPsA+6jb5ySWOFAVwp5ngrTPCoAleC0MBSW1tpHMVPsEhRRViR1DSTtMNn8AxUcvvyzzz77a1nrwsmKCAhDYVlRcJSAS8xGqRg+9EIg/iC8E0a/V6yAcmk4vrqzs/O8cjLhXGkEhJ0wLC1R/IxipZgVP2tL9UIgFYlRZkdw/hze39bPQZptZgdpYRZhd44VDZdYlSrDG4G4n76CYR+VKqhUOkDcyB+E7y91PqQXR0CYCbviZ0unKkaKVWkJ2xlvBFKxGNfF5rjNhKYmRo8fZRDwamu+sSovrISZg//Hoxg5ZC2exfutg0fKtRR1d/Hiyqbuo2F3BVeHaZpIWY0NeBLyXAB5/o1rFzq4t47/oq10yFcyi9caSKUwMVu3o4GSJZY+cSHA7ACgs0qLjO0zwkYYgYILeQai2HgF0TuBNmzYIPK49jRrMHC7yyf3vaKSQ2XCRNhgmutg9INRbLx65/0WJutwtLm9vX0Xu3NdrOU+vY21g9vZUZf8jZaHmmc8mG5h1Vwfl+Wd3t7eeWBqbp9WKsx7DaQCZSjtmTvZfl/JgGLnBZQACzVRU1NU8ziTRzGIYuGdPMOxLhZAX2k8at7KFAON2DstOP8W60Jqoh+dFNR5JrV5uJC2s17r6gpfar2NTsOXXPNXyje+kkCa83Sz/4e/5/0GHXMc9fwW8G6aNWvWC7xpYPqsjGN5uckGefS0pTHGq1IY9SS3ru4U+StmzeQWVlhqW1vbA9Qi7xemGfdn67EVQMdMP5F8lc/g5NpgVjPifWFvxNosnkkjerQVS5YsuYj5Ku+S7vL4Gasb4l7+MNXxE4CTyf08LqhWW2rbZvUwQx51EqZ5EXPfxIkT52zcuHFf1r5UhUBygqtKf3rexXpuGqcgzw6+Prq8p6fH/DGkNOVmnVcDo9HYlnl4otA28PmedR7txj2F6VntZ9oGKjSaNsx3M2fOFIGWkt5aeM64/zv+MLwSXf/lav34zTffrOvaSPN5pkyZ8jdq6G1gc4kRi9HiP1NL3wh5Phl9IqvjqtVAsQPURDdTRb/AcZoqOlandsK9dM9/GCfU01YzCaktNBnMPJ+niJ+6xd8OebwNlBYp41dJVSeQLIBEd0Kip9lNTSICcAw9z7S2tj62adOmL6Q/74smwEfzwu+CPD4eZESe5ZDn2Wr7XhMCycmoJtKE/DN8OB0RaSv9Hqt5z/tTHzp969B7W9GrN4s8EUcm6ra1uNo1T4xNzQgkAyDRHIB8mTVVwzp2Jt5CptdZVcNtA9hDcXottvio7wGoZ3056/U+bcBHNZhvwUfzbFBfdtSUQHICgGdwO3uN3TSP+KXwGATgXq7QHjo0d9FgHSol6DOdclr0iRX86oQ07eie7FN/pEvTX26APFV52iplf80JJMPUT8STlcZ70vS6lvJxOB0i/YT+t9n2se3Tf9UJtNpPqRc9SembhOhegO4FbK9ha/o+j8UI9L8/YcKE9mr081SyKxcEkpGrVq1qHRgYeJzd+yoZ7eM8QdDQSD+B7udK7o/2vyJ9UH/608/a4v9t6a83+nEJ7ZfJyE9G5iLkp1PDTGdfX0KdniVh0F+4PKke5jVr1hwpTKzVfm4IFAOgAVgCs56AeG0XxfrrdQtRNaq+IsuBURdsckcgOUG7aBok0iOp03wiFyBynucdyHMn7Z29ebMzlwQSSNRAmpS2kt3HWNuUNgaX4dmdjKivpQbKZY+7j06sTOIqwOhh/gfzeNXGWMeaSwAzcf6Er+vkuzDIK3nke25roNGBifqMuqmZLht9rpGOIctHrF217Nux4Fk3BIqdgkg3Q6KHWF0nqcWqcrWFNO+xroY4VR3LSgtC3REodpintfk0tEWk6+K0etxCmjdoIK/29a56tTGoWwLFQFEjXQmJVrJ2kHZ2nJ7z7Q8QZwvrWmqc1J9YqaWvdU+gGLyurq4J+/fvv43jZZBJk7JSj/THuj1t9TVUvRS4QZ+VS/tlME82pVbTMAQqRIJaaQokWkjaAtb57F9QeL5a+xBGr2nvZO1jfzu1jb5s21BLQxJodIQglAZs5xNEjVVdynYaW69dGOg8hs69bD9m20e7ZieEqelA52gcsjgeEwQaDZxe1jt48ODvSR8ex4JcGtM6n2ONmk+CANpqzGt4FJ3jQY41sq+txtAGSfsGkgyPoXHcT5/Nly7/2yJvWAICAYGAQEAgIBAQCAgEBAICAYGAQEAgIBAQCAgEBAICAYGAQEAgIBAQCAgEBAICAYEcIvB/Q079+h6myXwAAAAASUVORK5CYII=) 2x);
647}
648
649.icon-offline {
650 content: image-set(
651 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABIAQMAAABvIyEEAAAABlBMVEUAAABTU1OoaSf/AAAAAXRSTlMAQObYZgAAAGxJREFUeF7tyMEJwkAQRuFf5ipMKxYQiJ3Z2nSwrWwBA0+DQZcdxEOueaePp9+dQZFB7GpUcURSVU66yVNFj6LFICatThZB6r/ko/pbRpUgilY0Cbw5sNmb9txGXUKyuH7eV25x39DtJXUNPQGJtWFV+BT/QAAAAABJRU5ErkJggg==) 1x,
652 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQBAMAAAAVaP+LAAAAGFBMVEUAAABTU1NNTU1TU1NPT09SUlJSUlJTU1O8B7DEAAAAB3RSTlMAoArVKvVgBuEdKgAAAJ1JREFUeF7t1TEOwyAMQNG0Q6/UE+RMXD9d/tC6womIFSL9P+MnAYOXeTIzMzMzMzMzaz8J9Ri6HoITmuHXhISE8nEh9yxDh55aCEUoTGbbQwjqHwIkRAEiIaG0+0AA9VBMaE89Rogeoww936MQrWdBr4GN/z0IAdQ6nQ/FIpRXDwHcA+JIJcQowQAlFUA0MfQpXLlVQfkzR4igS6ENjknm/wiaGhsAAAAASUVORK5CYII=) 2x);
653 position: relative;
654}
655
656.icon-disabled {
657 content: image-set(
658 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHAAAABICAMAAAAZF4G5AAAABlBMVEVMaXFTU1OXUj8tAAAAAXRSTlMAQObYZgAAASZJREFUeAHd11Fq7jAMRGGf/W/6PoWB67YMqv5DybwG/CFjRuR8JBw3+ByiRjgV9W/TJ31P0tBfC6+cj1haUFXKHmVJo5wP98WwQ0ZCbfUc6LQ6VuUBz31ikADkLMkDrfUC4rR6QGW+gF6rx7NaHWCj1Y/W6lf4L7utvgBSt3rBFSS/XBMPUILcJINHCBWYUfpWn4NBi1ZfudIc3rf6/NGEvEA+AsYTJozmXemjXeLZAov+mnkN2HfzXpMSVQDnGw++57qNJ4D1xitA2sJ+VAWMygSEaYf2mYPTjZfk2K8wmP7HLIH5Mg4/pP+PEcDzUvDMvYbs/2NWwPO5vBdMZE4EE5UTQLiBFDaUlTDPBRoJ9HdAYIkIo06og3BNXtCzy7zA1aXk5x+tJARq63eAygAAAABJRU5ErkJggg==) 1x,
659 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOAAAACQAQMAAAArwfVjAAAABlBMVEVMaXFTU1OXUj8tAAAAAXRSTlMAQObYZgAAAYdJREFUeF7F1EFqwzAUBNARAmVj0FZe5QoBH6BX+dn4GlY2PYNzGx/A0CvkCIJuvIraKJKbgBvzf2g62weDGD7CYggpfFReis4J0ey9EGFIiEQQojFSlA9kSIiqd0KkFjKsewgRbStEN19mxUPTtmW9HQ/h6tyqNQ8NlSMZdzyE6qkoE0trVYGFm0n1WYeBhduzwbwBC7voS+vIxfeMjeaiLxsMMtQNwMPtuew+DjzcTHk8YMfDknEcIUOtf2lVfgVH3K4Xv5PRYAXRVMtItIJ3rfaCIVn9DsTH2NxisAVRex2Hh3hX+/mRUR08bAwPEYsI51ZxWH4Q0SpicQRXeyEaIug48FEdegARfMz/tADVsRciwTAxW308ehmC2gLraC+YCbV3QoTZexa+zegAEW5PhhgYfmbvJgcRqngGByOSXdFJcLk2JeDPEN0kxe1JhIt5FiFA+w+ItMELsUyPF2IaJ4aILqb4FbxPwhImwj6JauKgDUCYaxmYIsd4KXdMjIC9ItB5Bn4BNRwsG0XM2nwAAAAASUVORK5CYII=) 2x);
660 width: 112px;
661}
662
663.hidden {
664 display: none;
665}
666
667#suggestions-list a {
668 color: var(--google-blue-600);
669}
670
671#suggestions-list p {
672 margin-block-end: 0;
673}
674
675#suggestions-list ul {
676 margin-top: 0;
677}
678
679.single-suggestion {
680 list-style-type: none;
681 padding-inline-start: 0;
682}
683
684.link-button {
685 color: rgb(66, 133, 244);
686 display: inline-block;
687 font-weight: bold;
688 text-transform: uppercase;
689}
690
691#sub-frame-error-details {
692
693 color: #8F8F8F;
694
695 /* Not done on mobile for performance reasons. */
696 text-shadow: 0 1px 0 rgba(255,255,255,0.3);
697
698}
699
700.secondary-button {
701 background: #d9d9d9;
702 color: #696969;
703 margin-inline-end: 16px;
704}
705
706.snackbar {
707 background: #323232;
708 border-radius: 2px;
709 bottom: 24px;
710 box-sizing: border-box;
711 color: #fff;
712 font-size: .87em;
713 left: 24px;
714 max-width: 568px;
715 min-width: 288px;
716 opacity: 0;
717 padding: 16px 24px 12px;
718 position: fixed;
719 transform: translateY(90px);
720 will-change: opacity, transform;
721 z-index: 999;
722}
723
724.snackbar-show {
725 -webkit-animation:
726 show-snackbar 250ms cubic-bezier(0, 0, 0.2, 1) forwards,
727 hide-snackbar 250ms cubic-bezier(0.4, 0, 1, 1) forwards 5s;
728}
729
730@-webkit-keyframes show-snackbar {
731 100% {
732 opacity: 1;
733 transform: translateY(0);
734 }
735}
736
737@-webkit-keyframes hide-snackbar {
738 0% {
739 opacity: 1;
740 transform: translateY(0);
741 }
742 100% {
743 opacity: 0;
744 transform: translateY(90px);
745 }
746}
747
748.suggestions {
749 margin-top: 18px;
750}
751
752.suggestion-header {
753 font-weight: bold;
754 margin-bottom: 4px;
755}
756
757.suggestion-body {
758 color: #777;
759}
760
761/* Decrease padding at low sizes. */
762@media (max-width: 640px), (max-height: 640px) {
763 h1 {
764 margin: 0 0 15px;
765 }
766 .suggestions {
767 margin-top: 10px;
768 }
769 .suggestion-header {
770 margin-bottom: 0;
771 }
772}
773
774#cancel-save-page-button {
775 background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48Y2xpcFBhdGggaWQ9Im1hc2siPjxwYXRoIGQ9Ik0xMiAyQzYuNSAyIDIgNi41IDIgMTJzNC41IDEwIDEwIDEwIDEwLTQuNSAxMC0xMFMxNy41IDIgMTIgMnptNSAxNkg3di0yaDEwdjJ6bS02LjctNEw3IDEwLjdsMS40LTEuNCAxLjkgMS45IDUuMy01LjNMMTcgNy4zIDEwLjMgMTR6IiBmaWxsPSIjOUFBMEE2Ii8+PC9jbGlwUGF0aD48cGF0aCBjbGlwLXBhdGg9InVybCgjbWFzaykiIGZpbGw9IiM5QUEwQTYiIGQ9Ik0wIDBoMjR2MjRIMHoiLz48cGF0aCBjbGlwLXBhdGg9InVybCgjbWFzaykiIGZpbGw9IiMxQTczRTgiIHN0eWxlPSJhbmltYXRpb246b2ZmbGluZUFuaW1hdGlvbiA0cyBpbmZpbml0ZSIgZD0iTTAgMGgyNHYyNEgweiIvPjxzdHlsZT5Aa2V5ZnJhbWVzIG9mZmxpbmVBbmltYXRpb257MCUsMzUle2hlaWdodDowfTYwJXtoZWlnaHQ6MTAwJX05MCV7ZmlsbC1vcGFjaXR5OjF9dG97ZmlsbC1vcGFjaXR5OjB9fTwvc3R5bGU+PC9zdmc+);
776 background-position: right 27px center;
777 background-repeat: no-repeat;
778 border: 1px solid var(--google-gray-300);
779 border-radius: 5px;
780 color: var(--google-gray-700);
781 margin-bottom: 26px;
782 padding-bottom: 16px;
783 padding-inline-end: 88px;
784 padding-inline-start: 16px;
785 padding-top: 16px;
786 text-align: start;
787}
788
789html[dir='rtl'] #cancel-save-page-button {
790 background-position: left 27px center;
791}
792
793#save-page-for-later-button {
794 display: flex;
795 justify-content: start;
796}
797
798#save-page-for-later-button a::before {
799 content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxLjJlbSIgaGVpZ2h0PSIxLjJlbSIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNNSAyMGgxNHYtMkg1bTE0LTloLTRWM0g5djZINWw3IDcgNy03eiIgZmlsbD0iIzQyODVGNCIvPjwvc3ZnPg==);
800 display: inline-block;
801 margin-inline-end: 4px;
802 vertical-align: -webkit-baseline-middle;
803}
804
805.hidden#save-page-for-later-button {
806 display: none;
807}
808
809/* Don't allow overflow when in a subframe. */
810html[subframe] body {
811 overflow: hidden;
812}
813
814#sub-frame-error {
815 -webkit-align-items: center;
816 -webkit-flex-flow: column;
817 -webkit-justify-content: center;
818 background-color: #DDD;
819 display: -webkit-flex;
820 height: 100%;
821 left: 0;
822 position: absolute;
823 text-align: center;
824 top: 0;
825 transition: background-color 200ms ease-in-out;
826 width: 100%;
827}
828
829#sub-frame-error:hover {
830 background-color: #EEE;
831}
832
833#sub-frame-error .icon-generic {
834 margin: 0 0 16px;
835}
836
837#sub-frame-error-details {
838 margin: 0 10px;
839 text-align: center;
840 opacity: 0;
841}
842
843/* Show details only when hovering. */
844#sub-frame-error:hover #sub-frame-error-details {
845 opacity: 1;
846}
847
848/* If the iframe is too small, always hide the error code. */
849/* TODO(mmenke): See if overflow: no-display works better, once supported. */
850@media (max-width: 200px), (max-height: 95px) {
851 #sub-frame-error-details {
852 display: none;
853 }
854}
855
856/* Adjust icon for small embedded frames in apps. */
857@media (max-height: 100px) {
858 #sub-frame-error .icon-generic {
859 height: auto;
860 margin: 0;
861 padding-top: 0;
862 width: 25px;
863 }
864}
865
866/* details-button is special; it's a <button> element that looks like a link. */
867#details-button {
868 box-shadow: none;
869 min-width: 0;
870}
871
872/* Styles for platform dependent separation of controls and details button. */
873.suggested-left > #control-buttons,
874.suggested-right > #details-button {
875 float: left;
876}
877
878.suggested-right > #control-buttons,
879.suggested-left > #details-button {
880 float: right;
881}
882
883.suggested-left .secondary-button {
884 margin-inline-end: 0;
885 margin-inline-start: 16px;
886}
887
888#details-button.singular {
889 float: none;
890}
891
892/* download-button shows both icon and text. */
893#download-button {
894 padding-bottom: 4px;
895 padding-top: 4px;
896 position: relative;
897}
898
899#download-button::before {
900 background: image-set(
901 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAAAO0lEQVQ4y2NgGArgPxIY1YChsOE/LtBAmpYG0mxpIOSDBpKUo2lpIDZxNJCkHKqlYZAla3RAHQ1DFgAARRroHyLNTwwAAAAASUVORK5CYII=) 1x,
902 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAQAAAD9CzEMAAAAZElEQVRYw+3Ruw3AMAwDUY3OzZUmRRD4E9iim9wNwAdbEURHyk4AAAAATiCVK8lLyPsKeT9K3lsownnunfkPxO78hKiYHxBV8x2icr5BVM+/CMf8g3DN34Rzns6ViwHUAUQ/6wIAd5Km7l6c8AAAAABJRU5ErkJggg==) 2x)
903 no-repeat;
904 content: '';
905 display: inline-block;
906 height: 24px;
907 margin-inline-end: 4px;
908 margin-inline-start: -4px;
909 vertical-align: middle;
910 width: 24px;
911}
912
913#download-button:disabled {
914 background: rgb(180, 206, 249);
915 color: rgb(255, 255, 255);
916}
917
918#buttons::after {
919 clear: both;
920 content: '';
921 display: block;
922 width: 100%;
923}
924
925/* Offline page */
926html[dir='rtl'] .runner-container,
927html[dir='rtl'].offline .icon-offline {
928 transform: scaleX(-1);
929}
930
931.offline {
932 transition: filter 1.5s cubic-bezier(0.65, 0.05, 0.36, 1),
933 background-color 1.5s cubic-bezier(0.65, 0.05, 0.36, 1);
934
935 will-change: filter, background-color;
936
937}
938
939.offline body {
940 transition: background-color 1.5s cubic-bezier(0.65, 0.05, 0.36, 1);
941}
942
943.offline #main-message > p {
944 display: none;
945}
946
947.offline.inverted {
948 background-color: #fff;
949 filter: invert(1);
950}
951
952.offline.inverted body {
953 background-color: #fff;
954}
955
956.offline .interstitial-wrapper {
957 color: var(--text-color);
958 font-size: 1em;
959 line-height: 1.55;
960 margin: 0 auto;
961 max-width: 600px;
962 padding-top: 100px;
963 position: relative;
964 width: 100%;
965}
966
967.offline .runner-container {
968 direction: ltr;
969 height: 150px;
970 max-width: 600px;
971 overflow: hidden;
972 position: absolute;
973 top: 35px;
974 width: 44px;
975}
976
977.offline .runner-container:focus {
978 outline: none;
979}
980
981.offline .runner-container:focus-visible {
982 outline: 3px solid var(--google-blue-300);
983}
984
985.offline .runner-canvas {
986 height: 150px;
987 max-width: 600px;
988 opacity: 1;
989 overflow: hidden;
990 position: absolute;
991 top: 0;
992 z-index: 10;
993}
994
995.offline .controller {
996 height: 100vh;
997 left: 0;
998 position: absolute;
999 top: 0;
1000 width: 100vw;
1001 z-index: 9;
1002}
1003
1004#offline-resources {
1005 display: none;
1006}
1007
1008#offline-instruction {
1009 image-rendering: pixelated;
1010 left: 0;
1011 margin: auto;
1012 position: absolute;
1013 right: 0;
1014 top: 60px;
1015 width: fit-content;
1016}
1017
1018.offline-runner-live-region {
1019 bottom: 0;
1020 clip-path: polygon(0 0, 0 0, 0 0);
1021 color: var(--background-color);
1022 display: block;
1023 font-size: xx-small;
1024 overflow: hidden;
1025 position: absolute;
1026 text-align: center;
1027 transition: color 1.5s cubic-bezier(0.65, 0.05, 0.36, 1);
1028 user-select: none;
1029}
1030
1031/* Custom toggle */
1032.slow-speed-option {
1033 align-items: center;
1034 background: var(--google-gray-50);
1035 border-radius: 24px/50%;
1036 bottom: 0;
1037 color: var(--error-code-color);
1038 display: inline-flex;
1039 font-size: 1em;
1040 left: 0;
1041 line-height: 1.1em;
1042 margin: 5px auto;
1043 padding: 2px 12px 3px 20px;
1044 position: absolute;
1045 right: 0;
1046 width: max-content;
1047 z-index: 999;
1048}
1049
1050.slow-speed-option.hidden {
1051 display: none;
1052}
1053
1054.slow-speed-option [type=checkbox] {
1055 opacity: 0;
1056 pointer-events: none;
1057 position: absolute;
1058}
1059
1060.slow-speed-option .slow-speed-toggle {
1061 cursor: pointer;
1062 margin-inline-start: 8px;
1063 padding: 8px 4px;
1064 position: relative;
1065}
1066
1067.slow-speed-option [type=checkbox]:disabled ~ .slow-speed-toggle {
1068 cursor: default;
1069}
1070
1071.slow-speed-option-label [type=checkbox] {
1072 opacity: 0;
1073 pointer-events: none;
1074 position: absolute;
1075}
1076
1077.slow-speed-option .slow-speed-toggle::before,
1078.slow-speed-option .slow-speed-toggle::after {
1079 content: '';
1080 display: block;
1081 margin: 0 3px;
1082 transition: all 100ms cubic-bezier(0.4, 0, 1, 1);
1083}
1084
1085.slow-speed-option .slow-speed-toggle::before {
1086 background: rgb(189,193,198);
1087 border-radius: 0.65em;
1088 height: 0.9em;
1089 width: 2em;
1090}
1091
1092.slow-speed-option .slow-speed-toggle::after {
1093 background: #fff;
1094 border-radius: 50%;
1095 box-shadow: 0 1px 3px 0 rgb(0 0 0 / 40%);
1096 height: 1.2em;
1097 position: absolute;
1098 top: 51%;
1099 transform: translate(-20%, -50%);
1100 width: 1.1em;
1101}
1102
1103.slow-speed-option [type=checkbox]:focus + .slow-speed-toggle {
1104 box-shadow: 0 0 8px rgb(94, 158, 214);
1105 outline: 1px solid rgb(93, 157, 213);
1106}
1107
1108.slow-speed-option [type=checkbox]:checked + .slow-speed-toggle::before {
1109 background: var(--google-blue-600);
1110 opacity: 0.5;
1111}
1112
1113.slow-speed-option [type=checkbox]:checked + .slow-speed-toggle::after {
1114 background: var(--google-blue-600);
1115 transform: translate(calc(2em - 90%), -50%);
1116}
1117
1118.slow-speed-option [type=checkbox]:checked:disabled +
1119 .slow-speed-toggle::before {
1120 background: rgb(189,193,198);
1121}
1122
1123.slow-speed-option [type=checkbox]:checked:disabled +
1124 .slow-speed-toggle::after {
1125 background: var(--google-gray-50);
1126}
1127
1128@media (max-width: 420px) {
1129 #download-button {
1130 padding-bottom: 12px;
1131 padding-top: 12px;
1132 }
1133
1134 .suggested-left > #control-buttons,
1135 .suggested-right > #control-buttons {
1136 float: none;
1137 }
1138
1139 .snackbar {
1140 border-radius: 0;
1141 bottom: 0;
1142 left: 0;
1143 width: 100%;
1144 }
1145}
1146
1147@media (max-height: 350px) {
1148 h1 {
1149 margin: 0 0 15px;
1150 }
1151
1152 .icon-offline {
1153 margin: 0 0 10px;
1154 }
1155
1156 .interstitial-wrapper {
1157 margin-top: 5%;
1158 }
1159
1160 .nav-wrapper {
1161 margin-top: 30px;
1162 }
1163}
1164
1165@media (min-width: 420px) and (max-width: 736px) and
1166 (min-height: 240px) and (max-height: 420px) and
1167 (orientation:landscape) {
1168 .interstitial-wrapper {
1169 margin-bottom: 100px;
1170 }
1171}
1172
1173@media (max-width: 360px) and (max-height: 480px) {
1174 .offline .interstitial-wrapper {
1175 padding-top: 60px;
1176 }
1177
1178 .offline .runner-container {
1179 top: 8px;
1180 }
1181}
1182
1183@media (min-height: 240px) and (orientation: landscape) {
1184 .offline .interstitial-wrapper {
1185 margin-bottom: 90px;
1186 }
1187
1188 .icon-offline {
1189 margin-bottom: 20px;
1190 }
1191}
1192
1193@media (max-height: 320px) and (orientation: landscape) {
1194 .icon-offline {
1195 margin-bottom: 0;
1196 }
1197
1198 .offline .runner-container {
1199 top: 10px;
1200 }
1201}
1202
1203@media (max-width: 240px) {
1204 button {
1205 padding-inline-end: 12px;
1206 padding-inline-start: 12px;
1207 }
1208
1209 .interstitial-wrapper {
1210 overflow: inherit;
1211 padding: 0 8px;
1212 }
1213}
1214
1215@media (max-width: 120px) {
1216 button {
1217 width: auto;
1218 }
1219}
1220
1221.arcade-mode,
1222.arcade-mode .runner-container,
1223.arcade-mode .runner-canvas {
1224 image-rendering: pixelated;
1225 max-width: 100%;
1226 overflow: hidden;
1227}
1228
1229.arcade-mode #buttons,
1230.arcade-mode #main-content {
1231 opacity: 0;
1232 overflow: hidden;
1233}
1234
1235.arcade-mode .interstitial-wrapper {
1236 height: 100vh;
1237 max-width: 100%;
1238 overflow: hidden;
1239}
1240
1241.arcade-mode .runner-container {
1242 left: 0;
1243 margin: auto;
1244 right: 0;
1245 transform-origin: top center;
1246 transition: transform 250ms cubic-bezier(0.4, 0, 1, 1) 400ms;
1247 z-index: 2;
1248}
1249
1250@media (prefers-color-scheme: dark) {
1251 .icon {
1252 filter: invert(1);
1253 }
1254
1255 .offline .runner-canvas {
1256 filter: invert(1);
1257 }
1258
1259 .offline.inverted {
1260 background-color: var(--background-color);
1261 filter: invert(0);
1262 }
1263
1264 .offline.inverted body {
1265 background-color: #fff;
1266 }
1267
1268 .offline.inverted .offline-runner-live-region {
1269 color: #fff;
1270 }
1271
1272 #suggestions-list a {
1273 color: var(--link-color);
1274 }
1275
1276 .slow-speed-option {
1277 background: var(--google-gray-800);
1278 color: var(--google-gray-100);
1279 }
1280
1281 .slow-speed-option .slow-speed-toggle::before,
1282 .slow-speed-option [type=checkbox]:checked:disabled +
1283 .slow-speed-toggle::before {
1284 background: rgb(189,193,198);
1285 }
1286
1287 .slow-speed-option [type=checkbox]:checked + .slow-speed-toggle::after,
1288 .slow-speed-option [type=checkbox]:checked + .slow-speed-toggle::before {
1289 background: var(--google-blue-300);
1290 }
1291}
1292
1293#main-frame-error:not(.showing-details) #details {
1294 display: none;
1295}
1296
1297@media (min-width: 240px) and (max-width: 420px) and (min-height: 401px),
1298 (min-height: 240px) and (max-height: 560px) and (min-width: 421px) {
1299 #main-frame-error.showing-details #main-content,
1300 #main-frame-error.showing-details .runner-container {
1301 display: none;
1302 }
1303}
1304</style>
1305
1306
1307 <script>// Copyright 2022 The Chromium Authors
1308// Use of this source code is governed by a BSD-style license that can be
1309// found in the LICENSE file.
1310function assert(value,message){if(value){return}throw new Error("Assertion failed"+(message?`: ${message}`:""))}
1311// Copyright 2022 The Chromium Authors
1312// Use of this source code is governed by a BSD-style license that can be
1313// found in the LICENSE file.
1314class LoadTimeData{data_=null;set data(value){assert(!this.data_,"Re-setting data.");this.data_=value}valueExists(id){assert(this.data_,"No data. Did you remember to include strings.js?");return id in this.data_}getValue(id){assert(this.data_,"No data. Did you remember to include strings.js?");const value=this.data_[id];assert(typeof value!=="undefined","Could not find value for "+id);return value}getString(id){const value=this.getValue(id);assert(typeof value==="string",`[${value}] (${id}) is not a string`);return value}getStringF(id,...args){const value=this.getString(id);if(!value){return""}return this.substituteString(value,...args)}substituteString(label,...args){return label.replace(/\$(.|$|\n)/g,(function(m){assert(m.match(/\$[$1-9]/),"Unescaped $ found in localized string.");if(m==="$$"){return"$"}const substitute=args[Number(m[1])-1];if(substitute===undefined||substitute===null){return""}return substitute.toString()}))}getSubstitutedStringPieces(label,...args){const pieces=(label.match(/(\$[1-9])|(([^$]|\$([^1-9]|$))+)/g)||[]).map((function(p){if(!p.match(/^\$[1-9]$/)){assert((p.match(/\$/g)||[]).length%2===0,"Unescaped $ found in localized string.");return{value:p.replace(/\$\$/g,"$"),arg:null}}const substitute=args[Number(p[1])-1];if(substitute===undefined||substitute===null){return{value:"",arg:p}}return{value:substitute.toString(),arg:p}}));return pieces}getBoolean(id){const value=this.getValue(id);assert(typeof value==="boolean",`[${value}] (${id}) is not a boolean`);return value}getInteger(id){const value=this.getValue(id);assert(typeof value==="number",`[${value}] (${id}) is not a number`);assert(value===Math.floor(value),"Number isn't integer: "+value);return value}overrideValues(replacements){assert(typeof replacements==="object","Replacements must be a dictionary object.");assert(this.data_,"Data must exist before being overridden");for(const key in replacements){this.data_[key]=replacements[key]}}resetForTesting(newData=null){this.data_=newData}isInitialized(){return this.data_!==null}}const loadTimeData=new LoadTimeData;
1315// Copyright 2022 The Chromium Authors
1316// Use of this source code is governed by a BSD-style license that can be
1317// found in the LICENSE file.
1318function getRequiredElement(id){const el=document.querySelector(`#${id}`);assert(el);assert(el instanceof HTMLElement);return el}
1319/**
1320 * @license
1321 * Copyright 2019 Google LLC
1322 * SPDX-License-Identifier: BSD-3-Clause
1323 */const t$3=globalThis,e$3=t$3.ShadowRoot&&(void 0===t$3.ShadyCSS||t$3.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,s$3=Symbol(),o$4=new WeakMap;let n$3=class n{constructor(t,e,o){if(this._$cssResult$=!0,o!==s$3)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const s=this.t;if(e$3&&void 0===t){const e=void 0!==s&&1===s.length;e&&(t=o$4.get(s)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&o$4.set(s,t))}return t}toString(){return this.cssText}};const r$3=t=>new n$3("string"==typeof t?t:t+"",void 0,s$3),S$1=(s,o)=>{if(e$3)s.adoptedStyleSheets=o.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet));else for(const e of o){const o=document.createElement("style"),n=t$3.litNonce;void 0!==n&&o.setAttribute("nonce",n),o.textContent=e.cssText,s.appendChild(o)}},c$3=e$3?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const s of t.cssRules)e+=s.cssText;return r$3(e)})(t):t
1324/**
1325 * @license
1326 * Copyright 2017 Google LLC
1327 * SPDX-License-Identifier: BSD-3-Clause
1328 */;const{is:i$3,defineProperty:e$2,getOwnPropertyDescriptor:h$2,getOwnPropertyNames:r$2,getOwnPropertySymbols:o$3,getPrototypeOf:n$2}=Object,a$1=globalThis,c$2=a$1.trustedTypes,l$1=c$2?c$2.emptyScript:"",p$1=a$1.reactiveElementPolyfillSupport,d$1=(t,s)=>t,u$1={toAttribute(t,s){switch(s){case Boolean:t=t?l$1:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,s){let i=t;switch(s){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},f$3=(t,s)=>!i$3(t,s),b={attribute:!0,type:String,converter:u$1,reflect:!1,useDefault:!1,hasChanged:f$3};Symbol.metadata??=Symbol("metadata"),a$1.litPropertyMetadata??=new WeakMap;let y$1=class y extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,s=b){if(s.state&&(s.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((s=Object.create(s)).wrapped=!0),this.elementProperties.set(t,s),!s.noAccessor){const i=Symbol(),h=this.getPropertyDescriptor(t,i,s);void 0!==h&&e$2(this.prototype,t,h)}}static getPropertyDescriptor(t,s,i){const{get:e,set:r}=h$2(this.prototype,t)??{get(){return this[s]},set(t){this[s]=t}};return{get:e,set(s){const h=e?.call(this);r?.call(this,s),this.requestUpdate(t,h,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??b}static _$Ei(){if(this.hasOwnProperty(d$1("elementProperties")))return;const t=n$2(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(d$1("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(d$1("properties"))){const t=this.properties,s=[...r$2(t),...o$3(t)];for(const i of s)this.createProperty(i,t[i])}const t=this[Symbol.metadata];if(null!==t){const s=litPropertyMetadata.get(t);if(void 0!==s)for(const[t,i]of s)this.elementProperties.set(t,i)}this._$Eh=new Map;for(const[t,s]of this.elementProperties){const i=this._$Eu(t,s);void 0!==i&&this._$Eh.set(i,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(s){const i=[];if(Array.isArray(s)){const e=new Set(s.flat(1/0).reverse());for(const s of e)i.unshift(c$3(s))}else void 0!==s&&i.push(c$3(s));return i}static _$Eu(t,s){const i=s.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach((t=>t(this)))}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,s=this.constructor.elementProperties;for(const i of s.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return S$1(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach((t=>t.hostConnected?.()))}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach((t=>t.hostDisconnected?.()))}attributeChangedCallback(t,s,i){this._$AK(t,i)}_$ET(t,s){const i=this.constructor.elementProperties.get(t),e=this.constructor._$Eu(t,i);if(void 0!==e&&!0===i.reflect){const h=(void 0!==i.converter?.toAttribute?i.converter:u$1).toAttribute(s,i.type);this._$Em=t,null==h?this.removeAttribute(e):this.setAttribute(e,h),this._$Em=null}}_$AK(t,s){const i=this.constructor,e=i._$Eh.get(t);if(void 0!==e&&this._$Em!==e){const t=i.getPropertyOptions(e),h="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:u$1;this._$Em=e,this[e]=h.fromAttribute(s,t.type)??this._$Ej?.get(e)??null,this._$Em=null}}requestUpdate(t,s,i){if(void 0!==t){const e=this.constructor,h=this[t];if(i??=e.getPropertyOptions(t),!((i.hasChanged??f$3)(h,s)||i.useDefault&&i.reflect&&h===this._$Ej?.get(t)&&!this.hasAttribute(e._$Eu(t,i))))return;this.C(t,s,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(t,s,{useDefault:i,reflect:e,wrapped:h},r){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,r??s??this[t]),!0!==h||void 0!==r)||(this._$AL.has(t)||(this.hasUpdated||i||(s=void 0),this._$AL.set(t,s)),!0===e&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,s]of this._$Ep)this[t]=s;this._$Ep=void 0}const t=this.constructor.elementProperties;if(t.size>0)for(const[s,i]of t){const{wrapped:t}=i,e=this[s];!0!==t||this._$AL.has(s)||void 0===e||this.C(s,void 0,i,e)}}let t=!1;const s=this._$AL;try{t=this.shouldUpdate(s),t?(this.willUpdate(s),this._$EO?.forEach((t=>t.hostUpdate?.())),this.update(s)):this._$EM()}catch(s){throw t=!1,this._$EM(),s}t&&this._$AE(s)}willUpdate(t){}_$AE(t){this._$EO?.forEach((t=>t.hostUpdated?.())),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach((t=>this._$ET(t,this[t]))),this._$EM()}updated(t){}firstUpdated(t){}};y$1.elementStyles=[],y$1.shadowRootOptions={mode:"open"},y$1[d$1("elementProperties")]=new Map,y$1[d$1("finalized")]=new Map,p$1?.({ReactiveElement:y$1}),(a$1.reactiveElementVersions??=[]).push("2.1.0");
1329/**
1330 * @license
1331 * Copyright 2017 Google LLC
1332 * SPDX-License-Identifier: BSD-3-Clause
1333 */const t$2=globalThis,i$2=t$2.trustedTypes,s$2=i$2?i$2.createPolicy("lit-html-desktop",{createHTML:t=>t}):void 0,e$1="$lit$",h$1=`lit$${Math.random().toFixed(9).slice(2)}$`,o$2="?"+h$1,n$1=`<${o$2}>`,r$1=document,l=()=>r$1.createComment(""),c$1=t=>null===t||"object"!=typeof t&&"function"!=typeof t,a=Array.isArray,u=t=>a(t)||"function"==typeof t?.[Symbol.iterator],d="[ \t\n\f\r]",f$2=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,v=/-->/g,_=/>/g,m=RegExp(`>|${d}(?:([^\\s"'>=/]+)(${d}*=${d}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),p=/'/g,g=/"/g,$=/^(?:script|style|textarea|title)$/i,y=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),x=y(1),T=Symbol.for("lit-noChange"),E=Symbol.for("lit-nothing"),A=new WeakMap,C=r$1.createTreeWalker(r$1,129);function P(t,i){if(!a(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==s$2?s$2.createHTML(i):i}const V=(t,i)=>{const s=t.length-1,o=[];let r,l=2===i?"<svg>":3===i?"<math>":"",c=f$2;for(let i=0;i<s;i++){const s=t[i];let a,u,d=-1,y=0;for(;y<s.length&&(c.lastIndex=y,u=c.exec(s),null!==u);)y=c.lastIndex,c===f$2?"!--"===u[1]?c=v:void 0!==u[1]?c=_:void 0!==u[2]?($.test(u[2])&&(r=RegExp("</"+u[2],"g")),c=m):void 0!==u[3]&&(c=m):c===m?">"===u[0]?(c=r??f$2,d=-1):void 0===u[1]?d=-2:(d=c.lastIndex-u[2].length,a=u[1],c=void 0===u[3]?m:'"'===u[3]?g:p):c===g||c===p?c=m:c===v||c===_?c=f$2:(c=m,r=void 0);const x=c===m&&t[i+1].startsWith("/>")?" ":"";l+=c===f$2?s+n$1:d>=0?(o.push(a),s.slice(0,d)+e$1+s.slice(d)+h$1+x):s+h$1+(-2===d?i:x)}return[P(t,l+(t[s]||"<?>")+(2===i?"</svg>":3===i?"</math>":"")),o]};class N{constructor({strings:t,_$litType$:s},n){let r;this.parts=[];let c=0,a=0;const u=t.length-1,d=this.parts,[f,v]=V(t,s);if(this.el=N.createElement(f,n),C.currentNode=this.el.content,2===s||3===s){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(r=C.nextNode())&&d.length<u;){if(1===r.nodeType){if(r.hasAttributes())for(const t of r.getAttributeNames())if(t.endsWith(e$1)){const i=v[a++],s=r.getAttribute(t).split(h$1),e=/([.?@])?(.*)/.exec(i);d.push({type:1,index:c,name:e[2],strings:s,ctor:"."===e[1]?H:"?"===e[1]?I:"@"===e[1]?L:k}),r.removeAttribute(t)}else t.startsWith(h$1)&&(d.push({type:6,index:c}),r.removeAttribute(t));if($.test(r.tagName)){const t=r.textContent.split(h$1),s=t.length-1;if(s>0){r.textContent=i$2?i$2.emptyScript:"";for(let i=0;i<s;i++)r.append(t[i],l()),C.nextNode(),d.push({type:2,index:++c});r.append(t[s],l())}}}else if(8===r.nodeType)if(r.data===o$2)d.push({type:2,index:c});else{let t=-1;for(;-1!==(t=r.data.indexOf(h$1,t+1));)d.push({type:7,index:c}),t+=h$1.length-1}c++}}static createElement(t,i){const s=r$1.createElement("template");return s.innerHTML=t,s}}function S(t,i,s=t,e){if(i===T)return i;let h=void 0!==e?s._$Co?.[e]:s._$Cl;const o=c$1(i)?void 0:i._$litDirective$;return h?.constructor!==o&&(h?._$AO?.(!1),void 0===o?h=void 0:(h=new o(t),h._$AT(t,s,e)),void 0!==e?(s._$Co??=[])[e]=h:s._$Cl=h),void 0!==h&&(i=S(t,h._$AS(t,i.values),h,e)),i}class M{constructor(t,i){this._$AV=[],this._$AN=void 0,this._$AD=t,this._$AM=i}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(t){const{el:{content:i},parts:s}=this._$AD,e=(t?.creationScope??r$1).importNode(i,!0);C.currentNode=e;let h=C.nextNode(),o=0,n=0,l=s[0];for(;void 0!==l;){if(o===l.index){let i;2===l.type?i=new R(h,h.nextSibling,this,t):1===l.type?i=new l.ctor(h,l.name,l.strings,this,t):6===l.type&&(i=new z(h,this,t)),this._$AV.push(i),l=s[++n]}o!==l?.index&&(h=C.nextNode(),o++)}return C.currentNode=r$1,e}p(t){let i=0;for(const s of this._$AV)void 0!==s&&(void 0!==s.strings?(s._$AI(t,s,i),i+=s.strings.length-2):s._$AI(t[i])),i++}}class R{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(t,i,s,e){this.type=2,this._$AH=E,this._$AN=void 0,this._$AA=t,this._$AB=i,this._$AM=s,this.options=e,this._$Cv=e?.isConnected??!0}get parentNode(){let t=this._$AA.parentNode;const i=this._$AM;return void 0!==i&&11===t?.nodeType&&(t=i.parentNode),t}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(t,i=this){t=S(this,t,i),c$1(t)?t===E||null==t||""===t?(this._$AH!==E&&this._$AR(),this._$AH=E):t!==this._$AH&&t!==T&&this._(t):void 0!==t._$litType$?this.$(t):void 0!==t.nodeType?this.T(t):u(t)?this.k(t):this._(t)}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}_(t){this._$AH!==E&&c$1(this._$AH)?this._$AA.nextSibling.data=t:this.T(r$1.createTextNode(t)),this._$AH=t}$(t){const{values:i,_$litType$:s}=t,e="number"==typeof s?this._$AC(t):(void 0===s.el&&(s.el=N.createElement(P(s.h,s.h[0]),this.options)),s);if(this._$AH?._$AD===e)this._$AH.p(i);else{const t=new M(e,this),s=t.u(this.options);t.p(i),this.T(s),this._$AH=t}}_$AC(t){let i=A.get(t.strings);return void 0===i&&A.set(t.strings,i=new N(t)),i}k(t){a(this._$AH)||(this._$AH=[],this._$AR());const i=this._$AH;let s,e=0;for(const h of t)e===i.length?i.push(s=new R(this.O(l()),this.O(l()),this,this.options)):s=i[e],s._$AI(h),e++;e<i.length&&(this._$AR(s&&s._$AB.nextSibling,e),i.length=e)}_$AR(t=this._$AA.nextSibling,i){for(this._$AP?.(!1,!0,i);t&&t!==this._$AB;){const i=t.nextSibling;t.remove(),t=i}}setConnected(t){void 0===this._$AM&&(this._$Cv=t,this._$AP?.(t))}}class k{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(t,i,s,e,h){this.type=1,this._$AH=E,this._$AN=void 0,this.element=t,this.name=i,this._$AM=e,this.options=h,s.length>2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=E}_$AI(t,i=this,s,e){const h=this.strings;let o=!1;if(void 0===h)t=S(this,t,i,0),o=!c$1(t)||t!==this._$AH&&t!==T,o&&(this._$AH=t);else{const e=t;let n,r;for(t=h[0],n=0;n<h.length-1;n++)r=S(this,e[s+n],i,n),r===T&&(r=this._$AH[n]),o||=!c$1(r)||r!==this._$AH[n],r===E?t=E:t!==E&&(t+=(r??"")+h[n+1]),this._$AH[n]=r}o&&!e&&this.j(t)}j(t){t===E?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,t??"")}}class H extends k{constructor(){super(...arguments),this.type=3}j(t){this.element[this.name]=t===E?void 0:t}}class I extends k{constructor(){super(...arguments),this.type=4}j(t){this.element.toggleAttribute(this.name,!!t&&t!==E)}}class L extends k{constructor(t,i,s,e,h){super(t,i,s,e,h),this.type=5}_$AI(t,i=this){if((t=S(this,t,i,0)??E)===T)return;const s=this._$AH,e=t===E&&s!==E||t.capture!==s.capture||t.once!==s.once||t.passive!==s.passive,h=t!==E&&(s===E||e);e&&this.element.removeEventListener(this.name,this,s),h&&this.element.addEventListener(this.name,this,t),this._$AH=t}handleEvent(t){"function"==typeof this._$AH?this._$AH.call(this.options?.host??this.element,t):this._$AH.handleEvent(t)}}class z{constructor(t,i,s){this.element=t,this.type=6,this._$AN=void 0,this._$AM=i,this.options=s}get _$AU(){return this._$AM._$AU}_$AI(t){S(this,t)}}const j=t$2.litHtmlPolyfillSupport;j?.(N,R),(t$2.litHtmlVersions??=[]).push("3.3.0");const B=(t,i,s)=>{const e=s?.renderBefore??i;let h=e._$litPart$;if(void 0===h){const t=s?.renderBefore??null;e._$litPart$=h=new R(i.insertBefore(l(),t),t,void 0,s??{})}return h._$AI(t),h
1334/**
1335 * @license
1336 * Copyright 2017 Google LLC
1337 * SPDX-License-Identifier: BSD-3-Clause
1338 */};const s$1=globalThis;let i$1=class i extends y$1{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const r=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=B(r,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return T}};i$1._$litElement$=!0,i$1["finalized"]=!0,s$1.litElementHydrateSupport?.({LitElement:i$1});const o$1=s$1.litElementPolyfillSupport;o$1?.({LitElement:i$1});(s$1.litElementVersions??=[]).push("4.2.0");
1339// Copyright 2023 The Chromium Authors
1340// Use of this source code is governed by a BSD-style license that can be
1341// found in the LICENSE file.
1342const HIDDEN_CLASS="hidden";
1343// Copyright 2024 The Chromium Authors
1344// Use of this source code is governed by a BSD-style license that can be
1345// found in the LICENSE file.
1346const IS_IOS=/CriOS/.test(window.navigator.userAgent);const IS_HIDPI=window.devicePixelRatio>1;const IS_MOBILE=/Android/.test(window.navigator.userAgent)||IS_IOS;const IS_RTL=document.documentElement.dir==="rtl";const FPS=60;const DEFAULT_DIMENSIONS={width:600,height:150};
1347// Copyright 2025 The Chromium Authors
1348// Use of this source code is governed by a BSD-style license that can be
1349// found in the LICENSE file.
1350function getRandomNum(min,max){return Math.floor(Math.random()*(max-min+1))+min}function getTimeStamp(){return IS_IOS?(new Date).getTime():performance.now()}
1351// Copyright 2024 The Chromium Authors
1352// Use of this source code is governed by a BSD-style license that can be
1353// found in the LICENSE file.
1354var Dimensions;(function(Dimensions){Dimensions[Dimensions["WIDTH"]=10]="WIDTH";Dimensions[Dimensions["HEIGHT"]=13]="HEIGHT";Dimensions[Dimensions["DEST_WIDTH"]=11]="DEST_WIDTH"})(Dimensions||(Dimensions={}));var Config$2;(function(Config){Config[Config["MAX_DISTANCE_UNITS"]=5]="MAX_DISTANCE_UNITS";Config[Config["ACHIEVEMENT_DISTANCE"]=100]="ACHIEVEMENT_DISTANCE";Config[Config["COEFFICIENT"]=.025]="COEFFICIENT";Config[Config["FLASH_DURATION"]=250]="FLASH_DURATION";Config[Config["FLASH_ITERATIONS"]=3]="FLASH_ITERATIONS";Config[Config["HIGH_SCORE_HIT_AREA_PADDING"]=4]="HIGH_SCORE_HIT_AREA_PADDING"})(Config$2||(Config$2={}));class DistanceMeter{achievement=false;canvas;canvasCtx;image;spritePos;x=0;y=5;maxScore=0;highScore="0";digits=[];defaultString="";flashTimer=0;flashIterations=0;flashingRafId=null;highScoreBounds=null;highScoreFlashing=false;maxScoreUnits=Config$2.MAX_DISTANCE_UNITS;canvasWidth;frameTimeStamp;constructor(canvas,spritePos,canvasWidth,imageSpriteProvider){this.canvas=canvas;const canvasContext=canvas.getContext("2d");assert(canvasContext);this.canvasCtx=canvasContext;this.image=imageSpriteProvider.getRunnerImageSprite();this.spritePos=spritePos;this.canvasWidth=canvasWidth;this.init(canvasWidth)}init(width){let maxDistanceStr="";this.calcXpos(width);this.maxScore=this.maxScoreUnits;for(let i=0;i<this.maxScoreUnits;i++){this.draw(i,0);this.defaultString+="0";maxDistanceStr+="9"}this.maxScore=parseInt(maxDistanceStr,10)}calcXpos(canvasWidth){this.x=canvasWidth-Dimensions.DEST_WIDTH*(this.maxScoreUnits+1)}draw(digitPos,value,highScore){let sourceWidth=Dimensions.WIDTH;let sourceHeight=Dimensions.HEIGHT;let sourceX=Dimensions.WIDTH*value;let sourceY=0;const targetX=digitPos*Dimensions.DEST_WIDTH;const targetY=this.y;const targetWidth=Dimensions.WIDTH;const targetHeight=Dimensions.HEIGHT;if(IS_HIDPI){sourceWidth*=2;sourceHeight*=2;sourceX*=2}sourceX+=this.spritePos.x;sourceY+=this.spritePos.y;this.canvasCtx.save();if(IS_RTL){const translateX=highScore?this.canvasWidth-Dimensions.WIDTH*(this.maxScoreUnits+3):this.canvasWidth-Dimensions.WIDTH;this.canvasCtx.translate(translateX,this.y);this.canvasCtx.scale(-1,1)}else{const highScoreX=this.x-this.maxScoreUnits*2*Dimensions.WIDTH;this.canvasCtx.translate(highScore?highScoreX:this.x,this.y)}this.canvasCtx.drawImage(this.image,sourceX,sourceY,sourceWidth,sourceHeight,targetX,targetY,targetWidth,targetHeight);this.canvasCtx.restore()}getActualDistance(distance){return distance?Math.round(distance*Config$2.COEFFICIENT):0}update(deltaTime,distance){let paint=true;let playSound=false;if(!this.achievement){distance=this.getActualDistance(distance);if(distance>this.maxScore&&this.maxScoreUnits===Config$2.MAX_DISTANCE_UNITS){this.maxScoreUnits++;this.maxScore=parseInt(this.maxScore+"9",10)}if(distance>0){if(distance%Config$2.ACHIEVEMENT_DISTANCE===0){this.achievement=true;this.flashTimer=0;playSound=true}const distanceStr=(this.defaultString+distance).substr(-this.maxScoreUnits);this.digits=distanceStr.split("")}else{this.digits=this.defaultString.split("")}}else{if(this.flashIterations<=Config$2.FLASH_ITERATIONS){this.flashTimer+=deltaTime;if(this.flashTimer<Config$2.FLASH_DURATION){paint=false}else if(this.flashTimer>Config$2.FLASH_DURATION*2){this.flashTimer=0;this.flashIterations++}}else{this.achievement=false;this.flashIterations=0;this.flashTimer=0}}if(paint){for(let i=this.digits.length-1;i>=0;i--){this.draw(i,parseInt(this.digits[i],10))}}this.drawHighScore();return playSound}drawHighScore(){if(this.highScore.length>0){this.canvasCtx.save();this.canvasCtx.globalAlpha=.8;for(let i=this.highScore.length-1;i>=0;i--){const characterToDraw=this.highScore[i];let characterSpritePosition=parseInt(characterToDraw,10);if(isNaN(characterSpritePosition)){switch(characterToDraw){case"H":characterSpritePosition=10;break;case"I":characterSpritePosition=11;break;default:continue}}this.draw(i,characterSpritePosition,true)}this.canvasCtx.restore()}}setHighScore(distance){distance=this.getActualDistance(distance);const highScoreStr=(this.defaultString+distance).substr(-this.maxScoreUnits);this.highScore="HI "+highScoreStr}hasClickedOnHighScore(e){let x=0;let y=0;if(e instanceof TouchEvent){const canvasBounds=this.canvas.getBoundingClientRect();x=e.touches[0].clientX-canvasBounds.left;y=e.touches[0].clientY-canvasBounds.top}else if(e instanceof MouseEvent){x=e.offsetX;y=e.offsetY}this.highScoreBounds=this.getHighScoreBounds();return x>=this.highScoreBounds.x&&x<=this.highScoreBounds.x+this.highScoreBounds.width&&y>=this.highScoreBounds.y&&y<=this.highScoreBounds.y+this.highScoreBounds.height}getHighScoreBounds(){return{x:this.x-this.maxScoreUnits*2*Dimensions.WIDTH-Config$2.HIGH_SCORE_HIT_AREA_PADDING,y:this.y,width:Dimensions.WIDTH*(this.highScore.length+1)+Config$2.HIGH_SCORE_HIT_AREA_PADDING,height:Dimensions.HEIGHT+Config$2.HIGH_SCORE_HIT_AREA_PADDING*2}}flashHighScore(){const now=getTimeStamp();const deltaTime=now-(this.frameTimeStamp||now);let paint=true;this.frameTimeStamp=now;if(this.flashIterations>Config$2.FLASH_ITERATIONS*2){this.cancelHighScoreFlashing();return}this.flashTimer+=deltaTime;if(this.flashTimer<Config$2.FLASH_DURATION){paint=false}else if(this.flashTimer>Config$2.FLASH_DURATION*2){this.flashTimer=0;this.flashIterations++}if(paint){this.drawHighScore()}else{this.clearHighScoreBounds()}this.flashingRafId=requestAnimationFrame(this.flashHighScore.bind(this))}clearHighScoreBounds(){assert(this.highScoreBounds);this.canvasCtx.save();this.canvasCtx.fillStyle="#fff";this.canvasCtx.rect(this.highScoreBounds.x,this.highScoreBounds.y,this.highScoreBounds.width,this.highScoreBounds.height);this.canvasCtx.fill();this.canvasCtx.restore()}startHighScoreFlashing(){this.highScoreFlashing=true;this.flashHighScore()}isHighScoreFlashing(){return this.highScoreFlashing}cancelHighScoreFlashing(){if(this.flashingRafId){cancelAnimationFrame(this.flashingRafId)}this.flashIterations=0;this.flashTimer=0;this.highScoreFlashing=false;this.clearHighScoreBounds();this.drawHighScore()}resetHighScore(){this.setHighScore(0);this.cancelHighScoreFlashing()}reset(){this.update(0,0);this.achievement=false}}
1355// Copyright 2021 The Chromium Authors
1356// Use of this source code is governed by a BSD-style license that can be
1357// found in the LICENSE file.
1358const GAME_TYPE=[];class CollisionBox{x;y;width;height;constructor(x,y,width,height){this.x=x;this.y=y;this.width=width;this.height=height}}const spriteDefinitionByType={original:{ldpi:{backgroundEl:{x:86,y:2},cactusLarge:{x:332,y:2},cactusSmall:{x:228,y:2},obstacle2:{x:332,y:2},obstacle:{x:228,y:2},cloud:{x:86,y:2},horizon:{x:2,y:54},moon:{x:484,y:2},pterodactyl:{x:134,y:2},restart:{x:2,y:68},textSprite:{x:655,y:2},tRex:{x:848,y:2},star:{x:645,y:2},collectable:{x:0,y:0},altGameEnd:{x:32,y:0}},hdpi:{backgroundEl:{x:166,y:2},cactusLarge:{x:652,y:2},cactusSmall:{x:446,y:2},obstacle2:{x:652,y:2},obstacle:{x:446,y:2},cloud:{x:166,y:2},horizon:{x:2,y:104},moon:{x:954,y:2},pterodactyl:{x:260,y:2},restart:{x:2,y:130},textSprite:{x:1294,y:2},tRex:{x:1678,y:2},star:{x:1276,y:2},collectable:{x:0,y:0},altGameEnd:{x:64,y:0}},maxGapCoefficient:1.5,maxObstacleLength:3,hasClouds:true,bottomPad:10,obstacles:[{type:"cactusSmall",width:17,height:35,yPos:105,multipleSpeed:4,minGap:120,minSpeed:0,collisionBoxes:[{x:0,y:7,width:5,height:27},{x:4,y:0,width:6,height:34},{x:10,y:4,width:7,height:14}]},{type:"cactusLarge",width:25,height:50,yPos:90,multipleSpeed:7,minGap:120,minSpeed:0,collisionBoxes:[{x:0,y:12,width:7,height:38},{x:8,y:0,width:7,height:49},{x:13,y:10,width:10,height:38}]},{type:"pterodactyl",width:46,height:40,yPos:[100,75,50],yPosMobile:[100,50],multipleSpeed:999,minSpeed:8.5,minGap:150,collisionBoxes:[{x:15,y:15,width:16,height:5},{x:18,y:21,width:24,height:6},{x:2,y:14,width:4,height:3},{x:6,y:10,width:4,height:7},{x:10,y:8,width:6,height:9}],numFrames:2,frameRate:1e3/6,speedOffset:.8},{type:"collectable",width:31,height:24,yPos:104,multipleSpeed:1e3,minGap:9999,minSpeed:0,collisionBoxes:[{x:0,y:0,width:32,height:25}]}],backgroundEl:{CLOUD:{height:14,offset:4,width:46,xPos:1,fixed:false}},backgroundElConfig:{maxBgEls:1,maxGap:400,minGap:100,pos:0,speed:.5,yPos:125},lines:[{sourceX:2,sourceY:52,width:600,height:12,yPos:127}],altGameOverTextConfig:{textX:32,textY:0,textWidth:246,textHeight:17,flashDuration:1500,flashing:false}}};
1359// Copyright 2024 The Chromium Authors
1360// Use of this source code is governed by a BSD-style license that can be
1361// found in the LICENSE file.
1362const RESTART_ANIM_DURATION=875;const LOGO_PAUSE_DURATION=875;const FLASH_ITERATIONS=5;const animConfig={frames:[0,36,72,108,144,180,216,252],msPerFrame:RESTART_ANIM_DURATION/8};const defaultPanelDimensions={textX:0,textY:13,textWidth:191,textHeight:11,restartWidth:36,restartHeight:32};class GameOverPanel{canvasCtx;canvasDimensions;textImgPos;restartImgPos;imageSpriteProvider;altGameEndImgPos;altGameModeActive;frameTimeStamp=0;animTimer=0;currentFrame=0;gameOverRafId=null;flashTimer=0;flashCounter=0;originalText=true;constructor(canvas,textImgPos,restartImgPos,dimensions,imageSpriteProvider,altGameEndImgPos,altGameActive){const canvasContext=canvas.getContext("2d");assert(canvasContext);this.canvasCtx=canvasContext;this.canvasDimensions=dimensions;this.textImgPos=textImgPos;this.restartImgPos=restartImgPos;this.imageSpriteProvider=imageSpriteProvider;this.altGameEndImgPos=altGameEndImgPos??null;this.altGameModeActive=altGameActive??false}updateDimensions(width,height){this.canvasDimensions.width=width;if(height){this.canvasDimensions.height=height}this.currentFrame=animConfig.frames.length-1}drawGameOverText(dimensions,useAltText){const centerX=this.canvasDimensions.width/2;let textSourceX=dimensions.textX;let textSourceY=dimensions.textY;let textSourceWidth=dimensions.textWidth;let textSourceHeight=dimensions.textHeight;const textTargetX=Math.round(centerX-dimensions.textWidth/2);const textTargetY=Math.round((this.canvasDimensions.height-25)/3);const textTargetWidth=dimensions.textWidth;const textTargetHeight=dimensions.textHeight;if(IS_HIDPI){textSourceY*=2;textSourceX*=2;textSourceWidth*=2;textSourceHeight*=2}if(!useAltText){textSourceX+=this.textImgPos.x;textSourceY+=this.textImgPos.y}const spriteSource=useAltText?this.imageSpriteProvider.getAltCommonImageSprite():this.imageSpriteProvider.getOrigImageSprite();assert(spriteSource);this.canvasCtx.save();if(IS_RTL){this.canvasCtx.translate(this.canvasDimensions.width,0);this.canvasCtx.scale(-1,1)}this.canvasCtx.drawImage(spriteSource,textSourceX,textSourceY,textSourceWidth,textSourceHeight,textTargetX,textTargetY,textTargetWidth,textTargetHeight);this.canvasCtx.restore()}drawAltGameElements(tRex){const spriteDefinition=this.imageSpriteProvider.getSpriteDefinition();if(this.altGameModeActive&&spriteDefinition){assert(this.altGameEndImgPos);const altGameEndConfig=spriteDefinition.altGameEndConfig;assert(altGameEndConfig);let altGameEndSourceWidth=altGameEndConfig.width;let altGameEndSourceHeight=altGameEndConfig.height;const altGameEndTargetX=tRex.xPos+altGameEndConfig.xOffset;const altGameEndTargetY=tRex.yPos+altGameEndConfig.yOffset;if(IS_HIDPI){altGameEndSourceWidth*=2;altGameEndSourceHeight*=2}const altCommonImageSprite=this.imageSpriteProvider.getAltCommonImageSprite();assert(altCommonImageSprite);this.canvasCtx.drawImage(altCommonImageSprite,this.altGameEndImgPos.x,this.altGameEndImgPos.y,altGameEndSourceWidth,altGameEndSourceHeight,altGameEndTargetX,altGameEndTargetY,altGameEndConfig.width,altGameEndConfig.height)}}drawRestartButton(){const dimensions=defaultPanelDimensions;let framePosX=animConfig.frames[this.currentFrame];let restartSourceWidth=dimensions.restartWidth;let restartSourceHeight=dimensions.restartHeight;const restartTargetX=this.canvasDimensions.width/2-dimensions.restartHeight/2;const restartTargetY=this.canvasDimensions.height/2;if(IS_HIDPI){restartSourceWidth*=2;restartSourceHeight*=2;framePosX*=2}this.canvasCtx.save();if(IS_RTL){this.canvasCtx.translate(this.canvasDimensions.width,0);this.canvasCtx.scale(-1,1)}const origImageSprite=this.imageSpriteProvider.getOrigImageSprite();this.canvasCtx.drawImage(origImageSprite,this.restartImgPos.x+framePosX,this.restartImgPos.y,restartSourceWidth,restartSourceHeight,restartTargetX,restartTargetY,dimensions.restartWidth,dimensions.restartHeight);this.canvasCtx.restore()}draw(altGameModeActive,tRex){if(altGameModeActive){this.altGameModeActive=altGameModeActive}this.drawGameOverText(defaultPanelDimensions,false);this.drawRestartButton();if(tRex){this.drawAltGameElements(tRex)}this.update()}update(){const now=getTimeStamp();const deltaTime=now-(this.frameTimeStamp||now);this.frameTimeStamp=now;this.animTimer+=deltaTime;this.flashTimer+=deltaTime;if(this.currentFrame===0&&this.animTimer>LOGO_PAUSE_DURATION){this.animTimer=0;this.currentFrame++;this.drawRestartButton()}else if(this.currentFrame>0&&this.currentFrame<animConfig.frames.length){if(this.animTimer>=animConfig.msPerFrame){this.currentFrame++;this.drawRestartButton()}}else if(!this.altGameModeActive&&this.currentFrame===animConfig.frames.length){this.reset();return}if(this.altGameModeActive&&spriteDefinitionByType.original.altGameOverTextConfig){const altTextConfig=spriteDefinitionByType.original.altGameOverTextConfig;if(altTextConfig.flashing){if(this.flashCounter<FLASH_ITERATIONS&&this.flashTimer>altTextConfig.flashDuration){this.flashTimer=0;this.originalText=!this.originalText;this.clearGameOverTextBounds();if(this.originalText){this.drawGameOverText(defaultPanelDimensions,false);this.flashCounter++}else{this.drawGameOverText(altTextConfig,true)}}else if(this.flashCounter>=FLASH_ITERATIONS){this.reset();return}}else{this.clearGameOverTextBounds(altTextConfig);this.drawGameOverText(altTextConfig,true)}}this.gameOverRafId=requestAnimationFrame(this.update.bind(this))}clearGameOverTextBounds(dimensions=defaultPanelDimensions){this.canvasCtx.save();this.canvasCtx.clearRect(Math.round(this.canvasDimensions.width/2-dimensions.textWidth/2),Math.round((this.canvasDimensions.height-25)/3),dimensions.textWidth,dimensions.textHeight+4);this.canvasCtx.restore()}reset(){if(this.gameOverRafId){cancelAnimationFrame(this.gameOverRafId);this.gameOverRafId=null}this.animTimer=0;this.frameTimeStamp=0;this.currentFrame=0;this.flashTimer=0;this.flashCounter=0;this.originalText=true}}
1363// Copyright 2024 The Chromium Authors
1364// Use of this source code is governed by a BSD-style license that can be
1365// found in the LICENSE file.
1366class GeneratedSoundFx{constructor(){this.context=new AudioContext;if(IS_IOS){this.context.onstatechange=()=>{if(this.context.state!=="running"){this.context.resume()}};this.context.resume()}this.panner=this.context.createStereoPanner?this.context.createStereoPanner():null}context;panner=null;bgSoundIntervalId=null;stopAll(){this.cancelFootSteps()}playNote(frequency,startTime,duration,vol=.01,pan=0){const osc1=this.context.createOscillator();const osc2=this.context.createOscillator();const volume=this.context.createGain();osc1.type="triangle";osc2.type="triangle";volume.gain.value=.1;if(this.panner){this.panner.pan.value=pan;osc1.connect(volume).connect(this.panner);osc2.connect(volume).connect(this.panner);this.panner.connect(this.context.destination)}else{osc1.connect(volume);osc2.connect(volume);volume.connect(this.context.destination)}osc1.frequency.value=frequency+1;osc2.frequency.value=frequency-2;volume.gain.setValueAtTime(vol,startTime+duration-.05);volume.gain.linearRampToValueAtTime(1e-5,startTime+duration);osc1.start(startTime);osc2.start(startTime);osc1.stop(startTime+duration);osc2.stop(startTime+duration)}background(){const now=this.context.currentTime;this.playNote(493.883,now,.116);this.playNote(659.255,now+.116,.232);this.loopFootSteps()}loopFootSteps(){if(!this.bgSoundIntervalId){this.bgSoundIntervalId=setInterval((()=>{this.playNote(73.42,this.context.currentTime,.05,.16);this.playNote(69.3,this.context.currentTime+.116,.116,.16)}),280)}}cancelFootSteps(){if(this.bgSoundIntervalId){clearInterval(this.bgSoundIntervalId);this.bgSoundIntervalId=null;this.playNote(103.83,this.context.currentTime,.232,.02);this.playNote(116.54,this.context.currentTime+.116,.232,.02)}}collect(){this.cancelFootSteps();const now=this.context.currentTime;this.playNote(830.61,now,.116);this.playNote(1318.51,now+.116,.232)}jump(){const now=this.context.currentTime;this.playNote(659.25,now,.116,.3,-.6);this.playNote(880,now+.116,.232,.3,-.6)}}
1367// Copyright 2024 The Chromium Authors
1368// Use of this source code is governed by a BSD-style license that can be
1369// found in the LICENSE file.
1370let globalConfig={maxBgEls:0,maxGap:0,minGap:0,msPerFrame:0,pos:0,speed:0,yPos:0};function getGlobalConfig(){return globalConfig}function setGlobalConfig(config){globalConfig=config}class BackgroundEl{gap;xPos;remove=false;canvas;canvasCtx;spritePos;yPos=0;type;animTimer=0;spriteConfig;switchFrames=false;imageSpriteProvider;constructor(canvas,spritePos,containerWidth,type,imageSpriteProvider){this.canvas=canvas;const canvasContext=this.canvas.getContext("2d");assert(canvasContext);this.canvasCtx=canvasContext;this.spritePos=spritePos;this.imageSpriteProvider=imageSpriteProvider;this.xPos=containerWidth;this.type=type;this.gap=getRandomNum(getGlobalConfig().minGap,getGlobalConfig().maxGap);const spriteConfig=imageSpriteProvider.getSpriteDefinition().backgroundEl[this.type];assert(spriteConfig);this.spriteConfig=spriteConfig;this.init()}init(){if(this.spriteConfig.fixed){assert(this.spriteConfig.fixedXPos);this.xPos=this.spriteConfig.fixedXPos}this.yPos=getGlobalConfig().yPos-this.spriteConfig.height+this.spriteConfig.offset;this.draw()}draw(){this.canvasCtx.save();let sourceWidth=this.spriteConfig.width;let sourceHeight=this.spriteConfig.height;let sourceX=this.spriteConfig.xPos;const outputWidth=sourceWidth;const outputHeight=sourceHeight;const imageSprite=this.imageSpriteProvider.getRunnerImageSprite();assert(imageSprite);if(IS_HIDPI){sourceWidth*=2;sourceHeight*=2;sourceX*=2}this.canvasCtx.drawImage(imageSprite,sourceX,this.spritePos.y,sourceWidth,sourceHeight,this.xPos,this.yPos,outputWidth,outputHeight);this.canvasCtx.restore()}update(speed){if(!this.remove){if(this.spriteConfig.fixed){const globalConfig=getGlobalConfig();assert(globalConfig.msPerFrame);this.animTimer+=speed;if(this.animTimer>globalConfig.msPerFrame){this.animTimer=0;this.switchFrames=!this.switchFrames}if(this.spriteConfig.fixedYPos1&&this.spriteConfig.fixedYPos2){this.yPos=this.switchFrames?this.spriteConfig.fixedYPos1:this.spriteConfig.fixedYPos2}}else{this.xPos-=getGlobalConfig().speed}this.draw();if(!this.isVisible()){this.remove=true}}}isVisible(){return this.xPos+this.spriteConfig.width>0}}
1371// Copyright 2025 The Chromium Authors
1372// Use of this source code is governed by a BSD-style license that can be
1373// found in the LICENSE file.
1374class Cloud{gap;xPos;remove=false;yPos=0;canvasCtx;spritePos;imageSpriteProvider;constructor(canvas,spritePos,containerWidth,imageSpriteProvider){const canvasContext=canvas.getContext("2d");assert(canvasContext);this.canvasCtx=canvasContext;this.xPos=containerWidth;this.spritePos=spritePos;this.imageSpriteProvider=imageSpriteProvider;this.gap=getRandomNum(Config$1.MIN_CLOUD_GAP,Config$1.MAX_CLOUD_GAP);this.init()}init(){this.yPos=getRandomNum(Config$1.MAX_SKY_LEVEL,Config$1.MIN_SKY_LEVEL);this.draw()}draw(){const runnerImageSprite=this.imageSpriteProvider.getRunnerImageSprite();this.canvasCtx.save();let sourceWidth=Config$1.WIDTH;let sourceHeight=Config$1.HEIGHT;const outputWidth=sourceWidth;const outputHeight=sourceHeight;if(IS_HIDPI){sourceWidth=sourceWidth*2;sourceHeight=sourceHeight*2}this.canvasCtx.drawImage(runnerImageSprite,this.spritePos.x,this.spritePos.y,sourceWidth,sourceHeight,this.xPos,this.yPos,outputWidth,outputHeight);this.canvasCtx.restore()}update(speed){if(!this.remove){this.xPos-=Math.ceil(speed);this.draw();if(!this.isVisible()){this.remove=true}}}isVisible(){return this.xPos+Config$1.WIDTH>0}}var Config$1;(function(Config){Config[Config["HEIGHT"]=14]="HEIGHT";Config[Config["MAX_CLOUD_GAP"]=400]="MAX_CLOUD_GAP";Config[Config["MAX_SKY_LEVEL"]=30]="MAX_SKY_LEVEL";Config[Config["MIN_CLOUD_GAP"]=100]="MIN_CLOUD_GAP";Config[Config["MIN_SKY_LEVEL"]=71]="MIN_SKY_LEVEL";Config[Config["WIDTH"]=46]="WIDTH"})(Config$1||(Config$1={}));
1375// Copyright 2024 The Chromium Authors
1376// Use of this source code is governed by a BSD-style license that can be
1377// found in the LICENSE file.
1378class HorizonLine{canvasCtx;xPos;yPos=0;bumpThreshold=.5;sourceXPos;spritePos;sourceDimensions;dimensions;imageSpriteProvider;constructor(canvas,lineConfig,imageSpriteProvider){let sourceX=lineConfig.sourceX;let sourceY=lineConfig.sourceY;if(IS_HIDPI){sourceX*=2;sourceY*=2}this.spritePos={x:sourceX,y:sourceY};const canvasContext=canvas.getContext("2d");assert(canvasContext);this.canvasCtx=canvasContext;this.dimensions={width:lineConfig.width,height:lineConfig.height};this.imageSpriteProvider=imageSpriteProvider;this.sourceXPos=[this.spritePos.x,this.spritePos.x+this.dimensions.width];this.xPos=[0,this.dimensions.width];this.yPos=lineConfig.yPos;this.sourceDimensions={height:lineConfig.height,width:lineConfig.width};if(IS_HIDPI){this.sourceDimensions.width=lineConfig.width*2;this.sourceDimensions.height=lineConfig.height*2}this.draw()}getRandomType(){return Math.random()>this.bumpThreshold?this.dimensions.width:0}draw(){const runnerImageSprite=this.imageSpriteProvider.getRunnerImageSprite();assert(runnerImageSprite);this.canvasCtx.drawImage(runnerImageSprite,this.sourceXPos[0],this.spritePos.y,this.sourceDimensions.width,this.sourceDimensions.height,this.xPos[0],this.yPos,this.dimensions.width,this.dimensions.height);this.canvasCtx.drawImage(runnerImageSprite,this.sourceXPos[1],this.spritePos.y,this.sourceDimensions.width,this.sourceDimensions.height,this.xPos[1],this.yPos,this.dimensions.width,this.dimensions.height)}updatexPos(pos,increment){const line1=pos;const line2=pos===0?1:0;this.xPos[line1]-=increment;this.xPos[line2]=this.xPos[line1]+this.dimensions.width;if(this.xPos[line1]<=-this.dimensions.width){this.xPos[line1]+=this.dimensions.width*2;this.xPos[line2]=this.xPos[line1]-this.dimensions.width;this.sourceXPos[line1]=this.getRandomType()+this.spritePos.x}}update(deltaTime,speed){const increment=Math.floor(speed*(FPS/1e3)*deltaTime);this.updatexPos(this.xPos[0]<=0?0:1,increment);this.draw()}reset(){this.xPos[0]=0;this.xPos[1]=this.dimensions.width}}
1379// Copyright 2024 The Chromium Authors
1380// Use of this source code is governed by a BSD-style license that can be
1381// found in the LICENSE file.
1382const PHASES=[140,120,100,60,40,20,0];var Config;(function(Config){Config[Config["FADE_SPEED"]=.035]="FADE_SPEED";Config[Config["HEIGHT"]=40]="HEIGHT";Config[Config["MOON_SPEED"]=.25]="MOON_SPEED";Config[Config["NUM_STARS"]=2]="NUM_STARS";Config[Config["STAR_SIZE"]=9]="STAR_SIZE";Config[Config["STAR_SPEED"]=.3]="STAR_SPEED";Config[Config["STAR_MAX_Y"]=70]="STAR_MAX_Y";Config[Config["WIDTH"]=20]="WIDTH"})(Config||(Config={}));class NightMode{spritePos;canvasCtx;xPos=0;yPos=30;currentPhase=0;opacity=0;containerWidth;stars=new Array(Config.NUM_STARS);drawStars=false;imageSpriteProvider;constructor(canvas,spritePos,containerWidth,imageSpriteProvider){this.spritePos=spritePos;this.imageSpriteProvider=imageSpriteProvider;const canvasContext=canvas.getContext("2d");assert(canvasContext);this.canvasCtx=canvasContext;this.containerWidth=containerWidth;this.placeStars()}update(activated){if(activated&&this.opacity===0){this.currentPhase++;if(this.currentPhase>=PHASES.length){this.currentPhase=0}}if(activated&&(this.opacity<1||this.opacity===0)){this.opacity+=Config.FADE_SPEED}else if(this.opacity>0){this.opacity-=Config.FADE_SPEED}if(this.opacity>0){this.xPos=this.updateXpos(this.xPos,Config.MOON_SPEED);if(this.drawStars){for(let i=0;i<Config.NUM_STARS;i++){const star=this.stars[i];assert(star);star.x=this.updateXpos(star.x,Config.STAR_SPEED)}}this.draw()}else{this.opacity=0;this.placeStars()}this.drawStars=true}updateXpos(currentPos,speed){if(currentPos<-Config.WIDTH){currentPos=this.containerWidth}else{currentPos-=speed}return currentPos}draw(){let moonSourceWidth=this.currentPhase===3?Config.WIDTH*2:Config.WIDTH;let moonSourceHeight=Config.HEIGHT;const currentPhaseSpritePosition=PHASES[this.currentPhase];assert(currentPhaseSpritePosition!==undefined);let moonSourceX=this.spritePos.x+currentPhaseSpritePosition;const moonOutputWidth=moonSourceWidth;let starSize=Config.STAR_SIZE;let starSourceX=spriteDefinitionByType.original.ldpi.star.x;const runnerOrigImageSprite=this.imageSpriteProvider.getOrigImageSprite();assert(runnerOrigImageSprite);if(IS_HIDPI){moonSourceWidth*=2;moonSourceHeight*=2;moonSourceX=this.spritePos.x+currentPhaseSpritePosition*2;starSize*=2;starSourceX=spriteDefinitionByType.original.hdpi.star.x}this.canvasCtx.save();this.canvasCtx.globalAlpha=this.opacity;if(this.drawStars){for(const star of this.stars){this.canvasCtx.drawImage(runnerOrigImageSprite,starSourceX,star.sourceY,starSize,starSize,Math.round(star.x),star.y,Config.STAR_SIZE,Config.STAR_SIZE)}}this.canvasCtx.drawImage(runnerOrigImageSprite,moonSourceX,this.spritePos.y,moonSourceWidth,moonSourceHeight,Math.round(this.xPos),this.yPos,moonOutputWidth,Config.HEIGHT);this.canvasCtx.globalAlpha=1;this.canvasCtx.restore()}placeStars(){const segmentSize=Math.round(this.containerWidth/Config.NUM_STARS);for(let i=0;i<Config.NUM_STARS;i++){const starPosition={x:getRandomNum(segmentSize*i,segmentSize*(i+1)),y:getRandomNum(0,Config.STAR_MAX_Y),sourceY:0};if(IS_HIDPI){starPosition.sourceY=spriteDefinitionByType.original.hdpi.star.y+Config.STAR_SIZE*2*i}else{starPosition.sourceY=spriteDefinitionByType.original.ldpi.star.y+Config.STAR_SIZE*i}this.stars[i]=starPosition}}reset(){this.currentPhase=0;this.opacity=0;this.update(false)}}
1383// Copyright 2024 The Chromium Authors
1384// Use of this source code is governed by a BSD-style license that can be
1385// found in the LICENSE file.
1386let maxGapCoefficient=1.5;let maxObstacleLength=3;function setMaxGapCoefficient(coefficient){maxGapCoefficient=coefficient}function setMaxObstacleLength(length){maxObstacleLength=length}class Obstacle{collisionBoxes=[];followingObstacleCreated=false;gap=0;jumpAlerted=false;remove=false;size;width=0;xPos;yPos=0;typeConfig;canvasCtx;spritePos;gapCoefficient;speedOffset=0;altGameModeActive;imageSprite;currentFrame=0;timer=0;resourceProvider;constructor(canvasCtx,type,spriteImgPos,dimensions,gapCoefficient,speed,xOffset=0,resourceProvider,isAltGameMode=false){this.canvasCtx=canvasCtx;this.spritePos=spriteImgPos;this.typeConfig=type;this.resourceProvider=resourceProvider;this.gapCoefficient=this.resourceProvider.hasSlowdown?gapCoefficient*2:gapCoefficient;this.size=getRandomNum(1,maxObstacleLength);this.xPos=dimensions.width+xOffset;this.altGameModeActive=isAltGameMode;const imageSprite=this.typeConfig.type==="collectable"?this.resourceProvider.getAltCommonImageSprite():this.altGameModeActive?this.resourceProvider.getRunnerAltGameImageSprite():this.resourceProvider.getRunnerImageSprite();assert(imageSprite);this.imageSprite=imageSprite;this.init(speed)}init(speed){this.cloneCollisionBoxes();if(this.size>1&&this.typeConfig.multipleSpeed>speed){this.size=1}this.width=this.typeConfig.width*this.size;if(Array.isArray(this.typeConfig.yPos)){assert(Array.isArray(this.typeConfig.yPosMobile));const yPosConfig=IS_MOBILE?this.typeConfig.yPosMobile:this.typeConfig.yPos;const randomYPos=yPosConfig[getRandomNum(0,yPosConfig.length-1)];assert(randomYPos);this.yPos=randomYPos}else{this.yPos=this.typeConfig.yPos}this.draw();if(this.size>1){assert(this.collisionBoxes.length>=3);this.collisionBoxes[1].width=this.width-this.collisionBoxes[0].width-this.collisionBoxes[2].width;this.collisionBoxes[2].x=this.width-this.collisionBoxes[2].width}if(this.typeConfig.speedOffset){this.speedOffset=Math.random()>.5?this.typeConfig.speedOffset:-this.typeConfig.speedOffset}this.gap=this.getGap(this.gapCoefficient,speed);if(this.resourceProvider.hasAudioCues){this.gap*=2}}draw(){let sourceWidth=this.typeConfig.width;let sourceHeight=this.typeConfig.height;if(IS_HIDPI){sourceWidth=sourceWidth*2;sourceHeight=sourceHeight*2}let sourceX=sourceWidth*this.size*(.5*(this.size-1))+this.spritePos.x;if(this.currentFrame>0){sourceX+=sourceWidth*this.currentFrame}this.canvasCtx.drawImage(this.imageSprite,sourceX,this.spritePos.y,sourceWidth*this.size,sourceHeight,this.xPos,this.yPos,this.typeConfig.width*this.size,this.typeConfig.height)}update(deltaTime,speed){if(!this.remove){if(this.typeConfig.speedOffset){speed+=this.speedOffset}this.xPos-=Math.floor(speed*FPS/1e3*deltaTime);if(this.typeConfig.numFrames){assert(this.typeConfig.frameRate);this.timer+=deltaTime;if(this.timer>=this.typeConfig.frameRate){this.currentFrame=this.currentFrame===this.typeConfig.numFrames-1?0:this.currentFrame+1;this.timer=0}}this.draw();if(!this.isVisible()){this.remove=true}}}getGap(gapCoefficient,speed){const minGap=Math.round(this.width*speed+this.typeConfig.minGap*gapCoefficient);const maxGap=Math.round(minGap*maxGapCoefficient);return getRandomNum(minGap,maxGap)}isVisible(){return this.xPos+this.width>0}cloneCollisionBoxes(){const collisionBoxes=this.typeConfig.collisionBoxes;for(let i=collisionBoxes.length-1;i>=0;i--){this.collisionBoxes[i]=new CollisionBox(collisionBoxes[i].x,collisionBoxes[i].y,collisionBoxes[i].width,collisionBoxes[i].height)}}}
1387// Copyright 2024 The Chromium Authors
1388// Use of this source code is governed by a BSD-style license that can be
1389// found in the LICENSE file.
1390class Horizon{obstacles=[];canvas;canvasCtx;config=horizonConfig;dimensions;gapCoefficient;resourceProvider;obstacleHistory=[];cloudFrequency;spritePos;nightMode;altGameModeActive=false;obstacleTypes=[];clouds=[];cloudSpeed;backgroundEls=[];lastEl=null;horizonLines=[];constructor(canvas,spritePos,dimensions,gapCoefficient,resourceProvider){this.canvas=canvas;const canvasContext=canvas.getContext("2d");assert(canvasContext);this.canvasCtx=canvasContext;this.dimensions=dimensions;this.gapCoefficient=gapCoefficient;this.resourceProvider=resourceProvider;this.cloudFrequency=this.config.CLOUD_FREQUENCY;this.spritePos=spritePos;this.cloudSpeed=this.config.BG_CLOUD_SPEED;this.obstacleTypes=spriteDefinitionByType.original.obstacles;this.addCloud();const runnerSpriteDefinition=this.resourceProvider.getSpriteDefinition();assert(runnerSpriteDefinition);for(let i=0;i<runnerSpriteDefinition.lines.length;i++){this.horizonLines.push(new HorizonLine(this.canvas,runnerSpriteDefinition.lines[i],this.resourceProvider))}this.nightMode=new NightMode(this.canvas,this.spritePos.moon,this.dimensions.height,this.resourceProvider)}adjustObstacleSpeed(){for(let i=0;i<this.obstacleTypes.length;i++){if(this.resourceProvider.hasSlowdown){this.obstacleTypes[i].multipleSpeed=this.obstacleTypes[i].multipleSpeed/2;this.obstacleTypes[i].minGap*=1.5;this.obstacleTypes[i].minSpeed=this.obstacleTypes[i].minSpeed/2;const obstacleYpos=this.obstacleTypes[i].yPos;if(Array.isArray(obstacleYpos)&&obstacleYpos.length>1){this.obstacleTypes[i].yPos=obstacleYpos[0]}}}}enableAltGameMode(spritePos){const runnerSpriteDefinition=this.resourceProvider.getSpriteDefinition();assert(runnerSpriteDefinition);this.clouds=[];this.backgroundEls=[];this.altGameModeActive=true;this.spritePos=spritePos;this.obstacleTypes=runnerSpriteDefinition.obstacles;this.adjustObstacleSpeed();setMaxGapCoefficient(runnerSpriteDefinition.maxGapCoefficient);setMaxObstacleLength(runnerSpriteDefinition.maxObstacleLength);setGlobalConfig(runnerSpriteDefinition.backgroundElConfig);this.horizonLines=[];for(let i=0;i<runnerSpriteDefinition.lines.length;i++){this.horizonLines.push(new HorizonLine(this.canvas,runnerSpriteDefinition.lines[i],this.resourceProvider))}this.reset()}update(deltaTime,currentSpeed,updateObstacles,showNightMode){const runnerSpriteDefinition=this.resourceProvider.getSpriteDefinition();assert(runnerSpriteDefinition);if(this.altGameModeActive){this.updateBackgroundEls(deltaTime)}for(const line of this.horizonLines){line.update(deltaTime,currentSpeed)}if(!this.altGameModeActive||runnerSpriteDefinition.hasClouds){this.nightMode.update(showNightMode);this.updateClouds(deltaTime,currentSpeed)}if(updateObstacles){this.updateObstacles(deltaTime,currentSpeed)}}updateBackgroundEl(elSpeed,bgElArray,maxBgEl,bgElAddFunction,frequency){const numElements=bgElArray.length;if(!numElements){bgElAddFunction();return}for(let i=numElements-1;i>=0;i--){bgElArray[i].update(elSpeed)}const lastEl=bgElArray.at(-1);if(numElements<maxBgEl&&this.dimensions.width-lastEl.xPos>lastEl.gap&&frequency>Math.random()){bgElAddFunction()}}updateClouds(deltaTime,speed){const elSpeed=this.cloudSpeed/1e3*deltaTime*speed;this.updateBackgroundEl(elSpeed,this.clouds,this.config.MAX_CLOUDS,this.addCloud.bind(this),this.cloudFrequency);this.clouds=this.clouds.filter((obj=>!obj.remove))}updateBackgroundEls(deltaTime){this.updateBackgroundEl(deltaTime,this.backgroundEls,getGlobalConfig().maxBgEls,this.addBackgroundEl.bind(this),this.cloudFrequency);this.backgroundEls=this.backgroundEls.filter((obj=>!obj.remove))}updateObstacles(deltaTime,currentSpeed){const updatedObstacles=this.obstacles.slice(0);for(const obstacle of this.obstacles){obstacle.update(deltaTime,currentSpeed);if(obstacle.remove){updatedObstacles.shift()}}this.obstacles=updatedObstacles;if(this.obstacles.length>0){const lastObstacle=this.obstacles.at(-1);if(lastObstacle&&!lastObstacle.followingObstacleCreated&&lastObstacle.isVisible()&&lastObstacle.xPos+lastObstacle.width+lastObstacle.gap<this.dimensions.width){this.addNewObstacle(currentSpeed);lastObstacle.followingObstacleCreated=true}}else{this.addNewObstacle(currentSpeed)}}removeFirstObstacle(){this.obstacles.shift()}addNewObstacle(currentSpeed){const obstacleCount=this.obstacleTypes[this.obstacleTypes.length-1].type!=="collectable"||(this.resourceProvider.isAltGameModeEnabled()&&!this.altGameModeActive||this.altGameModeActive)?this.obstacleTypes.length-1:this.obstacleTypes.length-2;const obstacleTypeIndex=obstacleCount>0?getRandomNum(0,obstacleCount):0;const obstacleType=this.obstacleTypes[obstacleTypeIndex];if(obstacleCount>0&&this.duplicateObstacleCheck(obstacleType.type)||currentSpeed<obstacleType.minSpeed){this.addNewObstacle(currentSpeed)}else{const obstacleSpritePos=this.spritePos[obstacleType.type];this.obstacles.push(new Obstacle(this.canvasCtx,obstacleType,obstacleSpritePos,this.dimensions,this.gapCoefficient,currentSpeed,obstacleType.width,this.resourceProvider,this.altGameModeActive));this.obstacleHistory.unshift(obstacleType.type);if(this.obstacleHistory.length>1){const maxObstacleDuplicationValue=this.resourceProvider.getConfig().maxObstacleDuplication;assert(maxObstacleDuplicationValue);this.obstacleHistory.splice(maxObstacleDuplicationValue)}}}duplicateObstacleCheck(nextObstacleType){let duplicateCount=0;for(const obstacle of this.obstacleHistory){duplicateCount=obstacle===nextObstacleType?duplicateCount+1:0}const maxObstacleDuplicationValue=this.resourceProvider.getConfig().maxObstacleDuplication;assert(maxObstacleDuplicationValue);return duplicateCount>=maxObstacleDuplicationValue}reset(){this.obstacles=[];for(let l=0;l<this.horizonLines.length;l++){this.horizonLines[l].reset()}this.nightMode.reset()}resize(width,height){this.canvas.width=width;this.canvas.height=height}addCloud(){this.clouds.push(new Cloud(this.canvas,this.spritePos.cloud,this.dimensions.width,this.resourceProvider))}addBackgroundEl(){const runnerSpriteDefinition=this.resourceProvider.getSpriteDefinition();assert(runnerSpriteDefinition);const backgroundElTypes=Object.keys(runnerSpriteDefinition.backgroundEl);if(backgroundElTypes.length>0){let index=getRandomNum(0,backgroundElTypes.length-1);let type=backgroundElTypes[index];while(type===this.lastEl&&backgroundElTypes.length>1){index=getRandomNum(0,backgroundElTypes.length-1);type=backgroundElTypes[index]}this.lastEl=type;this.backgroundEls.push(new BackgroundEl(this.canvas,this.spritePos.backgroundEl,this.dimensions.width,type,this.resourceProvider))}}}const horizonConfig={BG_CLOUD_SPEED:.2,BUMPY_THRESHOLD:.3,CLOUD_FREQUENCY:.5,HORIZON_HEIGHT:16,MAX_CLOUDS:6};
1391// Copyright 2024 The Chromium Authors
1392// Use of this source code is governed by a BSD-style license that can be
1393// found in the LICENSE file.
1394const defaultTrexConfig={dropVelocity:-5,flashOff:175,flashOn:100,height:47,heightDuck:25,introDuration:1500,speedDropCoefficient:3,spriteWidth:262,startXPos:50,width:44,widthDuck:59,invertJump:false};const slowJumpConfig={gravity:.25,maxJumpHeight:50,minJumpHeight:45,initialJumpVelocity:-20};const normalJumpConfig={gravity:.6,maxJumpHeight:30,minJumpHeight:30,initialJumpVelocity:-10};const collisionBoxes={ducking:[new CollisionBox(1,18,55,25)],running:[new CollisionBox(22,0,17,16),new CollisionBox(1,18,30,9),new CollisionBox(10,35,14,8),new CollisionBox(1,24,29,5),new CollisionBox(5,30,21,4),new CollisionBox(9,34,15,4)]};var Status;(function(Status){Status[Status["CRASHED"]=0]="CRASHED";Status[Status["DUCKING"]=1]="DUCKING";Status[Status["JUMPING"]=2]="JUMPING";Status[Status["RUNNING"]=3]="RUNNING";Status[Status["WAITING"]=4]="WAITING"})(Status||(Status={}));const BLINK_TIMING=7e3;const animFrames={[Status.WAITING]:{frames:[44,0],msPerFrame:1e3/3},[Status.RUNNING]:{frames:[88,132],msPerFrame:1e3/12},[Status.CRASHED]:{frames:[220],msPerFrame:1e3/60},[Status.JUMPING]:{frames:[0],msPerFrame:1e3/60},[Status.DUCKING]:{frames:[264,323],msPerFrame:1e3/8}};class Trex{config;playingIntro=false;xPos=0;yPos=0;jumpCount=0;ducking=false;blinkCount=0;jumping=false;speedDrop=false;canvasCtx;spritePos;xInitialPos=0;groundYPos=0;currentFrame=0;currentAnimFrames=[];blinkDelay=0;animStartTime=0;timer=0;msPerFrame=1e3/FPS;status=Status.WAITING;jumpVelocity=0;reachedMinHeight=false;altGameModeEnabled=false;flashing=false;minJumpHeight;resourceProvider;constructor(canvas,spritePos,resourceProvider){const canvasContext=canvas.getContext("2d");assert(canvasContext);this.canvasCtx=canvasContext;this.spritePos=spritePos;this.resourceProvider=resourceProvider;this.config=Object.assign(defaultTrexConfig,normalJumpConfig);const runnerDefaultDimensions=DEFAULT_DIMENSIONS;const runnerBottomPadding=this.resourceProvider.getConfig().bottomPad;assert(runnerDefaultDimensions);assert(runnerBottomPadding);this.groundYPos=runnerDefaultDimensions.height-this.config.height-runnerBottomPadding;this.yPos=this.groundYPos;this.minJumpHeight=this.groundYPos-this.config.minJumpHeight;this.draw(0,0);this.update(0,Status.WAITING)}enableSlowConfig(){const jumpConfig=this.resourceProvider.hasSlowdown?slowJumpConfig:normalJumpConfig;this.config=Object.assign(defaultTrexConfig,jumpConfig);this.adjustAltGameConfigForSlowSpeed()}enableAltGameMode(spritePos){this.altGameModeEnabled=true;this.spritePos=spritePos;const spriteDefinition=this.resourceProvider.getSpriteDefinition();assert(spriteDefinition);const tRexSpriteDefinition=spriteDefinition.tRex;assert(tRexSpriteDefinition.running1);const runnerDefaultDimensions=DEFAULT_DIMENSIONS;animFrames[Status.RUNNING].frames=[tRexSpriteDefinition.running1.x,tRexSpriteDefinition.running2.x];animFrames[Status.CRASHED].frames=[tRexSpriteDefinition.crashed.x];if(typeof tRexSpriteDefinition.jumping.x==="object"){animFrames[Status.JUMPING].frames=tRexSpriteDefinition.jumping.x}else{animFrames[Status.JUMPING].frames=[tRexSpriteDefinition.jumping.x]}animFrames[Status.DUCKING].frames=[tRexSpriteDefinition.ducking1.x,tRexSpriteDefinition.ducking2.x];this.config.gravity=tRexSpriteDefinition.gravity||this.config.gravity;this.config.height=tRexSpriteDefinition.running1.h,this.config.initialJumpVelocity=tRexSpriteDefinition.initialJumpVelocity;this.config.maxJumpHeight=tRexSpriteDefinition.maxJumpHeight;this.config.minJumpHeight=tRexSpriteDefinition.minJumpHeight;this.config.width=tRexSpriteDefinition.running1.w;this.config.widthCrashed=tRexSpriteDefinition.crashed.w;this.config.widthJump=tRexSpriteDefinition.jumping.w;this.config.invertJump=tRexSpriteDefinition.invertJump;this.adjustAltGameConfigForSlowSpeed(tRexSpriteDefinition.gravity);this.groundYPos=runnerDefaultDimensions.height-this.config.height-spriteDefinition.bottomPad;this.yPos=this.groundYPos;this.reset()}adjustAltGameConfigForSlowSpeed(gravityValue){if(this.resourceProvider.hasSlowdown){if(gravityValue){this.config.gravity=gravityValue/1.5}this.config.minJumpHeight*=1.5;this.config.maxJumpHeight*=1.5;this.config.initialJumpVelocity*=1.5}}setFlashing(status){this.flashing=status}setJumpVelocity(setting){this.config.initialJumpVelocity=-setting;this.config.dropVelocity=-setting/2}update(deltaTime,status){this.timer+=deltaTime;if(status!==undefined){this.status=status;this.currentFrame=0;this.msPerFrame=animFrames[status].msPerFrame;this.currentAnimFrames=animFrames[status].frames;if(status===Status.WAITING){this.animStartTime=getTimeStamp();this.setBlinkDelay()}}if(this.playingIntro&&this.xPos<this.config.startXPos){this.xPos+=Math.round(this.config.startXPos/this.config.introDuration*deltaTime);this.xInitialPos=this.xPos}if(this.status===Status.WAITING){this.blink(getTimeStamp())}else{this.draw(this.currentAnimFrames[this.currentFrame],0)}if(!this.flashing&&this.timer>=this.msPerFrame){this.currentFrame=this.currentFrame===this.currentAnimFrames.length-1?0:this.currentFrame+1;this.timer=0}if(this.speedDrop&&this.yPos===this.groundYPos){this.speedDrop=false;this.setDuck(true)}}draw(x,y){let sourceX=x;let sourceY=y;let sourceWidth=this.ducking&&this.status!==Status.CRASHED?this.config.widthDuck:this.config.width;let sourceHeight=this.config.height;const outputHeight=sourceHeight;if(this.altGameModeEnabled){assert(this.config.widthCrashed)}const outputWidth=this.altGameModeEnabled&&this.status===Status.CRASHED?this.config.widthCrashed:this.config.width;const runnerImageSprite=this.resourceProvider.getRunnerImageSprite();assert(runnerImageSprite);if(this.altGameModeEnabled){if(this.jumping&&this.status!==Status.CRASHED){assert(this.config.widthJump);sourceWidth=this.config.widthJump}else if(this.status===Status.CRASHED){assert(this.config.widthCrashed);sourceWidth=this.config.widthCrashed}}if(IS_HIDPI){sourceX*=2;sourceY*=2;sourceWidth*=2;sourceHeight*=2}sourceX+=this.spritePos.x;sourceY+=this.spritePos.y;if(this.flashing){if(this.timer<this.config.flashOn){this.canvasCtx.globalAlpha=.5}else if(this.timer>this.config.flashOff){this.timer=0}}if(this.ducking&&this.status!==Status.CRASHED){this.canvasCtx.drawImage(runnerImageSprite,sourceX,sourceY,sourceWidth,sourceHeight,this.xPos,this.yPos,this.config.widthDuck,outputHeight)}else if(this.altGameModeEnabled&&this.jumping&&this.status!==Status.CRASHED){assert(this.config.widthJump);const spriteDefinition=this.resourceProvider.getSpriteDefinition();assert(spriteDefinition);assert(spriteDefinition.tRex);const jumpOffset=spriteDefinition.tRex.jumping.xOffset*(IS_HIDPI?2:1);this.canvasCtx.drawImage(runnerImageSprite,sourceX,sourceY,sourceWidth,sourceHeight,this.xPos-jumpOffset,this.yPos,this.config.widthJump,outputHeight)}else{if(this.ducking&&this.status===Status.CRASHED){this.xPos++}this.canvasCtx.drawImage(runnerImageSprite,sourceX,sourceY,sourceWidth,sourceHeight,this.xPos,this.yPos,outputWidth,outputHeight)}this.canvasCtx.globalAlpha=1}setBlinkDelay(){this.blinkDelay=Math.ceil(Math.random()*BLINK_TIMING)}blink(time){const deltaTime=time-this.animStartTime;if(deltaTime>=this.blinkDelay){this.draw(this.currentAnimFrames[this.currentFrame],0);if(this.currentFrame===1){this.setBlinkDelay();this.animStartTime=time;this.blinkCount++}}}startJump(speed){if(!this.jumping){this.update(0,Status.JUMPING);this.jumpVelocity=this.config.initialJumpVelocity-speed/10;this.jumping=true;this.reachedMinHeight=false;this.speedDrop=false;if(this.config.invertJump){this.minJumpHeight=this.groundYPos+this.config.minJumpHeight}}}endJump(){if(this.reachedMinHeight&&this.jumpVelocity<this.config.dropVelocity){this.jumpVelocity=this.config.dropVelocity}}updateJump(deltaTime){const msPerFrame=animFrames[this.status].msPerFrame;const framesElapsed=deltaTime/msPerFrame;if(this.speedDrop){this.yPos+=Math.round(this.jumpVelocity*this.config.speedDropCoefficient*framesElapsed)}else if(this.config.invertJump){this.yPos-=Math.round(this.jumpVelocity*framesElapsed)}else{this.yPos+=Math.round(this.jumpVelocity*framesElapsed)}this.jumpVelocity+=this.config.gravity*framesElapsed;if(this.config.invertJump&&this.yPos>this.minJumpHeight||!this.config.invertJump&&this.yPos<this.minJumpHeight||this.speedDrop){this.reachedMinHeight=true}if(this.config.invertJump&&this.yPos>-this.config.maxJumpHeight||!this.config.invertJump&&this.yPos<this.config.maxJumpHeight||this.speedDrop){this.endJump()}if(this.config.invertJump&&this.yPos<this.groundYPos||!this.config.invertJump&&this.yPos>this.groundYPos){this.reset();this.jumpCount++;if(this.resourceProvider.hasAudioCues){const generatedSoundFx=this.resourceProvider.getGeneratedSoundFx();assert(generatedSoundFx);generatedSoundFx.loopFootSteps()}}}setSpeedDrop(){this.speedDrop=true;this.jumpVelocity=1}setDuck(isDucking){if(isDucking&&this.status!==Status.DUCKING){this.update(0,Status.DUCKING);this.ducking=true}else if(this.status===Status.DUCKING){this.update(0,Status.RUNNING);this.ducking=false}}reset(){this.xPos=this.xInitialPos;this.yPos=this.groundYPos;this.jumpVelocity=0;this.jumping=false;this.ducking=false;this.update(0,Status.RUNNING);this.speedDrop=false;this.jumpCount=0}getCollisionBoxes(){return this.ducking?collisionBoxes.ducking:collisionBoxes.running}}
1395// Copyright 2014 The Chromium Authors
1396// Use of this source code is governed by a BSD-style license that can be
1397// found in the LICENSE file.
1398var A11yStrings;(function(A11yStrings){A11yStrings["ARIA_LABEL"]="dinoGameA11yAriaLabel";A11yStrings["DESCRIPTION"]="dinoGameA11yDescription";A11yStrings["GAME_OVER"]="dinoGameA11yGameOver";A11yStrings["HIGH_SCORE"]="dinoGameA11yHighScore";A11yStrings["JUMP"]="dinoGameA11yJump";A11yStrings["STARTED"]="dinoGameA11yStartGame";A11yStrings["SPEED_LABEL"]="dinoGameA11ySpeedToggle"})(A11yStrings||(A11yStrings={}));const defaultBaseConfig={audiocueProximityThreshold:190,audiocueProximityThresholdMobileA11y:250,bgCloudSpeed:.2,bottomPad:10,canvasInViewOffset:-10,clearTime:3e3,cloudFrequency:.5,fadeDuration:1,flashDuration:1e3,gameoverClearTime:1200,initialJumpVelocity:12,invertFadeDuration:12e3,maxBlinkCount:3,maxClouds:6,maxObstacleLength:3,maxObstacleDuplication:2,resourceTemplateId:"audio-resources",speed:6,speedDropCoefficient:3,arcadeModeInitialTopPosition:35,arcadeModeTopPositionPercent:.1};const normalModeConfig={acceleration:.001,audiocueProximityThreshold:190,audiocueProximityThresholdMobileA11y:250,gapCoefficient:.6,invertDistance:700,maxSpeed:13,mobileSpeedCoefficient:1.2,speed:6};const slowModeConfig={acceleration:5e-4,audiocueProximityThreshold:170,audiocueProximityThresholdMobileA11y:220,gapCoefficient:.3,invertDistance:350,maxSpeed:9,mobileSpeedCoefficient:1.5,speed:4.2};var RunnerClasses;(function(RunnerClasses){RunnerClasses["ARCADE_MODE"]="arcade-mode";RunnerClasses["CANVAS"]="runner-canvas";RunnerClasses["CONTAINER"]="runner-container";RunnerClasses["CRASHED"]="crashed";RunnerClasses["ICON"]="icon-offline";RunnerClasses["ICON_DISABLED"]="icon-disabled";RunnerClasses["INVERTED"]="inverted";RunnerClasses["SNACKBAR"]="snackbar";RunnerClasses["SNACKBAR_SHOW"]="snackbar-show";RunnerClasses["TOUCH_CONTROLLER"]="controller"})(RunnerClasses||(RunnerClasses={}));var RunnerSounds;(function(RunnerSounds){RunnerSounds["BUTTON_PRESS"]="offline-sound-press";RunnerSounds["HIT"]="offline-sound-hit";RunnerSounds["SCORE"]="offline-sound-reached"})(RunnerSounds||(RunnerSounds={}));const runnerKeycodes={jump:[38,32],duck:[40],restart:[13]};var RunnerEvents;(function(RunnerEvents){RunnerEvents["ANIM_END"]="webkitAnimationEnd";RunnerEvents["CLICK"]="click";RunnerEvents["KEYDOWN"]="keydown";RunnerEvents["KEYUP"]="keyup";RunnerEvents["POINTERDOWN"]="pointerdown";RunnerEvents["POINTERUP"]="pointerup";RunnerEvents["RESIZE"]="resize";RunnerEvents["TOUCHEND"]="touchend";RunnerEvents["TOUCHSTART"]="touchstart";RunnerEvents["VISIBILITY"]="visibilitychange";RunnerEvents["BLUR"]="blur";RunnerEvents["FOCUS"]="focus";RunnerEvents["LOAD"]="load";RunnerEvents["GAMEPADCONNECTED"]="gamepadconnected"})(RunnerEvents||(RunnerEvents={}));let runnerInstance=null;const ARCADE_MODE_URL="chrome://dino/";const RESOURCE_POSTFIX="offline-resources-";class Runner{outerContainerEl;containerEl=null;touchController=null;canvas=null;canvasCtx=null;a11yStatusEl=null;slowSpeedCheckboxLabel=null;slowSpeedCheckbox=null;slowSpeedToggleEl=null;origImageSprite=null;altCommonImageSprite=null;altGameImageSprite=null;imageSprite=null;config;dimensions=DEFAULT_DIMENSIONS;gameType=null;spriteDefinition=spriteDefinitionByType.original;spriteDef=null;altGameModeActive=false;altGameModeFlashTimer=null;altGameAssetsFailedToLoad=false;fadeInTimer=0;tRex=null;distanceMeter=null;gameOverPanel=null;horizon=null;msPerFrame=1e3/FPS;time=0;distanceRan=0;runningTime=0;currentSpeed;resizeTimerId;raqId=0;playCount=0;isDisabled=loadTimeData.valueExists("disabledEasterEgg");activated=false;playing=false;playingIntro=false;crashed=false;paused=false;inverted=false;isDarkMode=false;updatePending=false;hasSlowdownInternal=false;hasAudioCuesInternal=false;highestScore=0;syncHighestScore=false;invertTimer=0;invertTrigger=false;soundFx={};audioContext=null;generatedSoundFx=null;pollingGamepads=false;gamepadIndex;previousGamepad=null;static initializeInstance(outerContainerId,config){assert(runnerInstance===null);runnerInstance=new Runner(outerContainerId,config);if(!runnerInstance.isDisabled){runnerInstance.loadImages()}return runnerInstance}static getInstance(){assert(runnerInstance);return runnerInstance}constructor(outerContainerId,configParam){const outerContainerElement=document.querySelector(outerContainerId);assert(outerContainerElement);this.outerContainerEl=outerContainerElement;this.config=configParam||Object.assign({},defaultBaseConfig,normalModeConfig);this.currentSpeed=this.config.speed;if(this.isDisabled){this.setupDisabledRunner();return}if(this.isAltGameModeEnabled()){this.initAltGameType()}window.initializeEasterEggHighScore=this.initializeHighScore.bind(this)}get hasSlowdown(){return this.hasSlowdownInternal}get hasAudioCues(){return this.hasAudioCuesInternal}isAltGameModeEnabled(){if(this.altGameAssetsFailedToLoad){return false}return loadTimeData.valueExists("enableAltGameMode")}getGeneratedSoundFx(){assert(this.generatedSoundFx);return this.generatedSoundFx}getSpriteDefinition(){return this.spriteDefinition}getOrigImageSprite(){assert(this.origImageSprite);return this.origImageSprite}getRunnerImageSprite(){assert(this.imageSprite);return this.imageSprite}getRunnerAltGameImageSprite(){return this.altGameImageSprite}getAltCommonImageSprite(){return this.altCommonImageSprite}getConfig(){return this.config}initAltGameType(){assert(loadTimeData.valueExists("altGameType"));if(GAME_TYPE.length>0){const parsedValue=Number.parseInt(loadTimeData.getValue("altGameType"),10);const type=GAME_TYPE[parsedValue-1];this.gameType=type||null}}setupDisabledRunner(){this.containerEl=document.createElement("div");this.containerEl.className=RunnerClasses.SNACKBAR;this.containerEl.textContent=loadTimeData.getValue("disabledEasterEgg");this.outerContainerEl.appendChild(this.containerEl);document.addEventListener(RunnerEvents.KEYDOWN,(e=>{if(runnerKeycodes.jump.includes(e.keyCode)){assert(this.containerEl);this.containerEl.classList.add(RunnerClasses.SNACKBAR_SHOW);const iconElement=document.querySelector(".icon");assert(iconElement);iconElement.classList.add(RunnerClasses.ICON_DISABLED)}}))}updateConfigSetting(setting,value){this.config[setting]=value}updateTrexConfigSetting(setting,value){assert(this.tRex);switch(setting){case"gravity":case"minJumpHeight":case"speedDropCoefficient":this.tRex.config[setting]=value;break;case"initialJumpVelocity":this.tRex.setJumpVelocity(value);break;case"speed":this.setSpeed(value);break}}createImageElement(resourceName){const imgSrc=loadTimeData.valueExists(resourceName)?loadTimeData.getString(resourceName):null;if(imgSrc){const el=document.createElement("img");el.id=resourceName;el.src=imgSrc;const resourcesElement=document.getElementById("offline-resources");assert(resourcesElement);resourcesElement.appendChild(el);return el}return null}loadImages(){let scale="1x";this.spriteDef=this.getSpriteDefinition().ldpi;if(IS_HIDPI){scale="2x";this.spriteDef=this.getSpriteDefinition().hdpi}const imageSpriteElement=document.querySelector(`#${RESOURCE_POSTFIX+scale}`);assert(imageSpriteElement);this.imageSprite=imageSpriteElement;if(this.gameType){this.altGameImageSprite=this.createImageElement("altGameSpecificImage"+scale);this.altCommonImageSprite=this.createImageElement("altGameCommonImage"+scale)}this.origImageSprite=this.getRunnerImageSprite();if(!this.getRunnerAltGameImageSprite()===null||this.getAltCommonImageSprite()===null){this.altGameAssetsFailedToLoad=true;this.altGameModeActive=false}if(this.getRunnerImageSprite().complete){this.init()}else{this.getRunnerImageSprite().addEventListener(RunnerEvents.LOAD,this.init.bind(this))}}loadSounds(){if(IS_IOS){return}this.audioContext=new AudioContext;const resourceTemplateElement=document.querySelector(`#${this.config.resourceTemplateId}`);assert(resourceTemplateElement);const resourceTemplate=resourceTemplateElement.content;for(const sound in RunnerSounds){const audioElement=resourceTemplate.querySelector(`#${RunnerSounds[sound]}`);assert(audioElement);let soundSrc=audioElement.src;soundSrc=soundSrc.substr(soundSrc.indexOf(",")+1);const buffer=decodeBase64ToArrayBuffer(soundSrc);this.audioContext.decodeAudioData(buffer,(audioBuffer=>{this.soundFx={...this.soundFx,[sound]:audioBuffer}}))}}setSpeed(newSpeed){const speed=newSpeed||this.currentSpeed;if(this.dimensions.width<DEFAULT_DIMENSIONS.width){const mobileSpeed=this.hasSlowdown?speed:speed*this.dimensions.width/DEFAULT_DIMENSIONS.width*this.config.mobileSpeedCoefficient;this.currentSpeed=mobileSpeed>speed?speed:mobileSpeed}else if(newSpeed){this.currentSpeed=newSpeed}}init(){assert(this.spriteDef);const iconElement=document.querySelector("."+RunnerClasses.ICON);assert(iconElement);iconElement.style.visibility="hidden";if(this.isArcadeMode()){document.title=document.title+" - "+getA11yString(A11yStrings.ARIA_LABEL)}this.adjustDimensions();this.setSpeed();const ariaLabel=getA11yString(A11yStrings.ARIA_LABEL);this.containerEl=document.createElement("div");this.containerEl.setAttribute("role",IS_MOBILE?"button":"application");this.containerEl.setAttribute("tabindex","0");this.containerEl.setAttribute("title",getA11yString(A11yStrings.DESCRIPTION));this.containerEl.setAttribute("aria-label",ariaLabel);this.containerEl.className=RunnerClasses.CONTAINER;this.canvas=createCanvas(this.containerEl,this.dimensions.width,this.dimensions.height);this.a11yStatusEl=document.createElement("span");this.a11yStatusEl.className="offline-runner-live-region";this.a11yStatusEl.setAttribute("aria-live","assertive");this.a11yStatusEl.textContent="";this.slowSpeedCheckboxLabel=document.createElement("label");this.slowSpeedCheckboxLabel.className="slow-speed-option hidden";this.slowSpeedCheckboxLabel.textContent=getA11yString(A11yStrings.SPEED_LABEL);this.slowSpeedCheckbox=document.createElement("input");this.slowSpeedCheckbox.setAttribute("type","checkbox");this.slowSpeedCheckbox.setAttribute("title",getA11yString(A11yStrings.SPEED_LABEL));this.slowSpeedCheckbox.setAttribute("tabindex","0");this.slowSpeedCheckbox.setAttribute("checked","checked");this.slowSpeedToggleEl=document.createElement("span");this.slowSpeedToggleEl.className="slow-speed-toggle";this.slowSpeedCheckboxLabel.appendChild(this.slowSpeedCheckbox);this.slowSpeedCheckboxLabel.appendChild(this.slowSpeedToggleEl);if(IS_IOS){this.outerContainerEl.appendChild(this.a11yStatusEl)}else{this.containerEl.appendChild(this.a11yStatusEl)}const canvasContext=this.canvas.getContext("2d");assert(canvasContext);this.canvasCtx=canvasContext;this.canvasCtx.fillStyle="#f7f7f7";this.canvasCtx.fill();updateCanvasScaling(this.canvas);this.horizon=new Horizon(this.canvas,this.spriteDef,this.dimensions,this.config.gapCoefficient,this);this.distanceMeter=new DistanceMeter(this.canvas,this.spriteDef.textSprite,this.dimensions.width,this);this.tRex=new Trex(this.canvas,this.spriteDef.tRex,this);this.outerContainerEl.appendChild(this.containerEl);this.outerContainerEl.appendChild(this.slowSpeedCheckboxLabel);this.startListening();this.update();window.addEventListener(RunnerEvents.RESIZE,this.debounceResize.bind(this));const darkModeMediaQuery=window.matchMedia("(prefers-color-scheme: dark)");this.isDarkMode=darkModeMediaQuery&&darkModeMediaQuery.matches;darkModeMediaQuery.addListener((e=>{this.isDarkMode=e.matches}))}createTouchController(){this.touchController=document.createElement("div");this.touchController.className=RunnerClasses.TOUCH_CONTROLLER;this.touchController.addEventListener(RunnerEvents.TOUCHSTART,this);this.touchController.addEventListener(RunnerEvents.TOUCHEND,this);this.outerContainerEl.appendChild(this.touchController)}debounceResize(){if(this.resizeTimerId===undefined){this.resizeTimerId=setInterval(this.adjustDimensions.bind(this),250)}}adjustDimensions(){clearInterval(this.resizeTimerId);this.resizeTimerId=undefined;const boxStyles=window.getComputedStyle(this.outerContainerEl);const padding=Number(boxStyles.paddingLeft.substr(0,boxStyles.paddingLeft.length-2));this.dimensions.width=this.outerContainerEl.offsetWidth-padding*2;if(this.isArcadeMode()){this.dimensions.width=Math.min(DEFAULT_DIMENSIONS.width,this.dimensions.width);if(this.activated){this.setArcadeModeContainerScale()}}if(this.canvas){assert(this.distanceMeter);assert(this.horizon);assert(this.tRex);assert(this.containerEl);this.canvas.width=this.dimensions.width;this.canvas.height=this.dimensions.height;updateCanvasScaling(this.canvas);this.distanceMeter.calcXpos(this.dimensions.width);this.clearCanvas();this.horizon.update(0,0,true,false);this.tRex.update(0);if(this.playing||this.crashed||this.paused){this.containerEl.style.width=this.dimensions.width+"px";this.containerEl.style.height=this.dimensions.height+"px";this.distanceMeter.update(0,Math.ceil(this.distanceRan));this.stop()}else{this.tRex.draw(0,0)}if(this.crashed&&this.gameOverPanel){this.gameOverPanel.updateDimensions(this.dimensions.width);this.gameOverPanel.draw(this.altGameModeActive,this.tRex)}}}playIntro(){if(!this.activated&&!this.crashed){assert(this.tRex);assert(this.containerEl);this.playingIntro=true;this.tRex.playingIntro=true;const keyframes="@-webkit-keyframes intro { "+"from { width:"+this.tRex.config.width+"px }"+"to { width: "+this.dimensions.width+"px }"+"}";const styleSheet=document.styleSheets[0];assert(styleSheet);styleSheet.insertRule(keyframes,0);this.containerEl.addEventListener(RunnerEvents.ANIM_END,this.startGame.bind(this));this.containerEl.style.webkitAnimation="intro .4s ease-out 1 both";this.containerEl.style.width=this.dimensions.width+"px";this.setPlayStatus(true);this.activated=true}else if(this.crashed){this.restart()}}startGame(){assert(this.containerEl);assert(this.tRex);if(this.isArcadeMode()){this.setArcadeMode()}this.toggleSpeed();this.runningTime=0;this.playingIntro=false;this.tRex.playingIntro=false;this.containerEl.style.webkitAnimation="";this.playCount++;if(this.hasAudioCuesInternal){this.getGeneratedSoundFx().background();this.containerEl.setAttribute("title",getA11yString(A11yStrings.JUMP))}document.addEventListener(RunnerEvents.VISIBILITY,this.onVisibilityChange.bind(this));window.addEventListener(RunnerEvents.BLUR,this.onVisibilityChange.bind(this));window.addEventListener(RunnerEvents.FOCUS,this.onVisibilityChange.bind(this))}clearCanvas(){assert(this.canvasCtx);this.canvasCtx.clearRect(0,0,this.dimensions.width,this.dimensions.height)}isCanvasInView(){assert(this.containerEl);return this.containerEl.getBoundingClientRect().top>this.config.canvasInViewOffset}enableAltGameMode(){this.imageSprite=this.getRunnerAltGameImageSprite();assert(this.gameType);assert(this.tRex);assert(this.horizon);this.spriteDefinition=spriteDefinitionByType[this.gameType];if(IS_HIDPI){this.spriteDef=this.getSpriteDefinition().hdpi}else{this.spriteDef=this.getSpriteDefinition().ldpi}this.altGameModeActive=true;this.tRex.enableAltGameMode(this.spriteDef.tRex);this.horizon.enableAltGameMode(this.spriteDef);if(this.hasAudioCuesInternal){this.getGeneratedSoundFx()?.background()}}update(){assert(this.tRex);this.updatePending=false;const now=getTimeStamp();let deltaTime=now-(this.time||now);if(this.altGameModeFlashTimer!==null){if(this.altGameModeFlashTimer<=0){this.altGameModeFlashTimer=null;this.tRex.setFlashing(false);this.enableAltGameMode()}else if(this.altGameModeFlashTimer>0){this.altGameModeFlashTimer-=deltaTime;this.tRex.update(deltaTime);deltaTime=0}}this.time=now;if(this.playing){assert(this.distanceMeter);assert(this.horizon);assert(this.canvasCtx);this.clearCanvas();if(this.altGameModeActive&&this.fadeInTimer<=this.config.fadeDuration){this.fadeInTimer+=deltaTime/1e3;this.canvasCtx.globalAlpha=this.fadeInTimer}else{this.canvasCtx.globalAlpha=1}if(this.tRex.jumping){this.tRex.updateJump(deltaTime)}this.runningTime+=deltaTime;const hasObstacles=this.runningTime>this.config.clearTime;if(this.tRex.jumpCount===1&&!this.playingIntro){this.playIntro()}if(this.playingIntro){this.horizon.update(0,this.currentSpeed,hasObstacles,false)}else if(!this.crashed){const showNightMode=this.isDarkMode!==this.inverted;deltaTime=!this.activated?0:deltaTime;this.horizon.update(deltaTime,this.currentSpeed,hasObstacles,showNightMode)}const firstObstacle=this.horizon.obstacles[0];let collision=hasObstacles&&firstObstacle&&this.checkForCollision(firstObstacle,this.tRex);if(this.hasAudioCuesInternal&&hasObstacles){assert(firstObstacle);const jumpObstacle=firstObstacle.typeConfig.type!=="collectable";if(!firstObstacle.jumpAlerted){const threshold=this.config.audiocueProximityThreshold;const adjProximityThreshold=threshold+threshold*Math.log10(this.currentSpeed/this.config.speed);if(firstObstacle.xPos<adjProximityThreshold){if(jumpObstacle){this.getGeneratedSoundFx().jump()}firstObstacle.jumpAlerted=true}}}if(this.isAltGameModeEnabled()&&collision&&firstObstacle&&firstObstacle.typeConfig.type==="collectable"){this.horizon.removeFirstObstacle();this.tRex.setFlashing(true);collision=false;this.altGameModeFlashTimer=this.config.flashDuration;this.runningTime=0;if(this.hasAudioCuesInternal){this.getGeneratedSoundFx().collect()}}if(!collision){this.distanceRan+=this.currentSpeed*deltaTime/this.msPerFrame;if(this.currentSpeed<this.config.maxSpeed){this.currentSpeed+=this.config.acceleration}}else{this.gameOver()}const playAchievementSound=this.distanceMeter.update(deltaTime,Math.ceil(this.distanceRan));if(!this.hasAudioCuesInternal&&playAchievementSound){this.playSound(this.soundFx.SCORE)}if(!this.isAltGameModeEnabled()){if(this.invertTimer>this.config.invertFadeDuration){this.invertTimer=0;this.invertTrigger=false;this.invert(false)}else if(this.invertTimer){this.invertTimer+=deltaTime}else{const actualDistance=this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan));if(actualDistance>0){this.invertTrigger=!(actualDistance%this.config.invertDistance);if(this.invertTrigger&&this.invertTimer===0){this.invertTimer+=deltaTime;this.invert(false)}}}}}if(this.playing||!this.activated&&this.tRex.blinkCount<this.config.maxBlinkCount){this.tRex.update(deltaTime);this.scheduleNextUpdate()}}handleEvent(e){switch(e.type){case RunnerEvents.KEYDOWN:case RunnerEvents.TOUCHSTART:case RunnerEvents.POINTERDOWN:this.onKeyDown(e);break;case RunnerEvents.KEYUP:case RunnerEvents.TOUCHEND:case RunnerEvents.POINTERUP:this.onKeyUp(e);break;case RunnerEvents.GAMEPADCONNECTED:this.onGamepadConnected();break}}handleCanvasKeyPress(e){if(!this.activated&&!this.hasAudioCuesInternal){this.toggleSpeed();this.hasAudioCuesInternal=true;this.generatedSoundFx=new GeneratedSoundFx;this.config.clearTime*=1.2}else if(e instanceof KeyboardEvent&&runnerKeycodes.jump.includes(e.keyCode)){this.onKeyDown(e)}}preventScrolling(e){if(e.keyCode===32){e.preventDefault()}}toggleSpeed(){if(this.hasAudioCuesInternal){assert(this.slowSpeedCheckbox);const speedChange=this.hasSlowdown!==this.slowSpeedCheckbox.checked;if(speedChange){assert(this.horizon);assert(this.tRex);this.hasSlowdownInternal=this.slowSpeedCheckbox.checked;const updatedConfig=this.hasSlowdown?slowModeConfig:normalModeConfig;this.config=Object.assign(defaultBaseConfig,updatedConfig);this.currentSpeed=updatedConfig.speed;this.tRex.enableSlowConfig();this.horizon.adjustObstacleSpeed()}if(this.playing){this.disableSpeedToggle(true)}}}showSpeedToggle(e){const isFocusEvent=e&&e.type==="focus";if(this.hasAudioCuesInternal||isFocusEvent){assert(this.slowSpeedCheckboxLabel);this.slowSpeedCheckboxLabel.classList.toggle(HIDDEN_CLASS,isFocusEvent?false:!this.crashed)}}disableSpeedToggle(disable){assert(this.slowSpeedCheckbox);if(disable){this.slowSpeedCheckbox.setAttribute("disabled","disabled")}else{this.slowSpeedCheckbox.removeAttribute("disabled")}}startListening(){assert(this.containerEl);assert(this.canvas);this.containerEl.addEventListener(RunnerEvents.KEYDOWN,this.handleCanvasKeyPress.bind(this));if(!IS_MOBILE){this.containerEl.addEventListener(RunnerEvents.FOCUS,this.showSpeedToggle.bind(this))}this.canvas.addEventListener(RunnerEvents.KEYDOWN,this.preventScrolling.bind(this));this.canvas.addEventListener(RunnerEvents.KEYUP,this.preventScrolling.bind(this));document.addEventListener(RunnerEvents.KEYDOWN,this);document.addEventListener(RunnerEvents.KEYUP,this);this.containerEl.addEventListener(RunnerEvents.TOUCHSTART,this);document.addEventListener(RunnerEvents.POINTERDOWN,this);document.addEventListener(RunnerEvents.POINTERUP,this);if(this.isArcadeMode()){window.addEventListener(RunnerEvents.GAMEPADCONNECTED,this)}}onKeyDown(e){if(IS_MOBILE&&this.playing){e.preventDefault()}if(this.isCanvasInView()){if(e instanceof KeyboardEvent&&runnerKeycodes.jump.includes(e.keyCode)&&e.target===this.slowSpeedCheckbox){return}if(!this.crashed&&!this.paused){const isMobileMouseInput=IS_MOBILE&&e instanceof PointerEvent&&e.type===RunnerEvents.POINTERDOWN&&e.pointerType==="mouse"&&(e.target===this.containerEl||IS_IOS&&(e.target===this.touchController||e.target===this.canvas));assert(this.tRex);if(e instanceof KeyboardEvent&&runnerKeycodes.jump.includes(e.keyCode)||e.type===RunnerEvents.TOUCHSTART||isMobileMouseInput){e.preventDefault();if(!this.playing){if(!this.touchController&&e.type===RunnerEvents.TOUCHSTART){this.createTouchController()}if(isMobileMouseInput){this.handleCanvasKeyPress(e)}this.loadSounds();this.setPlayStatus(true);this.update();if(window.errorPageController){window.errorPageController.trackEasterEgg()}}if(!this.tRex.jumping&&!this.tRex.ducking){if(this.hasAudioCuesInternal){this.getGeneratedSoundFx().cancelFootSteps()}else{this.playSound(this.soundFx.BUTTON_PRESS)}this.tRex.startJump(this.currentSpeed)}}else if(this.playing&&e instanceof KeyboardEvent&&runnerKeycodes.duck.includes(e.keyCode)){e.preventDefault();if(this.tRex.jumping){this.tRex.setSpeedDrop()}else if(!this.tRex.jumping&&!this.tRex.ducking){this.tRex.setDuck(true)}}}}}onKeyUp(e){assert(this.tRex);const keyCode="keyCode"in e?e.keyCode:0;const isjumpKey=runnerKeycodes.jump.includes(keyCode)||e.type===RunnerEvents.TOUCHEND||e.type===RunnerEvents.POINTERUP;if(this.isRunning()&&isjumpKey){this.tRex.endJump()}else if(runnerKeycodes.duck.includes(keyCode)){this.tRex.speedDrop=false;this.tRex.setDuck(false)}else if(this.crashed){const deltaTime=getTimeStamp()-this.time;if(this.isCanvasInView()&&(runnerKeycodes.restart.includes(keyCode)||this.isLeftClickOnCanvas(e)||deltaTime>=this.config.gameoverClearTime&&runnerKeycodes.jump.includes(keyCode))){this.handleGameOverClicks(e)}}else if(this.paused&&isjumpKey){this.tRex.reset();this.play()}}onGamepadConnected(){if(!this.pollingGamepads){this.pollGamepadState()}}pollGamepadState(){const gamepads=navigator.getGamepads();this.pollActiveGamepad(gamepads);this.pollingGamepads=true;requestAnimationFrame(this.pollGamepadState.bind(this))}pollForActiveGamepad(gamepads){for(const[i,gamepad]of gamepads.entries()){if(gamepad&&gamepad.buttons.length>0&&gamepad.buttons[0].pressed){this.gamepadIndex=i;this.pollActiveGamepad(gamepads);return}}}pollActiveGamepad(gamepads){if(this.gamepadIndex===undefined){this.pollForActiveGamepad(gamepads);return}const gamepad=gamepads[this.gamepadIndex];if(!gamepad){this.gamepadIndex=undefined;this.pollForActiveGamepad(gamepads);return}this.pollGamepadButton(gamepad,0,38);if(gamepad.buttons.length>=2){this.pollGamepadButton(gamepad,1,40)}if(gamepad.buttons.length>=10){this.pollGamepadButton(gamepad,9,13)}this.previousGamepad=gamepad}pollGamepadButton(gamepad,buttonIndex,keyCode){const state=gamepad.buttons[buttonIndex]?.pressed||false;let previousState=false;if(this.previousGamepad){previousState=this.previousGamepad.buttons[buttonIndex]?.pressed||false}if(state!==previousState){const e=new KeyboardEvent(state?RunnerEvents.KEYDOWN:RunnerEvents.KEYUP,{keyCode:keyCode});document.dispatchEvent(e)}}handleGameOverClicks(e){if(e.target!==this.slowSpeedCheckbox){assert(this.distanceMeter);e.preventDefault();if(this.distanceMeter.hasClickedOnHighScore(e)&&this.highestScore){if(this.distanceMeter.isHighScoreFlashing()){this.saveHighScore(0,true);this.distanceMeter.resetHighScore()}else{this.distanceMeter.startHighScoreFlashing()}}else{this.distanceMeter.cancelHighScoreFlashing();this.restart()}}}isLeftClickOnCanvas(e){if(!(e instanceof MouseEvent)){return false}return e.button!=null&&e.button<2&&e.type===RunnerEvents.POINTERUP&&(e.target===this.canvas||IS_MOBILE&&this.hasAudioCuesInternal&&e.target===this.containerEl)}scheduleNextUpdate(){if(!this.updatePending){this.updatePending=true;this.raqId=requestAnimationFrame(this.update.bind(this))}}isRunning(){return!!this.raqId}initializeHighScore(highScore){assert(this.distanceMeter);this.syncHighestScore=true;highScore=Math.ceil(highScore);if(highScore<this.highestScore){if(window.errorPageController){window.errorPageController.updateEasterEggHighScore(this.highestScore)}return}this.highestScore=highScore;this.distanceMeter.setHighScore(this.highestScore)}saveHighScore(distanceRan,resetScore){assert(this.distanceMeter);this.highestScore=Math.ceil(distanceRan);this.distanceMeter.setHighScore(this.highestScore);if(this.syncHighestScore&&window.errorPageController){if(resetScore){window.errorPageController.resetEasterEggHighScore()}else{window.errorPageController.updateEasterEggHighScore(this.highestScore)}}}gameOver(){assert(this.distanceMeter);assert(this.tRex);assert(this.containerEl);this.playSound(this.soundFx.HIT);vibrate(200);this.stop();this.crashed=true;this.distanceMeter.achievement=false;this.tRex.update(100,Status.CRASHED);if(!this.gameOverPanel){const origSpriteDef=IS_HIDPI?spriteDefinitionByType.original.hdpi:spriteDefinitionByType.original.ldpi;if(this.canvas){if(this.isAltGameModeEnabled()){this.gameOverPanel=new GameOverPanel(this.canvas,origSpriteDef.textSprite,origSpriteDef.restart,this.dimensions,this,origSpriteDef.altGameEnd,this.altGameModeActive)}else{this.gameOverPanel=new GameOverPanel(this.canvas,origSpriteDef.textSprite,origSpriteDef.restart,this.dimensions,this)}}}assert(this.gameOverPanel);this.gameOverPanel.draw(this.altGameModeActive,this.tRex);if(this.distanceRan>this.highestScore){this.saveHighScore(this.distanceRan)}this.time=getTimeStamp();if(this.hasAudioCuesInternal){this.getGeneratedSoundFx().stopAll();assert(this.containerEl);this.announcePhrase(getA11yString(A11yStrings.GAME_OVER).replace("$1",this.distanceMeter.getActualDistance(this.distanceRan).toString())+" "+getA11yString(A11yStrings.HIGH_SCORE).replace("$1",this.distanceMeter.getActualDistance(this.highestScore).toString()));this.containerEl.setAttribute("title",getA11yString(A11yStrings.ARIA_LABEL))}this.showSpeedToggle();this.disableSpeedToggle(false)}stop(){this.setPlayStatus(false);this.paused=true;cancelAnimationFrame(this.raqId);this.raqId=0;if(this.hasAudioCuesInternal){this.getGeneratedSoundFx().stopAll()}}play(){if(!this.crashed){assert(this.tRex);this.setPlayStatus(true);this.paused=false;this.tRex.update(0,Status.RUNNING);this.time=getTimeStamp();this.update();if(this.hasAudioCuesInternal){this.getGeneratedSoundFx().background()}}}restart(){if(!this.raqId){assert(this.containerEl);assert(this.gameOverPanel);assert(this.tRex);assert(this.horizon);assert(this.distanceMeter);this.playCount++;this.runningTime=0;this.setPlayStatus(true);this.toggleSpeed();this.paused=false;this.crashed=false;this.distanceRan=0;this.setSpeed(this.config.speed);this.time=getTimeStamp();this.containerEl.classList.remove(RunnerClasses.CRASHED);this.clearCanvas();this.distanceMeter.reset();this.horizon.reset();this.tRex.reset();this.playSound(this.soundFx.BUTTON_PRESS);this.invert(true);this.update();this.gameOverPanel.reset();if(this.hasAudioCuesInternal){this.getGeneratedSoundFx().background()}this.containerEl.setAttribute("title",getA11yString(A11yStrings.JUMP));this.announcePhrase(getA11yString(A11yStrings.STARTED))}}setPlayStatus(isPlaying){if(this.touchController){this.touchController.classList.toggle(HIDDEN_CLASS,!isPlaying)}this.playing=isPlaying}isArcadeMode(){return IS_RTL?document.title.indexOf(ARCADE_MODE_URL)===1:document.title===ARCADE_MODE_URL}setArcadeMode(){document.body.classList.add(RunnerClasses.ARCADE_MODE);this.setArcadeModeContainerScale()}setArcadeModeContainerScale(){assert(this.containerEl);const windowHeight=window.innerHeight;const scaleHeight=windowHeight/this.dimensions.height;const scaleWidth=window.innerWidth/this.dimensions.width;const scale=Math.max(1,Math.min(scaleHeight,scaleWidth));const scaledCanvasHeight=this.dimensions.height*scale;const translateY=Math.ceil(Math.max(0,(windowHeight-scaledCanvasHeight-this.config.arcadeModeInitialTopPosition)*this.config.arcadeModeTopPositionPercent))*window.devicePixelRatio;const cssScale=IS_RTL?-scale+","+scale:scale;this.containerEl.style.transform="scale("+cssScale+") translateY("+translateY+"px)"}onVisibilityChange(e){if(document.hidden||e.type==="blur"||document.visibilityState!=="visible"){this.stop()}else if(!this.crashed){assert(this.tRex);this.tRex.reset();this.play()}}playSound(soundBuffer){if(soundBuffer){assert(this.audioContext);const sourceNode=this.audioContext.createBufferSource();sourceNode.buffer=soundBuffer;sourceNode.connect(this.audioContext.destination);sourceNode.start(0)}}invert(reset){const htmlEl=document.firstElementChild;assert(htmlEl);if(reset){htmlEl.classList.toggle(RunnerClasses.INVERTED,false);this.invertTimer=0;this.inverted=false}else{this.inverted=htmlEl.classList.toggle(RunnerClasses.INVERTED,this.invertTrigger)}}announcePhrase(phrase){if(this.a11yStatusEl){this.a11yStatusEl.textContent="";this.a11yStatusEl.textContent=phrase}}checkForCollision(obstacle,tRex,canvasCtx){const tRexBox=new CollisionBox(tRex.xPos+1,tRex.yPos+1,tRex.config.width-2,tRex.config.height-2);const obstacleBox=new CollisionBox(obstacle.xPos+1,obstacle.yPos+1,obstacle.typeConfig.width*obstacle.size-2,obstacle.typeConfig.height-2);if(canvasCtx){drawCollisionBoxes(canvasCtx,tRexBox,obstacleBox)}if(boxCompare(tRexBox,obstacleBox)){const collisionBoxes=obstacle.collisionBoxes;let tRexCollisionBoxes=[];if(this.isAltGameModeEnabled()){const runnerSpriteDefinition=this.getSpriteDefinition();assert(runnerSpriteDefinition);assert(runnerSpriteDefinition.tRex);tRexCollisionBoxes=runnerSpriteDefinition.tRex.collisionBoxes}else{tRexCollisionBoxes=tRex.getCollisionBoxes()}for(const tRexCollisionBox of tRexCollisionBoxes){for(const obstacleCollixionBox of collisionBoxes){const adjTrexBox=createAdjustedCollisionBox(tRexCollisionBox,tRexBox);const adjObstacleBox=createAdjustedCollisionBox(obstacleCollixionBox,obstacleBox);const crashed=boxCompare(adjTrexBox,adjObstacleBox);if(canvasCtx){drawCollisionBoxes(canvasCtx,adjTrexBox,adjObstacleBox)}if(crashed){return[adjTrexBox,adjObstacleBox]}}}}return null}}function updateCanvasScaling(canvas,width,height){const context=canvas.getContext("2d");assert(context);const devicePixelRatio=Math.floor(window.devicePixelRatio)||1;const backingStoreRatio="webkitBackingStorePixelRatio"in context?Math.floor(context.webkitBackingStorePixelRatio):1;const ratio=devicePixelRatio/backingStoreRatio;if(devicePixelRatio!==backingStoreRatio){const oldWidth=canvas.width;const oldHeight=canvas.height;canvas.width=oldWidth*ratio;canvas.height=oldHeight*ratio;canvas.style.width=oldWidth+"px";canvas.style.height=oldHeight+"px";context.scale(ratio,ratio);return true}else if(devicePixelRatio===1){canvas.style.width=canvas.width+"px";canvas.style.height=canvas.height+"px"}return false}function getA11yString(stringName){return loadTimeData.valueExists(stringName)?loadTimeData.getString(stringName):""}function vibrate(duration){if(IS_MOBILE&&window.navigator.vibrate){window.navigator.vibrate(duration)}}function createCanvas(container,width,height,classname){const canvas=document.createElement("canvas");canvas.className=RunnerClasses.CANVAS;canvas.width=width;canvas.height=height;container.appendChild(canvas);return canvas}function decodeBase64ToArrayBuffer(base64String){const len=base64String.length/4*3;const str=atob(base64String);const arrayBuffer=new ArrayBuffer(len);const bytes=new Uint8Array(arrayBuffer);for(let i=0;i<len;i++){bytes[i]=str.charCodeAt(i)}return bytes.buffer}function createAdjustedCollisionBox(box,adjustment){return new CollisionBox(box.x+adjustment.x,box.y+adjustment.y,box.width,box.height)}function drawCollisionBoxes(canvasCtx,tRexBox,obstacleBox){canvasCtx.save();canvasCtx.strokeStyle="#f00";canvasCtx.strokeRect(tRexBox.x,tRexBox.y,tRexBox.width,tRexBox.height);canvasCtx.strokeStyle="#0f0";canvasCtx.strokeRect(obstacleBox.x,obstacleBox.y,obstacleBox.width,obstacleBox.height);canvasCtx.restore()}function boxCompare(tRexBox,obstacleBox){const tRexBoxX=tRexBox.x;const tRexBoxY=tRexBox.y;const obstacleBoxX=obstacleBox.x;const obstacleBoxY=obstacleBox.y;if(tRexBoxX<obstacleBoxX+obstacleBox.width&&tRexBoxX+tRexBox.width>obstacleBoxX&&tRexBoxY<obstacleBoxY+obstacleBox.height&&tRexBox.height+tRexBoxY>obstacleBoxY){return true}return false}
1399// Copyright 2013 The Chromium Authors
1400// Use of this source code is governed by a BSD-style license that can be
1401// found in the LICENSE file.
1402let showingDetails=false;let lastData=null;let dinoGameInitialized=false;function toggleHelpBox(){showingDetails=!showingDetails;assert(lastData);B(getHtml(lastData,showingDetails),getRequiredElement("content"))}function diagnoseErrors(){if(window.errorPageController){window.errorPageController.diagnoseErrorsButtonClick()}}function portalSignin(){if(window.errorPageController){window.errorPageController.portalSigninButtonClick()}}let isSubFrame=false;if(window.top.location!==window.location){document.documentElement.setAttribute("subframe","");isSubFrame=true}function updateForDnsProbe(newData){updateInitialInstruction(newData);onTemplateDataReceived(newData)}function getMainFrameErrorCssClass(showingDetails){return showingDetails?"showing-details":""}function getMainFrameErrorIconCssClass(data){return isSubFrame?"":data.iconClass}function getSubFrameErrorIconCssClass(data){return isSubFrame?data.iconClass:""}function shouldShowSuggestionsSummaryList(data){return!!data.suggestionsSummaryList&&data.suggestionsSummaryList.length>0}function getSuggestionsSummaryItemCssClass(data){assert(data.suggestionsSummaryList);return data.suggestionsSummaryList.length===1?"single-suggestion":""}function reloadButtonClick(e){const url=e.target.dataset["url"];if(window.errorPageController){window.errorPageController.reloadButtonClick()}else{assert(url);window.location.href=url}}function downloadButtonClick(){if(window.errorPageController){window.errorPageController.downloadButtonClick();const downloadButton=getRequiredElement("download-button");downloadButton.disabled=true;downloadButton.textContent=downloadButton.disabledText}}function detailsButtonClick(){if(window.errorPageController){window.errorPageController.detailsButtonClick()}toggleHelpBox()}function setAutoFetchState(scheduled,canSchedule){getRequiredElement("cancel-save-page-button").classList.toggle(HIDDEN_CLASS,!scheduled);getRequiredElement("save-page-for-later-button").classList.toggle(HIDDEN_CLASS,!canSchedule)}function savePageLaterClick(){assert(window.errorPageController);window.errorPageController.savePageForLater()}function cancelSavePageClick(){assert(window.errorPageController);window.errorPageController.cancelSavePage();setAutoFetchState(false,true)}function shouldShowControlButtons(data){const downloadButtonVisible=!!data.downloadButton&&!!data.downloadButton.msg;const reloadButtonVisible=!!data.reloadButton&&!!data.reloadButton.msg;return reloadButtonVisible||downloadButtonVisible}function shouldShowDetailsButton(data){return!!data.suggestionsDetails&&data.suggestionsDetails.length>0}function getDetailsButtonCssClass(data){return shouldShowControlButtons(data)?"":"singular"}function getDetailsButtonText(data,showingDetails){assert(data.details);assert(data.hideDetails);return showingDetails?data.hideDetails:data.details}function getButtonsCssClass(){let primaryControlOnLeft=true;primaryControlOnLeft=false;return primaryControlOnLeft?"suggested-left":"suggested-right"}function updateInitialInstruction(data){if(!data.isOfflineError){return}if(navigator.maxTouchPoints===0){data.heading.msg=data.dinoGameInstructionsKeyboard;data.dinoGameA11yAriaLabel=data.dinoGameA11yAriaLabelKeyboard}else if(window.matchMedia("(hover: hover)").matches){data.heading.msg=data.dinoGameInstructionsHybrid;data.dinoGameA11yAriaLabel=data.dinoGameA11yAriaLabelHybrid}else{data.heading.msg=data.dinoGameInstructionsTouch;data.dinoGameA11yAriaLabel=data.dinoGameA11yAriaLabelTouch}}function onDocumentLoad(){const data=window.loadTimeDataRaw;updateInitialInstruction(data);onTemplateDataReceived(data)}function onTemplateDataReceived(newData){lastData=newData;B(getHtml(lastData,showingDetails),getRequiredElement("content"));if(!isSubFrame&&newData.iconClass==="icon-offline"){document.documentElement.classList.add("offline");if(!dinoGameInitialized){loadTimeData.data=newData;Runner.initializeInstance(".interstitial-wrapper");dinoGameInitialized=true}}}function getHtml(data,showingDetails){return x`
1403 <div id="main-frame-error" class="interstitial-wrapper ${getMainFrameErrorCssClass(showingDetails)}">
1404 <div id="main-content">
1405 <div class="icon ${getMainFrameErrorIconCssClass(data)}"></div>
1406 <div id="main-message">
1407 <h1>
1408 <span .innerHTML="${data.heading.msg}"></span>
1409 </h1>
1410 ${data.summary?x`
1411 <p .innerHTML="${data.summary.msg}"></p>
1412 `:""}
1413
1414 ${shouldShowSuggestionsSummaryList(data)?x`
1415 <div id="suggestions-list">
1416 <p>${data.suggestionsSummaryListHeader}</p>
1417 <ul class="${getSuggestionsSummaryItemCssClass(data)}">
1418 ${data.suggestionsSummaryList.map((item=>x`
1419 <li .innerHTML="${item.summary}"></li>
1420 `))}
1421 </ul>
1422 </div>
1423 `:""}
1424
1425 <div class="error-code">${data.errorCode}</div>
1426
1427 ${data.savePageLater?x`
1428 <div id="save-page-for-later-button">
1429 <a class="link-button" @click="${savePageLaterClick}">
1430 ${data.savePageLater.savePageMsg}
1431 </a>
1432 </div>
1433 <div id="cancel-save-page-button" class="hidden"
1434 @click="${cancelSavePageClick}"
1435 .innerHTML="${data.savePageLater.cancelMsg}">
1436 </div>
1437 `:""}
1438 </div>
1439 </div>
1440 <div id="buttons" class="nav-wrapper ${getButtonsCssClass()}">
1441 <div id="control-buttons" ?hidden="${!shouldShowControlButtons(data)}">
1442 ${data.reloadButton?x`
1443 <button id="reload-button"
1444 class="blue-button text-button"
1445 @click="${reloadButtonClick}"
1446 data-url="${data.reloadButton.reloadUrl}">
1447 ${data.reloadButton.msg}
1448 </button>
1449 `:""}
1450 ${data.downloadButton?x`
1451 <button id="download-button"
1452 class="blue-button text-button"
1453 @click="${downloadButtonClick}"
1454 .disabledText="${data.downloadButton.disabledMsg}">
1455 ${data.downloadButton.msg}
1456 </button>
1457 `:""}
1458 </div>
1459 ${shouldShowDetailsButton(data)?x`
1460 <button id="details-button" class="secondary-button text-button
1461 small-link ${getDetailsButtonCssClass(data)}"
1462 @click="${detailsButtonClick}">
1463 ${getDetailsButtonText(data,showingDetails)}
1464 </button>
1465 `:""}
1466 </div>
1467 ${data.suggestionsDetails?x`
1468 <div id="details">
1469 ${data.suggestionsDetails.map((item=>x`
1470 <div class="suggestions">
1471 <div class="suggestion-header" .innerHTML="${item.header}"></div>
1472 <div class="suggestion-body" .innerHTML="${item.body}"></div>
1473 </div>
1474 `))}
1475 </div>
1476 `:""}
1477 </div>
1478 ${data.summary?x`
1479 <div id="sub-frame-error">
1480 <!-- Show details when hovering over the icon, in case the details are
1481 hidden because they're too large. -->
1482 <div class="icon ${getSubFrameErrorIconCssClass(data)}"></div>
1483 <div id="sub-frame-error-details" .innerHTML="${data.summary.msg}">
1484 </div>
1485 </div>
1486 `:""}
1487 `}Object.assign(window,{diagnoseErrors:diagnoseErrors,portalSignin:portalSignin,toggleHelpBox:toggleHelpBox,updateForDnsProbe:updateForDnsProbe,updateInitialInstruction:updateInitialInstruction});document.addEventListener("DOMContentLoaded",onDocumentLoad);</script>
1488
1489</head>
1490<body class="neterror" style="font-family: system-ui, sans-serif; font-size: 75%">
1491 <div id="content"><!---->
1492 <div id="main-frame-error" class="interstitial-wrapper ">
1493 <div id="main-content">
1494 <div class="icon icon-generic"></div>
1495 <div id="main-message">
1496 <h1>
1497 <span>This site can’t be reached</span>
1498 </h1>
1499 <!--?lit$320104863$-->
1500 <p><strong>asdasddddd.com</strong>’s server IP address could not be found.</p>
1501
1502
1503 <!--?lit$320104863$-->
1504 <div id="suggestions-list">
1505 <p><!--?lit$320104863$-->Try:</p>
1506 <ul class="">
1507 <!--?lit$320104863$--><!---->
1508 <li>Checking the connection</li>
1509 <!----><!---->
1510 <li><a href="#buttons" onclick="toggleHelpBox()">Checking the proxy, firewall, and DNS configuration</a></li>
1511 <!---->
1512 </ul>
1513 </div>
1514
1515
1516 <div class="error-code"><!--?lit$320104863$-->ERR_NAME_NOT_RESOLVED</div>
1517
1518 <!--?lit$320104863$-->
1519 </div>
1520 </div>
1521 <div id="buttons" class="nav-wrapper suggested-right">
1522 <div id="control-buttons">
1523 <!--?lit$320104863$-->
1524 <button id="reload-button" class="blue-button text-button" data-url="https://asdasddddd.com/">
1525 <!--?lit$320104863$-->Reload
1526 </button>
1527
1528 <!--?lit$320104863$-->
1529 </div>
1530 <!--?lit$320104863$-->
1531 <button id="details-button" class="secondary-button text-button
1532 small-link ">
1533 <!--?lit$320104863$-->Details
1534 </button>
1535
1536 </div>
1537 <!--?lit$320104863$-->
1538 <div id="details">
1539 <!--?lit$320104863$--><!---->
1540 <div class="suggestions">
1541 <div class="suggestion-header">Check your Internet connection</div>
1542 <div class="suggestion-body">Check any cables and reboot any routers, modems, or other network
1543 devices you may be using.</div>
1544 </div>
1545 <!----><!---->
1546 <div class="suggestions">
1547 <div class="suggestion-header">Check your DNS settings</div>
1548 <div class="suggestion-body">Contact your network administrator if you're not sure what this means.</div>
1549 </div>
1550 <!----><!---->
1551 <div class="suggestions">
1552 <div class="suggestion-header">Try disabling network prediction</div>
1553 <div class="suggestion-body">Go to
1554 the Helium menu &gt;
1555 <span>Settings</span>
1556 &gt;
1557 <span>Show advanced settings…</span>
1558 and deselect "<span>Use a prediction service to load pages more quickly</span>."
1559 If this does not resolve the issue, we recommend selecting this option
1560 again for improved performance.</div>
1561 </div>
1562 <!----><!---->
1563 <div class="suggestions">
1564 <div class="suggestion-header">Allow Helium to access the network in your firewall or antivirus
1565 settings.</div>
1566 <div class="suggestion-body">If it is already listed as a program allowed to access the network, try
1567 removing it from the list and adding it again.</div>
1568 </div>
1569 <!----><!---->
1570 <div class="suggestions">
1571 <div class="suggestion-header">If you use a proxy server…</div>
1572 <div class="suggestion-body">Go to Applications &gt; System Settings &gt; Network, select the
1573 active network, click the Details… button, and deselect any proxies
1574 that may have been selected.</div>
1575 </div>
1576 <!---->
1577 </div>
1578
1579 </div>
1580 <!--?lit$320104863$-->
1581 <div id="sub-frame-error">
1582 <!-- Show details when hovering over the icon, in case the details are
1583 hidden because they're too large. -->
1584 <div class="icon "></div>
1585 <div id="sub-frame-error-details"><strong>asdasddddd.com</strong>’s server IP address could not be found.</div>
1586 </div>
1587
1588 </div>
1589 <div id="offline-resources">
1590 <img id="offline-resources-1x" src="data:image/png;base64,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">
1591 <img id="offline-resources-2x" src="data:image/png;base64,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">
1592 <template id="audio-resources">
1593 <audio id="offline-sound-press" src="data:audio/mpeg;base64,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"></audio>
1594 <audio id="offline-sound-hit" src="data:audio/mpeg;base64,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"></audio>
1595 <audio id="offline-sound-reached" src="data:audio/mpeg;base64,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"></audio>
1596 </template>
1597 </div>
1598
1599
1600<script>var loadTimeDataRaw = {"details":"Details","errorCode":"DNS_PROBE_POSSIBLE","fontfamily":"system-ui, sans-serif","fontfamilyMd":"system-ui, sans-serif","fontsize":"75%","heading":{"msg":"This site can’t be reached"},"hideDetails":"Hide details","iconClass":"icon-generic","isOfflineError":false,"language":"en","reloadButton":{"msg":"Reload","reloadUrl":"https://asdasddddd.com/"},"suggestionsDetails":[],"suggestionsSummaryList":[],"summary":{"msg":"\u003Cstrong>asdasddddd.com\u003C/strong>’s \u003Cabbr id=\"dnsDefinition\">DNS address\u003C/abbr> could not be found. Diagnosing the problem."},"textdirection":"ltr","title":"asdasddddd.com"};</script></body></html>
\ No newline at end of file
app-preview-browser-plan.md created+1829
......@@ -0,0 +1,1829 @@
1# In‑App Preview Browser — Implementation Plan
2
3> **Scope**: A Chromium‑backed preview browser slot in the chat workspace, available **only in the desktop build**. Web build shows nothing for this feature (no iframe fallback). Server tracks per‑thread preview session metadata so it survives reconnects and multi‑window/multi‑client. Reachable via three vectors: a keybinding, a "Open in preview" affordance on terminal URLs, and a `ProjectScript.previewUrl/autoOpenPreview` extension.
4>
5> **Reference implementation we're modelling on**: ami's `packages/desktop/src/browser-view-manager.ts` + `packages/interface/src/components/browser-view/`. We're porting a deliberately smaller subset.
6>
7> **Done in one shot**: this is a single multi‑PR‑sized landing. Below is the file‑by‑file checklist.
8
9---
10
11## 1. Architecture
12
13### Three‑actor model
14
15```
16┌────────────────────────────────────────────────────────────────────────────┐
17│ apps/server (Node, Effect) │
18│ ┌──────────────────────────────────────────────────────────────────────┐ │
19│ │ PreviewManager — Map<scopedThreadKey, PreviewSession> │ │
20│ │ • metadata only: { tabId, url, title, navStatus, lastError, … } │ │
21│ │ • broadcasts PreviewEvent over WS │ │
22│ │ • survives client disconnect, replays on reconnect │ │
23│ └──────────────────────────────────────────────────────────────────────┘ │
24└────────────────────────────────────────────────────────────────────────────┘
25 ▲ │
26 EnvironmentApi.preview │ preview.onEvent push
27 (WS RPC) ▼
28┌────────────────────────────────────────────────────────────────────────────┐
29│ apps/desktop (Electron renderer = apps/web) │
30│ ┌──────────────────────────────────────────────────────────────────────┐ │
31│ │ PreviewView (React) — chrome (URL bar, back/fwd/refresh) │ │
32│ │ ┌────────────────────────────────────────────────────────────────┐ │ │
33│ │ │ <webview partition="persist:t3code-preview" preload=…/> │ │ │
34│ │ └────────────────────────────────────────────────────────────────┘ │ │
35│ │ zustand: previewStateStore (mirrors terminalStateStore) │ │
36│ └──────────────────────────────────────────────────────────────────────┘ │
37└────────────────────────────────────────────────────────────────────────────┘
38 ▲ │
39 desktopBridge.preview.* │ desktopBridge.preview.onStateChange
40 (Electron IPC) ▼
41┌────────────────────────────────────────────────────────────────────────────┐
42│ apps/desktop main process (Node) │
43│ ┌──────────────────────────────────────────────────────────────────────┐ │
44│ │ PreviewViewManager — Map<tabId, TabRecord{ webContents, state }> │ │
45│ │ • createTab/closeTab/navigate/registerWebview/setVisibility │ │
46│ │ • attaches did-navigate / did-fail-load / page-title-updated │ │
47│ │ • partitioned session: persist:t3code-preview │ │
48│ │ • forwards app shortcuts to mainWindow (mod+w, mod+, mod+1..9, …) │ │
49│ └──────────────────────────────────────────────────────────────────────┘ │
50└────────────────────────────────────────────────────────────────────────────┘
51```
52
53### Why the server even cares
54
55The desktop already has the `<webview>` and could be the sole source of truth. We route through the server anyway because:
56
571. Reconnect/restart resilience matches the rest of t3code — terminal sessions, orchestration, etc. all use snapshot+replay.
582. A future second window or a remote viewer (e.g. mobile observer) sees the same URL the desktop is on.
593. Agent‑facing tooling (later) is RPC‑shaped and lives on the server, not the desktop bridge.
60
61### Why the renderer subscribes to two streams
62
63- **Server `preview.onEvent`** — authoritative for `url`, `title`, `lastError`. Replays on WS reconnect.
64- **`desktopBridge.preview.onStateChange`** — authoritative for low‑latency `canGoBack`, `canGoForward`, `loading`. Cheaper than round‑tripping through the server.
65
66The web side merges them in `previewStateStore.applyServerEvent` / `applyDesktopState`.
67
68### Web build behaviour
69
70When `window.desktopBridge?.preview == null`:
71
72- `previewStateStore` selectors return a frozen "unsupported" shape.
73- `PreviewPanel.tsx` short‑circuits to `null` (panel never renders).
74- `rightPanelStore` rejects `kind: "preview"` writes.
75- `preview.toggle` keybinding fires a toast: _"Preview is only available in the T3 Code desktop app."_
76- Terminal‑link "Open in preview" menu item is hidden (only "Open in browser" shows).
77- `ProjectScript.previewUrl` is still **stored** (so it round‑trips between web/desktop users of the same project) but ignored.
78
79---
80
81## 2. Right‑panel arbiter (prerequisite refactor)
82
83Today the right side has two implicit tenants and no arbiter:
84
85- **Diff panel** — driven by URL `?diff=1` (`apps/web/src/diffRouteSearch.ts`). Toggle wired to `diff.toggle` keybinding.
86- **Plan sidebar** — driven by local component state `planSidebarOpen` in `ChatView.tsx:688`. Renders inline (sibling div) when wide and as `<RightPanelSheet>` when narrow.
87
88They co‑exist by accident. With a third panel we need an explicit arbiter.
89
90### New: `apps/web/src/rightPanelStore.ts`
91
92```ts
93import { create } from "zustand";
94import { persist, createJSONStorage } from "zustand/middleware";
95import { type ScopedThreadRef, scopedThreadKey } from "@t3tools/client-runtime";
96import { resolveStorage } from "./lib/storage";
97
98export type RightPanelKind = "plan" | "diff" | "preview";
99
100interface ThreadRightPanelState {
101 /** null = closed; otherwise the active panel for this thread */
102 active: RightPanelKind | null;
103}
104
105const RIGHT_PANEL_STORAGE_KEY = "t3code:right-panel-state:v1";
106
107interface RightPanelStoreState {
108 byThreadKey: Record<string, ThreadRightPanelState>;
109 open: (ref: ScopedThreadRef, kind: RightPanelKind) => void;
110 close: (ref: ScopedThreadRef) => void;
111 toggle: (ref: ScopedThreadRef, kind: RightPanelKind) => void;
112}
113
114export const useRightPanelStore = create<RightPanelStoreState>()(
115 persist(/* … */, {
116 name: RIGHT_PANEL_STORAGE_KEY,
117 storage: createJSONStorage(() => resolveStorage(window.localStorage)),
118 version: 1,
119 }),
120);
121
122export function selectActiveRightPanel(
123 store: RightPanelStoreState,
124 ref: ScopedThreadRef | null,
125): RightPanelKind | null {
126 if (!ref) return null;
127 return store.byThreadKey[scopedThreadKey(ref)]?.active ?? null;
128}
129```
130
131### Diff panel migration
132
133`?diff=1` stays the URL source of truth (deep‑linking matters for diff). On router navigation, mirror it into `rightPanelStore`:
134
135- `parseDiffRouteSearch(...).diff === "1"` → `rightPanelStore.open(activeThreadRef, "diff")` in a `useEffect`.
136- Opening plan/preview removes the `?diff` param via `stripDiffSearchParams` (already exists in `apps/web/src/diffRouteSearch.ts`).
137- Closing diff via the X button does both: navigate to strip `?diff=1` **and** call `rightPanelStore.close(...)`.
138
139### Plan sidebar migration
140
141`apps/web/src/components/ChatView.tsx`:
142
143- Remove the local `planSidebarOpen` state (`:688`).
144- Replace `setPlanSidebarOpen(true)` callsites with `rightPanelStore.open(activeThreadRef, "plan")`.
145- Replace `closePlanSidebar` with `rightPanelStore.close(activeThreadRef)`.
146- Existing `planSidebarDismissedForTurnRef`/`planSidebarOpenOnNextThreadRef` logic stays local — it only governs whether to *call* `open()`/`close()` on turn change.
147
148### Render arbitration
149
150The single render decision in `ChatView.tsx`:
151
152```tsx
153const activeRightPanel = useRightPanelStore((s) =>
154 selectActiveRightPanel(s, activeThreadRef),
155);
156const useSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY);
157
158// Inline (wide):
159{activeRightPanel === "plan" && !useSheet && <PlanSidebar mode="sidebar" … />}
160{activeRightPanel === "preview" && !useSheet && <PreviewPanel mode="sidebar" … />}
161{/* DiffPanel inline rendering already lives in DiffPanelShell, just gate it on activeRightPanel === "diff" */}
162
163// Sheet (narrow):
164{useSheet && activeRightPanel !== null && (
165 <RightPanelSheet open onClose={() => rightPanelStore.close(activeThreadRef)}>
166 {activeRightPanel === "plan" && <PlanSidebar mode="sheet" … />}
167 {activeRightPanel === "diff" && <DiffPanel mode="sheet" … />}
168 {activeRightPanel === "preview" && <PreviewPanel mode="sheet" … />}
169 </RightPanelSheet>
170)}
171```
172
173---
174
175## 3. File map
176
177### NEW files
178
179| File | Purpose |
180|---|---|
181| `packages/contracts/src/preview.ts` | Effect/Schema schemas: inputs, snapshot, events, errors |
182| `packages/contracts/src/preview.test.ts` | Schema round‑trip tests |
183| `apps/server/src/preview/Services/Manager.ts` | `PreviewManager` Service tag + interface |
184| `apps/server/src/preview/Layers/Manager.ts` | Implementation: in‑memory map + event subject |
185| `apps/server/src/preview/Layers/Manager.test.ts` | Lifecycle, snapshot, event ordering |
186| `apps/desktop/src/preview-view-manager.ts` | Plain‑Node Electron port of ami's BrowserManager (subset) |
187| `apps/desktop/src/preview-preload.ts` | Webview preload (no‑op v1) |
188| `apps/web/src/previewStateStore.ts` | Per‑thread zustand store (mirrors `terminalStateStore.ts`) |
189| `apps/web/src/previewStateStore.test.ts` | Reducer tests |
190| `apps/web/src/rightPanelStore.ts` | Right‑panel arbiter |
191| `apps/web/src/rightPanelStore.test.ts` | Arbiter tests |
192| `apps/web/src/components/preview/PreviewPanel.tsx` | Right‑panel entry (wraps `PreviewPanelShell`) |
193| `apps/web/src/components/preview/PreviewPanelShell.tsx` | Shell mirroring `DiffPanelShell` (`mode: "inline"\|"sheet"\|"sidebar"`) |
194| `apps/web/src/components/preview/PreviewView.tsx` | Chrome bar (URL/back/fwd/refresh) + `<PreviewWebview>` |
195| `apps/web/src/components/preview/PreviewWebview.tsx` | Electron `<webview>` host; null on web build |
196| `apps/web/src/components/preview/PreviewEmptyState.tsx` | Pre‑URL empty state |
197| `apps/web/src/components/preview/PreviewUnsupportedToast.ts` | `"Preview is only available in the desktop app"` toast |
198
199### MODIFIED files
200
201| File | Change |
202|---|---|
203| `packages/contracts/src/keybindings.ts` | Add `preview.toggle`, `preview.refresh`, `preview.focusUrl` to `STATIC_KEYBINDING_COMMANDS`; add `previewFocus`, `previewOpen` to context keys |
204| `packages/contracts/src/project.ts` | Add `previewUrl?: string` and `autoOpenPreview?: boolean` to `ProjectScript` schema |
205| `packages/contracts/src/server.ts` | Extend `EnvironmentApi` with `preview` namespace |
206| `packages/contracts/src/index.ts` | Re‑export new types |
207| `apps/server/src/keybindings.ts` | Add defaults: `mod+shift+j` → `preview.toggle`, `mod+shift+r` → `preview.refresh` (when `previewFocus`) |
208| `apps/server/src/ws.ts` | Route `preview.open`, `preview.navigate`, `preview.refresh`, `preview.close`, `preview.list`, `preview.onEvent` |
209| `apps/server/src/orchestration/runtimeLayer.ts` (or equivalent) | Provide `PreviewManager.Default` |
210| `apps/web/src/environmentApi.ts` | Wire `preview` slot in `createEnvironmentApi` |
211| `apps/web/src/keybindings.ts` | Add `isPreviewToggleShortcut`, `isPreviewRefreshShortcut` helpers |
212| `apps/web/src/routes/_chat.tsx` | Handle `preview.toggle` in global shortcut handler |
213| `apps/web/src/components/ChatView.tsx` | Replace local `planSidebarOpen` with `rightPanelStore`; render `PreviewPanel` |
214| `apps/web/src/components/ThreadTerminalDrawer.tsx` | At terminal link activation, when `match.kind === "url"` and link looks like a dev URL, show context menu with "Open in preview" / "Open in browser"; pass through to `localApi.preview.openTab(...)` when chosen |
215| `apps/web/src/components/ProjectScriptsControl.tsx` | Add `previewUrl` + `autoOpenPreview` form fields in the Add/Edit dialog |
216| `apps/web/src/projectScripts.ts` | Carry the new fields through `commandForProjectScript` / serialization |
217| `apps/web/src/types.ts` | Add `PreviewSession` mirror types (or re‑export from contracts) |
218| `apps/web/src/lib/desktopBridge.d.ts` (or wherever bridge types live) | Add `preview` namespace shape |
219| `apps/desktop/src/main.ts` | Register `preview:*` IPC handlers; instantiate `previewViewManager`; wire `mainWindow` injection |
220| `apps/desktop/src/preload.ts` | Expose `desktopBridge.preview.*` |
221| `KEYBINDINGS.md` | Document new commands and `previewFocus`/`previewOpen` `when` keys |
222
223### NOT changed
224
225- `apps/web/src/components/DiffPanel.tsx` — unchanged, its open/close just becomes mediated by `rightPanelStore`. The `?diff=1` URL truth is preserved via a sync effect.
226- `apps/web/src/components/PlanSidebar.tsx` — unchanged surface; consumer in `ChatView.tsx` is what changes.
227- `packages/contracts/src/terminal.ts` — terminal stays single‑tab, no schema changes.
228
229---
230
231## 4. Schemas (`packages/contracts/src/preview.ts`)
232
233```ts
234import { Schema } from "effect";
235import { TrimmedNonEmptyString } from "./baseSchemas.ts";
236
237export const PreviewTabId = TrimmedNonEmptyString.check(Schema.isMaxLength(128));
238export type PreviewTabId = typeof PreviewTabId.Type;
239
240export const PreviewNavStatus = Schema.Union([
241 Schema.Struct({ _tag: Schema.Literal("Idle") }),
242 Schema.Struct({
243 _tag: Schema.Literal("Loading"),
244 url: TrimmedNonEmptyString,
245 title: Schema.String,
246 }),
247 Schema.Struct({
248 _tag: Schema.Literal("Success"),
249 url: TrimmedNonEmptyString,
250 title: Schema.String,
251 }),
252 Schema.Struct({
253 _tag: Schema.Literal("LoadFailed"),
254 url: TrimmedNonEmptyString,
255 title: Schema.String,
256 code: Schema.Int,
257 description: Schema.String,
258 }),
259]);
260export type PreviewNavStatus = typeof PreviewNavStatus.Type;
261
262export const PreviewSessionSnapshot = Schema.Struct({
263 threadId: TrimmedNonEmptyString,
264 tabId: PreviewTabId,
265 navStatus: PreviewNavStatus,
266 canGoBack: Schema.Boolean,
267 canGoForward: Schema.Boolean,
268 updatedAt: Schema.String,
269});
270export type PreviewSessionSnapshot = typeof PreviewSessionSnapshot.Type;
271
272export const PreviewOpenInput = Schema.Struct({
273 threadId: TrimmedNonEmptyString,
274 url: TrimmedNonEmptyString,
275});
276export const PreviewNavigateInput = Schema.Struct({
277 threadId: TrimmedNonEmptyString,
278 tabId: PreviewTabId,
279 url: TrimmedNonEmptyString,
280});
281export const PreviewRefreshInput = Schema.Struct({
282 threadId: TrimmedNonEmptyString,
283 tabId: PreviewTabId,
284});
285export const PreviewCloseInput = Schema.Struct({
286 threadId: TrimmedNonEmptyString,
287 tabId: Schema.optional(PreviewTabId),
288});
289
290const PreviewEventBase = Schema.Struct({
291 threadId: TrimmedNonEmptyString,
292 tabId: PreviewTabId,
293 createdAt: Schema.String,
294});
295
296export const PreviewEvent = Schema.Union([
297 Schema.Struct({
298 ...PreviewEventBase.fields,
299 type: Schema.Literal("opened"),
300 snapshot: PreviewSessionSnapshot,
301 }),
302 Schema.Struct({
303 ...PreviewEventBase.fields,
304 type: Schema.Literal("navigated"),
305 snapshot: PreviewSessionSnapshot,
306 }),
307 Schema.Struct({
308 ...PreviewEventBase.fields,
309 type: Schema.Literal("failed"),
310 code: Schema.Int,
311 description: Schema.String,
312 }),
313 Schema.Struct({
314 ...PreviewEventBase.fields,
315 type: Schema.Literal("closed"),
316 }),
317]);
318export type PreviewEvent = typeof PreviewEvent.Type;
319
320export class PreviewSessionLookupError extends Schema.TaggedErrorClass<PreviewSessionLookupError>()(
321 "PreviewSessionLookupError",
322 { threadId: Schema.String, tabId: Schema.String },
323) {
324 override get message() {
325 return `Unknown preview session: thread=${this.threadId}, tab=${this.tabId}`;
326 }
327}
328
329export class PreviewInvalidUrlError extends Schema.TaggedErrorClass<PreviewInvalidUrlError>()(
330 "PreviewInvalidUrlError",
331 { rawUrl: Schema.String },
332) {
333 override get message() {
334 return `Invalid preview URL: ${this.rawUrl}`;
335 }
336}
337
338export const PreviewError = Schema.Union([
339 PreviewSessionLookupError,
340 PreviewInvalidUrlError,
341]);
342export type PreviewError = typeof PreviewError.Type;
343```
344
345### `ProjectScript` extension (`packages/contracts/src/project.ts`)
346
347Add to the existing `ProjectScript` struct (additive, default both fields to undefined/false at runtime so existing persisted scripts decode unchanged):
348
349```ts
350previewUrl: Schema.optional(TrimmedNonEmptyString),
351autoOpenPreview: Schema.optional(Schema.Boolean),
352```
353
354### Keybindings (`packages/contracts/src/keybindings.ts:50`)
355
356```ts
357const STATIC_KEYBINDING_COMMANDS = [
358 "terminal.toggle",
359 "terminal.split",
360 "terminal.new",
361 "terminal.close",
362 "diff.toggle",
363 "preview.toggle", // NEW
364 "preview.refresh", // NEW
365 "preview.focusUrl", // NEW
366 "commandPalette.toggle",
367 "chat.new",
368 "chat.newLocal",
369 "editor.openFavorite",
370] as const;
371```
372
373Add `previewFocus` and `previewOpen` to the `ShortcutMatchContext` union (`apps/web/src/keybindings.ts:30`).
374
375### Defaults (`apps/server/src/keybindings.ts`)
376
377```ts
378{ key: "mod+shift+j", command: "preview.toggle" },
379{ key: "mod+r", command: "preview.refresh", when: "previewFocus" },
380{ key: "mod+l", command: "preview.focusUrl", when: "previewFocus" },
381```
382
383---
384
385## 5. Server: PreviewManager
386
387### `apps/server/src/preview/Services/Manager.ts`
388
389```ts
390import { Context, Effect } from "effect";
391import {
392 PreviewCloseInput,
393 PreviewError,
394 PreviewEvent,
395 PreviewNavigateInput,
396 PreviewOpenInput,
397 PreviewRefreshInput,
398 PreviewSessionSnapshot,
399} from "@t3tools/contracts";
400
401export interface PreviewManagerShape {
402 readonly open: (
403 input: PreviewOpenInput,
404 ) => Effect.Effect<PreviewSessionSnapshot, PreviewError>;
405 readonly navigate: (
406 input: PreviewNavigateInput,
407 ) => Effect.Effect<PreviewSessionSnapshot, PreviewError>;
408 readonly refresh: (
409 input: PreviewRefreshInput,
410 ) => Effect.Effect<void, PreviewError>;
411 readonly close: (input: PreviewCloseInput) => Effect.Effect<void, PreviewError>;
412 readonly list: (
413 threadId: string,
414 ) => Effect.Effect<ReadonlyArray<PreviewSessionSnapshot>>;
415 readonly subscribe: (
416 listener: (event: PreviewEvent) => Effect.Effect<void>,
417 ) => Effect.Effect<() => void>;
418}
419
420export class PreviewManager extends Context.Service<
421 PreviewManager,
422 PreviewManagerShape
423>()("t3/preview/Services/Manager/PreviewManager") {}
424```
425
426### `apps/server/src/preview/Layers/Manager.ts`
427
428In‑memory `Map<threadId, PreviewSession>` keyed by `threadId` (single tab per thread for v1; the schema has `tabId` so we can grow to multi‑tab without a migration). Maintains a subscriber set; emits events with monotonic `createdAt` from `Date.now().toISOString()`.
429
430URL normalization mirrors ami's helper (`browser-view-manager.ts:655`):
431
432```ts
433const normalizeUrl = (input: string) =>
434 Effect.try(() => {
435 const trimmed = input.trim();
436 if (!trimmed) throw new Error("empty");
437 // localhost stays http unless explicitly https
438 const useHttp =
439 /^(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|\/|$)/.test(trimmed);
440 const parsed = urlParseLax(trimmed, { https: !useHttp });
441 if (!parsed?.href) throw new Error("unparseable");
442 return parsed.href;
443 }).pipe(
444 Effect.catchAll((cause) =>
445 Effect.fail(new PreviewInvalidUrlError({ rawUrl: input, cause })),
446 ),
447 );
448```
449
450### `apps/server/src/ws.ts` routes
451
452Following the existing terminal route pattern, expose:
453
454```ts
455preview: {
456 open: (input) => Effect.runPromise(previewManager.open(input)),
457 navigate: (input) => Effect.runPromise(previewManager.navigate(input)),
458 refresh: (input) => Effect.runPromise(previewManager.refresh(input)),
459 close: (input) => Effect.runPromise(previewManager.close(input)),
460 list: (threadId) => Effect.runPromise(previewManager.list(threadId)),
461 onEvent: (callback) => /* subscribe + return unsubscribe */,
462}
463```
464
465---
466
467## 6. Desktop: `PreviewViewManager`
468
469### Style
470
471`apps/desktop/src/main.ts` is plain Node/Electron with no Effect — for parity, **drop Effect in `preview-view-manager.ts`**. Use plain async/await + a small typed‑error class style consistent with the rest of `apps/desktop`.
472
473### `apps/desktop/src/preview-view-manager.ts`
474
475Subset of ami's `BrowserManager`:
476
477```ts
478import * as path from "node:path";
479import { type BrowserWindow, type Session, session, webContents } from "electron";
480
481const PREVIEW_PARTITION = "persist:t3code-preview";
482
483export type NavStatus =
484 | { kind: "Idle" }
485 | { kind: "Loading"; url: string; title: string }
486 | { kind: "Success"; url: string; title: string }
487 | { kind: "LoadFailed"; url: string; title: string; code: number; description: string };
488
489export interface TabState {
490 tabId: string;
491 webContentsId: number | null;
492 navStatus: NavStatus;
493 canGoBack: boolean;
494 canGoForward: boolean;
495 visible: boolean;
496 updatedAt: string;
497}
498
499type Listener = (tabId: string, state: TabState) => void;
500
501export class PreviewViewManager {
502 private mainWindow: BrowserWindow | null = null;
503 private readonly tabs = new Map<string, TabState>();
504 private browserSession: Session | null = null;
505 private readonly listeners = new Set<Listener>();
506 private readonly preloadPath: string;
507
508 constructor() {
509 this.preloadPath = path.join(__dirname, "preview-preload.cjs");
510 }
511
512 setMainWindow(window: BrowserWindow): void {
513 this.mainWindow = window;
514 }
515
516 getPreloadPath(): string { return this.preloadPath; }
517 getBrowserPartition(): string { return PREVIEW_PARTITION; }
518
519 getBrowserSession(): Session {
520 if (this.browserSession) return this.browserSession;
521 const sess = session.fromPartition(PREVIEW_PARTITION);
522 // strip electron/t3code from UA so dev preview doesn't trip bot detection
523 const ua = sess.getUserAgent()
524 .replace(/Electron\/[\d.]+ /, "")
525 .replace(/\s*t3code\/[\d.]+/, "");
526 sess.setUserAgent(ua);
527 sess.setPermissionRequestHandler((_wc, perm, callback) => {
528 const allow = ["clipboard-read", "clipboard-write", "notifications", "geolocation"];
529 callback(allow.includes(perm));
530 });
531 this.browserSession = sess;
532 return sess;
533 }
534
535 createTab(tabId: string): TabState {
536 if (this.tabs.has(tabId)) return this.tabs.get(tabId)!;
537 const initial: TabState = {
538 tabId,
539 webContentsId: null,
540 navStatus: { kind: "Idle" },
541 canGoBack: false,
542 canGoForward: false,
543 visible: true,
544 updatedAt: new Date().toISOString(),
545 };
546 this.tabs.set(tabId, initial);
547 this.emit(tabId, initial);
548 return initial;
549 }
550
551 closeTab(tabId: string): void {
552 if (!this.tabs.delete(tabId)) return;
553 this.emit(tabId, { ...this.tabs.get(tabId)!, navStatus: { kind: "Idle" } });
554 }
555
556 setVisibility(tabId: string, visible: boolean): void {
557 const tab = this.tabs.get(tabId);
558 if (!tab) return;
559 if (tab.visible === visible) return;
560 this.update(tabId, { visible });
561 }
562
563 registerWebview(tabId: string, webContentsId: number): void {
564 const tab = this.tabs.get(tabId);
565 if (!tab) throw new PreviewTabNotFoundError(tabId);
566 const wc = webContents.fromId(webContentsId);
567 if (!wc) throw new PreviewWebContentsNotFoundError(tabId, webContentsId);
568
569 this.attachListeners(tabId, wc);
570 this.update(tabId, {
571 webContentsId,
572 navStatus: this.computeNavStatus(wc),
573 canGoBack: wc.navigationHistory.canGoBack(),
574 canGoForward: wc.navigationHistory.canGoForward(),
575 });
576 }
577
578 unregisterWebview(tabId: string): void {
579 const tab = this.tabs.get(tabId);
580 if (!tab) return;
581 this.update(tabId, {
582 webContentsId: null,
583 navStatus: { kind: "Idle" },
584 canGoBack: false,
585 canGoForward: false,
586 });
587 }
588
589 async navigate(tabId: string, rawUrl: string): Promise<void> {
590 const wc = this.requireWebContents(tabId);
591 const url = this.normalizeUrl(rawUrl);
592 if (wc.getURL() === url) return;
593 await wc.loadURL(url);
594 }
595
596 goBack(tabId: string): void { this.requireWebContents(tabId).navigationHistory.goBack(); }
597 goForward(tabId: string): void { this.requireWebContents(tabId).navigationHistory.goForward(); }
598 refresh(tabId: string): void { this.requireWebContents(tabId).reload(); }
599
600 onStateChange(listener: Listener): () => void {
601 this.listeners.add(listener);
602 return () => this.listeners.delete(listener);
603 }
604
605 private attachListeners(tabId: string, wc: Electron.WebContents): void {
606 const sync = () => {
607 this.update(tabId, {
608 navStatus: this.computeNavStatus(wc),
609 canGoBack: wc.navigationHistory.canGoBack(),
610 canGoForward: wc.navigationHistory.canGoForward(),
611 });
612 };
613 wc.on("did-navigate", sync);
614 wc.on("did-navigate-in-page", sync);
615 wc.on("page-title-updated", sync);
616 wc.on("did-start-loading", sync);
617 wc.on("did-stop-loading", sync);
618 wc.on("did-fail-load", (_event, code, description) => {
619 if (code === -3) return; // user aborted
620 this.update(tabId, {
621 navStatus: {
622 kind: "LoadFailed",
623 url: wc.getURL(),
624 title: wc.getTitle(),
625 code,
626 description,
627 },
628 });
629 });
630
631 // External link policy: load in same view (matches ami)
632 wc.setWindowOpenHandler(({ url }) => {
633 void wc.loadURL(url);
634 return { action: "deny" };
635 });
636
637 // Forward app shortcuts to the main window so mod+shift+j etc still work
638 wc.on("before-input-event", (event, input) => {
639 if (this.isAppShortcut(input) && this.mainWindow && !this.mainWindow.isDestroyed()) {
640 event.preventDefault();
641 this.mainWindow.webContents.sendInputEvent({
642 type: "keyDown",
643 keyCode: input.key,
644 modifiers: [
645 ...(input.meta ? ["meta" as const] : []),
646 ...(input.shift ? ["shift" as const] : []),
647 ...(input.control ? ["control" as const] : []),
648 ...(input.alt ? ["alt" as const] : []),
649 ],
650 });
651 }
652 });
653 }
654
655 private isAppShortcut(input: Electron.Input): boolean {
656 if (input.type !== "keyDown") return false;
657 // Mirror the t3code keybinding defaults that should always reach the main window.
658 const SHORTCUTS = [
659 { key: "j", meta: true, shift: true }, // preview.toggle
660 { key: "k", meta: true, shift: false }, // commandPalette.toggle
661 { key: ",", meta: true, shift: false }, // settings
662 { key: "w", meta: true, shift: false }, // close
663 // future: terminal.* if user wants them while preview focused
664 ];
665 return SHORTCUTS.some((s) =>
666 s.key.toLowerCase() === input.key.toLowerCase()
667 && s.meta === input.meta
668 && s.shift === input.shift,
669 );
670 }
671
672 private computeNavStatus(wc: Electron.WebContents): NavStatus {
673 const url = wc.getURL();
674 const title = wc.getTitle();
675 if (url === "" || url === "about:blank") return { kind: "Idle" };
676 if (wc.isLoading()) return { kind: "Loading", url, title };
677 return { kind: "Success", url, title };
678 }
679
680 private requireWebContents(tabId: string): Electron.WebContents {
681 const tab = this.tabs.get(tabId);
682 if (!tab) throw new PreviewTabNotFoundError(tabId);
683 if (tab.webContentsId == null) throw new PreviewWebviewNotInitializedError(tabId);
684 const wc = webContents.fromId(tab.webContentsId);
685 if (!wc) throw new PreviewWebContentsNotFoundError(tabId, tab.webContentsId);
686 return wc;
687 }
688
689 private update(tabId: string, patch: Partial<TabState>): void {
690 const current = this.tabs.get(tabId);
691 if (!current) return;
692 const next: TabState = { ...current, ...patch, updatedAt: new Date().toISOString() };
693 this.tabs.set(tabId, next);
694 this.emit(tabId, next);
695 }
696
697 private emit(tabId: string, state: TabState): void {
698 for (const listener of this.listeners) listener(tabId, state);
699 }
700
701 private normalizeUrl(input: string): string {
702 // Same heuristics as server-side normalization.
703 // Returns "https://..." or throws.
704 const trimmed = input.trim();
705 if (!trimmed) throw new PreviewInvalidUrlError(input);
706 const useHttp = /^(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|\/|$)/.test(trimmed);
707 const parsed = new URL(trimmed.includes("://")
708 ? trimmed
709 : `${useHttp ? "http" : "https"}://${trimmed}`);
710 return parsed.href;
711 }
712}
713
714export class PreviewTabNotFoundError extends Error {
715 constructor(public readonly tabId: string) {
716 super(`Preview tab not found: ${tabId}`);
717 }
718}
719export class PreviewWebContentsNotFoundError extends Error { /* … */ }
720export class PreviewWebviewNotInitializedError extends Error { /* … */ }
721export class PreviewInvalidUrlError extends Error { /* … */ }
722
723export const previewViewManager = new PreviewViewManager();
724```
725
726### `apps/desktop/src/main.ts` additions
727
728Right after `mainWindow = createWindow();` in `bootstrap()` and after every recreation:
729
730```ts
731previewViewManager.setMainWindow(mainWindow);
732```
733
734Register IPC handlers (in `registerIpcHandlers()`):
735
736```ts
737ipcMain.handle("preview:createTab", (_e, tabId: string) =>
738 previewViewManager.createTab(tabId));
739ipcMain.handle("preview:closeTab", (_e, tabId: string) =>
740 previewViewManager.closeTab(tabId));
741ipcMain.handle("preview:setVisibility", (_e, tabId: string, visible: boolean) =>
742 previewViewManager.setVisibility(tabId, visible));
743ipcMain.handle("preview:registerWebview", (_e, tabId: string, wcId: number) =>
744 previewViewManager.registerWebview(tabId, wcId));
745ipcMain.handle("preview:unregisterWebview", (_e, tabId: string) =>
746 previewViewManager.unregisterWebview(tabId));
747ipcMain.handle("preview:navigate", (_e, tabId: string, url: string) =>
748 previewViewManager.navigate(tabId, url));
749ipcMain.handle("preview:goBack", (_e, tabId: string) => previewViewManager.goBack(tabId));
750ipcMain.handle("preview:goForward", (_e, tabId: string) => previewViewManager.goForward(tabId));
751ipcMain.handle("preview:refresh", (_e, tabId: string) => previewViewManager.refresh(tabId));
752ipcMain.handle("preview:getPreloadPath", () => previewViewManager.getPreloadPath());
753ipcMain.handle("preview:getBrowserPartition", () => previewViewManager.getBrowserPartition());
754```
755
756State change broadcast (push to all renderer windows):
757
758```ts
759previewViewManager.onStateChange((tabId, state) => {
760 for (const win of BrowserWindow.getAllWindows()) {
761 if (win.isDestroyed()) continue;
762 win.webContents.send("preview:state-change", tabId, state);
763 }
764});
765```
766
767### `apps/desktop/src/preload.ts` additions
768
769```ts
770contextBridge.exposeInMainWorld("desktopBridge", {
771 // … existing fields …
772 preview: {
773 createTab: (tabId: string) => ipcRenderer.invoke("preview:createTab", tabId),
774 closeTab: (tabId: string) => ipcRenderer.invoke("preview:closeTab", tabId),
775 setVisibility: (tabId: string, visible: boolean) =>
776 ipcRenderer.invoke("preview:setVisibility", tabId, visible),
777 registerWebview: (tabId: string, wcId: number) =>
778 ipcRenderer.invoke("preview:registerWebview", tabId, wcId),
779 unregisterWebview: (tabId: string) =>
780 ipcRenderer.invoke("preview:unregisterWebview", tabId),
781 navigate: (tabId: string, url: string) =>
782 ipcRenderer.invoke("preview:navigate", tabId, url),
783 goBack: (tabId: string) => ipcRenderer.invoke("preview:goBack", tabId),
784 goForward: (tabId: string) => ipcRenderer.invoke("preview:goForward", tabId),
785 refresh: (tabId: string) => ipcRenderer.invoke("preview:refresh", tabId),
786 getPreloadPath: (): Promise<string> => ipcRenderer.invoke("preview:getPreloadPath"),
787 getBrowserPartition: (): Promise<string> =>
788 ipcRenderer.invoke("preview:getBrowserPartition"),
789 onStateChange: (cb: (tabId: string, state: DesktopPreviewTabState) => void) => {
790 const listener = (_e: unknown, tabId: string, state: DesktopPreviewTabState) =>
791 cb(tabId, state);
792 ipcRenderer.on("preview:state-change", listener);
793 return () => ipcRenderer.removeListener("preview:state-change", listener);
794 },
795 },
796});
797```
798
799### `apps/desktop/src/preview-preload.ts`
800
801```ts
802// Intentionally empty for v1.
803// Future: forward console.error to main, expose a tiny window.t3preview API.
804```
805
806Build config: add `preview-preload.ts` to `apps/desktop/tsdown.config.ts` outputs so it ships as `preview-preload.cjs` next to `preload.cjs`.
807
808---
809
810## 7. Web: state stores
811
812### `apps/web/src/previewStateStore.ts`
813
814Direct mirror of `terminalStateStore.ts` shape (one tab per thread for v1; structured to grow into multi‑tab the same way terminal grew into groups).
815
816```ts
817import { type ScopedThreadRef, scopedThreadKey } from "@t3tools/client-runtime";
818import { create } from "zustand";
819import { createJSONStorage, persist } from "zustand/middleware";
820import type { PreviewEvent, PreviewSessionSnapshot } from "@t3tools/contracts";
821import { resolveStorage } from "./lib/storage";
822
823interface ThreadPreviewState {
824 /** present if a preview tab exists for this thread */
825 snapshot: PreviewSessionSnapshot | null;
826 /** desktop-side immediate nav button state (overrides snapshot when fresher) */
827 desktopOverlay: {
828 canGoBack: boolean;
829 canGoForward: boolean;
830 visible: boolean;
831 } | null;
832 /** local UI: is the URL bar focused? */
833 urlBarFocused: boolean;
834 recentEventIds: number[];
835}
836
837interface PreviewEventEntry { id: number; event: PreviewEvent }
838
839interface PreviewStateStore {
840 byThreadKey: Record<string, ThreadPreviewState>;
841 recentEvents: Record<string, ReadonlyArray<PreviewEventEntry>>;
842 nextEventId: number;
843 applyServerEvent: (ref: ScopedThreadRef, event: PreviewEvent) => void;
844 applyDesktopState: (
845 ref: ScopedThreadRef,
846 overlay: ThreadPreviewState["desktopOverlay"],
847 ) => void;
848 setUrlBarFocused: (ref: ScopedThreadRef, focused: boolean) => void;
849 removeThread: (ref: ScopedThreadRef) => void;
850}
851
852const PERSISTED_FIELDS = ["byThreadKey"] as const;
853const STORAGE_KEY = "t3code:preview-state:v1";
854
855export const usePreviewStateStore = create<PreviewStateStore>()(
856 persist(/* … */, {
857 name: STORAGE_KEY,
858 storage: createJSONStorage(() =>
859 resolveStorage(typeof window !== "undefined" ? window.localStorage : undefined),
860 ),
861 version: 1,
862 partialize: (s) => Object.fromEntries(PERSISTED_FIELDS.map((k) => [k, s[k]])),
863 }),
864);
865
866export function selectThreadPreviewState(
867 byThreadKey: Record<string, ThreadPreviewState>,
868 ref: ScopedThreadRef | null,
869): ThreadPreviewState {
870 if (!ref) return EMPTY_THREAD_STATE;
871 return byThreadKey[scopedThreadKey(ref)] ?? EMPTY_THREAD_STATE;
872}
873```
874
875Key invariants (test in `previewStateStore.test.ts`):
876
877- `applyServerEvent("opened" | "navigated" | "failed")` updates `snapshot`; pushes into `recentEvents` ring buffer (cap 50).
878- `applyServerEvent("closed")` removes the thread entry entirely.
879- `applyDesktopState` only updates `desktopOverlay`; never touches `snapshot.url`/`title` (server is truth for those).
880
881### `apps/web/src/environmentApi.ts`
882
883```ts
884preview: {
885 open: (input) => rpcClient.preview.open(input as never),
886 navigate: (input) => rpcClient.preview.navigate(input as never),
887 refresh: (input) => rpcClient.preview.refresh(input as never),
888 close: (input) => rpcClient.preview.close(input as never),
889 list: (threadId) => rpcClient.preview.list(threadId),
890 onEvent: (callback) => rpcClient.preview.onEvent(callback),
891},
892```
893
894Plus mirror in the `EnvironmentApi` shape in `packages/contracts/src/server.ts` (or wherever `EnvironmentApi` lives).
895
896---
897
898## 8. Web: components
899
900### `apps/web/src/components/preview/PreviewPanelShell.tsx`
901
902Lifted from `DiffPanelShell.tsx` verbatim, renamed types. **Must** use the same className contract:
903
904```tsx
905<div
906 className={cn(
907 "flex h-full min-w-0 flex-col bg-background",
908 props.mode === "inline"
909 ? "w-[42vw] min-w-[360px] max-w-[560px] shrink-0 border-l border-border"
910 : "w-full",
911 )}
912>
913```
914
915So preview side panel is visually indistinguishable in spacing from the diff panel.
916
917### `apps/web/src/components/preview/PreviewView.tsx`
918
919Renderer of chrome bar + `<PreviewWebview>`. Direct port of ami's `browser-view.tsx` chrome (URL bar with protocol/host/path split, back/fwd/refresh/loading bar) but stripped of: devtools button, screenshot button, runtime errors badge, react-grab. Uses `lucide-react` icons that t3code already depends on (`ArrowLeft`, `ArrowRight`, `RefreshCw`, `X`) instead of `@hugeicons/react`.
920
921Key behaviour:
922
923- On mount: `await desktopBridge.preview.createTab(tabId)`, `await desktopBridge.preview.getPreloadPath()`.
924- Subscribes to `desktopBridge.preview.onStateChange(handleState)` → `previewStateStore.applyDesktopState`.
925- Subscribes to `EnvironmentApi.preview.onEvent(handleEvent)` → `previewStateStore.applyServerEvent`.
926- On `<webview>` `dom-ready`: read `webContentsId`, call `desktopBridge.preview.registerWebview`, then call `EnvironmentApi.preview.navigate({ threadId, tabId, url })` so server learns the resolved URL.
927- On unmount when panel hides (not when thread changes — see persistence note below): `setVisibility(tabId, false)`.
928
929### `apps/web/src/components/preview/PreviewWebview.tsx`
930
931```tsx
932"use client";
933
934import { useEffect, useState } from "react";
935import { isDesktop } from "~/env";
936
937interface Props {
938 tabId: string;
939 initialUrl: string | null;
940}
941
942declare global {
943 interface HTMLElementTagNameMap {
944 webview: Electron.WebviewTag;
945 }
946}
947
948export function PreviewWebview({ tabId, initialUrl }: Props) {
949 const [config, setConfig] = useState<{ partition: string; preload: string } | null>(null);
950
951 useEffect(() => {
952 if (!isDesktop || !window.desktopBridge?.preview) return;
953 void Promise.all([
954 window.desktopBridge.preview.getBrowserPartition(),
955 window.desktopBridge.preview.getPreloadPath(),
956 ]).then(([partition, preload]) => setConfig({ partition, preload }));
957 }, []);
958
959 if (!isDesktop || !window.desktopBridge?.preview || !config) return null;
960
961 const src = initialUrl ?? "about:blank";
962 return (
963 <webview
964 src={src}
965 partition={config.partition}
966 preload={`file://${config.preload}`}
967 allowpopups
968 data-preview-tab={tabId}
969 className="absolute inset-0 h-full w-full bg-background"
970 />
971 );
972}
973```
974
975### `apps/web/src/components/preview/PreviewPanel.tsx`
976
977The right‑panel entrypoint. Reads `previewStateStore` for the active thread; renders `<PreviewView>` if a session exists, otherwise `<PreviewEmptyState>` with a URL field that calls `EnvironmentApi.preview.open(...)` on submit.
978
979### Persistence across thread changes
980
981Following `PersistentThreadTerminalDrawer` pattern (`ChatView.tsx:3517`): keep multiple `<PreviewPanel>` instances mounted (capped, e.g. `MAX_HIDDEN_MOUNTED_PREVIEW_THREADS = 3`) and toggle `visible` via `desktopBridge.preview.setVisibility`. The `<webview>` element stays alive in the DOM but the desktop side knows it's hidden so it can later (v2) skip raster updates.
982
983For v1 the simpler version: only mount the active thread's `<PreviewPanel>`. Closing the right panel calls `desktopBridge.preview.setVisibility(tabId, false)` but does **not** close the tab. Switching threads closes the previous thread's tab. This matches the desktop‑only constraint and avoids hidden `<webview>`s eating GPU.
984
985---
986
987## 9. Discoverability glue
988
989### A. `preview.toggle` keybinding (`apps/web/src/routes/_chat.tsx`)
990
991Mirror the existing `chat.new` block (`:51–:78`):
992
993```ts
994if (command === "preview.toggle") {
995 event.preventDefault();
996 event.stopPropagation();
997 if (!window.desktopBridge?.preview) {
998 showPreviewUnsupportedToast();
999 return;
1000 }
1001 if (!routeThreadRef) return;
1002 rightPanelStore.toggle(routeThreadRef, "preview");
1003 return;
1004}
1005```
1006
1007`previewFocus` and `previewOpen` `when` context keys get computed in the global shortcut handler so `mod+r` (refresh) only matches when the preview is the active right panel and focused.
1008
1009### B. Terminal link → "Open in preview"
1010
1011In `ThreadTerminalDrawer.tsx`'s `terminal.registerLinkProvider({ provideLinks })` callback (`:454–:514`), update the `activate(event)` handler:
1012
1013```ts
1014activate: (event: MouseEvent) => {
1015 if (!isTerminalLinkActivation(event)) return;
1016 if (match.kind !== "url") {
1017 // existing path link handling
1018 return;
1019 }
1020 if (!isPreviewable(match.text) || !window.desktopBridge?.preview) {
1021 void localApi.shell.openExternal(match.text).catch(/* … */);
1022 return;
1023 }
1024 void localApi.contextMenu
1025 .show(
1026 [
1027 { id: "open-in-preview", label: "Open in preview" },
1028 { id: "open-in-browser", label: "Open in browser" },
1029 ],
1030 { x: event.clientX, y: event.clientY },
1031 )
1032 .then((choice) => {
1033 if (choice === "open-in-preview") {
1034 void api.preview.open({ threadId, url: match.text });
1035 rightPanelStore.open(threadRef, "preview");
1036 } else if (choice === "open-in-browser") {
1037 void localApi.shell.openExternal(match.text);
1038 }
1039 });
1040},
1041```
1042
1043`isPreviewable`: localhost / 127.0.0.1 / 0.0.0.0 by default. The `previewUrlPatterns` user setting (a `string[]` in `ServerSettings`) extends the allowlist to cover deploy preview hosts (e.g. `*.vercel.app`).
1044
1045### C. `ProjectScript.previewUrl` / `autoOpenPreview`
1046
1047Schema additions are listed in §4. UI changes in `ProjectScriptsControl.tsx`'s Add/Edit dialog form (`:378–:458`):
1048
1049```tsx
1050<div className="space-y-1.5">
1051 <Label htmlFor="script-preview-url">Preview URL (optional)</Label>
1052 <Input
1053 id="script-preview-url"
1054 placeholder="http://localhost:5173"
1055 value={previewUrl}
1056 onChange={(e) => setPreviewUrl(e.target.value)}
1057 />
1058 <p className="text-xs text-muted-foreground">
1059 Auto-open this URL in the preview panel when the script starts.
1060 </p>
1061</div>
1062<label className="flex items-center justify-between …">
1063 <span>Open preview automatically when this script runs</span>
1064 <Switch checked={autoOpenPreview} onCheckedChange={setAutoOpenPreview} />
1065</label>
1066```
1067
1068When `onRunScript(script)` fires in `ChatView.tsx`, after starting the terminal command, if `script.autoOpenPreview && script.previewUrl && desktopBridge.preview`:
1069
1070```ts
1071void api.preview.open({ threadId: activeThread.id, url: script.previewUrl });
1072rightPanelStore.open(activeThreadRef, "preview");
1073```
1074
1075---
1076
1077## 10. Migrations
1078
1079### Persisted state
1080
1081- `t3code:preview-state:v1` — new key, no migration needed.
1082- `t3code:right-panel-state:v1` — new key, no migration needed.
1083- Existing `t3code:terminal-state:v1` — untouched.
1084
1085### Schema additions
1086
1087`ProjectScript.previewUrl` and `autoOpenPreview` are both `Schema.optional(...)`. Existing serialized scripts decode unchanged. No data migration required.
1088
1089### Keybindings
1090
1091Adding `preview.toggle`, `preview.refresh`, `preview.focusUrl` to `STATIC_KEYBINDING_COMMANDS` is additive. Existing user `~/.t3/keybindings.json` files keep working. Default keybindings file picks up the new defaults on next read.
1092
1093---
1094
1095## 11. Testing strategy
1096
1097Following t3code's vitest patterns (`bun run test`, never `bun test`):
1098
1099| Test file | What it covers |
1100|---|---|
1101| `packages/contracts/src/preview.test.ts` | Schema encode/decode round-trips for inputs, snapshot, events; URL trimming; error tagged unions |
1102| `apps/server/src/preview/Layers/Manager.test.ts` | `open` creates session and emits `opened`; `navigate` updates and emits `navigated`; `close` removes and emits `closed`; subscribers receive monotonic events; `list` returns sorted snapshots |
1103| `apps/web/src/previewStateStore.test.ts` | `applyServerEvent` reducer correctness; ring buffer cap; `closed` removes entry; `desktopOverlay` is independent of snapshot fields |
1104| `apps/web/src/rightPanelStore.test.ts` | `open` / `close` / `toggle` semantics; per-thread isolation; `?diff=1` sync compatibility |
1105| `apps/web/src/components/preview/PreviewView.test.ts` (logic-only via `PreviewView.logic.ts` extraction) | URL bar input → navigation; navigation button enabled-state derivation; visibility toggle on panel hide |
1106| Existing `ThreadTerminalDrawer.test.ts` | Add a case: link activation with `kind: "url"` and `previewable: true` shows the context menu (mock `localApi.contextMenu.show`) |
1107
1108Manual smoke checklist (drop in `apps/desktop/test/smoke/`):
1109
11101. Boot desktop dev, open a thread, hit `mod+shift+j` → empty state appears in right panel.
11112. Type `https://example.com` → page loads, title updates in chrome bar.
11123. Navigate to `https://example.org` → back/forward enable correctly.
11134. Hit `mod+r` → page reloads, loading bar animates.
11145. Run a `bun dev` script in terminal printing `http://localhost:5173`, link in xterm → context menu offers "Open in preview".
11156. Restart server (kill `apps/server` child, watch it respawn) → preview tab survives, server replays `opened` event on reconnect, panel state matches reality.
11167. Switch threads → previous thread's preview tab is closed (v1); new thread shows empty state.
11178. Open a second window of the desktop (if supported) or open the same server from a browser tab in another window → `preview.list(threadId)` returns the snapshot, panel is empty in browser (correct: not desktop).
1118
1119---
1120
1121## 12. Risks and resolutions
1122
1123| Risk | Resolution |
1124|---|---|
1125| Hidden `<webview>` GPU cost when keeping multiple threads' previews mounted | v1 only mounts the active thread's preview. v2 can adopt the `PersistentThreadTerminalDrawer` pattern + `setVisibility` |
1126| `<webview>` keyboard capture eats `mod+shift+j` etc. | `before-input-event` forwarder in `PreviewViewManager` (mirror of ami's pattern, smaller shortcut list) |
1127| URL with `X-Frame-Options: DENY` works fine in `<webview>` (good — that's why we picked `<webview>` over iframe) | n/a |
1128| Server restart while desktop is alive: `<webview>` is still loaded but server has no record | On WS reconnect, web side sends a `preview.list(threadId)` and if empty, sends a `preview.open(...)` to re‑register the current URL. Add a small `useReconciliation` effect in `PreviewView.tsx` |
1129| Renderer process renders something into `<webview>` but never registered → orphaned tab in `PreviewViewManager` | `closeTab` is idempotent; on `<webview>` unmount, `unregisterWebview` is called; if that didn't fire (crash), the next `createTab` for the same `tabId` reuses the record |
1130| `autoOpenPreview` racing with terminal output (URL might not be in stdout yet by the time the script "starts") | Two‑phase: if `script.previewUrl` is set, open eagerly with that URL. If not set but `autoOpenPreview === true`, watch terminal output via existing `terminal-links` extraction and open the first `previewable` URL within a 60s window |
1131| Multiple windows of the desktop both rendering the same `<webview>` for the same thread | `<webview>` is per‑renderer; each window creates its own. The shared `persist:t3code-preview` partition keeps cookies in sync. Server records the last navigation URL but doesn't enforce single‑renderer |
1132
1133---
1134
1135## 13. Out of scope (v2+)
1136
1137Explicitly **not** in this landing:
1138
1139- Devtools support (would need to port ami's `WebContentsView` + `setDevToolsWebContents` + bounds sync — significant)
1140- Page screenshot capture
1141- JavaScript injection / `executeJavaScript`
1142- React Grab / element picker
1143- Console log capture
1144- Playwright/CDP agent automation tools (`browser_snapshot`, `browser_execute`)
1145- System cookie import (Chrome/Safari decryption)
1146- Multi-tab per thread (groups/splits)
1147- Agent control overlay ("Agent" pill while controlled)
1148- Web build iframe fallback
1149
1150Any of these can land in follow-up PRs against the same `PreviewViewManager` + `PreviewManager` shape — the v1 schemas are forward‑compatible (e.g. `tabId` is already there for multi‑tab; `recentEvents` ring buffer is already there for console/screenshot events).
1151
1152---
1153
1154## 14. UI design system — primitives we reuse
1155
1156Every visible element below is built on existing components. No new design primitives are introduced. References below are to the `apps/web/src/components/ui/` directory.
1157
1158| Need | Primitive | File ref | Notes |
1159|---|---|---|---|
1160| Theme colors / radii | CSS vars `--background`, `--card`, `--muted`, `--muted-foreground`, `--border`, `--input`, `--ring`, `--success`, etc. | `apps/web/src/index.css:86` | Light + `@variant dark` blocks. Always reference vars via tailwind utilities (`bg-card`, `text-muted-foreground`) — never hard‑code hex |
1161| `cn` class merger | `cn(...inputs)` from `~/lib/utils` | `apps/web/src/lib/utils.ts:8` | Wraps `cx` + `tailwind-merge` |
1162| Buttons (chrome bar back/fwd/refresh) | `Button` `variant="ghost"` `size="icon-xs"` | `apps/web/src/components/ui/button.tsx` | `icon-xs` is `size-7 rounded-md sm:size-6` — exactly the density in the screenshot |
1163| Buttons (URL field submit) | `Button` `variant="outline"` `size="sm"` | same | Matches the "ProjectScript primary action" density already in `BranchToolbar` |
1164| Button group (back / fwd / refresh as a unit) | `Group` + implicit segmenting (no `GroupSeparator`) | `apps/web/src/components/ui/group.tsx` | Group automatically removes outer borders between adjacent `[data-slot]` children |
1165| URL input (chrome bar editable) | `InputGroup` + `InputGroupInput` + `InputGroupAddon align="inline-start"` (globe icon) | `apps/web/src/components/ui/input-group.tsx` | Click‑anywhere‑to‑focus already wired in `InputGroupAddon`'s `onMouseDown` |
1166| URL input (chrome bar disabled / read‑only) | Same `InputGroup` with `disabled` on the input | same | Yields the muted look in the screenshot via `has-[input:disabled]:opacity-64` |
1167| Tab strip cells | Plain `<button>` + `cn` — **not** `Tabs` (base‑ui Tabs is over‑featured for browser tabs); `Group` semantics differ | new file `PreviewTabStrip.tsx` | Same hover/active treatment as the sidebar's `ChatPreview` rows |
1168| "Local" recommendation card | `Card` (no `CardHeader`/`CardPanel` — flat) | `apps/web/src/components/ui/card.tsx` | `Card` already gives `rounded-2xl border bg-card text-card-foreground shadow-xs/5` plus the inset stroke via `before:` |
1169| Status pulse dot (running) | `<span>` with `bg-success` and `animate-pulse` | tailwind built‑in | Theme‑aware via `--success` |
1170| Tooltips (back/fwd/refresh hover) | `Tooltip` + `TooltipTrigger` + `TooltipPopup` | `apps/web/src/components/ui/tooltip.tsx` | Sub‑200ms hover already configured |
1171| Empty state title + description | `Empty`, `EmptyHeader`, `EmptyTitle`, `EmptyDescription`, `EmptyContent`, `EmptyMedia` | `apps/web/src/components/ui/empty.tsx` | `EmptyMedia variant="icon"` gives the stacked‑cards icon with side rotation we want for "no preview yet" |
1172| Loading bar (top of webview while navigating) | Plain `<div>` with `bg-primary` + `transition-all` | n/a | Mirrors ami's loading bar (`browser-view.tsx:434`) — single‑pixel‑height div animating `width` |
1173| Spinner (inline in URL field while loading) | `Spinner` | `apps/web/src/components/ui/spinner.tsx` | Wraps `Loader2Icon` from lucide |
1174| Context menu (terminal link "Open in preview") | `localApi.contextMenu.show(...)` (already exists) | `apps/web/src/localApi.ts:58` | Native menu on desktop, fallback in `contextMenuFallback.ts` on web |
1175| Keybinding shortcut label in tooltips | `shortcutLabelForCommand(keybindings, "preview.toggle")` | `apps/web/src/keybindings.ts` | Renders `⌘⇧J` etc. |
1176| Right‑panel shell sizing | Match `DiffPanelShell` exactly: `w-[42vw] min-w-[360px] max-w-[560px] shrink-0 border-l border-border` | `apps/web/src/components/DiffPanelShell.tsx:32` | Single class string, do not reinvent |
1177
1178**Icons.** Use `lucide-react` everywhere (already in `package.json`). The exact icons we need:
1179
1180```ts
1181import {
1182 ArrowLeft, ArrowRight, // back / forward
1183 RotateCw, Loader2, // refresh / spinning
1184 Globe, // favicon fallback
1185 X, // close tab
1186 Plus, // new tab
1187 Copy, ExternalLink, // open in browser, copy URL
1188 Sidebar, Columns2, // right‑side toggles in tab strip (matches screenshot 2)
1189 PanelRightOpen, PanelRightClose, // alt for above
1190} from "lucide-react";
1191```
1192
1193`@hugeicons/react` is **not** a t3code dep — don't import it.
1194
1195**Density.** The reference points are `BranchToolbar` (`apps/web/src/components/BranchToolbar.tsx`) for the chrome row density and `ChatPreview` (`apps/web/src/components/sidebar/chat-preview.tsx`) for the tab strip cell density. Match those, don't invent new heights.
1196
1197---
1198
1199## 15. Visual designs (matching the provided mockups)
1200
1201Three states render inside `PreviewView.tsx` based on the `PreviewSession` for the active thread:
1202
1203| Session state | Component rendered |
1204|---|---|
1205| no tab open | `<PreviewEmptyState />` — disabled chrome row + "Local" recommendations |
1206| tab loading / loaded | `<PreviewWebview />` + chrome row + tab strip |
1207| tab navigation failed | `<PreviewUnreachable />` overlay above the webview |
1208
1209### 15.1 Empty state (`PreviewEmptyState.tsx`) — matches screenshot 1
1210
1211The chrome row shows but **everything is disabled**: back, forward, refresh, and the URL field. Below, a "Local" section lists discovered listening localhost ports as cards. Clicking a card calls `EnvironmentApi.preview.open({ threadId, url })`.
1212
1213Layout reference (annotated):
1214
1215```
1216┌────────────────────────────────────────────────────────────────┐
1217│ ← → ↻ [ 🌐 (disabled URL field) ] │ ← chrome row, all controls disabled
1218├────────────────────────────────────────────────────────────────┤
1219│ │
1220│ │
1221│ │
1222│ Local │ ← text-muted-foreground text-sm
1223│ ┌──────────────────────────────────────────────────────────┐ │
1224│ │ ┌──────┐ │ │
1225│ │ │ ●●● │ localhost:5175 ● │ │ ← Card row
1226│ │ │ ── │ localhost:5175 │ │ title text-sm font-medium text-foreground
1227│ │ │ ── │ │ │ subtitle text-xs text-muted-foreground
1228│ │ └──────┘ │ │ green dot bg-success animate-pulse
1229│ └──────────────────────────────────────────────────────────┘ │
1230│ ┌──────────────────────────────────────────────────────────┐ │
1231│ │ ┌──────┐ │ │
1232│ │ │ ●●● │ localhost:3000 ● │ │
1233│ │ │ ── │ next-server │ │
1234│ │ └──────┘ │ │
1235│ └──────────────────────────────────────────────────────────┘ │
1236│ │
1237│ │
1238└────────────────────────────────────────────────────────────────┘
1239```
1240
1241Skeleton (paste-able):
1242
1243```tsx
1244// apps/web/src/components/preview/PreviewEmptyState.tsx
1245"use client";
1246
1247import { useDiscoveredLocalServers } from "~/components/preview/useDiscoveredLocalServers";
1248import { Card } from "~/components/ui/card";
1249import { Empty, EmptyDescription, EmptyTitle } from "~/components/ui/empty";
1250
1251interface Props {
1252 threadId: string;
1253 onOpen: (url: string) => void;
1254}
1255
1256export function PreviewEmptyState({ threadId, onOpen }: Props) {
1257 const servers = useDiscoveredLocalServers(threadId);
1258
1259 return (
1260 <div className="flex h-full min-h-0 flex-col">
1261 <div className="px-6 pt-10 pb-4">
1262 <h2 className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
1263 Local
1264 </h2>
1265 </div>
1266
1267 {servers.length === 0 ? (
1268 <Empty>
1269 <EmptyTitle>No preview yet</EmptyTitle>
1270 <EmptyDescription>
1271 Run a dev script or paste a URL above to start previewing.
1272 </EmptyDescription>
1273 </Empty>
1274 ) : (
1275 <div className="flex flex-col gap-2 px-4 pb-6">
1276 {servers.map((s) => (
1277 <LocalServerCard key={s.url} server={s} onClick={() => onOpen(s.url)} />
1278 ))}
1279 </div>
1280 )}
1281 </div>
1282 );
1283}
1284
1285function LocalServerCard({
1286 server,
1287 onClick,
1288}: {
1289 server: { url: string; host: string; port: number; processName: string | null };
1290 onClick: () => void;
1291}) {
1292 return (
1293 <Card
1294 render={
1295 <button
1296 type="button"
1297 onClick={onClick}
1298 className="group flex items-center gap-3 px-3 py-3 text-left transition-colors hover:bg-accent/50"
1299 />
1300 }
1301 >
1302 <BrowserMockup className="size-10 shrink-0" />
1303 <div className="flex min-w-0 flex-1 flex-col">
1304 <span className="truncate text-sm font-medium text-foreground">
1305 {server.host}:{server.port}
1306 </span>
1307 <span className="truncate text-xs text-muted-foreground">
1308 {server.processName ?? `${server.host}:${server.port}`}
1309 </span>
1310 </div>
1311 <span
1312 className="size-2 shrink-0 rounded-full bg-success"
1313 aria-label="Listening"
1314 >
1315 <span className="block size-2 rounded-full bg-success animate-ping opacity-60" />
1316 </span>
1317 </Card>
1318 );
1319}
1320```
1321
1322The little browser thumbnail next to each card is a tiny tailwind-only mockup (matches the screenshot — three traffic-light dots over two muted lines):
1323
1324```tsx
1325// apps/web/src/components/preview/BrowserMockup.tsx
1326import { cn } from "~/lib/utils";
1327
1328export function BrowserMockup({ className }: { className?: string }) {
1329 return (
1330 <div
1331 className={cn(
1332 "relative flex flex-col gap-1 rounded-md border border-border bg-card p-1.5 shadow-xs/5",
1333 className,
1334 )}
1335 aria-hidden
1336 >
1337 <div className="flex gap-0.5">
1338 <span className="size-1 rounded-full bg-red-400/80" />
1339 <span className="size-1 rounded-full bg-amber-400/80" />
1340 <span className="size-1 rounded-full bg-emerald-400/80" />
1341 </div>
1342 <div className="flex flex-1 flex-col gap-0.5 pt-0.5">
1343 <span className="h-0.5 w-full rounded-full bg-muted-foreground/30" />
1344 <span className="h-0.5 w-3/5 rounded-full bg-muted-foreground/20" />
1345 </div>
1346 </div>
1347 );
1348}
1349```
1350
1351The chrome row in the empty state — note the `disabled` props on every interactive element:
1352
1353```tsx
1354// inside PreviewView.tsx, when previewState.snapshot == null
1355<PreviewChromeRow
1356 canGoBack={false}
1357 canGoForward={false}
1358 canRefresh={false}
1359 loading={false}
1360 url=""
1361 onBack={NOOP}
1362 onForward={NOOP}
1363 onRefresh={NOOP}
1364 onSubmitUrl={(url) => api.preview.open({ threadId, url })}
1365 inputDisabled={false} // we still want to allow paste-to-open
1366 buttonsDisabled // back/fwd/refresh are visually disabled
1367/>
1368```
1369
1370### 15.2 Tab strip (`PreviewTabStrip.tsx`) — matches screenshot 2
1371
1372The tab strip is one row above the chrome row. Each tab shows favicon + title + close button. Right-aligned at the end: a "Toggle as side panel" button and a "Toggle sidebar" button.
1373
1374```
1375┌────────────────────────────────────────────────────────────────┐
1376│ ┌─ 🌐 This site can't be re… ✕ ─┐ ⤡ □ │ ← tab strip
1377├────────────────────────────────────────────────────────────────┤
1378│ ← → ↻ [URL bar] │ ← chrome row
1379├────────────────────────────────────────────────────────────────┤
1380│ │
1381│ <webview content> │
1382│ │
1383└────────────────────────────────────────────────────────────────┘
1384```
1385
1386For v1 (single tab per thread) the strip renders exactly one tab. For v2 it grows. Either way we ship the strip now so the layout doesn't shift later.
1387
1388```tsx
1389// apps/web/src/components/preview/PreviewTabStrip.tsx
1390"use client";
1391
1392import { Globe, Plus, Sidebar, X } from "lucide-react";
1393import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
1394import { cn } from "~/lib/utils";
1395import { faviconUrlForOrigin } from "~/lib/favicon";
1396
1397interface PreviewTabDescriptor {
1398 tabId: string;
1399 title: string;
1400 url: string | null;
1401 active: boolean;
1402}
1403
1404interface Props {
1405 tabs: ReadonlyArray<PreviewTabDescriptor>;
1406 onActivate: (tabId: string) => void;
1407 onClose: (tabId: string) => void;
1408 onNewTab: () => void;
1409 onToggleSidePanel: () => void;
1410 onToggleSidebar: () => void;
1411}
1412
1413export function PreviewTabStrip({
1414 tabs,
1415 onActivate,
1416 onClose,
1417 onNewTab,
1418 onToggleSidePanel,
1419 onToggleSidebar,
1420}: Props) {
1421 return (
1422 <div className="flex h-9 items-center gap-1 border-b border-border bg-background px-2">
1423 <div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto">
1424 {tabs.map((tab) => (
1425 <PreviewTab
1426 key={tab.tabId}
1427 tab={tab}
1428 onActivate={() => onActivate(tab.tabId)}
1429 onClose={() => onClose(tab.tabId)}
1430 />
1431 ))}
1432 <button
1433 type="button"
1434 aria-label="New preview tab"
1435 onClick={onNewTab}
1436 className="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
1437 >
1438 <Plus className="size-3.5" />
1439 </button>
1440 </div>
1441
1442 <div className="flex shrink-0 items-center gap-0.5">
1443 <Tooltip>
1444 <TooltipTrigger
1445 render={
1446 <button
1447 type="button"
1448 aria-label="Open in browser"
1449 onClick={onToggleSidePanel}
1450 className="flex size-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
1451 />
1452 }
1453 >
1454 <ExternalLink className="size-3.5" />
1455 </TooltipTrigger>
1456 <TooltipPopup>Open in system browser</TooltipPopup>
1457 </Tooltip>
1458 <Tooltip>
1459 <TooltipTrigger
1460 render={
1461 <button
1462 type="button"
1463 aria-label="Close panel"
1464 onClick={onToggleSidebar}
1465 className="flex size-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
1466 />
1467 }
1468 >
1469 <Sidebar className="size-3.5" />
1470 </TooltipTrigger>
1471 <TooltipPopup>Close preview</TooltipPopup>
1472 </Tooltip>
1473 </div>
1474 </div>
1475 );
1476}
1477
1478function PreviewTab({
1479 tab,
1480 onActivate,
1481 onClose,
1482}: {
1483 tab: PreviewTabDescriptor;
1484 onActivate: () => void;
1485 onClose: () => void;
1486}) {
1487 return (
1488 <div
1489 className={cn(
1490 "group flex h-7 max-w-48 min-w-0 shrink-0 items-center gap-1.5 rounded-md border px-2 text-xs transition-colors",
1491 tab.active
1492 ? "border-input bg-card text-foreground"
1493 : "border-transparent text-muted-foreground hover:bg-accent/50 hover:text-foreground",
1494 )}
1495 >
1496 <button
1497 type="button"
1498 onClick={onActivate}
1499 className="flex min-w-0 flex-1 items-center gap-1.5 text-left"
1500 >
1501 <TabFavicon url={tab.url} />
1502 <span className="truncate">{tab.title}</span>
1503 </button>
1504 <button
1505 type="button"
1506 aria-label="Close tab"
1507 onClick={(e) => {
1508 e.stopPropagation();
1509 onClose();
1510 }}
1511 className="flex size-3.5 shrink-0 items-center justify-center rounded text-muted-foreground opacity-0 transition hover:bg-accent hover:text-foreground group-hover:opacity-100"
1512 >
1513 <X className="size-2.5" />
1514 </button>
1515 </div>
1516 );
1517}
1518```
1519
1520### 15.3 Favicon helper (`apps/web/src/lib/favicon.ts`)
1521
1522Borrowed from ami's `custom-tab.tsx:57` — Google's s2 favicon endpoint with `onError` fallback to a `<Globe />` icon. Lives in its own module so we can swap providers later (e.g. self-hosted favicon proxy).
1523
1524```ts
1525// apps/web/src/lib/favicon.ts
1526const FAVICON_PROVIDER = "https://www.google.com/s2/favicons";
1527
1528export function faviconUrlForOrigin(rawUrl: string | null, size = 32): string | null {
1529 if (!rawUrl) return null;
1530 try {
1531 const url = new URL(rawUrl);
1532 if (!url.host) return null;
1533 return `${FAVICON_PROVIDER}?domain=${encodeURIComponent(url.host)}&sz=${size}`;
1534 } catch {
1535 return null;
1536 }
1537}
1538```
1539
1540```tsx
1541// apps/web/src/components/preview/TabFavicon.tsx
1542import { Globe } from "lucide-react";
1543import { useEffect, useState } from "react";
1544import { faviconUrlForOrigin } from "~/lib/favicon";
1545
1546export function TabFavicon({ url }: { url: string | null }) {
1547 const src = faviconUrlForOrigin(url, 32);
1548 const [errored, setErrored] = useState(false);
1549 useEffect(() => setErrored(false), [src]);
1550
1551 if (!src || errored) {
1552 return <Globe className="size-3.5 shrink-0 text-muted-foreground" />;
1553 }
1554 return (
1555 <img
1556 src={src}
1557 alt=""
1558 className="size-3.5 shrink-0 rounded-sm"
1559 onError={() => setErrored(true)}
1560 />
1561 );
1562}
1563```
1564
1565### 15.4 Unreachable / error state (`PreviewUnreachable.tsx`) — port of 404.html
1566
1567When `navStatus._tag === "LoadFailed"`, render this instead of the failed `<webview>`. The original Chromium `404.html` (`asdasddddd.com` example) uses `--google-gray-*` vars; we map every Google gray to the closest theme variable so it auto-themes.
1568
1569Color mapping (Chromium → t3code theme):
1570
1571| Chromium | Tailwind/t3code |
1572|---|---|
1573| `--background-color: #fff` / `--google-gray-900` (dark) | `bg-background` |
1574| `--text-color: --google-gray-700` / `--google-gray-500` (dark) | `text-muted-foreground` |
1575| `--heading-color: --google-gray-900` / `--google-gray-500` (dark) | `text-foreground` |
1576| `--error-code-color: --google-gray-700` / `--google-gray-500` (dark) | `text-muted-foreground/70` |
1577| `--quiet-background-color: rgb(247,247,247)` / `--background` (dark) | `bg-muted/40` |
1578| `--primary-button-fill-color: --google-blue-600` / `--google-blue-300` (dark) | `bg-primary text-primary-foreground` (theme primary is `oklch(0.488 0.217 264)` light / `oklch(0.588 0.217 264)` dark — a very close blue) |
1579| `--secondary-button-*` | `Button variant="outline"` |
1580| `--link-color: rgb(88,88,88)` / `--google-blue-300` (dark) | `text-primary underline-offset-4 hover:underline` |
1581| Body font `system-ui, sans-serif; font-size: 75%` | We keep DM Sans (already in `apps/web/src/index.css:148`) and `text-sm` — the Chromium font tweak just compensates for their `html { font-size: 125% }` — we don't replicate that |
1582
1583Skeleton:
1584
1585```tsx
1586// apps/web/src/components/preview/PreviewUnreachable.tsx
1587"use client";
1588
1589import { useState } from "react";
1590import { Button } from "~/components/ui/button";
1591import { cn } from "~/lib/utils";
1592
1593interface Props {
1594 url: string;
1595 errorCode: string; // e.g. "ERR_NAME_NOT_RESOLVED", "ERR_CONNECTION_REFUSED"
1596 description: string;
1597 onReload: () => void;
1598}
1599
1600const ICON_GENERIC = (
1601 // Replace with an inline SVG that matches the Chromium "icon-generic"
1602 // (a stylized broken page). For v1 a Lucide MapPinOff is a fine stand-in —
1603 // we want a visual that reads "destination unreachable".
1604 <svg viewBox="0 0 64 64" className="size-10 text-muted-foreground/70" fill="none" stroke="currentColor" strokeWidth="2.5">
1605 <path d="M16 12 L48 12 L48 52 L16 52 Z" />
1606 <path d="M22 22 L42 22 M22 30 L36 30 M22 38 L40 38" strokeLinecap="round" />
1607 <path d="M52 8 L12 56" strokeLinecap="round" />
1608 </svg>
1609);
1610
1611export function PreviewUnreachable({ url, errorCode, description, onReload }: Props) {
1612 const [showDetails, setShowDetails] = useState(false);
1613 const host = safeHost(url) ?? url;
1614
1615 return (
1616 <div className="flex h-full min-h-0 w-full flex-col bg-background">
1617 <div className="mx-auto flex w-full max-w-xl flex-1 flex-col px-6 py-12">
1618 {/* Icon + headline */}
1619 <div className="mb-6 flex flex-col gap-4">
1620 {ICON_GENERIC}
1621 <h1 className="text-2xl font-semibold leading-tight text-foreground">
1622 This site can&rsquo;t be reached
1623 </h1>
1624 </div>
1625
1626 {/* Summary — uses dangerouslySetInnerHTML only for the bold host treatment */}
1627 <p className="text-sm leading-relaxed text-muted-foreground">
1628 <strong className="font-semibold text-foreground">{host}</strong>
1629 &rsquo;s server <abbr title="Domain Name System" className="cursor-help underline decoration-dotted underline-offset-2">DNS address</abbr>{" "}
1630 could not be found.
1631 </p>
1632
1633 {/* Suggestions list (when details open) */}
1634 {showDetails && (
1635 <div className="mt-6 rounded-lg border border-border bg-muted/40 p-4 text-sm">
1636 <p className="mb-2 font-medium text-foreground">Try:</p>
1637 <ul className="list-disc space-y-1 pl-5 text-muted-foreground">
1638 <li>Checking the connection</li>
1639 <li>Checking the proxy and the firewall</li>
1640 <li>Running Network Diagnostics</li>
1641 </ul>
1642 </div>
1643 )}
1644
1645 {/* Error code */}
1646 <div className="mt-8 text-xs uppercase tracking-wide text-muted-foreground/70">
1647 {errorCode}
1648 </div>
1649
1650 {/* Actions */}
1651 <div className="mt-auto flex items-center gap-2 pt-8">
1652 <Button
1653 type="button"
1654 variant="outline"
1655 size="sm"
1656 onClick={() => setShowDetails((v) => !v)}
1657 >
1658 {showDetails ? "Hide details" : "Details"}
1659 </Button>
1660 <div className="flex-1" />
1661 <Button type="button" size="sm" onClick={onReload}>
1662 Reload
1663 </Button>
1664 </div>
1665 </div>
1666 </div>
1667 );
1668}
1669
1670function safeHost(url: string): string | null {
1671 try { return new URL(url).host; } catch { return null; }
1672}
1673```
1674
1675The component intentionally:
1676- Uses theme tokens only — looks correct in light + dark with no extra wiring.
1677- Drops Chromium's "Diagnose connection" / "Portal sign-in" buttons (irrelevant in our shell).
1678- Drops the dinosaur game (RIP).
1679- Maps `errorCode` → human description in `PreviewView.tsx` via a small lookup (`ERR_CONNECTION_REFUSED` → "Connection refused", etc.) before rendering.
1680
1681### 15.5 Loading bar
1682
1683A 1.5px primary-colored bar that fills as the page loads, anchored to the bottom of the chrome row. Direct port of ami's pattern (`browser-view.tsx:434`):
1684
1685```tsx
1686{loadProgress > 0 && (
1687 <div
1688 className="pointer-events-none absolute bottom-0 left-0 z-10 h-0.5 rounded-full bg-primary transition-all duration-150 ease-out"
1689 style={{
1690 width: `${loadProgress}%`,
1691 boxShadow: "0 0 6px 1px var(--ring)",
1692 }}
1693 />
1694)}
1695```
1696
1697Progress is computed locally with `useLoadingProgress(isLoading)` — same hook signature as ami's, ~30 lines.
1698
1699---
1700
1701## 16. Local server discovery (port scanner)
1702
1703The "Local" recommendations in §15.1 need a feed. New backend service.
1704
1705### `apps/server/src/preview/Services/PortScanner.ts`
1706
1707```ts
1708import { Context, Effect } from "effect";
1709
1710export interface DiscoveredLocalServer {
1711 host: string; // "localhost"
1712 port: number; // 5175
1713 url: string; // "http://localhost:5175"
1714 processName: string | null; // "node", "vite", "next-server"
1715 pid: number | null;
1716}
1717
1718export interface PreviewPortScannerShape {
1719 /** One-shot snapshot of currently listening localhost ports. */
1720 readonly scan: () => Effect.Effect<ReadonlyArray<DiscoveredLocalServer>>;
1721 /** Subscribe to changes. Listener is called on every diff. */
1722 readonly subscribe: (
1723 listener: (servers: ReadonlyArray<DiscoveredLocalServer>) => Effect.Effect<void>,
1724 ) => Effect.Effect<() => void>;
1725 /** Hint that at least one client is interested → starts polling. Returns release fn. */
1726 readonly retain: () => Effect.Effect<() => void>;
1727}
1728
1729export class PreviewPortScanner extends Context.Service<
1730 PreviewPortScanner,
1731 PreviewPortScannerShape
1732>()("t3/preview/Services/PortScanner") {}
1733```
1734
1735### `apps/server/src/preview/Layers/PortScanner.ts`
1736
1737Two strategies, picked at startup:
1738
17391. **Preferred — `lsof`** on macOS/Linux: `lsof -iTCP -sTCP:LISTEN -P -n -F pcn` returns `pid`, `command`, `name` (host:port). Fast (<50ms typical), gives process name for free. Parsed via the `-F` field-format which is stable across versions.
17402. **Fallback — TCP connect probe**: iterate a curated list of common dev ports `[3000, 3001, 3333, 4173, 4200, 4321, 5000, 5173, 5174, 5175, 5500, 8000, 8080, 8081, 8888, 9000]` against `127.0.0.1`, mark any that accepts a connection. Used on Windows and as the safety net if `lsof` is missing.
1741
1742Polling cadence:
1743- When `retain()` count is 0 → not polling.
1744- When ≥1 → poll every 3s. Diff against last result; only emit `subscribe` callbacks when the set differs.
1745- `retain()` returns a release fn that decrements the counter; goes idle automatically when the empty state is hidden.
1746
1747The renderer side (`useDiscoveredLocalServers(threadId)`) calls `EnvironmentApi.preview.subscribePorts(callback)` and the WS handler calls `retain()` on first subscribe / releases on the last unsubscribe.
1748
1749### Augmenting the discovery feed
1750
1751The card list in §15.1 is the union of:
1752
17531. **Listening ports** from the scanner (above).
17542. **Recently seen URLs** — `apps/web/src/previewStateStore.ts` already has a `recentEvents` ring buffer; we additionally maintain a small per-thread `recentUrlsFromTerminal: string[]` populated from the existing `terminal-links` extraction (already wired in `ThreadTerminalDrawer.tsx:454`).
17553. **Configured URLs** — every active `ProjectScript.previewUrl` from the active project.
1756
1757Deduped by URL string. Sort: configured > listening > recent. This yields the "smart, contextual, no-thought-required" feel the screenshot implies.
1758
1759---
1760
1761## 17. File map (additions to §3)
1762
1763Append these to the file list:
1764
1765| File | Purpose |
1766|---|---|
1767| `apps/server/src/preview/Services/PortScanner.ts` | Service tag + interface |
1768| `apps/server/src/preview/Layers/PortScanner.ts` | `lsof` strategy + TCP probe fallback + polling |
1769| `apps/server/src/preview/Layers/PortScanner.test.ts` | Parser tests for `lsof -F pcn` output |
1770| `apps/web/src/lib/favicon.ts` | `faviconUrlForOrigin(url, size)` helper |
1771| `apps/web/src/components/preview/PreviewTabStrip.tsx` | Tab strip (screenshot 2) |
1772| `apps/web/src/components/preview/TabFavicon.tsx` | Favicon `<img>` w/ `<Globe>` fallback |
1773| `apps/web/src/components/preview/BrowserMockup.tsx` | Tiny tailwind browser thumbnail icon |
1774| `apps/web/src/components/preview/PreviewLocalServerCard.tsx` | Card row for a discovered server |
1775| `apps/web/src/components/preview/PreviewUnreachable.tsx` | 404.html rewritten in tailwind |
1776| `apps/web/src/components/preview/useDiscoveredLocalServers.ts` | Hook subscribing to `EnvironmentApi.preview.subscribePorts` + merging in `recentUrlsFromTerminal` and `ProjectScript.previewUrl` |
1777| `apps/web/src/components/preview/useLoadingProgress.ts` | 30-line progress simulator (port of ami's) |
1778| `apps/web/src/components/preview/errorCodeMessages.ts` | `ERR_*` → human-readable description map |
1779
1780Update the contract additions in §4 to add:
1781
1782```ts
1783// packages/contracts/src/preview.ts
1784export const DiscoveredLocalServer = Schema.Struct({
1785 host: TrimmedNonEmptyString,
1786 port: Schema.Int.check(Schema.isGreaterThan(0)).check(Schema.isLessThan(65536)),
1787 url: TrimmedNonEmptyString,
1788 processName: Schema.NullOr(TrimmedNonEmptyString),
1789 pid: Schema.NullOr(Schema.Int.check(Schema.isGreaterThan(0))),
1790});
1791export type DiscoveredLocalServer = typeof DiscoveredLocalServer.Type;
1792```
1793
1794WS routes added (§5):
1795
1796```
1797preview.subscribePorts(callback) → unsubscribe
1798preview.scanPortsOnce() → ReadonlyArray<DiscoveredLocalServer>
1799```
1800
1801---
1802
1803## 18. Implementation order (single‑shot landing) — REVISED
1804
1805To minimize cross‑file thrash while writing:
1806
18071. `packages/contracts/src/preview.ts` (+ test) — `PreviewSession`, `PreviewEvent`, `DiscoveredLocalServer` schemas; `keybindings.ts` / `project.ts` schema edits → **build contracts first**.
18082. `apps/server/src/preview/{Services,Layers}/Manager.ts` (+ test) and `{Services,Layers}/PortScanner.ts` (+ test).
18093. `apps/server/src/ws.ts` and `runtimeLayer` provision (`PreviewManager.Default`, `PreviewPortScanner.Default`).
18104. `apps/desktop/src/preview-view-manager.ts`, `preview-preload.ts`, `main.ts` IPC handlers, `preload.ts` `desktopBridge.preview` namespace; update `apps/desktop/tsdown.config.ts` to also bundle `preview-preload.ts` → `preview-preload.cjs`.
18115. `apps/web/src/lib/favicon.ts`; `apps/web/src/previewStateStore.ts`, `apps/web/src/rightPanelStore.ts` (+ tests).
18126. `apps/web/src/environmentApi.ts` — add `preview` slot.
18137. Right‑panel arbiter migration in `ChatView.tsx` (replace `planSidebarOpen`, render preview alongside diff/plan).
18148. Components, in dependency order:
1815 - `BrowserMockup.tsx`, `TabFavicon.tsx`, `useLoadingProgress.ts`, `errorCodeMessages.ts`, `useDiscoveredLocalServers.ts`
1816 - `PreviewLocalServerCard.tsx`, `PreviewEmptyState.tsx`
1817 - `PreviewUnreachable.tsx`
1818 - `PreviewTabStrip.tsx`
1819 - `PreviewWebview.tsx` (the `<webview>` host, no-op on web)
1820 - `PreviewView.tsx` (ties it all together: tab strip + chrome row + body switching between empty / webview / unreachable)
1821 - `PreviewPanelShell.tsx`, `PreviewPanel.tsx`
18229. Keybinding wiring: `_chat.tsx` `preview.toggle`/`preview.refresh`/`preview.focusUrl`; `keybindings.ts` shortcut helpers.
182310. `ProjectScriptsControl.tsx` form additions (`previewUrl`, `autoOpenPreview`).
182411. `ThreadTerminalDrawer.tsx` link‑activation update — context menu "Open in preview" / "Open in browser" for previewable URLs.
182512. `KEYBINDINGS.md` doc update — add `preview.*` commands and `previewFocus` / `previewOpen` `when` keys.
182613. Run `bun fmt && bun lint && bun typecheck && bun run test` — must all pass per `AGENTS.md`.
182714. Manual smoke against the dev desktop (`bun run dev:desktop`).
1828
1829Estimated diff size: ~3,500–4,500 lines added (mostly net‑new files), ~200 lines modified across `ChatView.tsx`, `ThreadTerminalDrawer.tsx`, `ProjectScriptsControl.tsx`, `_chat.tsx`, `main.ts`, `preload.ts`, `tsdown.config.ts`, `keybindings.ts`, `ws.ts`, `KEYBINDINGS.md`.