authorgravatar for aiden.bai05@gmail.comAiden Bai <aiden.bai05@gmail.com> 2026-05-03 05:22:47-07:00
committergravatar for me@theo.liTheo Browne <me@theo.li> 2026-06-13 22:18:37-07:00
log43e8bbd7aea2f73a0ea7b7a15af2903a471d5e4a
tree4d005c6a42e5c1d86eae637bb9cf566ce3031a71
parent52c77c1ec4c1c2c2c27b917baaec91d050d0a737

fix


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

404.html deleted-1600
...@@ -1,1600 +0,0 @@
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,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) 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 deleted-1853
...@@ -1,1853 +0,0 @@
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([PreviewSessionLookupError, PreviewInvalidUrlError]);
339export type PreviewError = typeof PreviewError.Type;
340```
341
342### `ProjectScript` extension (`packages/contracts/src/project.ts`)
343
344Add to the existing `ProjectScript` struct (additive, default both fields to undefined/false at runtime so existing persisted scripts decode unchanged):
345
346```ts
347previewUrl: Schema.optional(TrimmedNonEmptyString),
348autoOpenPreview: Schema.optional(Schema.Boolean),
349```
350
351### Keybindings (`packages/contracts/src/keybindings.ts:50`)
352
353```ts
354const STATIC_KEYBINDING_COMMANDS = [
355 "terminal.toggle",
356 "terminal.split",
357 "terminal.new",
358 "terminal.close",
359 "diff.toggle",
360 "preview.toggle", // NEW
361 "preview.refresh", // NEW
362 "preview.focusUrl", // NEW
363 "commandPalette.toggle",
364 "chat.new",
365 "chat.newLocal",
366 "editor.openFavorite",
367] as const;
368```
369
370Add `previewFocus` and `previewOpen` to the `ShortcutMatchContext` union (`apps/web/src/keybindings.ts:30`).
371
372### Defaults (`apps/server/src/keybindings.ts`)
373
374```ts
375{ key: "mod+shift+j", command: "preview.toggle" },
376{ key: "mod+r", command: "preview.refresh", when: "previewFocus" },
377{ key: "mod+l", command: "preview.focusUrl", when: "previewFocus" },
378```
379
380---
381
382## 5. Server: PreviewManager
383
384### `apps/server/src/preview/Services/Manager.ts`
385
386```ts
387import { Context, Effect } from "effect";
388import {
389 PreviewCloseInput,
390 PreviewError,
391 PreviewEvent,
392 PreviewNavigateInput,
393 PreviewOpenInput,
394 PreviewRefreshInput,
395 PreviewSessionSnapshot,
396} from "@t3tools/contracts";
397
398export interface PreviewManagerShape {
399 readonly open: (input: PreviewOpenInput) => Effect.Effect<PreviewSessionSnapshot, PreviewError>;
400 readonly navigate: (
401 input: PreviewNavigateInput,
402 ) => Effect.Effect<PreviewSessionSnapshot, PreviewError>;
403 readonly refresh: (input: PreviewRefreshInput) => Effect.Effect<void, PreviewError>;
404 readonly close: (input: PreviewCloseInput) => Effect.Effect<void, PreviewError>;
405 readonly list: (threadId: string) => Effect.Effect<ReadonlyArray<PreviewSessionSnapshot>>;
406 readonly subscribe: (
407 listener: (event: PreviewEvent) => Effect.Effect<void>,
408 ) => Effect.Effect<() => void>;
409}
410
411export class PreviewManager extends Context.Service<PreviewManager, PreviewManagerShape>()(
412 "t3/preview/Services/Manager/PreviewManager",
413) {}
414```
415
416### `apps/server/src/preview/Layers/Manager.ts`
417
418In‑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()`.
419
420URL normalization mirrors ami's helper (`browser-view-manager.ts:655`):
421
422```ts
423const normalizeUrl = (input: string) =>
424 Effect.try(() => {
425 const trimmed = input.trim();
426 if (!trimmed) throw new Error("empty");
427 // localhost stays http unless explicitly https
428 const useHttp = /^(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|\/|$)/.test(trimmed);
429 const parsed = urlParseLax(trimmed, { https: !useHttp });
430 if (!parsed?.href) throw new Error("unparseable");
431 return parsed.href;
432 }).pipe(
433 Effect.catchAll((cause) => Effect.fail(new PreviewInvalidUrlError({ rawUrl: input, cause }))),
434 );
435```
436
437### `apps/server/src/ws.ts` routes
438
439Following the existing terminal route pattern, expose:
440
441```ts
442preview: {
443 open: (input) => Effect.runPromise(previewManager.open(input)),
444 navigate: (input) => Effect.runPromise(previewManager.navigate(input)),
445 refresh: (input) => Effect.runPromise(previewManager.refresh(input)),
446 close: (input) => Effect.runPromise(previewManager.close(input)),
447 list: (threadId) => Effect.runPromise(previewManager.list(threadId)),
448 onEvent: (callback) => /* subscribe + return unsubscribe */,
449}
450```
451
452---
453
454## 6. Desktop: `PreviewViewManager`
455
456### Style
457
458`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`.
459
460### `apps/desktop/src/preview-view-manager.ts`
461
462Subset of ami's `BrowserManager`:
463
464```ts
465import * as path from "node:path";
466import { type BrowserWindow, type Session, session, webContents } from "electron";
467
468const PREVIEW_PARTITION = "persist:t3code-preview";
469
470export type NavStatus =
471 | { kind: "Idle" }
472 | { kind: "Loading"; url: string; title: string }
473 | { kind: "Success"; url: string; title: string }
474 | { kind: "LoadFailed"; url: string; title: string; code: number; description: string };
475
476export interface TabState {
477 tabId: string;
478 webContentsId: number | null;
479 navStatus: NavStatus;
480 canGoBack: boolean;
481 canGoForward: boolean;
482 visible: boolean;
483 updatedAt: string;
484}
485
486type Listener = (tabId: string, state: TabState) => void;
487
488export class PreviewViewManager {
489 private mainWindow: BrowserWindow | null = null;
490 private readonly tabs = new Map<string, TabState>();
491 private browserSession: Session | null = null;
492 private readonly listeners = new Set<Listener>();
493 private readonly preloadPath: string;
494
495 constructor() {
496 this.preloadPath = path.join(__dirname, "preview-preload.cjs");
497 }
498
499 setMainWindow(window: BrowserWindow): void {
500 this.mainWindow = window;
501 }
502
503 getPreloadPath(): string {
504 return this.preloadPath;
505 }
506 getBrowserPartition(): string {
507 return PREVIEW_PARTITION;
508 }
509
510 getBrowserSession(): Session {
511 if (this.browserSession) return this.browserSession;
512 const sess = session.fromPartition(PREVIEW_PARTITION);
513 // strip electron/t3code from UA so dev preview doesn't trip bot detection
514 const ua = sess
515 .getUserAgent()
516 .replace(/Electron\/[\d.]+ /, "")
517 .replace(/\s*t3code\/[\d.]+/, "");
518 sess.setUserAgent(ua);
519 sess.setPermissionRequestHandler((_wc, perm, callback) => {
520 const allow = ["clipboard-read", "clipboard-write", "notifications", "geolocation"];
521 callback(allow.includes(perm));
522 });
523 this.browserSession = sess;
524 return sess;
525 }
526
527 createTab(tabId: string): TabState {
528 if (this.tabs.has(tabId)) return this.tabs.get(tabId)!;
529 const initial: TabState = {
530 tabId,
531 webContentsId: null,
532 navStatus: { kind: "Idle" },
533 canGoBack: false,
534 canGoForward: false,
535 visible: true,
536 updatedAt: new Date().toISOString(),
537 };
538 this.tabs.set(tabId, initial);
539 this.emit(tabId, initial);
540 return initial;
541 }
542
543 closeTab(tabId: string): void {
544 if (!this.tabs.delete(tabId)) return;
545 this.emit(tabId, { ...this.tabs.get(tabId)!, navStatus: { kind: "Idle" } });
546 }
547
548 setVisibility(tabId: string, visible: boolean): void {
549 const tab = this.tabs.get(tabId);
550 if (!tab) return;
551 if (tab.visible === visible) return;
552 this.update(tabId, { visible });
553 }
554
555 registerWebview(tabId: string, webContentsId: number): void {
556 const tab = this.tabs.get(tabId);
557 if (!tab) throw new PreviewTabNotFoundError(tabId);
558 const wc = webContents.fromId(webContentsId);
559 if (!wc) throw new PreviewWebContentsNotFoundError(tabId, webContentsId);
560
561 this.attachListeners(tabId, wc);
562 this.update(tabId, {
563 webContentsId,
564 navStatus: this.computeNavStatus(wc),
565 canGoBack: wc.navigationHistory.canGoBack(),
566 canGoForward: wc.navigationHistory.canGoForward(),
567 });
568 }
569
570 unregisterWebview(tabId: string): void {
571 const tab = this.tabs.get(tabId);
572 if (!tab) return;
573 this.update(tabId, {
574 webContentsId: null,
575 navStatus: { kind: "Idle" },
576 canGoBack: false,
577 canGoForward: false,
578 });
579 }
580
581 async navigate(tabId: string, rawUrl: string): Promise<void> {
582 const wc = this.requireWebContents(tabId);
583 const url = this.normalizeUrl(rawUrl);
584 if (wc.getURL() === url) return;
585 await wc.loadURL(url);
586 }
587
588 goBack(tabId: string): void {
589 this.requireWebContents(tabId).navigationHistory.goBack();
590 }
591 goForward(tabId: string): void {
592 this.requireWebContents(tabId).navigationHistory.goForward();
593 }
594 refresh(tabId: string): void {
595 this.requireWebContents(tabId).reload();
596 }
597
598 onStateChange(listener: Listener): () => void {
599 this.listeners.add(listener);
600 return () => this.listeners.delete(listener);
601 }
602
603 private attachListeners(tabId: string, wc: Electron.WebContents): void {
604 const sync = () => {
605 this.update(tabId, {
606 navStatus: this.computeNavStatus(wc),
607 canGoBack: wc.navigationHistory.canGoBack(),
608 canGoForward: wc.navigationHistory.canGoForward(),
609 });
610 };
611 wc.on("did-navigate", sync);
612 wc.on("did-navigate-in-page", sync);
613 wc.on("page-title-updated", sync);
614 wc.on("did-start-loading", sync);
615 wc.on("did-stop-loading", sync);
616 wc.on("did-fail-load", (_event, code, description) => {
617 if (code === -3) return; // user aborted
618 this.update(tabId, {
619 navStatus: {
620 kind: "LoadFailed",
621 url: wc.getURL(),
622 title: wc.getTitle(),
623 code,
624 description,
625 },
626 });
627 });
628
629 // External link policy: load in same view (matches ami)
630 wc.setWindowOpenHandler(({ url }) => {
631 void wc.loadURL(url);
632 return { action: "deny" };
633 });
634
635 // Forward app shortcuts to the main window so mod+shift+j etc still work
636 wc.on("before-input-event", (event, input) => {
637 if (this.isAppShortcut(input) && this.mainWindow && !this.mainWindow.isDestroyed()) {
638 event.preventDefault();
639 this.mainWindow.webContents.sendInputEvent({
640 type: "keyDown",
641 keyCode: input.key,
642 modifiers: [
643 ...(input.meta ? ["meta" as const] : []),
644 ...(input.shift ? ["shift" as const] : []),
645 ...(input.control ? ["control" as const] : []),
646 ...(input.alt ? ["alt" as const] : []),
647 ],
648 });
649 }
650 });
651 }
652
653 private isAppShortcut(input: Electron.Input): boolean {
654 if (input.type !== "keyDown") return false;
655 // Mirror the t3code keybinding defaults that should always reach the main window.
656 const SHORTCUTS = [
657 { key: "j", meta: true, shift: true }, // preview.toggle
658 { key: "k", meta: true, shift: false }, // commandPalette.toggle
659 { key: ",", meta: true, shift: false }, // settings
660 { key: "w", meta: true, shift: false }, // close
661 // future: terminal.* if user wants them while preview focused
662 ];
663 return SHORTCUTS.some(
664 (s) =>
665 s.key.toLowerCase() === input.key.toLowerCase() &&
666 s.meta === input.meta &&
667 s.shift === input.shift,
668 );
669 }
670
671 private computeNavStatus(wc: Electron.WebContents): NavStatus {
672 const url = wc.getURL();
673 const title = wc.getTitle();
674 if (url === "" || url === "about:blank") return { kind: "Idle" };
675 if (wc.isLoading()) return { kind: "Loading", url, title };
676 return { kind: "Success", url, title };
677 }
678
679 private requireWebContents(tabId: string): Electron.WebContents {
680 const tab = this.tabs.get(tabId);
681 if (!tab) throw new PreviewTabNotFoundError(tabId);
682 if (tab.webContentsId == null) throw new PreviewWebviewNotInitializedError(tabId);
683 const wc = webContents.fromId(tab.webContentsId);
684 if (!wc) throw new PreviewWebContentsNotFoundError(tabId, tab.webContentsId);
685 return wc;
686 }
687
688 private update(tabId: string, patch: Partial<TabState>): void {
689 const current = this.tabs.get(tabId);
690 if (!current) return;
691 const next: TabState = { ...current, ...patch, updatedAt: new Date().toISOString() };
692 this.tabs.set(tabId, next);
693 this.emit(tabId, next);
694 }
695
696 private emit(tabId: string, state: TabState): void {
697 for (const listener of this.listeners) listener(tabId, state);
698 }
699
700 private normalizeUrl(input: string): string {
701 // Same heuristics as server-side normalization.
702 // Returns "https://..." or throws.
703 const trimmed = input.trim();
704 if (!trimmed) throw new PreviewInvalidUrlError(input);
705 const useHttp = /^(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|\/|$)/.test(trimmed);
706 const parsed = new URL(
707 trimmed.includes("://") ? trimmed : `${useHttp ? "http" : "https"}://${trimmed}`,
708 );
709 return parsed.href;
710 }
711}
712
713export class PreviewTabNotFoundError extends Error {
714 constructor(public readonly tabId: string) {
715 super(`Preview tab not found: ${tabId}`);
716 }
717}
718export class PreviewWebContentsNotFoundError extends Error {
719 /* … */
720}
721export class PreviewWebviewNotInitializedError extends Error {
722 /* … */
723}
724export class PreviewInvalidUrlError extends Error {
725 /* … */
726}
727
728export const previewViewManager = new PreviewViewManager();
729```
730
731### `apps/desktop/src/main.ts` additions
732
733Right after `mainWindow = createWindow();` in `bootstrap()` and after every recreation:
734
735```ts
736previewViewManager.setMainWindow(mainWindow);
737```
738
739Register IPC handlers (in `registerIpcHandlers()`):
740
741```ts
742ipcMain.handle("preview:createTab", (_e, tabId: string) => previewViewManager.createTab(tabId));
743ipcMain.handle("preview:closeTab", (_e, tabId: string) => previewViewManager.closeTab(tabId));
744ipcMain.handle("preview:setVisibility", (_e, tabId: string, visible: boolean) =>
745 previewViewManager.setVisibility(tabId, visible),
746);
747ipcMain.handle("preview:registerWebview", (_e, tabId: string, wcId: number) =>
748 previewViewManager.registerWebview(tabId, wcId),
749);
750ipcMain.handle("preview:unregisterWebview", (_e, tabId: string) =>
751 previewViewManager.unregisterWebview(tabId),
752);
753ipcMain.handle("preview:navigate", (_e, tabId: string, url: string) =>
754 previewViewManager.navigate(tabId, url),
755);
756ipcMain.handle("preview:goBack", (_e, tabId: string) => previewViewManager.goBack(tabId));
757ipcMain.handle("preview:goForward", (_e, tabId: string) => previewViewManager.goForward(tabId));
758ipcMain.handle("preview:refresh", (_e, tabId: string) => previewViewManager.refresh(tabId));
759ipcMain.handle("preview:getPreloadPath", () => previewViewManager.getPreloadPath());
760ipcMain.handle("preview:getBrowserPartition", () => previewViewManager.getBrowserPartition());
761```
762
763State change broadcast (push to all renderer windows):
764
765```ts
766previewViewManager.onStateChange((tabId, state) => {
767 for (const win of BrowserWindow.getAllWindows()) {
768 if (win.isDestroyed()) continue;
769 win.webContents.send("preview:state-change", tabId, state);
770 }
771});
772```
773
774### `apps/desktop/src/preload.ts` additions
775
776```ts
777contextBridge.exposeInMainWorld("desktopBridge", {
778 // … existing fields …
779 preview: {
780 createTab: (tabId: string) => ipcRenderer.invoke("preview:createTab", tabId),
781 closeTab: (tabId: string) => ipcRenderer.invoke("preview:closeTab", tabId),
782 setVisibility: (tabId: string, visible: boolean) =>
783 ipcRenderer.invoke("preview:setVisibility", tabId, visible),
784 registerWebview: (tabId: string, wcId: number) =>
785 ipcRenderer.invoke("preview:registerWebview", tabId, wcId),
786 unregisterWebview: (tabId: string) => ipcRenderer.invoke("preview:unregisterWebview", tabId),
787 navigate: (tabId: string, url: string) => ipcRenderer.invoke("preview:navigate", tabId, url),
788 goBack: (tabId: string) => ipcRenderer.invoke("preview:goBack", tabId),
789 goForward: (tabId: string) => ipcRenderer.invoke("preview:goForward", tabId),
790 refresh: (tabId: string) => ipcRenderer.invoke("preview:refresh", tabId),
791 getPreloadPath: (): Promise<string> => ipcRenderer.invoke("preview:getPreloadPath"),
792 getBrowserPartition: (): Promise<string> => ipcRenderer.invoke("preview:getBrowserPartition"),
793 onStateChange: (cb: (tabId: string, state: DesktopPreviewTabState) => void) => {
794 const listener = (_e: unknown, tabId: string, state: DesktopPreviewTabState) =>
795 cb(tabId, state);
796 ipcRenderer.on("preview:state-change", listener);
797 return () => ipcRenderer.removeListener("preview:state-change", listener);
798 },
799 },
800});
801```
802
803### `apps/desktop/src/preview-preload.ts`
804
805```ts
806// Intentionally empty for v1.
807// Future: forward console.error to main, expose a tiny window.t3preview API.
808```
809
810Build config: add `preview-preload.ts` to `apps/desktop/tsdown.config.ts` outputs so it ships as `preview-preload.cjs` next to `preload.cjs`.
811
812---
813
814## 7. Web: state stores
815
816### `apps/web/src/previewStateStore.ts`
817
818Direct mirror of `terminalStateStore.ts` shape (one tab per thread for v1; structured to grow into multi‑tab the same way terminal grew into groups).
819
820```ts
821import { type ScopedThreadRef, scopedThreadKey } from "@t3tools/client-runtime";
822import { create } from "zustand";
823import { createJSONStorage, persist } from "zustand/middleware";
824import type { PreviewEvent, PreviewSessionSnapshot } from "@t3tools/contracts";
825import { resolveStorage } from "./lib/storage";
826
827interface ThreadPreviewState {
828 /** present if a preview tab exists for this thread */
829 snapshot: PreviewSessionSnapshot | null;
830 /** desktop-side immediate nav button state (overrides snapshot when fresher) */
831 desktopOverlay: {
832 canGoBack: boolean;
833 canGoForward: boolean;
834 visible: boolean;
835 } | null;
836 /** local UI: is the URL bar focused? */
837 urlBarFocused: boolean;
838 recentEventIds: number[];
839}
840
841interface PreviewEventEntry { id: number; event: PreviewEvent }
842
843interface PreviewStateStore {
844 byThreadKey: Record<string, ThreadPreviewState>;
845 recentEvents: Record<string, ReadonlyArray<PreviewEventEntry>>;
846 nextEventId: number;
847 applyServerEvent: (ref: ScopedThreadRef, event: PreviewEvent) => void;
848 applyDesktopState: (
849 ref: ScopedThreadRef,
850 overlay: ThreadPreviewState["desktopOverlay"],
851 ) => void;
852 setUrlBarFocused: (ref: ScopedThreadRef, focused: boolean) => void;
853 removeThread: (ref: ScopedThreadRef) => void;
854}
855
856const PERSISTED_FIELDS = ["byThreadKey"] as const;
857const STORAGE_KEY = "t3code:preview-state:v1";
858
859export const usePreviewStateStore = create<PreviewStateStore>()(
860 persist(/* … */, {
861 name: STORAGE_KEY,
862 storage: createJSONStorage(() =>
863 resolveStorage(typeof window !== "undefined" ? window.localStorage : undefined),
864 ),
865 version: 1,
866 partialize: (s) => Object.fromEntries(PERSISTED_FIELDS.map((k) => [k, s[k]])),
867 }),
868);
869
870export function selectThreadPreviewState(
871 byThreadKey: Record<string, ThreadPreviewState>,
872 ref: ScopedThreadRef | null,
873): ThreadPreviewState {
874 if (!ref) return EMPTY_THREAD_STATE;
875 return byThreadKey[scopedThreadKey(ref)] ?? EMPTY_THREAD_STATE;
876}
877```
878
879Key invariants (test in `previewStateStore.test.ts`):
880
881- `applyServerEvent("opened" | "navigated" | "failed")` updates `snapshot`; pushes into `recentEvents` ring buffer (cap 50).
882- `applyServerEvent("closed")` removes the thread entry entirely.
883- `applyDesktopState` only updates `desktopOverlay`; never touches `snapshot.url`/`title` (server is truth for those).
884
885### `apps/web/src/environmentApi.ts`
886
887```ts
888preview: {
889 open: (input) => rpcClient.preview.open(input as never),
890 navigate: (input) => rpcClient.preview.navigate(input as never),
891 refresh: (input) => rpcClient.preview.refresh(input as never),
892 close: (input) => rpcClient.preview.close(input as never),
893 list: (threadId) => rpcClient.preview.list(threadId),
894 onEvent: (callback) => rpcClient.preview.onEvent(callback),
895},
896```
897
898Plus mirror in the `EnvironmentApi` shape in `packages/contracts/src/server.ts` (or wherever `EnvironmentApi` lives).
899
900---
901
902## 8. Web: components
903
904### `apps/web/src/components/preview/PreviewPanelShell.tsx`
905
906Lifted from `DiffPanelShell.tsx` verbatim, renamed types. **Must** use the same className contract:
907
908```tsx
909<div
910 className={cn(
911 "flex h-full min-w-0 flex-col bg-background",
912 props.mode === "inline"
913 ? "w-[42vw] min-w-[360px] max-w-[560px] shrink-0 border-l border-border"
914 : "w-full",
915 )}
916>
917```
918
919So preview side panel is visually indistinguishable in spacing from the diff panel.
920
921### `apps/web/src/components/preview/PreviewView.tsx`
922
923Renderer 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`.
924
925Key behaviour:
926
927- On mount: `await desktopBridge.preview.createTab(tabId)`, `await desktopBridge.preview.getPreloadPath()`.
928- Subscribes to `desktopBridge.preview.onStateChange(handleState)` → `previewStateStore.applyDesktopState`.
929- Subscribes to `EnvironmentApi.preview.onEvent(handleEvent)` → `previewStateStore.applyServerEvent`.
930- On `<webview>` `dom-ready`: read `webContentsId`, call `desktopBridge.preview.registerWebview`, then call `EnvironmentApi.preview.navigate({ threadId, tabId, url })` so server learns the resolved URL.
931- On unmount when panel hides (not when thread changes — see persistence note below): `setVisibility(tabId, false)`.
932
933### `apps/web/src/components/preview/PreviewWebview.tsx`
934
935```tsx
936"use client";
937
938import { useEffect, useState } from "react";
939import { isDesktop } from "~/env";
940
941interface Props {
942 tabId: string;
943 initialUrl: string | null;
944}
945
946declare global {
947 interface HTMLElementTagNameMap {
948 webview: Electron.WebviewTag;
949 }
950}
951
952export function PreviewWebview({ tabId, initialUrl }: Props) {
953 const [config, setConfig] = useState<{ partition: string; preload: string } | null>(null);
954
955 useEffect(() => {
956 if (!isDesktop || !window.desktopBridge?.preview) return;
957 void Promise.all([
958 window.desktopBridge.preview.getBrowserPartition(),
959 window.desktopBridge.preview.getPreloadPath(),
960 ]).then(([partition, preload]) => setConfig({ partition, preload }));
961 }, []);
962
963 if (!isDesktop || !window.desktopBridge?.preview || !config) return null;
964
965 const src = initialUrl ?? "about:blank";
966 return (
967 <webview
968 src={src}
969 partition={config.partition}
970 preload={`file://${config.preload}`}
971 allowpopups
972 data-preview-tab={tabId}
973 className="absolute inset-0 h-full w-full bg-background"
974 />
975 );
976}
977```
978
979### `apps/web/src/components/preview/PreviewPanel.tsx`
980
981The 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.
982
983### Persistence across thread changes
984
985Following `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.
986
987For 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.
988
989---
990
991## 9. Discoverability glue
992
993### A. `preview.toggle` keybinding (`apps/web/src/routes/_chat.tsx`)
994
995Mirror the existing `chat.new` block (`:51–:78`):
996
997```ts
998if (command === "preview.toggle") {
999 event.preventDefault();
1000 event.stopPropagation();
1001 if (!window.desktopBridge?.preview) {
1002 showPreviewUnsupportedToast();
1003 return;
1004 }
1005 if (!routeThreadRef) return;
1006 rightPanelStore.toggle(routeThreadRef, "preview");
1007 return;
1008}
1009```
1010
1011`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.
1012
1013### B. Terminal link → "Open in preview"
1014
1015In `ThreadTerminalDrawer.tsx`'s `terminal.registerLinkProvider({ provideLinks })` callback (`:454–:514`), update the `activate(event)` handler:
1016
1017```ts
1018activate: (event: MouseEvent) => {
1019 if (!isTerminalLinkActivation(event)) return;
1020 if (match.kind !== "url") {
1021 // existing path link handling
1022 return;
1023 }
1024 if (!isPreviewable(match.text) || !window.desktopBridge?.preview) {
1025 void localApi.shell.openExternal(match.text).catch(/* … */);
1026 return;
1027 }
1028 void localApi.contextMenu
1029 .show(
1030 [
1031 { id: "open-in-preview", label: "Open in preview" },
1032 { id: "open-in-browser", label: "Open in browser" },
1033 ],
1034 { x: event.clientX, y: event.clientY },
1035 )
1036 .then((choice) => {
1037 if (choice === "open-in-preview") {
1038 void api.preview.open({ threadId, url: match.text });
1039 rightPanelStore.open(threadRef, "preview");
1040 } else if (choice === "open-in-browser") {
1041 void localApi.shell.openExternal(match.text);
1042 }
1043 });
1044},
1045```
1046
1047`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`).
1048
1049### C. `ProjectScript.previewUrl` / `autoOpenPreview`
1050
1051Schema additions are listed in §4. UI changes in `ProjectScriptsControl.tsx`'s Add/Edit dialog form (`:378–:458`):
1052
1053```tsx
1054<div className="space-y-1.5">
1055 <Label htmlFor="script-preview-url">Preview URL (optional)</Label>
1056 <Input
1057 id="script-preview-url"
1058 placeholder="http://localhost:5173"
1059 value={previewUrl}
1060 onChange={(e) => setPreviewUrl(e.target.value)}
1061 />
1062 <p className="text-xs text-muted-foreground">
1063 Auto-open this URL in the preview panel when the script starts.
1064 </p>
1065</div>
1066<label className="flex items-center justify-between …">
1067 <span>Open preview automatically when this script runs</span>
1068 <Switch checked={autoOpenPreview} onCheckedChange={setAutoOpenPreview} />
1069</label>
1070```
1071
1072When `onRunScript(script)` fires in `ChatView.tsx`, after starting the terminal command, if `script.autoOpenPreview && script.previewUrl && desktopBridge.preview`:
1073
1074```ts
1075void api.preview.open({ threadId: activeThread.id, url: script.previewUrl });
1076rightPanelStore.open(activeThreadRef, "preview");
1077```
1078
1079---
1080
1081## 10. Migrations
1082
1083### Persisted state
1084
1085- `t3code:preview-state:v1` — new key, no migration needed.
1086- `t3code:right-panel-state:v1` — new key, no migration needed.
1087- Existing `t3code:terminal-state:v1` — untouched.
1088
1089### Schema additions
1090
1091`ProjectScript.previewUrl` and `autoOpenPreview` are both `Schema.optional(...)`. Existing serialized scripts decode unchanged. No data migration required.
1092
1093### Keybindings
1094
1095Adding `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.
1096
1097---
1098
1099## 11. Testing strategy
1100
1101Following t3code's vitest patterns (`bun run test`, never `bun test`):
1102
1103| Test file | What it covers |
1104| -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1105| `packages/contracts/src/preview.test.ts` | Schema encode/decode round-trips for inputs, snapshot, events; URL trimming; error tagged unions |
1106| `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 |
1107| `apps/web/src/previewStateStore.test.ts` | `applyServerEvent` reducer correctness; ring buffer cap; `closed` removes entry; `desktopOverlay` is independent of snapshot fields |
1108| `apps/web/src/rightPanelStore.test.ts` | `open` / `close` / `toggle` semantics; per-thread isolation; `?diff=1` sync compatibility |
1109| `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 |
1110| Existing `ThreadTerminalDrawer.test.ts` | Add a case: link activation with `kind: "url"` and `previewable: true` shows the context menu (mock `localApi.contextMenu.show`) |
1111
1112Manual smoke checklist (drop in `apps/desktop/test/smoke/`):
1113
11141. Boot desktop dev, open a thread, hit `mod+shift+j` → empty state appears in right panel.
11152. Type `https://example.com` → page loads, title updates in chrome bar.
11163. Navigate to `https://example.org` → back/forward enable correctly.
11174. Hit `mod+r` → page reloads, loading bar animates.
11185. Run a `bun dev` script in terminal printing `http://localhost:5173`, link in xterm → context menu offers "Open in preview".
11196. Restart server (kill `apps/server` child, watch it respawn) → preview tab survives, server replays `opened` event on reconnect, panel state matches reality.
11207. Switch threads → previous thread's preview tab is closed (v1); new thread shows empty state.
11218. 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).
1122
1123---
1124
1125## 12. Risks and resolutions
1126
1127| Risk | Resolution |
1128| ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1129| 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` |
1130| `<webview>` keyboard capture eats `mod+shift+j` etc. | `before-input-event` forwarder in `PreviewViewManager` (mirror of ami's pattern, smaller shortcut list) |
1131| URL with `X-Frame-Options: DENY` works fine in `<webview>` (good — that's why we picked `<webview>` over iframe) | n/a |
1132| 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` |
1133| 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 |
1134| `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 |
1135| 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 |
1136
1137---
1138
1139## 13. Out of scope (v2+)
1140
1141Explicitly **not** in this landing:
1142
1143- Devtools support (would need to port ami's `WebContentsView` + `setDevToolsWebContents` + bounds sync — significant)
1144- Page screenshot capture
1145- JavaScript injection / `executeJavaScript`
1146- React Grab / element picker
1147- Console log capture
1148- Playwright/CDP agent automation tools (`browser_snapshot`, `browser_execute`)
1149- System cookie import (Chrome/Safari decryption)
1150- Multi-tab per thread (groups/splits)
1151- Agent control overlay ("Agent" pill while controlled)
1152- Web build iframe fallback
1153
1154Any 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).
1155
1156---
1157
1158## 14. UI design system — primitives we reuse
1159
1160Every 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.
1161
1162| Need | Primitive | File ref | Notes |
1163| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
1164| 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 |
1165| `cn` class merger | `cn(...inputs)` from `~/lib/utils` | `apps/web/src/lib/utils.ts:8` | Wraps `cx` + `tailwind-merge` |
1166| 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 |
1167| Buttons (URL field submit) | `Button` `variant="outline"` `size="sm"` | same | Matches the "ProjectScript primary action" density already in `BranchToolbar` |
1168| 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 |
1169| 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` |
1170| 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` |
1171| 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 |
1172| "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:` |
1173| Status pulse dot (running) | `<span>` with `bg-success` and `animate-pulse` | tailwind built‑in | Theme‑aware via `--success` |
1174| Tooltips (back/fwd/refresh hover) | `Tooltip` + `TooltipTrigger` + `TooltipPopup` | `apps/web/src/components/ui/tooltip.tsx` | Sub‑200ms hover already configured |
1175| 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" |
1176| 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` |
1177| Spinner (inline in URL field while loading) | `Spinner` | `apps/web/src/components/ui/spinner.tsx` | Wraps `Loader2Icon` from lucide |
1178| 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 |
1179| Keybinding shortcut label in tooltips | `shortcutLabelForCommand(keybindings, "preview.toggle")` | `apps/web/src/keybindings.ts` | Renders `⌘⇧J` etc. |
1180| 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 |
1181
1182**Icons.** Use `lucide-react` everywhere (already in `package.json`). The exact icons we need:
1183
1184```ts
1185import {
1186 ArrowLeft,
1187 ArrowRight, // back / forward
1188 RotateCw,
1189 Loader2, // refresh / spinning
1190 Globe, // favicon fallback
1191 X, // close tab
1192 Plus, // new tab
1193 Copy,
1194 ExternalLink, // open in browser, copy URL
1195 Sidebar,
1196 Columns2, // right‑side toggles in tab strip (matches screenshot 2)
1197 PanelRightOpen,
1198 PanelRightClose, // alt for above
1199} from "lucide-react";
1200```
1201
1202`@hugeicons/react` is **not** a t3code dep — don't import it.
1203
1204**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.
1205
1206---
1207
1208## 15. Visual designs (matching the provided mockups)
1209
1210Three states render inside `PreviewView.tsx` based on the `PreviewSession` for the active thread:
1211
1212| Session state | Component rendered |
1213| --------------------- | ----------------------------------------------------------------------- |
1214| no tab open | `<PreviewEmptyState />` — disabled chrome row + "Local" recommendations |
1215| tab loading / loaded | `<PreviewWebview />` + chrome row + tab strip |
1216| tab navigation failed | `<PreviewUnreachable />` overlay above the webview |
1217
1218### 15.1 Empty state (`PreviewEmptyState.tsx`) — matches screenshot 1
1219
1220The 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 })`.
1221
1222Layout reference (annotated):
1223
1224```
1225┌────────────────────────────────────────────────────────────────┐
1226│ ← → ↻ [ 🌐 (disabled URL field) ] │ ← chrome row, all controls disabled
1227├────────────────────────────────────────────────────────────────┤
1228│ │
1229│ │
1230│ │
1231│ Local │ ← text-muted-foreground text-sm
1232│ ┌──────────────────────────────────────────────────────────┐ │
1233│ │ ┌──────┐ │ │
1234│ │ │ ●●● │ localhost:5175 ● │ │ ← Card row
1235│ │ │ ── │ localhost:5175 │ │ title text-sm font-medium text-foreground
1236│ │ │ ── │ │ │ subtitle text-xs text-muted-foreground
1237│ │ └──────┘ │ │ green dot bg-success animate-pulse
1238│ └──────────────────────────────────────────────────────────┘ │
1239│ ┌──────────────────────────────────────────────────────────┐ │
1240│ │ ┌──────┐ │ │
1241│ │ │ ●●● │ localhost:3000 ● │ │
1242│ │ │ ── │ next-server │ │
1243│ │ └──────┘ │ │
1244│ └──────────────────────────────────────────────────────────┘ │
1245│ │
1246│ │
1247└────────────────────────────────────────────────────────────────┘
1248```
1249
1250Skeleton (paste-able):
1251
1252```tsx
1253// apps/web/src/components/preview/PreviewEmptyState.tsx
1254"use client";
1255
1256import { useDiscoveredLocalServers } from "~/components/preview/useDiscoveredLocalServers";
1257import { Card } from "~/components/ui/card";
1258import { Empty, EmptyDescription, EmptyTitle } from "~/components/ui/empty";
1259
1260interface Props {
1261 threadId: string;
1262 onOpen: (url: string) => void;
1263}
1264
1265export function PreviewEmptyState({ threadId, onOpen }: Props) {
1266 const servers = useDiscoveredLocalServers(threadId);
1267
1268 return (
1269 <div className="flex h-full min-h-0 flex-col">
1270 <div className="px-6 pt-10 pb-4">
1271 <h2 className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Local</h2>
1272 </div>
1273
1274 {servers.length === 0 ? (
1275 <Empty>
1276 <EmptyTitle>No preview yet</EmptyTitle>
1277 <EmptyDescription>
1278 Run a dev script or paste a URL above to start previewing.
1279 </EmptyDescription>
1280 </Empty>
1281 ) : (
1282 <div className="flex flex-col gap-2 px-4 pb-6">
1283 {servers.map((s) => (
1284 <LocalServerCard key={s.url} server={s} onClick={() => onOpen(s.url)} />
1285 ))}
1286 </div>
1287 )}
1288 </div>
1289 );
1290}
1291
1292function LocalServerCard({
1293 server,
1294 onClick,
1295}: {
1296 server: { url: string; host: string; port: number; processName: string | null };
1297 onClick: () => void;
1298}) {
1299 return (
1300 <Card
1301 render={
1302 <button
1303 type="button"
1304 onClick={onClick}
1305 className="group flex items-center gap-3 px-3 py-3 text-left transition-colors hover:bg-accent/50"
1306 />
1307 }
1308 >
1309 <BrowserMockup className="size-10 shrink-0" />
1310 <div className="flex min-w-0 flex-1 flex-col">
1311 <span className="truncate text-sm font-medium text-foreground">
1312 {server.host}:{server.port}
1313 </span>
1314 <span className="truncate text-xs text-muted-foreground">
1315 {server.processName ?? `${server.host}:${server.port}`}
1316 </span>
1317 </div>
1318 <span className="size-2 shrink-0 rounded-full bg-success" aria-label="Listening">
1319 <span className="block size-2 rounded-full bg-success animate-ping opacity-60" />
1320 </span>
1321 </Card>
1322 );
1323}
1324```
1325
1326The little browser thumbnail next to each card is a tiny tailwind-only mockup (matches the screenshot — three traffic-light dots over two muted lines):
1327
1328```tsx
1329// apps/web/src/components/preview/BrowserMockup.tsx
1330import { cn } from "~/lib/utils";
1331
1332export function BrowserMockup({ className }: { className?: string }) {
1333 return (
1334 <div
1335 className={cn(
1336 "relative flex flex-col gap-1 rounded-md border border-border bg-card p-1.5 shadow-xs/5",
1337 className,
1338 )}
1339 aria-hidden
1340 >
1341 <div className="flex gap-0.5">
1342 <span className="size-1 rounded-full bg-red-400/80" />
1343 <span className="size-1 rounded-full bg-amber-400/80" />
1344 <span className="size-1 rounded-full bg-emerald-400/80" />
1345 </div>
1346 <div className="flex flex-1 flex-col gap-0.5 pt-0.5">
1347 <span className="h-0.5 w-full rounded-full bg-muted-foreground/30" />
1348 <span className="h-0.5 w-3/5 rounded-full bg-muted-foreground/20" />
1349 </div>
1350 </div>
1351 );
1352}
1353```
1354
1355The chrome row in the empty state — note the `disabled` props on every interactive element:
1356
1357```tsx
1358// inside PreviewView.tsx, when previewState.snapshot == null
1359<PreviewChromeRow
1360 canGoBack={false}
1361 canGoForward={false}
1362 canRefresh={false}
1363 loading={false}
1364 url=""
1365 onBack={NOOP}
1366 onForward={NOOP}
1367 onRefresh={NOOP}
1368 onSubmitUrl={(url) => api.preview.open({ threadId, url })}
1369 inputDisabled={false} // we still want to allow paste-to-open
1370 buttonsDisabled // back/fwd/refresh are visually disabled
1371/>
1372```
1373
1374### 15.2 Tab strip (`PreviewTabStrip.tsx`) — matches screenshot 2
1375
1376The 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.
1377
1378```
1379┌────────────────────────────────────────────────────────────────┐
1380│ ┌─ 🌐 This site can't be re… ✕ ─┐ ⤡ □ │ ← tab strip
1381├────────────────────────────────────────────────────────────────┤
1382│ ← → ↻ [URL bar] │ ← chrome row
1383├────────────────────────────────────────────────────────────────┤
1384│ │
1385│ <webview content> │
1386│ │
1387└────────────────────────────────────────────────────────────────┘
1388```
1389
1390For 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.
1391
1392```tsx
1393// apps/web/src/components/preview/PreviewTabStrip.tsx
1394"use client";
1395
1396import { Globe, Plus, Sidebar, X } from "lucide-react";
1397import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
1398import { cn } from "~/lib/utils";
1399import { faviconUrlForOrigin } from "~/lib/favicon";
1400
1401interface PreviewTabDescriptor {
1402 tabId: string;
1403 title: string;
1404 url: string | null;
1405 active: boolean;
1406}
1407
1408interface Props {
1409 tabs: ReadonlyArray<PreviewTabDescriptor>;
1410 onActivate: (tabId: string) => void;
1411 onClose: (tabId: string) => void;
1412 onNewTab: () => void;
1413 onToggleSidePanel: () => void;
1414 onToggleSidebar: () => void;
1415}
1416
1417export function PreviewTabStrip({
1418 tabs,
1419 onActivate,
1420 onClose,
1421 onNewTab,
1422 onToggleSidePanel,
1423 onToggleSidebar,
1424}: Props) {
1425 return (
1426 <div className="flex h-9 items-center gap-1 border-b border-border bg-background px-2">
1427 <div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto">
1428 {tabs.map((tab) => (
1429 <PreviewTab
1430 key={tab.tabId}
1431 tab={tab}
1432 onActivate={() => onActivate(tab.tabId)}
1433 onClose={() => onClose(tab.tabId)}
1434 />
1435 ))}
1436 <button
1437 type="button"
1438 aria-label="New preview tab"
1439 onClick={onNewTab}
1440 className="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
1441 >
1442 <Plus className="size-3.5" />
1443 </button>
1444 </div>
1445
1446 <div className="flex shrink-0 items-center gap-0.5">
1447 <Tooltip>
1448 <TooltipTrigger
1449 render={
1450 <button
1451 type="button"
1452 aria-label="Open in browser"
1453 onClick={onToggleSidePanel}
1454 className="flex size-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
1455 />
1456 }
1457 >
1458 <ExternalLink className="size-3.5" />
1459 </TooltipTrigger>
1460 <TooltipPopup>Open in system browser</TooltipPopup>
1461 </Tooltip>
1462 <Tooltip>
1463 <TooltipTrigger
1464 render={
1465 <button
1466 type="button"
1467 aria-label="Close panel"
1468 onClick={onToggleSidebar}
1469 className="flex size-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
1470 />
1471 }
1472 >
1473 <Sidebar className="size-3.5" />
1474 </TooltipTrigger>
1475 <TooltipPopup>Close preview</TooltipPopup>
1476 </Tooltip>
1477 </div>
1478 </div>
1479 );
1480}
1481
1482function PreviewTab({
1483 tab,
1484 onActivate,
1485 onClose,
1486}: {
1487 tab: PreviewTabDescriptor;
1488 onActivate: () => void;
1489 onClose: () => void;
1490}) {
1491 return (
1492 <div
1493 className={cn(
1494 "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",
1495 tab.active
1496 ? "border-input bg-card text-foreground"
1497 : "border-transparent text-muted-foreground hover:bg-accent/50 hover:text-foreground",
1498 )}
1499 >
1500 <button
1501 type="button"
1502 onClick={onActivate}
1503 className="flex min-w-0 flex-1 items-center gap-1.5 text-left"
1504 >
1505 <TabFavicon url={tab.url} />
1506 <span className="truncate">{tab.title}</span>
1507 </button>
1508 <button
1509 type="button"
1510 aria-label="Close tab"
1511 onClick={(e) => {
1512 e.stopPropagation();
1513 onClose();
1514 }}
1515 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"
1516 >
1517 <X className="size-2.5" />
1518 </button>
1519 </div>
1520 );
1521}
1522```
1523
1524### 15.3 Favicon helper (`apps/web/src/lib/favicon.ts`)
1525
1526Borrowed 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).
1527
1528```ts
1529// apps/web/src/lib/favicon.ts
1530const FAVICON_PROVIDER = "https://www.google.com/s2/favicons";
1531
1532export function faviconUrlForOrigin(rawUrl: string | null, size = 32): string | null {
1533 if (!rawUrl) return null;
1534 try {
1535 const url = new URL(rawUrl);
1536 if (!url.host) return null;
1537 return `${FAVICON_PROVIDER}?domain=${encodeURIComponent(url.host)}&sz=${size}`;
1538 } catch {
1539 return null;
1540 }
1541}
1542```
1543
1544```tsx
1545// apps/web/src/components/preview/TabFavicon.tsx
1546import { Globe } from "lucide-react";
1547import { useEffect, useState } from "react";
1548import { faviconUrlForOrigin } from "~/lib/favicon";
1549
1550export function TabFavicon({ url }: { url: string | null }) {
1551 const src = faviconUrlForOrigin(url, 32);
1552 const [errored, setErrored] = useState(false);
1553 useEffect(() => setErrored(false), [src]);
1554
1555 if (!src || errored) {
1556 return <Globe className="size-3.5 shrink-0 text-muted-foreground" />;
1557 }
1558 return (
1559 <img
1560 src={src}
1561 alt=""
1562 className="size-3.5 shrink-0 rounded-sm"
1563 onError={() => setErrored(true)}
1564 />
1565 );
1566}
1567```
1568
1569### 15.4 Unreachable / error state (`PreviewUnreachable.tsx`) — port of 404.html
1570
1571When `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.
1572
1573Color mapping (Chromium → t3code theme):
1574
1575| Chromium | Tailwind/t3code |
1576| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1577| `--background-color: #fff` / `--google-gray-900` (dark) | `bg-background` |
1578| `--text-color: --google-gray-700` / `--google-gray-500` (dark) | `text-muted-foreground` |
1579| `--heading-color: --google-gray-900` / `--google-gray-500` (dark) | `text-foreground` |
1580| `--error-code-color: --google-gray-700` / `--google-gray-500` (dark) | `text-muted-foreground/70` |
1581| `--quiet-background-color: rgb(247,247,247)` / `--background` (dark) | `bg-muted/40` |
1582| `--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) |
1583| `--secondary-button-*` | `Button variant="outline"` |
1584| `--link-color: rgb(88,88,88)` / `--google-blue-300` (dark) | `text-primary underline-offset-4 hover:underline` |
1585| 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 |
1586
1587Skeleton:
1588
1589```tsx
1590// apps/web/src/components/preview/PreviewUnreachable.tsx
1591"use client";
1592
1593import { useState } from "react";
1594import { Button } from "~/components/ui/button";
1595import { cn } from "~/lib/utils";
1596
1597interface Props {
1598 url: string;
1599 errorCode: string; // e.g. "ERR_NAME_NOT_RESOLVED", "ERR_CONNECTION_REFUSED"
1600 description: string;
1601 onReload: () => void;
1602}
1603
1604const ICON_GENERIC = (
1605 // Replace with an inline SVG that matches the Chromium "icon-generic"
1606 // (a stylized broken page). For v1 a Lucide MapPinOff is a fine stand-in —
1607 // we want a visual that reads "destination unreachable".
1608 <svg
1609 viewBox="0 0 64 64"
1610 className="size-10 text-muted-foreground/70"
1611 fill="none"
1612 stroke="currentColor"
1613 strokeWidth="2.5"
1614 >
1615 <path d="M16 12 L48 12 L48 52 L16 52 Z" />
1616 <path d="M22 22 L42 22 M22 30 L36 30 M22 38 L40 38" strokeLinecap="round" />
1617 <path d="M52 8 L12 56" strokeLinecap="round" />
1618 </svg>
1619);
1620
1621export function PreviewUnreachable({ url, errorCode, description, onReload }: Props) {
1622 const [showDetails, setShowDetails] = useState(false);
1623 const host = safeHost(url) ?? url;
1624
1625 return (
1626 <div className="flex h-full min-h-0 w-full flex-col bg-background">
1627 <div className="mx-auto flex w-full max-w-xl flex-1 flex-col px-6 py-12">
1628 {/* Icon + headline */}
1629 <div className="mb-6 flex flex-col gap-4">
1630 {ICON_GENERIC}
1631 <h1 className="text-2xl font-semibold leading-tight text-foreground">
1632 This site can&rsquo;t be reached
1633 </h1>
1634 </div>
1635
1636 {/* Summary — uses dangerouslySetInnerHTML only for the bold host treatment */}
1637 <p className="text-sm leading-relaxed text-muted-foreground">
1638 <strong className="font-semibold text-foreground">{host}</strong>
1639 &rsquo;s server{" "}
1640 <abbr
1641 title="Domain Name System"
1642 className="cursor-help underline decoration-dotted underline-offset-2"
1643 >
1644 DNS address
1645 </abbr>{" "}
1646 could not be found.
1647 </p>
1648
1649 {/* Suggestions list (when details open) */}
1650 {showDetails && (
1651 <div className="mt-6 rounded-lg border border-border bg-muted/40 p-4 text-sm">
1652 <p className="mb-2 font-medium text-foreground">Try:</p>
1653 <ul className="list-disc space-y-1 pl-5 text-muted-foreground">
1654 <li>Checking the connection</li>
1655 <li>Checking the proxy and the firewall</li>
1656 <li>Running Network Diagnostics</li>
1657 </ul>
1658 </div>
1659 )}
1660
1661 {/* Error code */}
1662 <div className="mt-8 text-xs uppercase tracking-wide text-muted-foreground/70">
1663 {errorCode}
1664 </div>
1665
1666 {/* Actions */}
1667 <div className="mt-auto flex items-center gap-2 pt-8">
1668 <Button
1669 type="button"
1670 variant="outline"
1671 size="sm"
1672 onClick={() => setShowDetails((v) => !v)}
1673 >
1674 {showDetails ? "Hide details" : "Details"}
1675 </Button>
1676 <div className="flex-1" />
1677 <Button type="button" size="sm" onClick={onReload}>
1678 Reload
1679 </Button>
1680 </div>
1681 </div>
1682 </div>
1683 );
1684}
1685
1686function safeHost(url: string): string | null {
1687 try {
1688 return new URL(url).host;
1689 } catch {
1690 return null;
1691 }
1692}
1693```
1694
1695The component intentionally:
1696
1697- Uses theme tokens only — looks correct in light + dark with no extra wiring.
1698- Drops Chromium's "Diagnose connection" / "Portal sign-in" buttons (irrelevant in our shell).
1699- Drops the dinosaur game (RIP).
1700- Maps `errorCode` → human description in `PreviewView.tsx` via a small lookup (`ERR_CONNECTION_REFUSED` → "Connection refused", etc.) before rendering.
1701
1702### 15.5 Loading bar
1703
1704A 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`):
1705
1706```tsx
1707{
1708 loadProgress > 0 && (
1709 <div
1710 className="pointer-events-none absolute bottom-0 left-0 z-10 h-0.5 rounded-full bg-primary transition-all duration-150 ease-out"
1711 style={{
1712 width: `${loadProgress}%`,
1713 boxShadow: "0 0 6px 1px var(--ring)",
1714 }}
1715 />
1716 );
1717}
1718```
1719
1720Progress is computed locally with `useLoadingProgress(isLoading)` — same hook signature as ami's, ~30 lines.
1721
1722---
1723
1724## 16. Local server discovery (port scanner)
1725
1726The "Local" recommendations in §15.1 need a feed. New backend service.
1727
1728### `apps/server/src/preview/Services/PortScanner.ts`
1729
1730```ts
1731import { Context, Effect } from "effect";
1732
1733export interface DiscoveredLocalServer {
1734 host: string; // "localhost"
1735 port: number; // 5175
1736 url: string; // "http://localhost:5175"
1737 processName: string | null; // "node", "vite", "next-server"
1738 pid: number | null;
1739}
1740
1741export interface PreviewPortScannerShape {
1742 /** One-shot snapshot of currently listening localhost ports. */
1743 readonly scan: () => Effect.Effect<ReadonlyArray<DiscoveredLocalServer>>;
1744 /** Subscribe to changes. Listener is called on every diff. */
1745 readonly subscribe: (
1746 listener: (servers: ReadonlyArray<DiscoveredLocalServer>) => Effect.Effect<void>,
1747 ) => Effect.Effect<() => void>;
1748 /** Hint that at least one client is interested → starts polling. Returns release fn. */
1749 readonly retain: () => Effect.Effect<() => void>;
1750}
1751
1752export class PreviewPortScanner extends Context.Service<
1753 PreviewPortScanner,
1754 PreviewPortScannerShape
1755>()("t3/preview/Services/PortScanner") {}
1756```
1757
1758### `apps/server/src/preview/Layers/PortScanner.ts`
1759
1760Two strategies, picked at startup:
1761
17621. **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.
17632. **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.
1764
1765Polling cadence:
1766
1767- When `retain()` count is 0 → not polling.
1768- When ≥1 → poll every 3s. Diff against last result; only emit `subscribe` callbacks when the set differs.
1769- `retain()` returns a release fn that decrements the counter; goes idle automatically when the empty state is hidden.
1770
1771The renderer side (`useDiscoveredLocalServers(threadId)`) calls `EnvironmentApi.preview.subscribePorts(callback)` and the WS handler calls `retain()` on first subscribe / releases on the last unsubscribe.
1772
1773### Augmenting the discovery feed
1774
1775The card list in §15.1 is the union of:
1776
17771. **Listening ports** from the scanner (above).
17782. **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`).
17793. **Configured URLs** — every active `ProjectScript.previewUrl` from the active project.
1780
1781Deduped by URL string. Sort: configured > listening > recent. This yields the "smart, contextual, no-thought-required" feel the screenshot implies.
1782
1783---
1784
1785## 17. File map (additions to §3)
1786
1787Append these to the file list:
1788
1789| File | Purpose |
1790| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
1791| `apps/server/src/preview/Services/PortScanner.ts` | Service tag + interface |
1792| `apps/server/src/preview/Layers/PortScanner.ts` | `lsof` strategy + TCP probe fallback + polling |
1793| `apps/server/src/preview/Layers/PortScanner.test.ts` | Parser tests for `lsof -F pcn` output |
1794| `apps/web/src/lib/favicon.ts` | `faviconUrlForOrigin(url, size)` helper |
1795| `apps/web/src/components/preview/PreviewTabStrip.tsx` | Tab strip (screenshot 2) |
1796| `apps/web/src/components/preview/TabFavicon.tsx` | Favicon `<img>` w/ `<Globe>` fallback |
1797| `apps/web/src/components/preview/BrowserMockup.tsx` | Tiny tailwind browser thumbnail icon |
1798| `apps/web/src/components/preview/PreviewLocalServerCard.tsx` | Card row for a discovered server |
1799| `apps/web/src/components/preview/PreviewUnreachable.tsx` | 404.html rewritten in tailwind |
1800| `apps/web/src/components/preview/useDiscoveredLocalServers.ts` | Hook subscribing to `EnvironmentApi.preview.subscribePorts` + merging in `recentUrlsFromTerminal` and `ProjectScript.previewUrl` |
1801| `apps/web/src/components/preview/useLoadingProgress.ts` | 30-line progress simulator (port of ami's) |
1802| `apps/web/src/components/preview/errorCodeMessages.ts` | `ERR_*` → human-readable description map |
1803
1804Update the contract additions in §4 to add:
1805
1806```ts
1807// packages/contracts/src/preview.ts
1808export const DiscoveredLocalServer = Schema.Struct({
1809 host: TrimmedNonEmptyString,
1810 port: Schema.Int.check(Schema.isGreaterThan(0)).check(Schema.isLessThan(65536)),
1811 url: TrimmedNonEmptyString,
1812 processName: Schema.NullOr(TrimmedNonEmptyString),
1813 pid: Schema.NullOr(Schema.Int.check(Schema.isGreaterThan(0))),
1814});
1815export type DiscoveredLocalServer = typeof DiscoveredLocalServer.Type;
1816```
1817
1818WS routes added (§5):
1819
1820```
1821preview.subscribePorts(callback) → unsubscribe
1822preview.scanPortsOnce() → ReadonlyArray<DiscoveredLocalServer>
1823```
1824
1825---
1826
1827## 18. Implementation order (single‑shot landing) — REVISED
1828
1829To minimize cross‑file thrash while writing:
1830
18311. `packages/contracts/src/preview.ts` (+ test) — `PreviewSession`, `PreviewEvent`, `DiscoveredLocalServer` schemas; `keybindings.ts` / `project.ts` schema edits → **build contracts first**.
18322. `apps/server/src/preview/{Services,Layers}/Manager.ts` (+ test) and `{Services,Layers}/PortScanner.ts` (+ test).
18333. `apps/server/src/ws.ts` and `runtimeLayer` provision (`PreviewManager.Default`, `PreviewPortScanner.Default`).
18344. `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`.
18355. `apps/web/src/lib/favicon.ts`; `apps/web/src/previewStateStore.ts`, `apps/web/src/rightPanelStore.ts` (+ tests).
18366. `apps/web/src/environmentApi.ts` — add `preview` slot.
18377. Right‑panel arbiter migration in `ChatView.tsx` (replace `planSidebarOpen`, render preview alongside diff/plan).
18388. Components, in dependency order:
1839 - `BrowserMockup.tsx`, `TabFavicon.tsx`, `useLoadingProgress.ts`, `errorCodeMessages.ts`, `useDiscoveredLocalServers.ts`
1840 - `PreviewLocalServerCard.tsx`, `PreviewEmptyState.tsx`
1841 - `PreviewUnreachable.tsx`
1842 - `PreviewTabStrip.tsx`
1843 - `PreviewWebview.tsx` (the `<webview>` host, no-op on web)
1844 - `PreviewView.tsx` (ties it all together: tab strip + chrome row + body switching between empty / webview / unreachable)
1845 - `PreviewPanelShell.tsx`, `PreviewPanel.tsx`
18469. Keybinding wiring: `_chat.tsx` `preview.toggle`/`preview.refresh`/`preview.focusUrl`; `keybindings.ts` shortcut helpers.
184710. `ProjectScriptsControl.tsx` form additions (`previewUrl`, `autoOpenPreview`).
184811. `ThreadTerminalDrawer.tsx` link‑activation update — context menu "Open in preview" / "Open in browser" for previewable URLs.
184912. `KEYBINDINGS.md` doc update — add `preview.*` commands and `previewFocus` / `previewOpen` `when` keys.
185013. Run `bun fmt && bun lint && bun typecheck && bun run test` — must all pass per `AGENTS.md`.
185114. Manual smoke against the dev desktop (`bun run dev:desktop`).
1852
1853Estimated 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`.