| author | |
| committer | |
| log | 43e8bbd7aea2f73a0ea7b7a15af2903a471d5e4a |
| tree | 4d005c6a42e5c1d86eae637bb9cf566ce3031a71 |
| parent | 52c77c1ec4c1c2c2c27b917baaec91d050d0a737 |
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 | |||
| 12 | a { | ||
| 13 | color: var(--link-color); | ||
| 14 | } | ||
| 15 | |||
| 16 | body { | ||
| 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 | |||
| 63 | html { | ||
| 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 | |||
| 96 | button { | ||
| 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 | |||
| 132 | button: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 | |||
| 190 | h1 { | ||
| 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 | |||
| 198 | h2 { | ||
| 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 | |||
| 209 | input[type=checkbox] { | ||
| 210 | opacity: 0; | ||
| 211 | } | ||
| 212 | |||
| 213 | input[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 | |||
| 327 | input[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. */ | ||
| 608 | html[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. */ | ||
| 613 | html:not([subframe]) #sub-frame-error { | ||
| 614 | display: none; | ||
| 615 | } | ||
| 616 | |||
| 617 | h1 { | ||
| 618 | margin-top: 0; | ||
| 619 | word-wrap: break-word; | ||
| 620 | } | ||
| 621 | |||
| 622 | h1 span { | ||
| 623 | font-weight: 500; | ||
| 624 | } | ||
| 625 | |||
| 626 | a { | ||
| 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 | |||
| 789 | html[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. */ | ||
| 810 | html[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 */ | ||
| 926 | html[dir='rtl'] .runner-container, | ||
| 927 | html[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. | ||
| 1310 | function 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. | ||
| 1314 | class 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. | ||
| 1318 | function 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. | ||
| 1342 | const 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. | ||
| 1346 | const 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. | ||
| 1350 | function 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. | ||
| 1354 | var 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. | ||
| 1358 | const 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. | ||
| 1362 | const 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. | ||
| 1366 | class 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. | ||
| 1370 | let 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. | ||
| 1374 | class 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. | ||
| 1378 | class 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. | ||
| 1382 | const 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. | ||
| 1386 | let 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. | ||
| 1390 | class 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. | ||
| 1394 | const 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. | ||
| 1398 | var 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. | ||
| 1402 | let 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,iVBORw0KGgoAAAANSUhEUgAACY4AAADCCAMAAADT9DSoAAAANlBMVEUAAADa2tr/////9/e5ubn39/dTU1P29vbv7+/+/v74+Pjw8PD///9ZWVlfX1/z8/P5+fn///9RgilMAAAAEnRSTlMA///////////////2////9gn80juWAAAR/UlEQVR4AezdAW+jOBPG8QcgVPv9P+xqHQPvu9nrTWWd1enNuY7D/ydpS+gwdqRq44yN0WUBAAAAAAAA06u/sVPPbZZ0/Ie5LNvIEWbRu11msCsK7duYZM4OcaWzf1+rVk13fbTpj1SctXMWZJHluSLYTmxlUBlVxJlkZz/py2a/txeV/o1qls9B3q55/TALAAAAHa16KeU340nT4+gKZq36LesYPMIsWmR2mbGuqGvZxqkrOsct+wNgOAYA2Gy6bysmEo3N/71HKhWzg+W1haTCZqdr06Blu5tSvS/GpLIhAAzHmsxMWyWsqJA980zxKinb+4zWxh4Zs46RIyoVosWqRGNcYRGOrJE2zCTjjzsD+SwysJLTFXdaRCjf+DA7P74yeTvmrdtUKCTWjr2uaZIAoHR7k5a3H+oLANZX+W4zdf4WjFmHP+IyrM616/ucQ+S1nFO3FWTn/r6Gsbi50Sb+3l+aykxk5Q5Mu9xstTshK20UL5MAMBwbzsmyXgCF22yD5OVx/EthAMBw7NSobP1Yh2qV7X4WyjF/shLMIio5Xrw2tsTrY/3XjQXiLPYMxFktLZ7v3O04azRYA/+z9stL3s0Zk/ibHkqvqUwA2Opzl9ock5B2J2Qtn50t5ky38txW6R8AhmM9xt4w/mrVnyMpB3I8MjyOKyyimqO9+r2O16sRswdZtv+HNN01KGRJK/1tmfdhbZ4Xq67AtoS11wDwcLsLAK49HEvhqvrU9O7Po2HudpVAq0Udn0bocfQ4DuRo0NOB7nXsULPrsG7s9MUZ/zouTV3Wj0lZq6Z7juyclFQe1yYh7ZxxXJvKBJvsd+XvTbKTQHxtc+u8WPXyJp3Fh8kkAAAAhmMxzu/G/WHWccF7HesWazVYswOw0l/L++zAvmP1Oy0BoLr5a8WmIsC9lasdBVgeE8sMgOHYFl4nczZ7lqRsPVez3Nle2/qxXrvhN8hh903CqmB7uGYX3x/sDOdzaLj/2BTNB8Ahf1NerNz+DgAAwHCs/Vox9hdr2Yp/tzFqYw1XrZ1C9KmYSdrKab+tOh+42XXldqxJFf8Q95VrN5lUucuzov4+gP5r3TDrwqb/E4BLur39KI57AYCVfccra7v65Lb1Y4HqU7O9wQbdocvqUezcD3PuR3HcCwCsTGEAYDf+v4+TCkn1M/Wz9d8l/7X1vvj7l+wAAMMxoMeu+vErAhW45nVB92O/JpXOxndVtr+78tTkiiu/fFlctnqvHXcBAOtYS/incq/9oNPyALic27xrmeef6goAVqFc21Vfy9Uot+ptXozVf/y76nuvWKox8Tbsmn2op23i3MW+eAAYjn11YuOsTlUAgN9ttoHt8jj+JQBgOAb+GOKrvLr0yiIWixngaZvUxd5lgf3jyQuGYw5n5RwANH1wW3LHOyNT5WUtvpBav6n2/dwcwR0BDMfy06wb8++XewRzG9aPlfWfwBUXqEpNMqczTq3j2t9dGYg7Ncnisuw/wOkuAGBX/n4A4CYAoDrWFQ5lrboiIGvVdM/Vebq6Mn6TNt+F23u8U1JU8aasqzGBftb7M38y7zA7P86y5SBvPG+p2dxNojoGADyzEsD4qI41GtP3Xze2+r8jxHPHOXKuofqY5aAcG9+hHzyzEgBWCQB4ZmVgpvLr85VXAYDhGLIOzZ9G/HbYfWYNWrFVOtdQ26F/0TMBz6x81uei5Opv6x9buVNe8to3jOSIKSXnWqpDDURaZe0YAAAA1bEOY++ee56tzv3Bao5GuQ9X1coTYfnmSt9irVj+rPUCxVnboZ/a2MjKzV0796RDZ+wO0Jb93AQ8S93p6NVqJR4AAACsHUO80neEIoqVYYEcplihVrRyHfv7g6u1qwTAPbNScXTIS94WNVCbI5r/dSXpGKjVSwKA2zz/tJ8f+efp3GFFZn/+pJbqPazP2Mb7WSYHsI783cYh3F52rvEyJlv+JrmPatQh442o1caiOcor5korPSxda2O2O1m3XrHzmP18QQBm5+gjW2yHVg+75noAYHuTljfpJgBogclKnjdpEcH1Z/5W1kArr10bszrYx9rY0nV3MuS//p3u2b+Va8mCt6EfzFefq03tp0TTp/eUe+cRskrkbZ+3vvfY5pyyTs62Z2ef7QqvDq0yHAOA2ywbHD+OfwnAeKiOdRh793C41niZLHO0zN20PmYttG/le+0d60+7ngfO3Y6zXheA1RmTu7Vq8QAAm698IpvKHsbfVHJflVr2s5yvBBg0Yli2m5cjonUr6wB/XFYfu3Kf8PHvebqrK8SrBtnieuUlb7F+bHMuo9yaDVdW/7vo1SrPrASA25setrcf6gkA1qG+2wzA1sDF16a5cjt2LLGIAFcrSXN9z31qUdW9+JcufcK5T/f1URs7/LNs9cjUOD4itbwqBdImXRpAdQwAbvbzdQFg7RhgtTHqY7YXf3muR5+Qle0nhv94yn3ykjf+2LD4vFn8HXdvdVZHAAAAWIf5bjOALHPE9zYL5u4vh3q7fH4ucMVejVia18aWyrn9S704JU36Y9LpijPt4zzOb42bKnFdAQDVMQC46YUBoDoGHFKz2tiuXYvnCosvrrcIRxvOVmL2IqPvnfyPvXvRkRMHogAKYdT//70ImH3WitHGkTXuCpQ4Z59NsD2iETE3hWGEujHXG/2m9zvwNH9HJVfVUaVjAADSsYajaJ1YOEbfjdl9fNinPWf/Rpv+BG6ZxsnGAOqTjgEASMcgaTWwSIiiRXo2tvf/VL85FYynHP/5d//TlfEsZv7TlXlPS86eqqyv9Yx5hX7123j3pPox6RgAgHRsfO5dp27suKx2Tj62T3tfi9hvMBv7yzJeaZZSMQfEFVm/tfpdJ6RjAABqx9pzb+Rj/VlXTz7WNjBGo0Xs+159Kd+sMqqrygz1Y/pVP7ZdOKp0rD4AQDqmfkySl+1Xb27ce1sM2L+R2oX0fOyNT0PO0+d4f5e9q3J+c38AascAAKRjcL98bBlokZnaLZ0VcNlA/dim39x+k+rH1t9WP7Y1JjsfP9nnuHTUS9MxAAA+Kt3btHGcjuRRd48Cqd1ym7xutN4rnsQc70/dGIDaMQAA6RjXO4rv8YAV1GLbafvy5vX258QkaE5LmGYrjvVSP9ZR8aPf/H5H6sfWod/jfnyjkuvoXGfs2lEvTccAAPi4yb2NNcd4bGYW2VjV+rHoR90YcK3ty+RmKzCqdAwAQO0YyMf2+He4dQXZnNDrrGqshfgzlsbnGv3+4+O/7du/KcjxvX6jz5sfh6gfa30e89E4CltqzXLre/1VJnZIxwAApGO9c+8CDnVjpK1ftk/vE8nV3L9fO0vr769dQfbGGq9ZzRhYmatz/f5zivbP5yNv1NAY9XnpGACAdOz1zRX3X+Nvt4JC9sjGUkXqVZOqMSLnCNtPntk/7t9vvPXw5Bh6X2OkL9cfhz5rZv3YBSsfnEfarMoPACAd60yxeubea5H7NKjh86r9CvysQPm8tMN2bnfNqNIxAABPVgJAQr1OjX4/T/0eb8yFtvPnAsdhzVsF/7K6sZF3TkrHAACkY9mzVwCA4zajSscAAKRjr1MqBgDjq0wd7W236neOVdmz0pcCxyFmAmt72+BR+NH+SZPPw17SMQAA6dga8723zr1hmfb6LULiGPkAkI4BAEjH8r0e+75KCdjS+JW/tu+XtAjtFpliDHiarbGtQL95ChyHtXNblaO9SccAAKRjnV4x/33b3HudeJjIgRrJ1f7PP/kt+jO7aDFc4dU/BgBqxwAAeJt5gjK1Y/uFLZZGiz1anPbaT59O+8W48SuxtWsMAJ6SjgEA4MlKiDqp9pOF+S36K8rO2/f/fQr7lxH209beMQB4cjoGAACwLAVaJIwQbQB4djoGAAAAAAAAYN0xAF5eYEKVM9AZq3YMAEA6BkBCMrF+/XBPOAOdsdIxAADpGAAJNTtrM3qA689AZ6x0DABAOgZAfs1OO4CAa85AZ2zJdAwAgDmmqABU0C7R6WzabgwJZ+D62JNuvWM6BgCA2jEAz8M9sY4H1I4BACAdA+DVX+UCSMcAAKRj+dULNb0S7iQd1fzvBN+d65Wj6jsh/7uTjgEAlDRXmre/prVnteACqxknHIPe1mWOSv5Ryr9H7x+x8qhxtON7zP8ZXK9cr1yv8rleqR0DAFA7RlWvafVzXHt/XOn4q+bBdcL1yvXKk5UAAMzJM92EWXx+zUh+bUD+/D7vT9VlKfXvbOURrleuV2RwvZKOAQAUNZ/v3HJmuu3+3ZlTt0agfX6Pn2PuctvHz/WK+3K9cr2SjgEAAAAAAADFzN6R3vZHe3ew27YORGH4DDHLbu77P2Q3WQ40FygCI0xpj0xJjST8H9A2qugTZ3cwZqiFnDoHAACwdwwAAODKXLfW5JoXWoocci4NAACmYwAAALDVjW3RvD7n3LOxTmghp8jZBgAAeF2guotlvoi5FG/mNDaXAwCAu7OXXcjVianG0/rmF0Vz2q2ONbm2C4mcA+djAADA325CbZkpY95/x1iT80u2pjdm9/WHAAAArsTfH0s1LW+VMY/ht40ipypjD6b88vUvCtkNAABAHWvdrXKwVW8a2zhos+J26qIAAAC8no3ND8ia5FXO7GysY8przscAAAD87RLlkqIcbNWlztcO2kyd+w3IUiYAAMAxsA9t7oWH5dj5Hr6ZqdPKvPjPCAAAdaxNvnJ82zfnmFYyXVdSfgAAoI51166Ce9WjmtzrmDrHtJpduj9lMh8DAIA6doCIuthdj+3byEwAAIA6Nr4salSrY2vxcrHtVWvysz9lKk80I2M+BgAA2pEvdp/rdfuPmVKZn/0idaIOBQAA4IPTJ0r1Ute5WI5bW6pn6+N6OZWjHJ54kXM5gyQpD83ZDgAA+GjcFa512vJ6bBYbc2xw0qu96BCWawpUms4CAADA9c+EXCOuUMW0leV+J7IORm2zOT2bzxm98vic7QAAgI9OuQg9xMxYq41K2CPMV+dY96/likck5Yo+ZtqP6cQAAADTMX+0se8Nz3+w8aRtmo+lCrM5tuf7sR/LmQcAAHWsPa5C8r45DVtUjMdaTUNdMYuvWa4Y5di3NmB5umdXmgAAALbxw1MfTS4e3ev9KVnxO4wm5VSHsj56fi+aTedIUvYJNpUz/g1I2zXnwgAAYCu/y0cb+KN4DynZ3qOvNAEAAJy6jnkMplcuhULyvlK9Fl9iunj/8nFoEZKy2btjtttRZnbOTz3tTDkAAKDQdCh3H5Q+7xb4P+wHZ2tQAAAA7Vkzi/j7KUfuLwPGORqK+BbbqrqTuU9JMqVuzexUOQAAoOA/EuqhCXm/z/IYtAEAAK+fR7mNK/bpY3Qf9o0BAMDescO4CwAAgOnYYfzJ8ysBAADQDhhYub4JjQEAAKDpXwgVKGwAAIA6pkV/iXhy+epVS0TUc7Y6JzUjBQAAwHTM9U0MLwEAAOAvb8W65YWQFN5fO8dWAAAArOlX8VgSmtX1uvDYv3elAAAAOOjitZB3Ba8TAgAAoI5paeGDI/VDz4S0qDfOiaJzjXPS9LacX9Aj504AAGA65uM2BgAAAO/HWk/vx7i7LRpZWl3JipxuPJard46lnjOlaqYCOVcAAADTseiDn3Q6to4BAAD4eEhVKdYvbZ+ctPe2NOXW+Y+pRg4AANiTPzvXohQ80BIAAGA71wZL/XHlfM6EVMW0ATkAAOD4OrY0heT1nq8Ytagu57uYaWNpk20MAACAgy5CXnw3AAAA+MpN+F4OtYqcGGZElZPGcAwAANxaG5WsUCGiaFGj214sHEvaGAAAuLU2u69+WbEg9CkihqUuVuQkbQwAANyZj3pUi683Qp2o21iXUy0qpYw2BgAAbsunTnFdVOtzXPHWtrEP/ZKt3kCWekh9CAAA4Dr8WY8KSfJ+UbxsY0Wv876J+Ts5aYzGAADA7RT769suh7YuiienX0SV08/HZHUZYzZ2DwAAUMeKQrZoqMgJySVFP2KrC9modHnIpc+/eh8CAAC4Fi+24v8Rk2WsL3bR/+e8ePwBAAC4PFOpTRSoI3P+0x+/VWpybRNaTpfTpFO9HwAAsI2rtGjCgTm/BQAAcB+mW2vy7bOfk+U0STrZzwUAAOY1ATgnAADTMfrmcracLuEs7wcAADAdAwAAuLD/AQPLUxmjjeldAAAAAElFTkSuQmCC"> | ||
| 1592 | <template id="audio-resources"> | ||
| 1593 | <audio id="offline-sound-press" src="data:audio/mpeg;base64,T2dnUwACAAAAAAAAAABVDxppAAAAABYzHfUBHgF2b3JiaXMAAAAAAkSsAAD/////AHcBAP////+4AU9nZ1MAAAAAAAAAAAAAVQ8aaQEAAAC9PVXbEEf//////////////////+IDdm9yYmlzNwAAAEFPOyBhb1R1ViBiNSBbMjAwNjEwMjRdIChiYXNlZCBvbiBYaXBoLk9yZydzIGxpYlZvcmJpcykAAAAAAQV2b3JiaXMlQkNWAQBAAAAkcxgqRqVzFoQQGkJQGeMcQs5r7BlCTBGCHDJMW8slc5AhpKBCiFsogdCQVQAAQAAAh0F4FISKQQghhCU9WJKDJz0IIYSIOXgUhGlBCCGEEEIIIYQQQgghhEU5aJKDJ0EIHYTjMDgMg+U4+ByERTlYEIMnQegghA9CuJqDrDkIIYQkNUhQgwY56ByEwiwoioLEMLgWhAQ1KIyC5DDI1IMLQoiag0k1+BqEZ0F4FoRpQQghhCRBSJCDBkHIGIRGQViSgwY5uBSEy0GoGoQqOQgfhCA0ZBUAkAAAoKIoiqIoChAasgoAyAAAEEBRFMdxHMmRHMmxHAsIDVkFAAABAAgAAKBIiqRIjuRIkiRZkiVZkiVZkuaJqizLsizLsizLMhAasgoASAAAUFEMRXEUBwgNWQUAZAAACKA4iqVYiqVoiueIjgiEhqwCAIAAAAQAABA0Q1M8R5REz1RV17Zt27Zt27Zt27Zt27ZtW5ZlGQgNWQUAQAAAENJpZqkGiDADGQZCQ1YBAAgAAIARijDEgNCQVQAAQAAAgBhKDqIJrTnfnOOgWQ6aSrE5HZxItXmSm4q5Oeecc87J5pwxzjnnnKKcWQyaCa0555zEoFkKmgmtOeecJ7F50JoqrTnnnHHO6WCcEcY555wmrXmQmo21OeecBa1pjppLsTnnnEi5eVKbS7U555xzzjnnnHPOOeec6sXpHJwTzjnnnKi9uZab0MU555xPxunenBDOOeecc84555xzzjnnnCA0ZBUAAAQAQBCGjWHcKQjS52ggRhFiGjLpQffoMAkag5xC6tHoaKSUOggllXFSSicIDVkFAAACAEAIIYUUUkghhRRSSCGFFGKIIYYYcsopp6CCSiqpqKKMMssss8wyyyyzzDrsrLMOOwwxxBBDK63EUlNtNdZYa+4555qDtFZaa621UkoppZRSCkJDVgEAIAAABEIGGWSQUUghhRRiiCmnnHIKKqiA0JBVAAAgAIAAAAAAT/Ic0REd0REd0REd0REd0fEczxElURIlURIt0zI101NFVXVl15Z1Wbd9W9iFXfd93fd93fh1YViWZVmWZVmWZVmWZVmWZVmWIDRkFQAAAgAAIIQQQkghhRRSSCnGGHPMOegklBAIDVkFAAACAAgAAABwFEdxHMmRHEmyJEvSJM3SLE/zNE8TPVEURdM0VdEVXVE3bVE2ZdM1XVM2XVVWbVeWbVu2dduXZdv3fd/3fd/3fd/3fd/3fV0HQkNWAQASAAA6kiMpkiIpkuM4jiRJQGjIKgBABgBAAACK4iiO4ziSJEmSJWmSZ3mWqJma6ZmeKqpAaMgqAAAQAEAAAAAAAACKpniKqXiKqHiO6IiSaJmWqKmaK8qm7Lqu67qu67qu67qu67qu67qu67qu67qu67qu67qu67qu67quC4SGrAIAJAAAdCRHciRHUiRFUiRHcoDQkFUAgAwAgAAAHMMxJEVyLMvSNE/zNE8TPdETPdNTRVd0gdCQVQAAIACAAAAAAAAADMmwFMvRHE0SJdVSLVVTLdVSRdVTVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTdM0TRMIDVkJAJABAKAQW0utxdwJahxi0nLMJHROYhCqsQgiR7W3yjGlHMWeGoiUURJ7qihjiknMMbTQKSet1lI6hRSkmFMKFVIOWiA0ZIUAEJoB4HAcQLIsQLI0AAAAAAAAAJA0DdA8D7A8DwAAAAAAAAAkTQMsTwM0zwMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQNI0QPM8QPM8AAAAAAAAANA8D/BEEfBEEQAAAAAAAAAszwM80QM8UQQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwNE0QPM8QPM8AAAAAAAAALA8D/BEEfA8EQAAAAAAAAA0zwM8UQQ8UQQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABDgAAAQYCEUGrIiAIgTADA4DjQNmgbPAziWBc+D50EUAY5lwfPgeRBFAAAAAAAAAAAAADTPg6pCVeGqAM3zYKpQVaguAAAAAAAAAAAAAJbnQVWhqnBdgOV5MFWYKlQVAAAAAAAAAAAAAE8UobpQXbgqwDNFuCpcFaoLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAABhwAAAIMKEMFBqyIgCIEwBwOIplAQCA4ziWBQAAjuNYFgAAWJYligAAYFmaKAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAGHAAAAgwoQwUGrISAIgCADAoimUBy7IsYFmWBTTNsgCWBtA8gOcBRBEACAAAKHAAAAiwQVNicYBCQ1YCAFEAAAZFsSxNE0WapmmaJoo0TdM0TRR5nqZ5nmlC0zzPNCGKnmeaEEXPM02YpiiqKhBFVRUAAFDgAAAQYIOmxOIAhYasBABCAgAMjmJZnieKoiiKpqmqNE3TPE8URdE0VdVVaZqmeZ4oiqJpqqrq8jxNE0XTFEXTVFXXhaaJommaommqquvC80TRNE1TVVXVdeF5omiapqmqruu6EEVRNE3TVFXXdV0giqZpmqrqurIMRNE0VVVVXVeWgSiapqqqquvKMjBN01RV15VdWQaYpqq6rizLMkBVXdd1ZVm2Aarquq4ry7INcF3XlWVZtm0ArivLsmzbAgAADhwAAAKMoJOMKouw0YQLD0ChISsCgCgAAMAYphRTyjAmIaQQGsYkhBJCJiWVlEqqIKRSUikVhFRSKiWjklJqKVUQUikplQpCKqWVVAAA2IEDANiBhVBoyEoAIA8AgCBGKcYYYwwyphRjzjkHlVKKMeeck4wxxphzzkkpGWPMOeeklIw555xzUkrmnHPOOSmlc84555yUUkrnnHNOSiklhM45J6WU0jnnnBMAAFTgAAAQYKPI5gQjQYWGrAQAUgEADI5jWZqmaZ4nipYkaZrneZ4omqZmSZrmeZ4niqbJ8zxPFEXRNFWV53meKIqiaaoq1xVF0zRNVVVVsiyKpmmaquq6ME3TVFXXdWWYpmmqquu6LmzbVFXVdWUZtq2aqiq7sgxcV3Vl17aB67qu7Nq2AADwBAcAoAIbVkc4KRoLLDRkJQCQAQBAGIOMQgghhRBCCiGElFIICQAAGHAAAAgwoQwUGrISAEgFAACQsdZaa6211kBHKaWUUkqpcIxSSimllFJKKaWUUkoppZRKSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoFAC5VOADoPtiwOsJJ0VhgoSErAYBUAADAGKWYck5CKRVCjDkmIaUWK4QYc05KSjEWzzkHoZTWWiyecw5CKa3FWFTqnJSUWoqtqBQyKSml1mIQwpSUWmultSCEKqnEllprQQhdU2opltiCELa2klKMMQbhg4+xlVhqDD74IFsrMdVaAABmgwMARIINqyOcFI0FFhqyEgAICQAgjFGKMcYYc8455yRjjDHmnHMQQgihZIwx55xzDkIIIZTOOeeccxBCCCGEUkrHnHMOQgghhFBS6pxzEEIIoYQQSiqdcw5CCCGEUkpJpXMQQgihhFBCSSWl1DkIIYQQQikppZRCCCGEEkIoJaWUUgghhBBCKKGklFIKIYRSQgillJRSSimFEEoIpZSSUkkppRJKCSGEUlJJKaUUQggllFJKKimllEoJoYRSSimlpJRSSiGUUEIpBQAAHDgAAAQYQScZVRZhowkXHoBCQ1YCAGQAAJSyUkoorVVAIqUYpNpCR5mDFHOJLHMMWs2lYg4pBq2GyjGlGLQWMgiZUkxKCSV1TCknLcWYSuecpJhzjaVzEAAAAEEAgICQAAADBAUzAMDgAOFzEHQCBEcbAIAgRGaIRMNCcHhQCRARUwFAYoJCLgBUWFykXVxAlwEu6OKuAyEEIQhBLA6ggAQcnHDDE294wg1O0CkqdSAAAAAAAAwA8AAAkFwAERHRzGFkaGxwdHh8gISIjJAIAAAAAAAYAHwAACQlQERENHMYGRobHB0eHyAhIiMkAQCAAAIAAAAAIIAABAQEAAAAAAACAAAABARPZ2dTAARhGAAAAAAAAFUPGmkCAAAAO/2ofAwjXh4fIzYx6uqzbla00kVmK6iQVrrIbAUVUqrKzBmtJH2+gRvgBmJVbdRjKgQGAlI5/X/Ofo9yCQZsoHL6/5z9HuUSDNgAAAAACIDB4P/BQA4NcAAHhzYgQAhyZEChScMgZPzmQwZwkcYjJguOaCaT6Sp/Kand3Luej5yp9HApCHVtClzDUAdARABQMgC00kVNVxCUVrqo6QqCoqpkHqdBZaA+ViWsfXWfDxS00kVNVxDkVrqo6QqCjKoGkDPMI4eZeZZqpq8aZ9AMtNJFzVYQ1Fa6qNkKgqoiGrbSkmkbqXv3aIeKI/3mh4gORh4cy6gShGMZVYJwm9SKkJkzqK64CkyLTGbMGExnzhyrNcyYMQl0nE4rwzDkq0+D/PO1japBzB9E1XqdAUTVep0BnDStQJsDk7gaNQK5UeTMGgwzILIr00nCYH0Gd4wp1aAOEwlvhGwA2nl9c0KAu9LTJUSPIOXVyCVQpPP65oQAd6WnS4geQcqrkUugiC8QZa1eq9eqRUYCAFAWY/oggB0gm5gFWYhtgB6gSIeJS8FxMiAGycBBm2ABURdHBNQRQF0JAJDJ8PhkMplMJtcxH+aYTMhkjut1vXIdkwEAHryuAQAgk/lcyZXZ7Darzd2J3RBRoGf+V69evXJtviwAxOMBNqACAAIoAAAgM2tuRDEpAGAD0Khcc8kAQDgMAKDRbGlmFJENAACaaSYCoJkoAAA6mKlYAAA6TgBwxpkKAIDrBACdBAwA8LyGDACacTIRBoAA/in9zlAB4aA4Vczai/R/roGKBP4+pd8ZKiAcFKeKWXuR/s81UJHAn26QimqtBBQ2MW2QKUBUG+oBegpQ1GslgCIboA3IoId6DZeCg2QgkAyIQR3iYgwursY4RgGEH7/rmjBQwUUVgziioIgrroJRBECGTxaUDEAgvF4nYCagzZa1WbJGkhlJGobRMJpMM0yT0Z/6TFiwa/WXHgAKwAABmgLQiOy5yTVDATQdAACaDYCKrDkyA4A2TgoAAB1mTgpAGycjAAAYZ0yjxAEAmQ6FcQWAR4cHAOhDKACAeGkA0WEaGABQSfYcWSMAHhn9f87rKPpQpe8viN3YXQ08cCAy+v+c11H0oUrfXxC7sbsaeOAAmaAXkPWQ6sBBKRAe/UEYxiuPH7/j9bo+M0cAE31NOzEaVBBMChqRNUdWWTIFGRpCZo7ssuXMUBwgACpJZcmZRQMFQJNxMgoCAGKcjNEAEnoDqEoD1t37wH7KXc7FayXfFzrSQHQ7nxi7yVsKXN6eo7ewMrL+kxn/0wYf0gGXcpEoDSQI4CABFsAJ8AgeGf1/zn9NcuIMGEBk9P85/zXJiTNgAAAAPPz/rwAEHBDgGqgSAgQQAuaOAHj6ELgGOaBqRSpIg+J0EC3U8kFGa5qapr41xuXsTB/BpNn2BcPaFfV5vCYu12wisH/m1IkQmqJLYAKBHAAQBRCgAR75/H/Of01yCQbiZkgoRD7/n/Nfk1yCgbgZEgoAAAAAEADBcPgHQRjEAR4Aj8HFGaAAeIATDng74SYAwgEn8BBHUxA4Tyi3ZtOwTfcbkBQ4DAImJ6AA"></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 | |||
| 55 | The desktop already has the `<webview>` and could be the sole source of truth. We route through the server anyway because: | ||
| 56 | |||
| 57 | 1. Reconnect/restart resilience matches the rest of t3code — terminal sessions, orchestration, etc. all use snapshot+replay. | ||
| 58 | 2. A future second window or a remote viewer (e.g. mobile observer) sees the same URL the desktop is on. | ||
| 59 | 3. 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 | |||
| 66 | The web side merges them in `previewStateStore.applyServerEvent` / `applyDesktopState`. | ||
| 67 | |||
| 68 | ### Web build behaviour | ||
| 69 | |||
| 70 | When `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 | |||
| 83 | Today 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 | |||
| 88 | They co‑exist by accident. With a third panel we need an explicit arbiter. | ||
| 89 | |||
| 90 | ### New: `apps/web/src/rightPanelStore.ts` | ||
| 91 | |||
| 92 | ```ts | ||
| 93 | import { create } from "zustand"; | ||
| 94 | import { persist, createJSONStorage } from "zustand/middleware"; | ||
| 95 | import { type ScopedThreadRef, scopedThreadKey } from "@t3tools/client-runtime"; | ||
| 96 | import { resolveStorage } from "./lib/storage"; | ||
| 97 | |||
| 98 | export type RightPanelKind = "plan" | "diff" | "preview"; | ||
| 99 | |||
| 100 | interface ThreadRightPanelState { | ||
| 101 | /** null = closed; otherwise the active panel for this thread */ | ||
| 102 | active: RightPanelKind | null; | ||
| 103 | } | ||
| 104 | |||
| 105 | const RIGHT_PANEL_STORAGE_KEY = "t3code:right-panel-state:v1"; | ||
| 106 | |||
| 107 | interface 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 | |||
| 114 | export const useRightPanelStore = create<RightPanelStoreState>()( | ||
| 115 | persist(/* … */, { | ||
| 116 | name: RIGHT_PANEL_STORAGE_KEY, | ||
| 117 | storage: createJSONStorage(() => resolveStorage(window.localStorage)), | ||
| 118 | version: 1, | ||
| 119 | }), | ||
| 120 | ); | ||
| 121 | |||
| 122 | export 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 | |||
| 150 | The single render decision in `ChatView.tsx`: | ||
| 151 | |||
| 152 | ```tsx | ||
| 153 | const activeRightPanel = useRightPanelStore((s) => | ||
| 154 | selectActiveRightPanel(s, activeThreadRef), | ||
| 155 | ); | ||
| 156 | const 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 | ||
| 234 | import { Schema } from "effect"; | ||
| 235 | import { TrimmedNonEmptyString } from "./baseSchemas.ts"; | ||
| 236 | |||
| 237 | export const PreviewTabId = TrimmedNonEmptyString.check(Schema.isMaxLength(128)); | ||
| 238 | export type PreviewTabId = typeof PreviewTabId.Type; | ||
| 239 | |||
| 240 | export 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 | ]); | ||
| 260 | export type PreviewNavStatus = typeof PreviewNavStatus.Type; | ||
| 261 | |||
| 262 | export 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 | }); | ||
| 270 | export type PreviewSessionSnapshot = typeof PreviewSessionSnapshot.Type; | ||
| 271 | |||
| 272 | export const PreviewOpenInput = Schema.Struct({ | ||
| 273 | threadId: TrimmedNonEmptyString, | ||
| 274 | url: TrimmedNonEmptyString, | ||
| 275 | }); | ||
| 276 | export const PreviewNavigateInput = Schema.Struct({ | ||
| 277 | threadId: TrimmedNonEmptyString, | ||
| 278 | tabId: PreviewTabId, | ||
| 279 | url: TrimmedNonEmptyString, | ||
| 280 | }); | ||
| 281 | export const PreviewRefreshInput = Schema.Struct({ | ||
| 282 | threadId: TrimmedNonEmptyString, | ||
| 283 | tabId: PreviewTabId, | ||
| 284 | }); | ||
| 285 | export const PreviewCloseInput = Schema.Struct({ | ||
| 286 | threadId: TrimmedNonEmptyString, | ||
| 287 | tabId: Schema.optional(PreviewTabId), | ||
| 288 | }); | ||
| 289 | |||
| 290 | const PreviewEventBase = Schema.Struct({ | ||
| 291 | threadId: TrimmedNonEmptyString, | ||
| 292 | tabId: PreviewTabId, | ||
| 293 | createdAt: Schema.String, | ||
| 294 | }); | ||
| 295 | |||
| 296 | export 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 | ]); | ||
| 318 | export type PreviewEvent = typeof PreviewEvent.Type; | ||
| 319 | |||
| 320 | export 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 | |||
| 329 | export 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 | |||
| 338 | export const PreviewError = Schema.Union([PreviewSessionLookupError, PreviewInvalidUrlError]); | ||
| 339 | export type PreviewError = typeof PreviewError.Type; | ||
| 340 | ``` | ||
| 341 | |||
| 342 | ### `ProjectScript` extension (`packages/contracts/src/project.ts`) | ||
| 343 | |||
| 344 | Add to the existing `ProjectScript` struct (additive, default both fields to undefined/false at runtime so existing persisted scripts decode unchanged): | ||
| 345 | |||
| 346 | ```ts | ||
| 347 | previewUrl: Schema.optional(TrimmedNonEmptyString), | ||
| 348 | autoOpenPreview: Schema.optional(Schema.Boolean), | ||
| 349 | ``` | ||
| 350 | |||
| 351 | ### Keybindings (`packages/contracts/src/keybindings.ts:50`) | ||
| 352 | |||
| 353 | ```ts | ||
| 354 | const 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 | |||
| 370 | Add `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 | ||
| 387 | import { Context, Effect } from "effect"; | ||
| 388 | import { | ||
| 389 | PreviewCloseInput, | ||
| 390 | PreviewError, | ||
| 391 | PreviewEvent, | ||
| 392 | PreviewNavigateInput, | ||
| 393 | PreviewOpenInput, | ||
| 394 | PreviewRefreshInput, | ||
| 395 | PreviewSessionSnapshot, | ||
| 396 | } from "@t3tools/contracts"; | ||
| 397 | |||
| 398 | export 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 | |||
| 411 | export 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 | |||
| 418 | In‑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 | |||
| 420 | URL normalization mirrors ami's helper (`browser-view-manager.ts:655`): | ||
| 421 | |||
| 422 | ```ts | ||
| 423 | const 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 | |||
| 439 | Following the existing terminal route pattern, expose: | ||
| 440 | |||
| 441 | ```ts | ||
| 442 | preview: { | ||
| 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 | |||
| 462 | Subset of ami's `BrowserManager`: | ||
| 463 | |||
| 464 | ```ts | ||
| 465 | import * as path from "node:path"; | ||
| 466 | import { type BrowserWindow, type Session, session, webContents } from "electron"; | ||
| 467 | |||
| 468 | const PREVIEW_PARTITION = "persist:t3code-preview"; | ||
| 469 | |||
| 470 | export 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 | |||
| 476 | export 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 | |||
| 486 | type Listener = (tabId: string, state: TabState) => void; | ||
| 487 | |||
| 488 | export 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 | |||
| 713 | export class PreviewTabNotFoundError extends Error { | ||
| 714 | constructor(public readonly tabId: string) { | ||
| 715 | super(`Preview tab not found: ${tabId}`); | ||
| 716 | } | ||
| 717 | } | ||
| 718 | export class PreviewWebContentsNotFoundError extends Error { | ||
| 719 | /* … */ | ||
| 720 | } | ||
| 721 | export class PreviewWebviewNotInitializedError extends Error { | ||
| 722 | /* … */ | ||
| 723 | } | ||
| 724 | export class PreviewInvalidUrlError extends Error { | ||
| 725 | /* … */ | ||
| 726 | } | ||
| 727 | |||
| 728 | export const previewViewManager = new PreviewViewManager(); | ||
| 729 | ``` | ||
| 730 | |||
| 731 | ### `apps/desktop/src/main.ts` additions | ||
| 732 | |||
| 733 | Right after `mainWindow = createWindow();` in `bootstrap()` and after every recreation: | ||
| 734 | |||
| 735 | ```ts | ||
| 736 | previewViewManager.setMainWindow(mainWindow); | ||
| 737 | ``` | ||
| 738 | |||
| 739 | Register IPC handlers (in `registerIpcHandlers()`): | ||
| 740 | |||
| 741 | ```ts | ||
| 742 | ipcMain.handle("preview:createTab", (_e, tabId: string) => previewViewManager.createTab(tabId)); | ||
| 743 | ipcMain.handle("preview:closeTab", (_e, tabId: string) => previewViewManager.closeTab(tabId)); | ||
| 744 | ipcMain.handle("preview:setVisibility", (_e, tabId: string, visible: boolean) => | ||
| 745 | previewViewManager.setVisibility(tabId, visible), | ||
| 746 | ); | ||
| 747 | ipcMain.handle("preview:registerWebview", (_e, tabId: string, wcId: number) => | ||
| 748 | previewViewManager.registerWebview(tabId, wcId), | ||
| 749 | ); | ||
| 750 | ipcMain.handle("preview:unregisterWebview", (_e, tabId: string) => | ||
| 751 | previewViewManager.unregisterWebview(tabId), | ||
| 752 | ); | ||
| 753 | ipcMain.handle("preview:navigate", (_e, tabId: string, url: string) => | ||
| 754 | previewViewManager.navigate(tabId, url), | ||
| 755 | ); | ||
| 756 | ipcMain.handle("preview:goBack", (_e, tabId: string) => previewViewManager.goBack(tabId)); | ||
| 757 | ipcMain.handle("preview:goForward", (_e, tabId: string) => previewViewManager.goForward(tabId)); | ||
| 758 | ipcMain.handle("preview:refresh", (_e, tabId: string) => previewViewManager.refresh(tabId)); | ||
| 759 | ipcMain.handle("preview:getPreloadPath", () => previewViewManager.getPreloadPath()); | ||
| 760 | ipcMain.handle("preview:getBrowserPartition", () => previewViewManager.getBrowserPartition()); | ||
| 761 | ``` | ||
| 762 | |||
| 763 | State change broadcast (push to all renderer windows): | ||
| 764 | |||
| 765 | ```ts | ||
| 766 | previewViewManager.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 | ||
| 777 | contextBridge.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 | |||
| 810 | Build 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 | |||
| 818 | Direct 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 | ||
| 821 | import { type ScopedThreadRef, scopedThreadKey } from "@t3tools/client-runtime"; | ||
| 822 | import { create } from "zustand"; | ||
| 823 | import { createJSONStorage, persist } from "zustand/middleware"; | ||
| 824 | import type { PreviewEvent, PreviewSessionSnapshot } from "@t3tools/contracts"; | ||
| 825 | import { resolveStorage } from "./lib/storage"; | ||
| 826 | |||
| 827 | interface 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 | |||
| 841 | interface PreviewEventEntry { id: number; event: PreviewEvent } | ||
| 842 | |||
| 843 | interface 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 | |||
| 856 | const PERSISTED_FIELDS = ["byThreadKey"] as const; | ||
| 857 | const STORAGE_KEY = "t3code:preview-state:v1"; | ||
| 858 | |||
| 859 | export 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 | |||
| 870 | export 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 | |||
| 879 | Key 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 | ||
| 888 | preview: { | ||
| 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 | |||
| 898 | Plus 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 | |||
| 906 | Lifted 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 | |||
| 919 | So preview side panel is visually indistinguishable in spacing from the diff panel. | ||
| 920 | |||
| 921 | ### `apps/web/src/components/preview/PreviewView.tsx` | ||
| 922 | |||
| 923 | Renderer 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 | |||
| 925 | Key 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 | |||
| 938 | import { useEffect, useState } from "react"; | ||
| 939 | import { isDesktop } from "~/env"; | ||
| 940 | |||
| 941 | interface Props { | ||
| 942 | tabId: string; | ||
| 943 | initialUrl: string | null; | ||
| 944 | } | ||
| 945 | |||
| 946 | declare global { | ||
| 947 | interface HTMLElementTagNameMap { | ||
| 948 | webview: Electron.WebviewTag; | ||
| 949 | } | ||
| 950 | } | ||
| 951 | |||
| 952 | export 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 | |||
| 981 | The 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 | |||
| 985 | Following `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 | |||
| 987 | For 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 | |||
| 995 | Mirror the existing `chat.new` block (`:51–:78`): | ||
| 996 | |||
| 997 | ```ts | ||
| 998 | if (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 | |||
| 1015 | In `ThreadTerminalDrawer.tsx`'s `terminal.registerLinkProvider({ provideLinks })` callback (`:454–:514`), update the `activate(event)` handler: | ||
| 1016 | |||
| 1017 | ```ts | ||
| 1018 | activate: (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 | |||
| 1051 | Schema 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 | |||
| 1072 | When `onRunScript(script)` fires in `ChatView.tsx`, after starting the terminal command, if `script.autoOpenPreview && script.previewUrl && desktopBridge.preview`: | ||
| 1073 | |||
| 1074 | ```ts | ||
| 1075 | void api.preview.open({ threadId: activeThread.id, url: script.previewUrl }); | ||
| 1076 | rightPanelStore.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 | |||
| 1095 | Adding `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 | |||
| 1101 | Following 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 | |||
| 1112 | Manual smoke checklist (drop in `apps/desktop/test/smoke/`): | ||
| 1113 | |||
| 1114 | 1. Boot desktop dev, open a thread, hit `mod+shift+j` → empty state appears in right panel. | ||
| 1115 | 2. Type `https://example.com` → page loads, title updates in chrome bar. | ||
| 1116 | 3. Navigate to `https://example.org` → back/forward enable correctly. | ||
| 1117 | 4. Hit `mod+r` → page reloads, loading bar animates. | ||
| 1118 | 5. Run a `bun dev` script in terminal printing `http://localhost:5173`, link in xterm → context menu offers "Open in preview". | ||
| 1119 | 6. Restart server (kill `apps/server` child, watch it respawn) → preview tab survives, server replays `opened` event on reconnect, panel state matches reality. | ||
| 1120 | 7. Switch threads → previous thread's preview tab is closed (v1); new thread shows empty state. | ||
| 1121 | 8. 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 | |||
| 1141 | Explicitly **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 | |||
| 1154 | Any 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 | |||
| 1160 | Every 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 | ||
| 1185 | import { | ||
| 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 | |||
| 1210 | Three 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 | |||
| 1220 | The 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 | |||
| 1222 | Layout 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 | |||
| 1250 | Skeleton (paste-able): | ||
| 1251 | |||
| 1252 | ```tsx | ||
| 1253 | // apps/web/src/components/preview/PreviewEmptyState.tsx | ||
| 1254 | "use client"; | ||
| 1255 | |||
| 1256 | import { useDiscoveredLocalServers } from "~/components/preview/useDiscoveredLocalServers"; | ||
| 1257 | import { Card } from "~/components/ui/card"; | ||
| 1258 | import { Empty, EmptyDescription, EmptyTitle } from "~/components/ui/empty"; | ||
| 1259 | |||
| 1260 | interface Props { | ||
| 1261 | threadId: string; | ||
| 1262 | onOpen: (url: string) => void; | ||
| 1263 | } | ||
| 1264 | |||
| 1265 | export 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 | |||
| 1292 | function 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 | |||
| 1326 | The 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 | ||
| 1330 | import { cn } from "~/lib/utils"; | ||
| 1331 | |||
| 1332 | export 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 | |||
| 1355 | The 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 | |||
| 1376 | The 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 | |||
| 1390 | For 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 | |||
| 1396 | import { Globe, Plus, Sidebar, X } from "lucide-react"; | ||
| 1397 | import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip"; | ||
| 1398 | import { cn } from "~/lib/utils"; | ||
| 1399 | import { faviconUrlForOrigin } from "~/lib/favicon"; | ||
| 1400 | |||
| 1401 | interface PreviewTabDescriptor { | ||
| 1402 | tabId: string; | ||
| 1403 | title: string; | ||
| 1404 | url: string | null; | ||
| 1405 | active: boolean; | ||
| 1406 | } | ||
| 1407 | |||
| 1408 | interface 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 | |||
| 1417 | export 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 | |||
| 1482 | function 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 | |||
| 1526 | Borrowed 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 | ||
| 1530 | const FAVICON_PROVIDER = "https://www.google.com/s2/favicons"; | ||
| 1531 | |||
| 1532 | export 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 | ||
| 1546 | import { Globe } from "lucide-react"; | ||
| 1547 | import { useEffect, useState } from "react"; | ||
| 1548 | import { faviconUrlForOrigin } from "~/lib/favicon"; | ||
| 1549 | |||
| 1550 | export 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 | |||
| 1571 | When `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 | |||
| 1573 | Color 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 | |||
| 1587 | Skeleton: | ||
| 1588 | |||
| 1589 | ```tsx | ||
| 1590 | // apps/web/src/components/preview/PreviewUnreachable.tsx | ||
| 1591 | "use client"; | ||
| 1592 | |||
| 1593 | import { useState } from "react"; | ||
| 1594 | import { Button } from "~/components/ui/button"; | ||
| 1595 | import { cn } from "~/lib/utils"; | ||
| 1596 | |||
| 1597 | interface Props { | ||
| 1598 | url: string; | ||
| 1599 | errorCode: string; // e.g. "ERR_NAME_NOT_RESOLVED", "ERR_CONNECTION_REFUSED" | ||
| 1600 | description: string; | ||
| 1601 | onReload: () => void; | ||
| 1602 | } | ||
| 1603 | |||
| 1604 | const 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 | |||
| 1621 | export 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 | |||
| 1686 | function safeHost(url: string): string | null { | ||
| 1687 | try { | ||
| 1688 | return new URL(url).host; | ||
| 1689 | } catch { | ||
| 1690 | return null; | ||
| 1691 | } | ||
| 1692 | } | ||
| 1693 | ``` | ||
| 1694 | |||
| 1695 | The 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 | |||
| 1704 | A 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 | |||
| 1720 | Progress 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 | |||
| 1726 | The "Local" recommendations in §15.1 need a feed. New backend service. | ||
| 1727 | |||
| 1728 | ### `apps/server/src/preview/Services/PortScanner.ts` | ||
| 1729 | |||
| 1730 | ```ts | ||
| 1731 | import { Context, Effect } from "effect"; | ||
| 1732 | |||
| 1733 | export 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 | |||
| 1741 | export 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 | |||
| 1752 | export 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 | |||
| 1760 | Two strategies, picked at startup: | ||
| 1761 | |||
| 1762 | 1. **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. | ||
| 1763 | 2. **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 | |||
| 1765 | Polling 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 | |||
| 1771 | The 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 | |||
| 1775 | The card list in §15.1 is the union of: | ||
| 1776 | |||
| 1777 | 1. **Listening ports** from the scanner (above). | ||
| 1778 | 2. **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`). | ||
| 1779 | 3. **Configured URLs** — every active `ProjectScript.previewUrl` from the active project. | ||
| 1780 | |||
| 1781 | Deduped 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 | |||
| 1787 | Append 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 | |||
| 1804 | Update the contract additions in §4 to add: | ||
| 1805 | |||
| 1806 | ```ts | ||
| 1807 | // packages/contracts/src/preview.ts | ||
| 1808 | export 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 | }); | ||
| 1815 | export type DiscoveredLocalServer = typeof DiscoveredLocalServer.Type; | ||
| 1816 | ``` | ||
| 1817 | |||
| 1818 | WS routes added (§5): | ||
| 1819 | |||
| 1820 | ``` | ||
| 1821 | preview.subscribePorts(callback) → unsubscribe | ||
| 1822 | preview.scanPortsOnce() → ReadonlyArray<DiscoveredLocalServer> | ||
| 1823 | ``` | ||
| 1824 | |||
| 1825 | --- | ||
| 1826 | |||
| 1827 | ## 18. Implementation order (single‑shot landing) — REVISED | ||
| 1828 | |||
| 1829 | To minimize cross‑file thrash while writing: | ||
| 1830 | |||
| 1831 | 1. `packages/contracts/src/preview.ts` (+ test) — `PreviewSession`, `PreviewEvent`, `DiscoveredLocalServer` schemas; `keybindings.ts` / `project.ts` schema edits → **build contracts first**. | ||
| 1832 | 2. `apps/server/src/preview/{Services,Layers}/Manager.ts` (+ test) and `{Services,Layers}/PortScanner.ts` (+ test). | ||
| 1833 | 3. `apps/server/src/ws.ts` and `runtimeLayer` provision (`PreviewManager.Default`, `PreviewPortScanner.Default`). | ||
| 1834 | 4. `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`. | ||
| 1835 | 5. `apps/web/src/lib/favicon.ts`; `apps/web/src/previewStateStore.ts`, `apps/web/src/rightPanelStore.ts` (+ tests). | ||
| 1836 | 6. `apps/web/src/environmentApi.ts` — add `preview` slot. | ||
| 1837 | 7. Right‑panel arbiter migration in `ChatView.tsx` (replace `planSidebarOpen`, render preview alongside diff/plan). | ||
| 1838 | 8. 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` | ||
| 1846 | 9. Keybinding wiring: `_chat.tsx` `preview.toggle`/`preview.refresh`/`preview.focusUrl`; `keybindings.ts` shortcut helpers. | ||
| 1847 | 10. `ProjectScriptsControl.tsx` form additions (`previewUrl`, `autoOpenPreview`). | ||
| 1848 | 11. `ThreadTerminalDrawer.tsx` link‑activation update — context menu "Open in preview" / "Open in browser" for previewable URLs. | ||
| 1849 | 12. `KEYBINDINGS.md` doc update — add `preview.*` commands and `previewFocus` / `previewOpen` `when` keys. | ||
| 1850 | 13. Run `bun fmt && bun lint && bun typecheck && bun run test` — must all pass per `AGENTS.md`. | ||
| 1851 | 14. Manual smoke against the dev desktop (`bun run dev:desktop`). | ||
| 1852 | |||
| 1853 | Estimated 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`. | ||