A tool for people curious about the React Server Components protocol
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

clean up styles

+1587 -2385
-1174
index.html
··· 37 37 background: var(--bg); 38 38 color: var(--text); 39 39 } 40 - header { 41 - height: 44px; 42 - padding: 0 16px; 43 - display: flex; 44 - align-items: center; 45 - gap: 16px; 46 - border-bottom: 1px solid var(--border); 47 - background: var(--surface); 48 - flex-shrink: 0; 49 - position: relative; 50 - z-index: 100; 51 - } 52 - h1 { 53 - margin: 0; 54 - font-size: 13px; 55 - font-weight: 600; 56 - color: var(--text); 57 - } 58 - .example-select-wrapper { 59 - display: flex; 60 - align-items: center; 61 - gap: 10px; 62 - padding-left: 16px; 63 - border-left: 1px solid var(--border); 64 - } 65 - .example-select-wrapper label { 66 - font-size: 11px; 67 - color: var(--text-dim); 68 - text-transform: uppercase; 69 - letter-spacing: 0.5px; 70 - } 71 - header select { 72 - -webkit-appearance: none; 73 - appearance: none; 74 - background: 75 - url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2.5'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") 76 - no-repeat right 8px center, 77 - linear-gradient(to bottom, #333, #2a2a2a); 78 - border: 1px solid #555; 79 - color: #fff; 80 - padding: 5px 28px 5px 10px; 81 - border-radius: 4px; 82 - font-size: 13px; 83 - font-weight: 500; 84 - cursor: pointer; 85 - min-width: 150px; 86 - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); 87 - } 88 - header select:hover { 89 - border-color: #666; 90 - background: 91 - url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23bbb' stroke-width='2.5'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") 92 - no-repeat right 8px center, 93 - linear-gradient(to bottom, #3a3a3a, #333); 94 - } 95 - header select:focus { 96 - outline: none; 97 - border-color: #ffd54f; 98 - } 99 - header select option { 100 - background: #2a2a2a; 101 - color: #e0e0e0; 102 - padding: 8px; 103 - font-size: 13px; 104 - } 105 - header select option:hover, 106 - header select option:checked { 107 - background: #3a3a3a; 108 - color: #ffd54f; 109 - } 110 - header .save-btn { 111 - background: var(--surface); 112 - border: 1px solid var(--border); 113 - color: var(--text); 114 - padding: 5px 8px; 115 - border-radius: 4px; 116 - cursor: pointer; 117 - display: flex; 118 - align-items: center; 119 - justify-content: center; 120 - } 121 - header .save-btn:hover:not(:disabled) { 122 - border-color: #444; 123 - background: #2a2a2a; 124 - } 125 - header .save-btn:disabled { 126 - opacity: 0.4; 127 - cursor: not-allowed; 128 - } 129 - header .embed-btn { 130 - background: var(--surface); 131 - border: 1px solid var(--border); 132 - color: var(--text); 133 - padding: 5px 8px; 134 - border-radius: 4px; 135 - cursor: pointer; 136 - display: flex; 137 - align-items: center; 138 - justify-content: center; 139 - } 140 - header .embed-btn:hover:not(:disabled) { 141 - border-color: #444; 142 - background: #2a2a2a; 143 - } 144 - header .embed-btn:disabled { 145 - opacity: 0.4; 146 - cursor: not-allowed; 147 - } 148 - /* Modal */ 149 - .modal-overlay { 150 - position: fixed; 151 - inset: 0; 152 - background: rgba(0, 0, 0, 0.7); 153 - display: flex; 154 - align-items: center; 155 - justify-content: center; 156 - z-index: 1000; 157 - } 158 - .modal { 159 - background: var(--surface); 160 - border: 1px solid var(--border); 161 - border-radius: 8px; 162 - width: 90%; 163 - max-width: 600px; 164 - max-height: 80vh; 165 - display: flex; 166 - flex-direction: column; 167 - } 168 - .modal-header { 169 - display: flex; 170 - align-items: center; 171 - justify-content: space-between; 172 - padding: 16px; 173 - border-bottom: 1px solid var(--border); 174 - } 175 - .modal-header h2 { 176 - margin: 0; 177 - font-size: 16px; 178 - font-weight: 600; 179 - color: var(--text-bright); 180 - } 181 - .modal-close { 182 - background: none; 183 - border: none; 184 - color: var(--text-dim); 185 - font-size: 24px; 186 - cursor: pointer; 187 - padding: 0; 188 - line-height: 1; 189 - } 190 - .modal-close:hover { 191 - color: var(--text-bright); 192 - } 193 - .modal-body { 194 - padding: 16px; 195 - overflow: auto; 196 - } 197 - .modal-body p { 198 - margin: 0 0 12px; 199 - font-size: 13px; 200 - color: var(--text); 201 - } 202 - .modal-body textarea { 203 - width: 100%; 204 - height: 250px; 205 - background: var(--bg); 206 - border: 1px solid var(--border); 207 - border-radius: 4px; 208 - color: var(--text); 209 - font-family: var(--font-mono); 210 - font-size: 12px; 211 - padding: 12px; 212 - resize: none; 213 - } 214 - .modal-body textarea:focus { 215 - outline: none; 216 - border-color: #555; 217 - } 218 - .modal-footer { 219 - padding: 16px; 220 - border-top: 1px solid var(--border); 221 - display: flex; 222 - justify-content: flex-end; 223 - } 224 - .copy-btn { 225 - background: #ffd54f; 226 - border: none; 227 - color: #000; 228 - padding: 8px 16px; 229 - border-radius: 4px; 230 - font-size: 13px; 231 - font-weight: 500; 232 - cursor: pointer; 233 - } 234 - .copy-btn:hover { 235 - background: #ffe566; 236 - } 237 - .header-spacer { 238 - flex: 1; 239 - } 240 - .build-switcher { 241 - display: flex; 242 - align-items: center; 243 - gap: 8px; 244 - } 245 - .build-switcher label { 246 - font-size: 11px; 247 - color: var(--text-dim); 248 - text-transform: uppercase; 249 - letter-spacing: 0.5px; 250 - } 251 - .build-switcher .mode-select { 252 - min-width: 70px; 253 - } 254 - .header-links { 255 - display: flex; 256 - align-items: center; 257 - gap: 8px; 258 - padding-right: 16px; 259 - border-right: 1px solid var(--border); 260 - } 261 - .github-link, 262 - .tangled-link { 263 - display: flex; 264 - align-items: center; 265 - justify-content: center; 266 - color: var(--text-dim); 267 - padding: 4px; 268 - border-radius: 4px; 269 - transition: color 0.15s; 270 - } 271 - .github-link:hover, 272 - .tangled-link:hover { 273 - color: var(--text-bright); 274 - } 275 - @media (max-width: 900px) { 276 - .header-links { 277 - display: none; 278 - } 279 - } 280 - @media (max-width: 768px) { 281 - header { 282 - padding: 0 8px; 283 - gap: 4px; 284 - } 285 - h1 { 286 - font-size: 11px; 287 - } 288 - .header-spacer { 289 - flex: 0; 290 - min-width: 0; 291 - } 292 - .example-select-wrapper { 293 - padding-left: 6px; 294 - margin-left: 2px; 295 - gap: 4px; 296 - } 297 - .example-select-wrapper label { 298 - display: none; 299 - } 300 - header select, 301 - header select:hover { 302 - min-width: 0; 303 - width: auto; 304 - padding: 4px 20px 4px 6px; 305 - font-size: 16px; /* Prevents iOS zoom */ 306 - background-position: right 4px center; 307 - } 308 - .build-switcher { 309 - padding-left: 6px; 310 - gap: 4px; 311 - margin-left: auto; 312 - } 313 - .build-switcher label { 314 - display: none; 315 - } 316 - .build-switcher select { 317 - min-width: 0; 318 - } 319 - main { 320 - grid-template-columns: 100%; 321 - grid-template-rows: 1fr 1fr 1fr 1fr; 322 - } 323 - .pane:nth-child(1), 324 - .pane:nth-child(3) { 325 - border-right: none; 326 - } 327 - .pane { 328 - border-bottom: 1px solid var(--border); 329 - } 330 - /* Prevent iOS zoom on all form elements */ 331 - input, 332 - textarea, 333 - select { 334 - font-size: 16px !important; 335 - } 336 - .raw-input-payload { 337 - font-size: 16px !important; 338 - } 339 - /* Playback controls responsive */ 340 - .playback-container { 341 - padding: 6px 8px; 342 - gap: 6px; 343 - } 344 - .playback-controls { 345 - gap: 2px; 346 - } 347 - .control-btn { 348 - width: 26px; 349 - height: 26px; 350 - } 351 - .step-info { 352 - min-width: 50px; 353 - font-size: 10px; 354 - } 355 - } 356 - @media (max-width: 480px) { 357 - header { 358 - padding: 0 6px; 359 - gap: 3px; 360 - } 361 - h1 { 362 - font-size: 10px; 363 - } 364 - header select { 365 - padding: 3px 18px 3px 5px; 366 - font-size: 16px; 367 - max-width: 80px; 368 - text-overflow: ellipsis; 369 - overflow: hidden; 370 - white-space: nowrap; 371 - } 372 - .example-select-wrapper select { 373 - max-width: 110px; 374 - } 375 - .example-select-wrapper { 376 - padding-left: 4px; 377 - margin-left: 0; 378 - border-left: none; 379 - } 380 - .build-switcher { 381 - padding-left: 4px; 382 - border-left: none; 383 - } 384 - /* Playback: hide slider and status, keep buttons compact */ 385 - .step-slider, 386 - .step-info { 387 - display: none; 388 - } 389 - .playback-container { 390 - padding: 4px 6px; 391 - gap: 4px; 392 - } 393 - .control-btn { 394 - width: 24px; 395 - height: 24px; 396 - } 397 - .control-btn svg { 398 - width: 14px; 399 - height: 14px; 400 - } 401 - } 402 - @media (max-width: 360px) { 403 - h1 { 404 - display: none; 405 - } 406 - header select { 407 - max-width: 75px; 408 - } 409 - } 410 - main { 411 - flex: 1; 412 - min-height: 0; 413 - display: grid; 414 - grid-template-columns: 50% 50%; 415 - grid-template-rows: 50% 50%; 416 - overflow: hidden; 417 - } 418 - .pane { 419 - display: flex; 420 - flex-direction: column; 421 - overflow: hidden; 422 - } 423 - /* Left column border */ 424 - .pane:nth-child(1), 425 - .pane:nth-child(3) { 426 - border-right: 1px solid var(--border); 427 - } 428 - /* Top row border */ 429 - .pane:nth-child(1), 430 - .pane:nth-child(2) { 431 - border-bottom: 1px solid var(--border); 432 - } 433 - .pane-header { 434 - padding: 6px 12px; 435 - font-size: 10px; 436 - text-transform: uppercase; 437 - letter-spacing: 1px; 438 - color: var(--text-dim); 439 - flex-shrink: 0; 440 - border-bottom: 1px solid var(--border); 441 - } 442 - .editor-container { 443 - flex: 1; 444 - min-height: 0; 445 - position: relative; 446 - overflow: hidden; 447 - background: var(--bg); 448 - } 449 - .editor-container .cm-editor { 450 - position: absolute !important; 451 - top: 0; 452 - left: 0; 453 - right: 0; 454 - bottom: 0; 455 - height: auto !important; 456 - background: transparent; 457 - } 458 - .editor-container .cm-editor .cm-scroller { 459 - overflow: auto !important; 460 - } 461 - .flight-output { 462 - flex: 1; 463 - min-height: 0; 464 - margin: 0; 465 - padding: 12px; 466 - font-family: var(--font-mono); 467 - font-size: 12px; 468 - line-height: 1.6; 469 - overflow: auto; 470 - white-space: pre-wrap; 471 - word-break: break-all; 472 - background: var(--bg); 473 - color: var(--text-dim); 474 - } 475 - .flight-output.error { 476 - color: #e57373; 477 - } 478 - .action-args { 479 - color: var(--text); 480 - } 481 - 482 - /* Flight log */ 483 - .flight-log { 484 - flex: 1; 485 - overflow: auto; 486 - padding: 8px; 487 - display: flex; 488 - flex-direction: column; 489 - gap: 6px; 490 - } 491 - .log-entry { 492 - background: var(--surface); 493 - border: 1px solid var(--border); 494 - border-radius: 4px; 495 - transition: all 0.15s ease; 496 - border-left: 3px solid #555; 497 - } 498 - .log-entry + .log-entry { 499 - margin-top: 12px; 500 - } 501 - .log-entry.active { 502 - border-left-color: #ffd54f; 503 - } 504 - .log-entry.done-entry { 505 - border-left-color: #555; 506 - opacity: 0.8; 507 - } 508 - .log-entry.pending-entry { 509 - border-left-color: #333; 510 - opacity: 0.4; 511 - } 512 - .log-entry-header { 513 - display: flex; 514 - align-items: center; 515 - justify-content: space-between; 516 - gap: 8px; 517 - padding: 6px 10px; 518 - font-size: 11px; 519 - border-bottom: 1px solid var(--border); 520 - } 521 - .log-entry-direction { 522 - font-family: var(--font-mono); 523 - font-weight: 600; 524 - color: var(--text-dim); 525 - } 526 - .log-entry.active .log-entry-direction { 527 - color: #ffd54f; 528 - } 529 - .log-entry-args { 530 - padding: 6px 10px; 531 - font-family: var(--font-mono); 532 - font-size: 11px; 533 - border-bottom: 1px solid var(--border); 534 - color: var(--text-dim); 535 - } 536 - .action-args-label { 537 - margin-right: 6px; 538 - color: var(--text-dim); 539 - } 540 - .log-entry-label { 541 - color: var(--text); 542 - font-weight: 500; 543 - } 544 - .log-entry-count { 545 - margin-left: auto; 546 - color: var(--text-dim); 547 - font-family: var(--font-mono); 548 - } 549 - .log-entry-content { 550 - margin: 0; 551 - padding: 8px 10px; 552 - font-family: var(--font-mono); 553 - font-size: 11px; 554 - line-height: 1.5; 555 - white-space: pre-wrap; 556 - word-break: break-all; 557 - } 558 - .log-entry .action-args { 559 - color: #81c784; 560 - } 561 - 562 - /* Action request (args display) */ 563 - .log-entry-request { 564 - padding: 8px 10px; 565 - background: rgba(0, 0, 0, 0.2); 566 - border-bottom: 1px solid var(--border); 567 - } 568 - .log-entry-request-args { 569 - margin: 0; 570 - font-family: var(--font-mono); 571 - font-size: 11px; 572 - line-height: 1.4; 573 - color: #81c784; 574 - white-space: pre-wrap; 575 - word-break: break-all; 576 - } 577 - 578 - /* Log entry preview (embedded scrubber) */ 579 - .log-entry-preview { 580 - border-top: 1px solid var(--border); 581 - padding: 8px; 582 - } 583 - .log-entry-preview-controls { 584 - display: flex; 585 - align-items: center; 586 - gap: 8px; 587 - margin-bottom: 8px; 588 - } 589 - .log-step-btn { 590 - background: var(--border); 591 - border: none; 592 - color: #ffd54f; 593 - width: 24px; 594 - height: 24px; 595 - border-radius: 3px; 596 - cursor: pointer; 597 - font-size: 10px; 598 - display: flex; 599 - align-items: center; 600 - justify-content: center; 601 - } 602 - .log-step-btn:hover:not(:disabled) { 603 - background: #444; 604 - } 605 - .log-step-btn:disabled { 606 - opacity: 0.3; 607 - cursor: not-allowed; 608 - } 609 - .log-entry-slider { 610 - flex: 1; 611 - height: 4px; 612 - -webkit-appearance: none; 613 - appearance: none; 614 - background: var(--border); 615 - border-radius: 2px; 616 - outline: none; 617 - } 618 - .log-entry-slider::-webkit-slider-thumb { 619 - -webkit-appearance: none; 620 - width: 14px; 621 - height: 14px; 622 - background: #ffd54f; 623 - border-radius: 50%; 624 - cursor: pointer; 625 - } 626 - .log-entry-slider::-moz-range-thumb { 627 - width: 14px; 628 - height: 14px; 629 - background: #ffd54f; 630 - border-radius: 50%; 631 - cursor: pointer; 632 - border: none; 633 - } 634 - .value-pending { 635 - color: #999; 636 - font-style: italic; 637 - font-size: 11px; 638 - } 639 - .value-loading { 640 - color: #999; 641 - font-style: italic; 642 - } 643 - .log-entry-step-info { 644 - font-size: 11px; 645 - color: var(--text-dim); 646 - font-family: var(--font-mono); 647 - } 648 - .log-entry-flight-lines { 649 - margin: 0; 650 - padding: 6px; 651 - background: var(--bg); 652 - border-radius: 3px; 653 - font-size: 11px; 654 - line-height: 1.4; 655 - overflow: auto; 656 - } 657 - .log-entry-flight-lines .flight-line { 658 - display: block; 659 - padding: 6px 8px; 660 - margin-bottom: 3px; 661 - border-radius: 4px; 662 - word-break: break-all; 663 - white-space: pre-wrap; 664 - border-left: 2px solid transparent; 665 - transition: all 0.15s ease; 666 - } 667 - .log-entry-flight-lines .flight-line:last-child { 668 - margin-bottom: 0; 669 - } 670 - .log-entry-flight-lines .line-done { 671 - color: #999; 672 - background: rgba(255, 255, 255, 0.03); 673 - border-left-color: #555; 674 - } 675 - .log-entry-flight-lines .line-next { 676 - color: #e0e0e0; 677 - background: rgba(255, 213, 79, 0.12); 678 - border-left-color: #ffd54f; 679 - } 680 - .log-entry-flight-lines .line-pending { 681 - color: #444; 682 - background: transparent; 683 - border-left-color: #333; 684 - opacity: 0.4; 685 - } 686 - 687 - /* Split layout: left=stream (scrolls), right=tree (dictates height) */ 688 - .log-entry-split { 689 - display: flex; 690 - gap: 8px; 691 - align-items: stretch; 692 - } 693 - .log-entry-split .log-entry-flight-lines-wrapper { 694 - flex: 1; 695 - min-width: 0; 696 - position: relative; 697 - min-height: 150px; 698 - } 699 - .log-entry-split .log-entry-flight-lines { 700 - position: absolute; 701 - top: 0; 702 - left: 0; 703 - right: 0; 704 - bottom: 0; 705 - } 706 - .log-entry-split .log-entry-tree { 707 - flex: 1; 708 - min-width: 0; 709 - display: flex; 710 - flex-direction: column; 711 - } 712 - /* Tree view in log entry - expands to fill height */ 713 - .log-entry-tree { 714 - flex: 1; 715 - min-width: 0; 716 - border-radius: 4px; 717 - overflow: auto; 718 - border: 1px solid var(--border); 719 - } 720 - .log-entry-tree:has(.flight-tree) { 721 - background: #000; 722 - } 723 - .log-entry-tree.full-width { 724 - flex: none; 725 - width: 100%; 726 - } 727 - .log-entry-tree .flight-tree { 728 - padding: 8px; 729 - font-size: 11px; 730 - line-height: 1.6; 731 - } 732 - .jsx-output { 733 - margin: 0; 734 - white-space: pre-wrap; 735 - word-break: break-word; 736 - color: var(--text); 737 - } 738 - .value-content { 739 - background: #f8f8f8; 740 - color: #111; 741 - font-family: -apple-system, BlinkMacSystemFont, sans-serif; 742 - padding: 8px; 743 - border-radius: 3px; 744 - margin: -8px; 745 - } 746 - .value-string { 747 - color: #22863a; 748 - } 749 - .value-number { 750 - color: #005cc5; 751 - } 752 - .value-boolean { 753 - color: #d73a49; 754 - } 755 - .value-null, 756 - .value-undefined { 757 - color: #6a737d; 758 - font-style: italic; 759 - } 760 - .value-key { 761 - color: #005cc5; 762 - } 763 - .value-indent { 764 - display: block; 765 - padding-left: 16px; 766 - } 767 - .value-array-item, 768 - .value-object-entry { 769 - display: block; 770 - } 771 - .value-react-element { 772 - display: inline; 773 - background: rgba(0, 0, 0, 0.04); 774 - padding: 2px 4px; 775 - border-radius: 3px; 776 - } 777 - .value-error { 778 - color: #e57373; 779 - font-style: italic; 780 - } 781 - .value-loading { 782 - color: var(--text-dim); 783 - font-style: italic; 784 - } 785 - 786 - /* Flight Tree View */ 787 - .flight-tree { 788 - flex: 1; 789 - min-height: 0; 790 - padding: 12px; 791 - font-family: var(--font-mono); 792 - font-size: 12px; 793 - line-height: 1.8; 794 - overflow: auto; 795 - background: var(--bg); 796 - } 797 - .tree-empty { 798 - color: var(--text-dim); 799 - font-style: italic; 800 - } 801 - .tree-element, 802 - .tree-client-component, 803 - .tree-suspense, 804 - .tree-lazy { 805 - margin-left: 0; 806 - } 807 - .tree-children { 808 - margin-left: 16px; 809 - border-left: 1px solid #333; 810 - padding-left: 12px; 811 - } 812 - .tree-tag { 813 - color: #e06c75; 814 - } 815 - .tree-client-tag { 816 - color: #c678dd; 817 - } 818 - .tree-client-ref { 819 - color: #5c6370; 820 - font-size: 10px; 821 - } 822 - .tree-react-tag { 823 - color: #e5c07b; 824 - } 825 - .tree-pending { 826 - display: inline-block; 827 - color: #64b5f6; 828 - background: rgba(100, 181, 246, 0.15); 829 - padding: 2px 8px; 830 - border-radius: 10px; 831 - border: 1px solid rgba(100, 181, 246, 0.4); 832 - font-size: 10px; 833 - font-weight: 500; 834 - letter-spacing: 0.5px; 835 - animation: pending-pulse 2s ease-in-out infinite; 836 - } 837 - @keyframes pending-pulse { 838 - 0%, 839 - 100% { 840 - opacity: 0.7; 841 - } 842 - 50% { 843 - opacity: 1; 844 - } 845 - } 846 - .tree-string { 847 - color: #98c379; 848 - } 849 - .tree-number { 850 - color: #d19a66; 851 - } 852 - .tree-boolean { 853 - color: #56b6c2; 854 - } 855 - .tree-null, 856 - .tree-undefined { 857 - color: #5c6370; 858 - font-style: italic; 859 - } 860 - .tree-key, 861 - .tree-prop-name { 862 - color: #61afef; 863 - } 864 - .tree-ref { 865 - color: #c678dd; 866 - } 867 - .tree-object { 868 - color: var(--text-dim); 869 - } 870 - .tree-array { 871 - /* Arrays render children inline */ 872 - } 873 - .tree-props { 874 - color: var(--text-dim); 875 - } 876 - .tree-prop { 877 - color: var(--text-dim); 878 - } 879 - .tree-error { 880 - display: inline-block; 881 - color: #e57373; 882 - background: rgba(229, 115, 115, 0.15); 883 - padding: 2px 8px; 884 - border-radius: 10px; 885 - border: 1px solid rgba(229, 115, 115, 0.4); 886 - font-size: 10px; 887 - font-weight: 500; 888 - letter-spacing: 0.5px; 889 - } 890 - .tree-pending-tag { 891 - color: #ffd54f; 892 - background: rgba(255, 213, 79, 0.1); 893 - padding: 1px 4px; 894 - border-radius: 3px; 895 - border: 1px solid rgba(255, 213, 79, 0.3); 896 - } 897 - .tree-error-keyword { 898 - color: #c678dd; 899 - font-weight: 600; 900 - } 901 - .tree-error-message { 902 - color: #e57373; 903 - } 904 - .tree-function { 905 - color: #61afef; 906 - font-style: italic; 907 - } 908 - .tree-placeholder { 909 - color: #444; 910 - font-style: italic; 911 - } 912 - 913 - /* Playback controls */ 914 - .playback-container { 915 - display: flex; 916 - align-items: center; 917 - gap: 10px; 918 - padding: 8px 12px; 919 - background: var(--surface); 920 - border-bottom: 1px solid var(--border); 921 - } 922 - .playback-controls { 923 - display: flex; 924 - align-items: center; 925 - gap: 4px; 926 - } 927 - .control-btn { 928 - background: transparent; 929 - border: none; 930 - color: var(--text); 931 - width: 30px; 932 - height: 30px; 933 - border-radius: 4px; 934 - cursor: pointer; 935 - display: flex; 936 - align-items: center; 937 - justify-content: center; 938 - transition: all 0.1s; 939 - } 940 - .control-btn svg { 941 - width: 16px; 942 - height: 16px; 943 - } 944 - .control-btn:hover:not(:disabled) { 945 - background: var(--border); 946 - color: var(--text-bright); 947 - } 948 - .control-btn:disabled { 949 - opacity: 0.3; 950 - cursor: not-allowed; 951 - } 952 - .control-btn.play-btn.playing { 953 - color: #ffd54f; 954 - } 955 - .control-btn.step-btn { 956 - background: #ffd54f; 957 - color: #000; 958 - animation: pulse-step 1.5s ease-in-out infinite; 959 - } 960 - .control-btn.step-btn:hover:not(:disabled) { 961 - background: #ffe566; 962 - color: #000; 963 - animation: none; 964 - } 965 - .control-btn.step-btn:disabled { 966 - background: transparent; 967 - color: var(--text); 968 - animation: none; 969 - } 970 - @keyframes pulse-step { 971 - 0%, 972 - 100% { 973 - opacity: 1; 974 - } 975 - 50% { 976 - opacity: 0.7; 977 - } 978 - } 979 - .step-slider { 980 - flex: 1; 981 - height: 4px; 982 - -webkit-appearance: none; 983 - appearance: none; 984 - background: var(--border); 985 - border-radius: 2px; 986 - outline: none; 987 - } 988 - .step-slider::-webkit-slider-thumb { 989 - -webkit-appearance: none; 990 - appearance: none; 991 - width: 14px; 992 - height: 14px; 993 - background: #ffd54f; 994 - border-radius: 50%; 995 - cursor: pointer; 996 - border: none; 997 - } 998 - .step-slider::-moz-range-thumb { 999 - width: 14px; 1000 - height: 14px; 1001 - background: #ffd54f; 1002 - border-radius: 50%; 1003 - cursor: pointer; 1004 - border: none; 1005 - } 1006 - .step-slider:disabled { 1007 - opacity: 0.5; 1008 - } 1009 - .step-info { 1010 - font-size: 11px; 1011 - color: var(--text-dim); 1012 - font-family: var(--font-mono); 1013 - min-width: 60px; 1014 - text-align: right; 1015 - } 1016 - 1017 - /* Preview pane */ 1018 - .preview-container { 1019 - flex: 1; 1020 - padding: 20px; 1021 - background: #fff; 1022 - color: #111; 1023 - overflow: auto; 1024 - font-family: -apple-system, BlinkMacSystemFont, sans-serif; 1025 - font-size: 16px; 1026 - line-height: 1.5; 1027 - } 1028 - .preview-container h1 { 1029 - font-size: 28px; 1030 - color: #000; 1031 - margin: 0 0 16px; 1032 - } 1033 - .preview-container h2 { 1034 - font-size: 22px; 1035 - color: #000; 1036 - } 1037 - .preview-container h3 { 1038 - font-size: 18px; 1039 - color: #000; 1040 - } 1041 - .preview-container p { 1042 - color: #111; 1043 - margin: 8px 0; 1044 - } 1045 - .preview-container button { 1046 - background: #333; 1047 - color: #fff; 1048 - border: none; 1049 - padding: 8px 14px; 1050 - border-radius: 4px; 1051 - cursor: pointer; 1052 - font-size: 14px; 1053 - } 1054 - .preview-container button:hover { 1055 - background: #444; 1056 - } 1057 - .preview-container .empty { 1058 - color: #999; 1059 - font-style: italic; 1060 - } 1061 - .preview-container .empty.error { 1062 - color: #c0392b; 1063 - } 1064 - .flight-output .empty { 1065 - color: var(--text-dim); 1066 - font-style: italic; 1067 - } 1068 - /* Pulsing dots for empty/waiting states */ 1069 - .empty::after { 1070 - content: ""; 1071 - animation: none; 1072 - } 1073 - .waiting-dots::after { 1074 - content: "..."; 1075 - animation: pulse 1.5s ease-in-out infinite; 1076 - } 1077 - @keyframes pulse { 1078 - 0%, 1079 - 100% { 1080 - opacity: 0.3; 1081 - } 1082 - 50% { 1083 - opacity: 1; 1084 - } 1085 - } 1086 - 1087 - /* Raw input form */ 1088 - .add-raw-btn-wrapper { 1089 - display: flex; 1090 - justify-content: center; 1091 - margin-top: 8px; 1092 - } 1093 - .add-raw-btn { 1094 - width: 24px; 1095 - height: 24px; 1096 - padding: 0; 1097 - background: var(--border); 1098 - border: 1px solid #444; 1099 - border-radius: 50%; 1100 - color: var(--text-dim); 1101 - cursor: pointer; 1102 - font-size: 14px; 1103 - line-height: 22px; 1104 - transition: all 0.15s; 1105 - } 1106 - .add-raw-btn:hover { 1107 - background: #3a3a3a; 1108 - border-color: #666; 1109 - color: var(--text); 1110 - } 1111 - .raw-input-form { 1112 - margin-top: 8px; 1113 - padding: 10px; 1114 - background: var(--surface); 1115 - border: 1px solid var(--border); 1116 - border-radius: 4px; 1117 - } 1118 - .raw-input-action { 1119 - -webkit-appearance: none; 1120 - appearance: none; 1121 - width: 100%; 1122 - padding: 5px 28px 5px 10px; 1123 - margin-bottom: 8px; 1124 - background: 1125 - url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2.5'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") 1126 - no-repeat right 8px center, 1127 - linear-gradient(to bottom, #333, #2a2a2a); 1128 - border: 1px solid #555; 1129 - border-radius: 4px; 1130 - color: #fff; 1131 - font-size: 12px; 1132 - cursor: pointer; 1133 - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); 1134 - } 1135 - .raw-input-action:hover { 1136 - border-color: #666; 1137 - background: 1138 - url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23bbb' stroke-width='2.5'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") 1139 - no-repeat right 8px center, 1140 - linear-gradient(to bottom, #3a3a3a, #333); 1141 - } 1142 - .raw-input-action:focus { 1143 - outline: none; 1144 - border-color: #ffd54f; 1145 - } 1146 - .raw-input-payload { 1147 - width: 100%; 1148 - padding: 8px; 1149 - background: var(--bg); 1150 - border: 1px solid var(--border); 1151 - border-radius: 3px; 1152 - color: var(--text); 1153 - font-family: var(--font-mono); 1154 - font-size: 11px; 1155 - resize: vertical; 1156 - min-height: 100px; 1157 - } 1158 - .raw-input-payload:focus { 1159 - outline: none; 1160 - border-color: #555; 1161 - } 1162 - .raw-input-buttons { 1163 - display: flex; 1164 - gap: 8px; 1165 - margin-top: 8px; 1166 - } 1167 - .raw-input-buttons button { 1168 - padding: 5px 12px; 1169 - border-radius: 3px; 1170 - font-size: 11px; 1171 - cursor: pointer; 1172 - } 1173 - .raw-input-buttons button:first-child { 1174 - background: #ffd54f; 1175 - border: none; 1176 - color: #000; 1177 - } 1178 - .raw-input-buttons button:first-child:disabled { 1179 - background: #555; 1180 - color: #888; 1181 - cursor: not-allowed; 1182 - } 1183 - .raw-input-buttons button:last-child { 1184 - background: transparent; 1185 - border: 1px solid var(--border); 1186 - color: var(--text-dim); 1187 - } 1188 - .raw-input-buttons button:last-child:hover { 1189 - border-color: #555; 1190 - color: var(--text); 1191 - } 1192 - .log-entry-header-right { 1193 - display: flex; 1194 - align-items: center; 1195 - gap: 8px; 1196 - } 1197 - .delete-entry-btn { 1198 - background: transparent; 1199 - border: none; 1200 - color: var(--text-dim); 1201 - cursor: pointer; 1202 - font-size: 14px; 1203 - padding: 0 4px; 1204 - line-height: 1; 1205 - opacity: 0.5; 1206 - transition: 1207 - opacity 0.15s, 1208 - color 0.15s; 1209 - } 1210 - .delete-entry-btn:hover { 1211 - opacity: 1; 1212 - color: #e57373; 1213 - } 1214 40 </style> 1215 41 <!-- Privacy-friendly analytics by Plausible --> 1216 42 <script async src="https://plausible.io/js/pa-CtwoQWR5DSFU93v-DPr1p.js"></script>
+4 -139
src/client/embed.tsx
··· 1 1 import "../shared/webpack-shim.ts"; 2 2 import "../shared/polyfill.ts"; 3 3 4 - import React, { useState, useEffect } from "react"; 5 4 import { createRoot } from "react-dom/client"; 6 - import { Workspace } from "./ui/Workspace.tsx"; 7 - import "./styles/workspace.css"; 8 - 9 - const DEFAULT_SERVER = `export default function App() { 10 - return <h1>RSC Explorer</h1>; 11 - }`; 12 - 13 - const DEFAULT_CLIENT = `'use client' 14 - 15 - export function Button({ children }) { 16 - return <button>{children}</button>; 17 - }`; 18 - 19 - type CodeState = { 20 - server: string; 21 - client: string; 22 - }; 23 - 24 - type EmbedInitMessage = { 25 - type: "rsc-embed:init"; 26 - code?: { 27 - server?: string; 28 - client?: string; 29 - }; 30 - showFullscreen?: boolean; 31 - }; 32 - 33 - type EmbedReadyMessage = { 34 - type: "rsc-embed:ready"; 35 - }; 36 - 37 - type EmbedCodeChangedMessage = { 38 - type: "rsc-embed:code-changed"; 39 - code: { 40 - server: string; 41 - client: string; 42 - }; 43 - }; 44 - 45 - function isEmbedInitMessage(data: unknown): data is EmbedInitMessage { 46 - return ( 47 - typeof data === "object" && 48 - data !== null && 49 - (data as { type?: string }).type === "rsc-embed:init" 50 - ); 51 - } 52 - 53 - function EmbedApp(): React.ReactElement | null { 54 - const [code, setCode] = useState<CodeState | null>(null); 55 - const [showFullscreen, setShowFullscreen] = useState(false); 56 - 57 - useEffect(() => { 58 - const handleMessage = (event: MessageEvent<unknown>): void => { 59 - const { data } = event; 60 - if (isEmbedInitMessage(data)) { 61 - setCode({ 62 - server: (data.code?.server ?? DEFAULT_SERVER).trim(), 63 - client: (data.code?.client ?? DEFAULT_CLIENT).trim(), 64 - }); 65 - if (data.showFullscreen !== false) { 66 - setShowFullscreen(true); 67 - } 68 - } 69 - }; 5 + import { EmbedApp } from "./ui/EmbedApp.tsx"; 70 6 71 - window.addEventListener("message", handleMessage); 72 - 73 - if (window.parent !== window) { 74 - const readyMessage: EmbedReadyMessage = { type: "rsc-embed:ready" }; 75 - window.parent.postMessage(readyMessage, "*"); 76 - } 77 - 78 - return () => window.removeEventListener("message", handleMessage); 79 - }, []); 80 - 81 - const handleCodeChange = (server: string, client: string): void => { 82 - if (window.parent !== window) { 83 - const changedMessage: EmbedCodeChangedMessage = { 84 - type: "rsc-embed:code-changed", 85 - code: { server, client }, 86 - }; 87 - window.parent.postMessage(changedMessage, "*"); 88 - } 89 - }; 90 - 91 - const getFullscreenUrl = (): string => { 92 - if (!code) return "#"; 93 - const json = JSON.stringify({ server: code.server, client: code.client }); 94 - const encoded = encodeURIComponent(btoa(unescape(encodeURIComponent(json)))); 95 - return `https://rscexplorer.dev/?c=${encoded}`; 96 - }; 97 - 98 - if (!code) { 99 - return null; 100 - } 101 - 102 - return ( 103 - <> 104 - {showFullscreen && ( 105 - <div className="embed-header"> 106 - <span className="embed-title">RSC Explorer</span> 107 - <a 108 - href={getFullscreenUrl()} 109 - target="_blank" 110 - rel="noopener noreferrer" 111 - className="embed-fullscreen-link" 112 - title="Open in RSC Explorer" 113 - > 114 - <svg 115 - width="14" 116 - height="14" 117 - viewBox="0 0 24 24" 118 - fill="none" 119 - stroke="currentColor" 120 - strokeWidth="2" 121 - > 122 - <path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3" /> 123 - </svg> 124 - </a> 125 - </div> 126 - )} 127 - <Workspace 128 - key={`${code.server}:${code.client}`} 129 - initialServerCode={code.server} 130 - initialClientCode={code.client} 131 - onCodeChange={handleCodeChange} 132 - /> 133 - </> 134 - ); 135 - } 136 - 137 - document.addEventListener("DOMContentLoaded", () => { 138 - const container = document.getElementById("embed-root"); 139 - if (!container) { 140 - throw new Error("Could not find #embed-root element"); 141 - } 142 - const root = createRoot(container); 143 - root.render(<EmbedApp />); 144 - }); 7 + const container = document.getElementById("embed-root")!; 8 + const root = createRoot(container); 9 + root.render(<EmbedApp />);
-909
src/client/styles/workspace.css
··· 1 - /* Workspace styles - shared between main app and embed */ 2 - 3 - /* Embed header */ 4 - .embed-header { 5 - display: flex; 6 - align-items: center; 7 - justify-content: space-between; 8 - padding: 0 12px; 9 - height: 32px; 10 - background: var(--surface); 11 - border-bottom: 1px solid var(--border); 12 - flex-shrink: 0; 13 - } 14 - .embed-title { 15 - font-size: 11px; 16 - font-weight: 600; 17 - color: var(--text-dim); 18 - letter-spacing: 0.3px; 19 - } 20 - .embed-fullscreen-link { 21 - display: flex; 22 - align-items: center; 23 - justify-content: center; 24 - width: 28px; 25 - height: 28px; 26 - margin-right: -10px; 27 - border-radius: 4px; 28 - color: var(--text-dim); 29 - text-decoration: none; 30 - transition: all 0.15s; 31 - } 32 - .embed-fullscreen-link:hover { 33 - background: var(--border); 34 - color: var(--text-bright); 35 - } 36 - 37 - main { 38 - flex: 1; 39 - min-height: 0; 40 - display: grid; 41 - grid-template-columns: 50% 50%; 42 - grid-template-rows: 50% 50%; 43 - grid-template-areas: 44 - "server flight" 45 - "client preview"; 46 - overflow: hidden; 47 - } 48 - /* Grid area assignments */ 49 - .pane.editor-server { 50 - grid-area: server; 51 - } 52 - .pane.editor-client { 53 - grid-area: client; 54 - } 55 - .pane.flight-pane { 56 - grid-area: flight; 57 - } 58 - .pane.preview-pane { 59 - grid-area: preview; 60 - } 61 - .pane { 62 - display: flex; 63 - flex-direction: column; 64 - overflow: hidden; 65 - } 66 - /* Left column border */ 67 - .pane.editor-server, 68 - .pane.editor-client { 69 - border-right: 1px solid var(--border); 70 - } 71 - /* Top row border */ 72 - .pane.editor-server, 73 - .pane.flight-pane { 74 - border-bottom: 1px solid var(--border); 75 - } 76 - .pane-header { 77 - padding: 6px 12px; 78 - font-size: 10px; 79 - text-transform: uppercase; 80 - letter-spacing: 1px; 81 - color: var(--text-dim); 82 - flex-shrink: 0; 83 - border-bottom: 1px solid var(--border); 84 - } 85 - .editor-container { 86 - flex: 1; 87 - min-height: 0; 88 - position: relative; 89 - overflow: hidden; 90 - background: var(--bg); 91 - } 92 - .editor-container .cm-editor { 93 - position: absolute !important; 94 - top: 0; 95 - left: 0; 96 - right: 0; 97 - bottom: 0; 98 - height: auto !important; 99 - background: transparent; 100 - } 101 - .editor-container .cm-editor .cm-scroller { 102 - overflow: auto !important; 103 - } 104 - .flight-output { 105 - flex: 1; 106 - min-height: 0; 107 - margin: 0; 108 - padding: 12px; 109 - font-family: var(--font-mono); 110 - font-size: 12px; 111 - line-height: 1.6; 112 - overflow: auto; 113 - white-space: pre-wrap; 114 - word-break: break-all; 115 - background: var(--bg); 116 - color: var(--text-dim); 117 - } 118 - .flight-output.error { 119 - color: #e57373; 120 - } 121 - .action-args { 122 - color: var(--text); 123 - } 124 - 125 - /* Flight log */ 126 - .flight-log { 127 - flex: 1; 128 - overflow: auto; 129 - padding: 8px; 130 - display: flex; 131 - flex-direction: column; 132 - gap: 6px; 133 - } 134 - .log-entry { 135 - background: var(--surface); 136 - border: 1px solid var(--border); 137 - border-radius: 4px; 138 - transition: all 0.15s ease; 139 - border-left: 3px solid #555; 140 - } 141 - .log-entry + .log-entry { 142 - margin-top: 12px; 143 - } 144 - .log-entry.active { 145 - border-left-color: #ffd54f; 146 - } 147 - .log-entry.done-entry { 148 - border-left-color: #555; 149 - opacity: 0.8; 150 - } 151 - .log-entry.pending-entry { 152 - border-left-color: #333; 153 - opacity: 0.4; 154 - } 155 - .log-entry-header { 156 - display: flex; 157 - align-items: center; 158 - justify-content: space-between; 159 - gap: 8px; 160 - padding: 6px 10px; 161 - font-size: 11px; 162 - border-bottom: 1px solid var(--border); 163 - } 164 - .log-entry-direction { 165 - font-family: var(--font-mono); 166 - font-weight: 600; 167 - color: var(--text-dim); 168 - } 169 - .log-entry.active .log-entry-direction { 170 - color: #ffd54f; 171 - } 172 - .log-entry-args { 173 - padding: 6px 10px; 174 - font-family: var(--font-mono); 175 - font-size: 11px; 176 - border-bottom: 1px solid var(--border); 177 - color: var(--text-dim); 178 - } 179 - .action-args-label { 180 - margin-right: 6px; 181 - color: var(--text-dim); 182 - } 183 - .log-entry-label { 184 - color: var(--text); 185 - font-weight: 500; 186 - } 187 - .log-entry-count { 188 - margin-left: auto; 189 - color: var(--text-dim); 190 - font-family: var(--font-mono); 191 - } 192 - .log-entry-content { 193 - margin: 0; 194 - padding: 8px 10px; 195 - font-family: var(--font-mono); 196 - font-size: 11px; 197 - line-height: 1.5; 198 - white-space: pre-wrap; 199 - word-break: break-all; 200 - } 201 - .log-entry .action-args { 202 - color: #81c784; 203 - } 204 - 205 - /* Action request (args display) */ 206 - .log-entry-request { 207 - padding: 8px 10px; 208 - background: rgba(0, 0, 0, 0.2); 209 - border-bottom: 1px solid var(--border); 210 - } 211 - .log-entry-request-args { 212 - margin: 0; 213 - font-family: var(--font-mono); 214 - font-size: 11px; 215 - line-height: 1.4; 216 - color: #81c784; 217 - white-space: pre-wrap; 218 - word-break: break-all; 219 - } 220 - 221 - /* Log entry preview (embedded scrubber) */ 222 - .log-entry-preview { 223 - border-top: 1px solid var(--border); 224 - padding: 8px; 225 - } 226 - .log-entry-preview-controls { 227 - display: flex; 228 - align-items: center; 229 - gap: 8px; 230 - margin-bottom: 8px; 231 - } 232 - .log-step-btn { 233 - background: var(--border); 234 - border: none; 235 - color: #ffd54f; 236 - width: 24px; 237 - height: 24px; 238 - border-radius: 3px; 239 - cursor: pointer; 240 - font-size: 10px; 241 - display: flex; 242 - align-items: center; 243 - justify-content: center; 244 - } 245 - .log-step-btn:hover:not(:disabled) { 246 - background: #444; 247 - } 248 - .log-step-btn:disabled { 249 - opacity: 0.3; 250 - cursor: not-allowed; 251 - } 252 - .log-entry-slider { 253 - flex: 1; 254 - height: 4px; 255 - -webkit-appearance: none; 256 - appearance: none; 257 - background: var(--border); 258 - border-radius: 2px; 259 - outline: none; 260 - } 261 - .log-entry-slider::-webkit-slider-thumb { 262 - -webkit-appearance: none; 263 - width: 14px; 264 - height: 14px; 265 - background: #ffd54f; 266 - border-radius: 50%; 267 - cursor: pointer; 268 - } 269 - .log-entry-slider::-moz-range-thumb { 270 - width: 14px; 271 - height: 14px; 272 - background: #ffd54f; 273 - border-radius: 50%; 274 - cursor: pointer; 275 - border: none; 276 - } 277 - .value-pending { 278 - color: #999; 279 - font-style: italic; 280 - font-size: 11px; 281 - } 282 - .value-loading { 283 - color: #999; 284 - font-style: italic; 285 - } 286 - .log-entry-step-info { 287 - font-size: 11px; 288 - color: var(--text-dim); 289 - font-family: var(--font-mono); 290 - } 291 - .log-entry-flight-lines { 292 - margin: 0; 293 - padding: 6px; 294 - background: var(--bg); 295 - border-radius: 3px; 296 - font-size: 11px; 297 - line-height: 1.4; 298 - overflow: auto; 299 - } 300 - .log-entry-flight-lines .flight-line { 301 - display: block; 302 - padding: 6px 8px; 303 - margin-bottom: 3px; 304 - border-radius: 4px; 305 - word-break: break-all; 306 - white-space: pre-wrap; 307 - border-left: 2px solid transparent; 308 - transition: all 0.15s ease; 309 - } 310 - .log-entry-flight-lines .flight-line:last-child { 311 - margin-bottom: 0; 312 - } 313 - .log-entry-flight-lines .line-done { 314 - color: #999; 315 - background: rgba(255, 255, 255, 0.03); 316 - border-left-color: #555; 317 - } 318 - .log-entry-flight-lines .line-next { 319 - color: #e0e0e0; 320 - background: rgba(255, 213, 79, 0.12); 321 - border-left-color: #ffd54f; 322 - } 323 - .log-entry-flight-lines .line-pending { 324 - color: #444; 325 - background: transparent; 326 - border-left-color: #333; 327 - opacity: 0.4; 328 - } 329 - 330 - /* Split layout: left=stream (scrolls), right=tree (dictates height) */ 331 - .log-entry-split { 332 - display: flex; 333 - gap: 8px; 334 - align-items: stretch; 335 - } 336 - .log-entry-split .log-entry-flight-lines-wrapper { 337 - flex: 1; 338 - min-width: 0; 339 - position: relative; 340 - min-height: 150px; 341 - } 342 - .log-entry-split .log-entry-flight-lines { 343 - position: absolute; 344 - top: 0; 345 - left: 0; 346 - right: 0; 347 - bottom: 0; 348 - } 349 - .log-entry-split .log-entry-tree { 350 - flex: 1; 351 - min-width: 0; 352 - display: flex; 353 - flex-direction: column; 354 - } 355 - /* Tree view in log entry - expands to fill height */ 356 - .log-entry-tree { 357 - flex: 1; 358 - min-width: 0; 359 - border-radius: 4px; 360 - overflow: auto; 361 - border: 1px solid var(--border); 362 - } 363 - .log-entry-tree:has(.flight-tree) { 364 - background: #000; 365 - } 366 - .log-entry-tree.full-width { 367 - flex: none; 368 - width: 100%; 369 - } 370 - .log-entry-tree .flight-tree { 371 - padding: 8px; 372 - font-size: 11px; 373 - line-height: 1.6; 374 - } 375 - .jsx-output { 376 - margin: 0; 377 - white-space: pre-wrap; 378 - word-break: break-word; 379 - color: var(--text); 380 - } 381 - .value-content { 382 - background: #f8f8f8; 383 - color: #111; 384 - font-family: -apple-system, BlinkMacSystemFont, sans-serif; 385 - padding: 8px; 386 - border-radius: 3px; 387 - margin: -8px; 388 - } 389 - .value-string { 390 - color: #22863a; 391 - } 392 - .value-number { 393 - color: #005cc5; 394 - } 395 - .value-boolean { 396 - color: #d73a49; 397 - } 398 - .value-null, 399 - .value-undefined { 400 - color: #6a737d; 401 - font-style: italic; 402 - } 403 - .value-key { 404 - color: #005cc5; 405 - } 406 - .value-indent { 407 - display: block; 408 - padding-left: 16px; 409 - } 410 - .value-array-item, 411 - .value-object-entry { 412 - display: block; 413 - } 414 - .value-react-element { 415 - display: inline; 416 - background: rgba(0, 0, 0, 0.04); 417 - padding: 2px 4px; 418 - border-radius: 3px; 419 - } 420 - .value-error { 421 - color: #e57373; 422 - font-style: italic; 423 - } 424 - .value-loading { 425 - color: var(--text-dim); 426 - font-style: italic; 427 - } 428 - 429 - /* Flight Tree View */ 430 - .flight-tree { 431 - flex: 1; 432 - min-height: 0; 433 - padding: 12px; 434 - font-family: var(--font-mono); 435 - font-size: 12px; 436 - line-height: 1.8; 437 - overflow: auto; 438 - background: var(--bg); 439 - } 440 - .tree-empty { 441 - color: var(--text-dim); 442 - font-style: italic; 443 - } 444 - .tree-element, 445 - .tree-client-component, 446 - .tree-suspense, 447 - .tree-lazy { 448 - margin-left: 0; 449 - } 450 - .tree-children { 451 - margin-left: 16px; 452 - border-left: 1px solid #333; 453 - padding-left: 12px; 454 - } 455 - .tree-tag { 456 - color: #e06c75; 457 - } 458 - .tree-client-tag { 459 - color: #c678dd; 460 - } 461 - .tree-client-ref { 462 - color: #5c6370; 463 - font-size: 10px; 464 - } 465 - .tree-react-tag { 466 - color: #e5c07b; 467 - } 468 - .tree-pending { 469 - display: inline-block; 470 - color: #64b5f6; 471 - background: rgba(100, 181, 246, 0.15); 472 - padding: 2px 8px; 473 - border-radius: 10px; 474 - border: 1px solid rgba(100, 181, 246, 0.4); 475 - font-size: 10px; 476 - font-weight: 500; 477 - letter-spacing: 0.5px; 478 - animation: pending-pulse 2s ease-in-out infinite; 479 - } 480 - @keyframes pending-pulse { 481 - 0%, 482 - 100% { 483 - opacity: 0.7; 484 - } 485 - 50% { 486 - opacity: 1; 487 - } 488 - } 489 - .tree-string { 490 - color: #98c379; 491 - } 492 - .tree-number { 493 - color: #d19a66; 494 - } 495 - .tree-boolean { 496 - color: #56b6c2; 497 - } 498 - .tree-null, 499 - .tree-undefined { 500 - color: #5c6370; 501 - font-style: italic; 502 - } 503 - .tree-key, 504 - .tree-prop-name { 505 - color: #61afef; 506 - } 507 - .tree-ref { 508 - color: #c678dd; 509 - } 510 - .tree-object { 511 - color: var(--text-dim); 512 - } 513 - .tree-array { 514 - /* Arrays render children inline */ 515 - } 516 - .tree-props { 517 - color: var(--text-dim); 518 - } 519 - .tree-prop { 520 - color: var(--text-dim); 521 - } 522 - .tree-error { 523 - display: inline-block; 524 - color: #e57373; 525 - background: rgba(229, 115, 115, 0.15); 526 - padding: 2px 8px; 527 - border-radius: 10px; 528 - border: 1px solid rgba(229, 115, 115, 0.4); 529 - font-size: 10px; 530 - font-weight: 500; 531 - letter-spacing: 0.5px; 532 - } 533 - .tree-pending-tag { 534 - color: #ffd54f; 535 - background: rgba(255, 213, 79, 0.1); 536 - padding: 1px 4px; 537 - border-radius: 3px; 538 - border: 1px solid rgba(255, 213, 79, 0.3); 539 - } 540 - .tree-error-keyword { 541 - color: #c678dd; 542 - font-weight: 600; 543 - } 544 - .tree-error-message { 545 - color: #e57373; 546 - } 547 - .tree-function { 548 - color: #61afef; 549 - font-style: italic; 550 - } 551 - .tree-placeholder { 552 - color: #444; 553 - font-style: italic; 554 - } 555 - 556 - /* Playback controls */ 557 - .playback-container { 558 - display: flex; 559 - align-items: center; 560 - gap: 10px; 561 - padding: 8px 12px; 562 - background: var(--surface); 563 - border-bottom: 1px solid var(--border); 564 - } 565 - .playback-controls { 566 - display: flex; 567 - align-items: center; 568 - gap: 4px; 569 - } 570 - .control-btn { 571 - background: transparent; 572 - border: none; 573 - color: var(--text); 574 - width: 40px; 575 - height: 40px; 576 - border-radius: 6px; 577 - cursor: pointer; 578 - display: flex; 579 - align-items: center; 580 - justify-content: center; 581 - transition: all 0.1s; 582 - } 583 - .control-btn svg { 584 - width: 20px; 585 - height: 20px; 586 - } 587 - .control-btn:hover:not(:disabled) { 588 - background: var(--border); 589 - color: var(--text-bright); 590 - } 591 - .control-btn:disabled { 592 - opacity: 0.3; 593 - cursor: not-allowed; 594 - } 595 - .control-btn.play-btn.playing { 596 - color: #ffd54f; 597 - } 598 - .control-btn.step-btn { 599 - background: #ffd54f; 600 - color: #000; 601 - animation: pulse-step 1.5s ease-in-out infinite; 602 - } 603 - .control-btn.step-btn:hover:not(:disabled) { 604 - background: #ffe566; 605 - color: #000; 606 - animation: none; 607 - } 608 - .control-btn.step-btn:disabled { 609 - background: transparent; 610 - color: var(--text); 611 - animation: none; 612 - } 613 - @keyframes pulse-step { 614 - 0%, 615 - 100% { 616 - opacity: 1; 617 - } 618 - 50% { 619 - opacity: 0.7; 620 - } 621 - } 622 - .step-slider { 623 - flex: 1; 624 - height: 4px; 625 - -webkit-appearance: none; 626 - appearance: none; 627 - background: var(--border); 628 - border-radius: 2px; 629 - outline: none; 630 - } 631 - .step-slider::-webkit-slider-thumb { 632 - -webkit-appearance: none; 633 - appearance: none; 634 - width: 14px; 635 - height: 14px; 636 - background: #ffd54f; 637 - border-radius: 50%; 638 - cursor: pointer; 639 - border: none; 640 - } 641 - .step-slider::-moz-range-thumb { 642 - width: 14px; 643 - height: 14px; 644 - background: #ffd54f; 645 - border-radius: 50%; 646 - cursor: pointer; 647 - border: none; 648 - } 649 - .step-slider:disabled { 650 - opacity: 0.5; 651 - } 652 - .step-info { 653 - font-size: 11px; 654 - color: var(--text-dim); 655 - font-family: var(--font-mono); 656 - min-width: 60px; 657 - text-align: right; 658 - } 659 - 660 - /* Preview pane */ 661 - .preview-container { 662 - flex: 1; 663 - padding: 20px; 664 - background: #fff; 665 - color: #111; 666 - overflow: auto; 667 - font-family: -apple-system, BlinkMacSystemFont, sans-serif; 668 - font-size: 16px; 669 - line-height: 1.5; 670 - } 671 - .preview-container h1 { 672 - font-size: 28px; 673 - color: #000; 674 - margin: 0 0 16px; 675 - } 676 - .preview-container h2 { 677 - font-size: 22px; 678 - color: #000; 679 - } 680 - .preview-container h3 { 681 - font-size: 18px; 682 - color: #000; 683 - } 684 - .preview-container p { 685 - color: #111; 686 - margin: 8px 0; 687 - } 688 - .preview-container button { 689 - background: #333; 690 - color: #fff; 691 - border: none; 692 - padding: 8px 14px; 693 - border-radius: 4px; 694 - cursor: pointer; 695 - font-size: 14px; 696 - } 697 - .preview-container button:hover { 698 - background: #444; 699 - } 700 - .preview-container .empty { 701 - color: #999; 702 - font-style: italic; 703 - } 704 - .preview-container .empty.error { 705 - color: #c0392b; 706 - } 707 - .flight-output .empty { 708 - color: var(--text-dim); 709 - font-style: italic; 710 - } 711 - /* Pulsing dots for empty/waiting states */ 712 - .empty::after { 713 - content: ""; 714 - animation: none; 715 - } 716 - .waiting-dots::after { 717 - content: "..."; 718 - animation: pulse 1.5s ease-in-out infinite; 719 - } 720 - @keyframes pulse { 721 - 0%, 722 - 100% { 723 - opacity: 0.3; 724 - } 725 - 50% { 726 - opacity: 1; 727 - } 728 - } 729 - 730 - /* Raw input form */ 731 - .add-raw-btn-wrapper { 732 - display: flex; 733 - justify-content: center; 734 - margin-top: 8px; 735 - } 736 - .add-raw-btn { 737 - width: 24px; 738 - height: 24px; 739 - padding: 0; 740 - background: var(--border); 741 - border: 1px solid #444; 742 - border-radius: 50%; 743 - color: var(--text-dim); 744 - cursor: pointer; 745 - font-size: 14px; 746 - line-height: 22px; 747 - transition: all 0.15s; 748 - } 749 - .add-raw-btn:hover { 750 - background: #3a3a3a; 751 - border-color: #666; 752 - color: var(--text); 753 - } 754 - .raw-input-form { 755 - margin-top: 8px; 756 - padding: 10px; 757 - background: var(--surface); 758 - border: 1px solid var(--border); 759 - border-radius: 4px; 760 - } 761 - .raw-input-action { 762 - -webkit-appearance: none; 763 - appearance: none; 764 - width: 100%; 765 - padding: 5px 28px 5px 10px; 766 - margin-bottom: 8px; 767 - background: 768 - url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2.5'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") 769 - no-repeat right 8px center, 770 - linear-gradient(to bottom, #333, #2a2a2a); 771 - border: 1px solid #555; 772 - border-radius: 4px; 773 - color: #fff; 774 - font-size: 12px; 775 - cursor: pointer; 776 - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); 777 - } 778 - .raw-input-action:hover { 779 - border-color: #666; 780 - background: 781 - url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23bbb' stroke-width='2.5'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") 782 - no-repeat right 8px center, 783 - linear-gradient(to bottom, #3a3a3a, #333); 784 - } 785 - .raw-input-action:focus { 786 - outline: none; 787 - border-color: #ffd54f; 788 - } 789 - .raw-input-payload { 790 - width: 100%; 791 - padding: 8px; 792 - background: var(--bg); 793 - border: 1px solid var(--border); 794 - border-radius: 3px; 795 - color: var(--text); 796 - font-family: var(--font-mono); 797 - font-size: 11px; 798 - resize: vertical; 799 - min-height: 100px; 800 - } 801 - .raw-input-payload:focus { 802 - outline: none; 803 - border-color: #555; 804 - } 805 - .raw-input-buttons { 806 - display: flex; 807 - gap: 8px; 808 - margin-top: 8px; 809 - } 810 - .raw-input-buttons button { 811 - padding: 5px 12px; 812 - border-radius: 3px; 813 - font-size: 11px; 814 - cursor: pointer; 815 - } 816 - .raw-input-buttons button:first-child { 817 - background: #ffd54f; 818 - border: none; 819 - color: #000; 820 - } 821 - .raw-input-buttons button:first-child:disabled { 822 - background: #555; 823 - color: #888; 824 - cursor: not-allowed; 825 - } 826 - .raw-input-buttons button:last-child { 827 - background: transparent; 828 - border: 1px solid var(--border); 829 - color: var(--text-dim); 830 - } 831 - .raw-input-buttons button:last-child:hover { 832 - border-color: #555; 833 - color: var(--text); 834 - } 835 - .log-entry-header-right { 836 - display: flex; 837 - align-items: center; 838 - gap: 8px; 839 - } 840 - .delete-entry-btn { 841 - background: transparent; 842 - border: none; 843 - color: var(--text-dim); 844 - cursor: pointer; 845 - font-size: 14px; 846 - padding: 0 4px; 847 - line-height: 1; 848 - opacity: 0.5; 849 - transition: 850 - opacity 0.15s, 851 - color 0.15s; 852 - } 853 - .delete-entry-btn:hover { 854 - opacity: 1; 855 - color: #e57373; 856 - } 857 - 858 - /* Responsive - mobile */ 859 - @media (max-width: 768px) { 860 - /* Keep 4-grid layout on mobile */ 861 - /* Prevent iOS zoom on all form elements */ 862 - input, 863 - textarea, 864 - select { 865 - font-size: 16px !important; 866 - } 867 - .raw-input-payload { 868 - font-size: 16px !important; 869 - } 870 - /* Playback controls responsive */ 871 - .playback-container { 872 - padding: 8px 12px; 873 - gap: 8px; 874 - } 875 - .playback-controls { 876 - gap: 6px; 877 - } 878 - .control-btn { 879 - width: 44px; 880 - height: 44px; 881 - } 882 - .control-btn svg { 883 - width: 20px; 884 - height: 20px; 885 - } 886 - .step-info { 887 - min-width: 50px; 888 - font-size: 11px; 889 - } 890 - } 891 - @media (max-width: 480px) { 892 - /* Playback: hide slider and status, keep buttons tappable */ 893 - .step-slider, 894 - .step-info { 895 - display: none; 896 - } 897 - .playback-container { 898 - padding: 6px 8px; 899 - gap: 6px; 900 - } 901 - .control-btn { 902 - width: 44px; 903 - height: 44px; 904 - } 905 - .control-btn svg { 906 - width: 18px; 907 - height: 18px; 908 - } 909 - }
+313
src/client/ui/App.css
··· 1 + /* App component styles */ 2 + 3 + .App-header { 4 + height: 44px; 5 + padding: 0 16px; 6 + display: flex; 7 + align-items: center; 8 + gap: 16px; 9 + border-bottom: 1px solid var(--border); 10 + background: var(--surface); 11 + flex-shrink: 0; 12 + position: relative; 13 + z-index: 100; 14 + } 15 + 16 + .App-title { 17 + margin: 0; 18 + font-size: 13px; 19 + font-weight: 600; 20 + color: var(--text); 21 + white-space: nowrap; 22 + } 23 + 24 + .App-headerSpacer { 25 + flex: 1; 26 + } 27 + 28 + .App-headerLinks { 29 + display: flex; 30 + align-items: center; 31 + gap: 8px; 32 + padding-right: 16px; 33 + border-right: 1px solid var(--border); 34 + } 35 + 36 + .App-headerLink { 37 + display: flex; 38 + align-items: center; 39 + justify-content: center; 40 + color: var(--text-dim); 41 + padding: 4px; 42 + border-radius: 4px; 43 + transition: color 0.15s; 44 + } 45 + 46 + .App-headerLink:hover { 47 + color: var(--text-bright); 48 + } 49 + 50 + /* ExampleSelect */ 51 + 52 + .ExampleSelect { 53 + display: flex; 54 + align-items: center; 55 + gap: 10px; 56 + padding-left: 16px; 57 + border-left: 1px solid var(--border); 58 + } 59 + 60 + .ExampleSelect-label { 61 + font-size: 11px; 62 + color: var(--text-dim); 63 + text-transform: uppercase; 64 + letter-spacing: 0.5px; 65 + } 66 + 67 + .ExampleSelect-selectWrapper { 68 + min-width: 150px; 69 + } 70 + 71 + .ExampleSelect-saveBtn, 72 + .ExampleSelect-embedBtn { 73 + background: var(--surface); 74 + border: 1px solid var(--border); 75 + color: var(--text); 76 + padding: 5px 8px; 77 + border-radius: 4px; 78 + cursor: pointer; 79 + display: flex; 80 + align-items: center; 81 + justify-content: center; 82 + } 83 + 84 + .ExampleSelect-saveBtn:hover:not(:disabled), 85 + .ExampleSelect-embedBtn:hover:not(:disabled) { 86 + border-color: #444; 87 + background: #2a2a2a; 88 + } 89 + 90 + .ExampleSelect-saveBtn:disabled, 91 + .ExampleSelect-embedBtn:disabled { 92 + opacity: 0.4; 93 + cursor: not-allowed; 94 + } 95 + 96 + /* BuildSwitcher */ 97 + 98 + .BuildSwitcher { 99 + display: flex; 100 + align-items: center; 101 + gap: 8px; 102 + } 103 + 104 + .BuildSwitcher-label { 105 + font-size: 11px; 106 + color: var(--text-dim); 107 + text-transform: uppercase; 108 + letter-spacing: 0.5px; 109 + } 110 + 111 + .BuildSwitcher-version { 112 + min-width: 150px; 113 + } 114 + 115 + .BuildSwitcher-mode { 116 + min-width: 70px; 117 + } 118 + 119 + /* EmbedModal */ 120 + 121 + .EmbedModal-overlay { 122 + position: fixed; 123 + inset: 0; 124 + background: rgba(0, 0, 0, 0.7); 125 + display: flex; 126 + align-items: center; 127 + justify-content: center; 128 + z-index: 1000; 129 + } 130 + 131 + .EmbedModal { 132 + background: var(--surface); 133 + border: 1px solid var(--border); 134 + border-radius: 8px; 135 + width: 90%; 136 + max-width: 600px; 137 + max-height: 80vh; 138 + display: flex; 139 + flex-direction: column; 140 + } 141 + 142 + .EmbedModal-header { 143 + display: flex; 144 + align-items: center; 145 + justify-content: space-between; 146 + padding: 16px; 147 + border-bottom: 1px solid var(--border); 148 + } 149 + 150 + .EmbedModal-title { 151 + margin: 0; 152 + font-size: 16px; 153 + font-weight: 600; 154 + color: var(--text-bright); 155 + } 156 + 157 + .EmbedModal-closeBtn { 158 + background: none; 159 + border: none; 160 + color: var(--text-dim); 161 + font-size: 24px; 162 + cursor: pointer; 163 + padding: 0; 164 + line-height: 1; 165 + } 166 + 167 + .EmbedModal-closeBtn:hover { 168 + color: var(--text-bright); 169 + } 170 + 171 + .EmbedModal-body { 172 + padding: 16px; 173 + overflow: auto; 174 + } 175 + 176 + .EmbedModal-description { 177 + margin: 0 0 12px; 178 + font-size: 13px; 179 + color: var(--text); 180 + } 181 + 182 + .EmbedModal-textarea { 183 + width: 100%; 184 + height: 250px; 185 + background: var(--bg); 186 + border: 1px solid var(--border); 187 + border-radius: 4px; 188 + color: var(--text); 189 + font-family: var(--font-mono); 190 + font-size: 12px; 191 + padding: 12px; 192 + resize: none; 193 + } 194 + 195 + .EmbedModal-textarea:focus { 196 + outline: none; 197 + border-color: #555; 198 + } 199 + 200 + .EmbedModal-footer { 201 + padding: 16px; 202 + border-top: 1px solid var(--border); 203 + display: flex; 204 + justify-content: flex-end; 205 + } 206 + 207 + .EmbedModal-copyBtn { 208 + background: #ffd54f; 209 + border: none; 210 + color: #000; 211 + padding: 8px 16px; 212 + border-radius: 4px; 213 + font-size: 13px; 214 + font-weight: 500; 215 + cursor: pointer; 216 + } 217 + 218 + .EmbedModal-copyBtn:hover { 219 + background: #ffe566; 220 + } 221 + 222 + /* Responsive */ 223 + 224 + @media (max-width: 900px) { 225 + .ExampleSelect-label, 226 + .BuildSwitcher-label { 227 + display: none; 228 + } 229 + } 230 + 231 + @media (max-width: 480px) { 232 + .App-headerLink--tangled { 233 + display: none; 234 + } 235 + } 236 + 237 + @media (max-width: 768px) { 238 + .App-header { 239 + padding: 0 8px; 240 + gap: 4px; 241 + } 242 + 243 + .App-title { 244 + display: none; 245 + } 246 + 247 + .ExampleSelect { 248 + border-left: none; 249 + } 250 + 251 + .App-headerSpacer { 252 + flex: 1; 253 + min-width: 0; 254 + } 255 + 256 + .ExampleSelect { 257 + padding-left: 6px; 258 + margin-left: 2px; 259 + gap: 4px; 260 + } 261 + 262 + .ExampleSelect-selectWrapper { 263 + min-width: 0; 264 + } 265 + 266 + .BuildSwitcher-version, 267 + .BuildSwitcher-mode { 268 + min-width: 0; 269 + } 270 + 271 + .BuildSwitcher { 272 + padding-left: 6px; 273 + gap: 4px; 274 + } 275 + } 276 + 277 + @media (max-width: 480px) { 278 + .App-header { 279 + padding: 0 6px; 280 + gap: 3px; 281 + } 282 + 283 + .App-title { 284 + font-size: 10px; 285 + } 286 + 287 + .ExampleSelect { 288 + padding-left: 4px; 289 + margin-left: 0; 290 + border-left: none; 291 + } 292 + 293 + .ExampleSelect-selectWrapper { 294 + max-width: 110px; 295 + } 296 + 297 + .BuildSwitcher { 298 + padding-left: 4px; 299 + border-left: none; 300 + } 301 + 302 + .BuildSwitcher-version, 303 + .BuildSwitcher-mode { 304 + max-width: 80px; 305 + } 306 + } 307 + 308 + @media (max-width: 360px) { 309 + .BuildSwitcher-version, 310 + .BuildSwitcher-mode { 311 + max-width: 75px; 312 + } 313 + }
+58 -45
src/client/ui/App.tsx
··· 2 2 import { version } from "react"; 3 3 import { SAMPLES, type Sample } from "../samples.ts"; 4 4 import REACT_VERSIONS from "../../../scripts/versions.json"; 5 + import { Select } from "./Select.tsx"; 6 + import "./App.css"; 5 7 6 8 const isDev = process.env.NODE_ENV === "development"; 7 9 ··· 25 27 }; 26 28 27 29 return ( 28 - <div className="build-switcher"> 29 - <label>React</label> 30 - <select value={version} onChange={handleVersionChange} disabled={isDisabled}> 31 - {(REACT_VERSIONS as string[]).map((v) => ( 32 - <option key={v} value={v}> 33 - {v} 34 - </option> 35 - ))} 36 - </select> 37 - <select 38 - value={isDev ? "dev" : "prod"} 39 - onChange={handleModeChange} 40 - className="mode-select" 41 - disabled={isDisabled} 42 - > 43 - <option value="prod">prod</option> 44 - <option value="dev">dev</option> 45 - </select> 30 + <div className="BuildSwitcher"> 31 + <label className="BuildSwitcher-label">React</label> 32 + <div className="BuildSwitcher-version"> 33 + <Select value={version} onChange={handleVersionChange} disabled={isDisabled}> 34 + {(REACT_VERSIONS as string[]).map((v) => ( 35 + <option key={v} value={v}> 36 + {v} 37 + </option> 38 + ))} 39 + </Select> 40 + </div> 41 + <div className="BuildSwitcher-mode"> 42 + <Select value={isDev ? "dev" : "prod"} onChange={handleModeChange} disabled={isDisabled}> 43 + <option value="prod">prod</option> 44 + <option value="dev">dev</option> 45 + </Select> 46 + </div> 46 47 </div> 47 48 ); 48 49 } ··· 141 142 }; 142 143 143 144 return ( 144 - <div className="modal-overlay" onClick={onClose}> 145 - <div className="modal" onClick={(e: MouseEvent) => e.stopPropagation()}> 146 - <div className="modal-header"> 147 - <h2>Embed this example</h2> 148 - <button className="modal-close" onClick={onClose}> 145 + <div className="EmbedModal-overlay" onClick={onClose}> 146 + <div className="EmbedModal" onClick={(e: MouseEvent) => e.stopPropagation()}> 147 + <div className="EmbedModal-header"> 148 + <h2 className="EmbedModal-title">Embed this example</h2> 149 + <button className="EmbedModal-closeBtn" onClick={onClose}> 149 150 &times; 150 151 </button> 151 152 </div> 152 - <div className="modal-body"> 153 - <p>Copy and paste this code into your HTML:</p> 153 + <div className="EmbedModal-body"> 154 + <p className="EmbedModal-description">Copy and paste this code into your HTML:</p> 154 155 <textarea 155 156 ref={textareaRef} 157 + className="EmbedModal-textarea" 156 158 readOnly 157 159 value={embedCode} 158 160 onClick={(e) => (e.target as HTMLTextAreaElement).select()} 159 161 /> 160 162 </div> 161 - <div className="modal-footer"> 162 - <button className="copy-btn" onClick={handleCopy}> 163 + <div className="EmbedModal-footer"> 164 + <button className="EmbedModal-copyBtn" onClick={handleCopy}> 163 165 {copied ? "Copied!" : "Copy to clipboard"} 164 166 </button> 165 167 </div> ··· 245 247 246 248 return ( 247 249 <> 248 - <header> 249 - <h1>RSC Explorer</h1> 250 - <div className="example-select-wrapper"> 251 - <label>Example</label> 252 - <select value={currentSample ?? ""} onChange={handleSampleChange}> 253 - {!currentSample && <option value="">Custom</option>} 254 - {Object.entries(SAMPLES).map(([key, sample]) => ( 255 - <option key={key} value={key}> 256 - {sample.name} 257 - </option> 258 - ))} 259 - </select> 260 - <button className="save-btn" onClick={handleSave} disabled={!isDirty} title="Save to URL"> 250 + <header className="App-header"> 251 + <h1 className="App-title">RSC Explorer</h1> 252 + <div className="ExampleSelect"> 253 + <label className="ExampleSelect-label">Example</label> 254 + <div className="ExampleSelect-selectWrapper"> 255 + <Select value={currentSample ?? ""} onChange={handleSampleChange}> 256 + {!currentSample && <option value="">Custom</option>} 257 + {Object.entries(SAMPLES).map(([key, sample]) => ( 258 + <option key={key} value={key}> 259 + {sample.name} 260 + </option> 261 + ))} 262 + </Select> 263 + </div> 264 + <button 265 + className="ExampleSelect-saveBtn" 266 + onClick={handleSave} 267 + disabled={!isDirty} 268 + title="Save to URL" 269 + > 261 270 <svg 262 271 width="16" 263 272 height="16" ··· 271 280 <polyline points="7 3 7 8 15 8" /> 272 281 </svg> 273 282 </button> 274 - <button className="embed-btn" onClick={() => setShowEmbedModal(true)} title="Embed"> 283 + <button 284 + className="ExampleSelect-embedBtn" 285 + onClick={() => setShowEmbedModal(true)} 286 + title="Embed" 287 + > 275 288 <svg 276 289 width="16" 277 290 height="16" ··· 285 298 </svg> 286 299 </button> 287 300 </div> 288 - <div className="header-spacer" /> 289 - <div className="header-links"> 301 + <div className="App-headerSpacer" /> 302 + <div className="App-headerLinks"> 290 303 <a 291 304 href="https://github.com/gaearon/rscexplorer" 292 305 target="_blank" 293 306 rel="noopener noreferrer" 294 - className="github-link" 307 + className="App-headerLink" 295 308 title="View on GitHub" 296 309 > 297 310 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> ··· 302 315 href="https://tangled.sh/danabra.mov/rscexplorer" 303 316 target="_blank" 304 317 rel="noopener noreferrer" 305 - className="tangled-link" 318 + className="App-headerLink App-headerLink--tangled" 306 319 title="View on Tangled" 307 320 > 308 321 <svg width="20" height="20" viewBox="0 0 26 26" fill="currentColor">
+23
src/client/ui/CodeEditor.css
··· 1 + /* CodeEditor component styles */ 2 + 3 + .CodeEditor-container { 4 + flex: 1; 5 + min-height: 0; 6 + position: relative; 7 + overflow: hidden; 8 + background: var(--bg); 9 + } 10 + 11 + .CodeEditor-container .cm-editor { 12 + position: absolute !important; 13 + top: 0; 14 + left: 0; 15 + right: 0; 16 + bottom: 0; 17 + height: auto !important; 18 + background: transparent; 19 + } 20 + 21 + .CodeEditor-container .cm-editor .cm-scroller { 22 + overflow: auto !important; 23 + }
+6 -5
src/client/ui/CodeEditor.tsx
··· 5 5 import { tags } from "@lezer/highlight"; 6 6 import { history, historyKeymap, defaultKeymap } from "@codemirror/commands"; 7 7 import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete"; 8 + import "./CodeEditor.css"; 8 9 9 10 const highlightStyle = HighlightStyle.define([ 10 11 { tag: tags.keyword, color: "#c678dd" }, ··· 43 44 defaultValue: string; 44 45 onChange: (code: string) => void; 45 46 label: string; 46 - className?: string; 47 + paneClass?: string; 47 48 }; 48 49 49 50 export function CodeEditor({ 50 51 defaultValue, 51 52 onChange, 52 53 label, 53 - className, 54 + paneClass, 54 55 }: CodeEditorProps): React.ReactElement { 55 56 const [initialDefaultValue] = useState(defaultValue); 56 57 const containerRef = useRef<HTMLDivElement>(null); ··· 86 87 }, [initialDefaultValue]); 87 88 88 89 return ( 89 - <div className={`pane${className ? ` ${className}` : ""}`}> 90 - <div className="pane-header">{label}</div> 91 - <div className="editor-container" ref={containerRef} /> 90 + <div className={`Workspace-pane${paneClass ? ` ${paneClass}` : ""}`}> 91 + <div className="Workspace-paneHeader">{label}</div> 92 + <div className="CodeEditor-container" ref={containerRef} /> 92 93 </div> 93 94 ); 94 95 }
+37
src/client/ui/EmbedApp.css
··· 1 + /* Embed component styles */ 2 + 3 + .EmbedApp-header { 4 + display: flex; 5 + align-items: center; 6 + justify-content: space-between; 7 + padding: 0 12px; 8 + height: 32px; 9 + background: var(--surface); 10 + border-bottom: 1px solid var(--border); 11 + flex-shrink: 0; 12 + } 13 + 14 + .EmbedApp-title { 15 + font-size: 11px; 16 + font-weight: 600; 17 + color: var(--text-dim); 18 + letter-spacing: 0.3px; 19 + } 20 + 21 + .EmbedApp-fullscreenLink { 22 + display: flex; 23 + align-items: center; 24 + justify-content: center; 25 + width: 28px; 26 + height: 28px; 27 + margin-right: -10px; 28 + border-radius: 4px; 29 + color: var(--text-dim); 30 + text-decoration: none; 31 + transition: all 0.15s; 32 + } 33 + 34 + .EmbedApp-fullscreenLink:hover { 35 + background: var(--border); 36 + color: var(--text-bright); 37 + }
+131
src/client/ui/EmbedApp.tsx
··· 1 + import React, { useState, useEffect } from "react"; 2 + import { Workspace } from "./Workspace.tsx"; 3 + import "./EmbedApp.css"; 4 + 5 + const DEFAULT_SERVER = `export default function App() { 6 + return <h1>RSC Explorer</h1>; 7 + }`; 8 + 9 + const DEFAULT_CLIENT = `'use client' 10 + 11 + export function Button({ children }) { 12 + return <button>{children}</button>; 13 + }`; 14 + 15 + type CodeState = { 16 + server: string; 17 + client: string; 18 + }; 19 + 20 + type EmbedInitMessage = { 21 + type: "rsc-embed:init"; 22 + code?: { 23 + server?: string; 24 + client?: string; 25 + }; 26 + showFullscreen?: boolean; 27 + }; 28 + 29 + type EmbedReadyMessage = { 30 + type: "rsc-embed:ready"; 31 + }; 32 + 33 + type EmbedCodeChangedMessage = { 34 + type: "rsc-embed:code-changed"; 35 + code: { 36 + server: string; 37 + client: string; 38 + }; 39 + }; 40 + 41 + function isEmbedInitMessage(data: unknown): data is EmbedInitMessage { 42 + return ( 43 + typeof data === "object" && 44 + data !== null && 45 + (data as { type?: string }).type === "rsc-embed:init" 46 + ); 47 + } 48 + 49 + export function EmbedApp(): React.ReactElement | null { 50 + const [code, setCode] = useState<CodeState | null>(null); 51 + const [showFullscreen, setShowFullscreen] = useState(false); 52 + 53 + useEffect(() => { 54 + const handleMessage = (event: MessageEvent<unknown>): void => { 55 + const { data } = event; 56 + if (isEmbedInitMessage(data)) { 57 + setCode({ 58 + server: (data.code?.server ?? DEFAULT_SERVER).trim(), 59 + client: (data.code?.client ?? DEFAULT_CLIENT).trim(), 60 + }); 61 + if (data.showFullscreen !== false) { 62 + setShowFullscreen(true); 63 + } 64 + } 65 + }; 66 + 67 + window.addEventListener("message", handleMessage); 68 + 69 + if (window.parent !== window) { 70 + const readyMessage: EmbedReadyMessage = { type: "rsc-embed:ready" }; 71 + window.parent.postMessage(readyMessage, "*"); 72 + } 73 + 74 + return () => window.removeEventListener("message", handleMessage); 75 + }, []); 76 + 77 + const handleCodeChange = (server: string, client: string): void => { 78 + if (window.parent !== window) { 79 + const changedMessage: EmbedCodeChangedMessage = { 80 + type: "rsc-embed:code-changed", 81 + code: { server, client }, 82 + }; 83 + window.parent.postMessage(changedMessage, "*"); 84 + } 85 + }; 86 + 87 + const getFullscreenUrl = (): string => { 88 + if (!code) return "#"; 89 + const json = JSON.stringify({ server: code.server, client: code.client }); 90 + const encoded = encodeURIComponent(btoa(unescape(encodeURIComponent(json)))); 91 + return `https://rscexplorer.dev/?c=${encoded}`; 92 + }; 93 + 94 + if (!code) { 95 + return null; 96 + } 97 + 98 + return ( 99 + <> 100 + {showFullscreen && ( 101 + <div className="EmbedApp-header"> 102 + <span className="EmbedApp-title">RSC Explorer</span> 103 + <a 104 + href={getFullscreenUrl()} 105 + target="_blank" 106 + rel="noopener noreferrer" 107 + className="EmbedApp-fullscreenLink" 108 + title="Open in RSC Explorer" 109 + > 110 + <svg 111 + width="14" 112 + height="14" 113 + viewBox="0 0 24 24" 114 + fill="none" 115 + stroke="currentColor" 116 + strokeWidth="2" 117 + > 118 + <path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3" /> 119 + </svg> 120 + </a> 121 + </div> 122 + )} 123 + <Workspace 124 + key={`${code.server}:${code.client}`} 125 + initialServerCode={code.server} 126 + initialClientCode={code.client} 127 + onCodeChange={handleCodeChange} 128 + /> 129 + </> 130 + ); 131 + }
+319
src/client/ui/FlightLog.css
··· 1 + /* FlightLog component styles */ 2 + 3 + .FlightLog { 4 + flex: 1; 5 + overflow: auto; 6 + padding: 8px; 7 + display: flex; 8 + flex-direction: column; 9 + gap: 6px; 10 + } 11 + 12 + .FlightLog-output { 13 + flex: 1; 14 + min-height: 0; 15 + margin: 0; 16 + padding: 12px; 17 + font-family: var(--font-mono); 18 + font-size: 12px; 19 + line-height: 1.6; 20 + overflow: auto; 21 + white-space: pre-wrap; 22 + word-break: break-all; 23 + background: var(--bg); 24 + color: var(--text-dim); 25 + } 26 + 27 + .FlightLog-output--error { 28 + color: #e57373; 29 + } 30 + 31 + .FlightLog-empty { 32 + color: var(--text-dim); 33 + font-style: italic; 34 + } 35 + 36 + .FlightLog-empty--waiting::after { 37 + content: "..."; 38 + animation: flightLogPulse 1.5s ease-in-out infinite; 39 + } 40 + 41 + @keyframes flightLogPulse { 42 + 0%, 43 + 100% { 44 + opacity: 0.3; 45 + } 46 + 50% { 47 + opacity: 1; 48 + } 49 + } 50 + 51 + /* FlightLogEntry */ 52 + 53 + .FlightLogEntry { 54 + background: var(--surface); 55 + border: 1px solid var(--border); 56 + border-radius: 4px; 57 + transition: all 0.15s ease; 58 + border-left: 3px solid #555; 59 + } 60 + 61 + .FlightLogEntry + .FlightLogEntry { 62 + margin-top: 12px; 63 + } 64 + 65 + .FlightLogEntry--active { 66 + border-left-color: #ffd54f; 67 + } 68 + 69 + .FlightLogEntry--done { 70 + border-left-color: #555; 71 + opacity: 0.8; 72 + } 73 + 74 + .FlightLogEntry--pending { 75 + border-left-color: #333; 76 + opacity: 0.4; 77 + } 78 + 79 + .FlightLogEntry-header { 80 + display: flex; 81 + align-items: center; 82 + justify-content: space-between; 83 + gap: 8px; 84 + padding: 6px 10px; 85 + font-size: 11px; 86 + border-bottom: 1px solid var(--border); 87 + } 88 + 89 + .FlightLogEntry-label { 90 + color: var(--text); 91 + font-weight: 500; 92 + } 93 + 94 + .FlightLogEntry-headerRight { 95 + display: flex; 96 + align-items: center; 97 + gap: 8px; 98 + } 99 + 100 + .FlightLogEntry-deleteBtn { 101 + background: transparent; 102 + border: none; 103 + color: var(--text-dim); 104 + cursor: pointer; 105 + font-size: 14px; 106 + padding: 0 4px; 107 + line-height: 1; 108 + opacity: 0.5; 109 + transition: 110 + opacity 0.15s, 111 + color 0.15s; 112 + } 113 + 114 + .FlightLogEntry-deleteBtn:hover { 115 + opacity: 1; 116 + color: #e57373; 117 + } 118 + 119 + .FlightLogEntry-request { 120 + padding: 8px 10px; 121 + background: rgba(0, 0, 0, 0.2); 122 + border-bottom: 1px solid var(--border); 123 + } 124 + 125 + .FlightLogEntry-requestArgs { 126 + margin: 0; 127 + font-family: var(--font-mono); 128 + font-size: 11px; 129 + line-height: 1.4; 130 + color: #81c784; 131 + white-space: pre-wrap; 132 + word-break: break-all; 133 + } 134 + 135 + /* RenderLogView */ 136 + 137 + .RenderLogView { 138 + border-top: 1px solid var(--border); 139 + padding: 8px; 140 + } 141 + 142 + .RenderLogView-split { 143 + display: flex; 144 + gap: 8px; 145 + align-items: stretch; 146 + } 147 + 148 + .RenderLogView-linesWrapper { 149 + flex: 1; 150 + min-width: 0; 151 + position: relative; 152 + min-height: 150px; 153 + } 154 + 155 + .RenderLogView-lines { 156 + position: absolute; 157 + top: 0; 158 + left: 0; 159 + right: 0; 160 + bottom: 0; 161 + margin: 0; 162 + padding: 6px; 163 + background: var(--bg); 164 + border-radius: 3px; 165 + font-size: 11px; 166 + line-height: 1.4; 167 + overflow: auto; 168 + } 169 + 170 + .RenderLogView-line { 171 + display: block; 172 + padding: 6px 8px; 173 + margin-bottom: 3px; 174 + border-radius: 4px; 175 + word-break: break-all; 176 + white-space: pre-wrap; 177 + border-left: 2px solid transparent; 178 + transition: all 0.15s ease; 179 + } 180 + 181 + .RenderLogView-line:last-child { 182 + margin-bottom: 0; 183 + } 184 + 185 + .RenderLogView-line--done { 186 + color: #999; 187 + background: rgba(255, 255, 255, 0.03); 188 + border-left-color: #555; 189 + } 190 + 191 + .RenderLogView-line--next { 192 + color: #e0e0e0; 193 + background: rgba(255, 213, 79, 0.12); 194 + border-left-color: #ffd54f; 195 + } 196 + 197 + .RenderLogView-line--pending { 198 + color: #444; 199 + background: transparent; 200 + border-left-color: #333; 201 + opacity: 0.4; 202 + } 203 + 204 + .RenderLogView-tree { 205 + flex: 1; 206 + min-width: 0; 207 + border-radius: 4px; 208 + overflow: auto; 209 + border: 1px solid var(--border); 210 + display: flex; 211 + flex-direction: column; 212 + } 213 + 214 + .RenderLogView-tree:has(.FlightTreeView) { 215 + background: #000; 216 + } 217 + 218 + /* RawActionForm */ 219 + 220 + .RawActionForm { 221 + margin-top: 8px; 222 + padding: 10px; 223 + background: var(--surface); 224 + border: 1px solid var(--border); 225 + border-radius: 4px; 226 + display: flex; 227 + flex-direction: column; 228 + gap: 8px; 229 + } 230 + 231 + .RawActionForm-textarea { 232 + width: 100%; 233 + padding: 8px; 234 + background: var(--bg); 235 + border: 1px solid var(--border); 236 + border-radius: 3px; 237 + color: var(--text); 238 + font-family: var(--font-mono); 239 + font-size: 11px; 240 + resize: vertical; 241 + min-height: 100px; 242 + } 243 + 244 + .RawActionForm-textarea:focus { 245 + outline: none; 246 + border-color: #555; 247 + } 248 + 249 + .RawActionForm-buttons { 250 + display: flex; 251 + gap: 8px; 252 + } 253 + 254 + .RawActionForm-submitBtn { 255 + padding: 5px 12px; 256 + border-radius: 3px; 257 + font-size: 11px; 258 + cursor: pointer; 259 + background: #ffd54f; 260 + border: none; 261 + color: #000; 262 + } 263 + 264 + .RawActionForm-submitBtn:disabled { 265 + background: #555; 266 + color: #888; 267 + cursor: not-allowed; 268 + } 269 + 270 + .RawActionForm-cancelBtn { 271 + padding: 5px 12px; 272 + border-radius: 3px; 273 + font-size: 11px; 274 + cursor: pointer; 275 + background: transparent; 276 + border: 1px solid var(--border); 277 + color: var(--text-dim); 278 + } 279 + 280 + .RawActionForm-cancelBtn:hover { 281 + border-color: #555; 282 + color: var(--text); 283 + } 284 + 285 + /* AddActionButton */ 286 + 287 + .AddActionButton-wrapper { 288 + display: flex; 289 + justify-content: center; 290 + margin-top: 8px; 291 + } 292 + 293 + .AddActionButton { 294 + width: 24px; 295 + height: 24px; 296 + padding: 0; 297 + background: var(--border); 298 + border: 1px solid #444; 299 + border-radius: 50%; 300 + color: var(--text-dim); 301 + cursor: pointer; 302 + font-size: 14px; 303 + line-height: 22px; 304 + transition: all 0.15s; 305 + } 306 + 307 + .AddActionButton:hover { 308 + background: #3a3a3a; 309 + border-color: #666; 310 + color: var(--text); 311 + } 312 + 313 + /* Responsive */ 314 + 315 + @media (max-width: 768px) { 316 + .RawActionForm-textarea { 317 + font-size: 16px !important; 318 + } 319 + }
+46 -34
src/client/ui/FlightLog.tsx
··· 1 1 import React, { useState, useRef, useEffect } from "react"; 2 2 import { FlightTreeView } from "./TreeView.tsx"; 3 + import { Select } from "./Select.tsx"; 3 4 import type { EntryView } from "../runtime/index.ts"; 5 + import "./FlightLog.css"; 4 6 5 7 function escapeHtml(str: string): string { 6 8 return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); ··· 28 30 29 31 const getLineClass = (i: number): string => { 30 32 const globalChunk = chunkStart + i; 31 - if (globalChunk < cursor) return "line-done"; 32 - if (globalChunk === cursor) return "line-next"; 33 - return "line-pending"; 33 + if (globalChunk < cursor) return "RenderLogView-line--done"; 34 + if (globalChunk === cursor) return "RenderLogView-line--next"; 35 + return "RenderLogView-line--pending"; 34 36 }; 35 37 36 38 const showTree = cursor >= chunkStart; 37 39 38 40 return ( 39 - <div className="log-entry-preview"> 40 - <div className="log-entry-split"> 41 - <div className="log-entry-flight-lines-wrapper"> 42 - <pre className="log-entry-flight-lines"> 41 + <div className="RenderLogView"> 42 + <div className="RenderLogView-split"> 43 + <div className="RenderLogView-linesWrapper"> 44 + <pre className="RenderLogView-lines"> 43 45 {rows.map((line, i) => ( 44 46 <span 45 47 key={i} 46 48 ref={i === nextLineIndex ? activeRef : null} 47 - className={`flight-line ${getLineClass(i)}`} 49 + className={`RenderLogView-line ${getLineClass(i)}`} 48 50 > 49 51 {escapeHtml(line)} 50 52 </span> 51 53 ))} 52 54 </pre> 53 55 </div> 54 - <div className="log-entry-tree"> 55 - {showTree && <FlightTreeView flightPromise={flightPromise ?? null} />} 56 + <div className="RenderLogView-tree"> 57 + {showTree && <FlightTreeView flightPromise={flightPromise ?? null} inEntry />} 56 58 </div> 57 59 </div> 58 60 </div> ··· 72 74 cursor, 73 75 onDelete, 74 76 }: FlightLogEntryProps): React.ReactElement { 75 - const entryClass = entry.isActive ? "active" : entry.isDone ? "done-entry" : "pending-entry"; 77 + const modifierClass = entry.isActive 78 + ? "FlightLogEntry--active" 79 + : entry.isDone 80 + ? "FlightLogEntry--done" 81 + : "FlightLogEntry--pending"; 76 82 77 83 return ( 78 - <div className={`log-entry ${entryClass}`}> 79 - <div className="log-entry-header"> 80 - <span className="log-entry-label"> 84 + <div className={`FlightLogEntry ${modifierClass}`}> 85 + <div className="FlightLogEntry-header"> 86 + <span className="FlightLogEntry-label"> 81 87 {entry.type === "render" ? "Render" : `Action: ${entry.name}`} 82 88 </span> 83 - <span className="log-entry-header-right"> 89 + <span className="FlightLogEntry-headerRight"> 84 90 {entry.canDelete && ( 85 - <button className="delete-entry-btn" onClick={() => onDelete(index)} title="Delete"> 91 + <button 92 + className="FlightLogEntry-deleteBtn" 93 + onClick={() => onDelete(index)} 94 + title="Delete" 95 + > 86 96 × 87 97 </button> 88 98 )} 89 99 </span> 90 100 </div> 91 101 {entry.type === "action" && entry.args && ( 92 - <div className="log-entry-request"> 93 - <pre className="log-entry-request-args">{entry.args}</pre> 102 + <div className="FlightLogEntry-request"> 103 + <pre className="FlightLogEntry-requestArgs">{entry.args}</pre> 94 104 </div> 95 105 )} 96 106 <RenderLogView entry={entry} cursor={cursor} /> ··· 134 144 135 145 if (entries.length === 0) { 136 146 return ( 137 - <div className="flight-output"> 138 - <span className="empty waiting-dots">Compiling</span> 147 + <div className="FlightLog-output"> 148 + <span className="FlightLog-empty FlightLog-empty--waiting">Compiling</span> 139 149 </div> 140 150 ); 141 151 } 142 152 143 153 return ( 144 - <div className="flight-log" ref={logRef}> 154 + <div className="FlightLog" ref={logRef}> 145 155 {entries.map((entry, i) => ( 146 156 <FlightLogEntry key={i} entry={entry} index={i} cursor={cursor} onDelete={onDeleteEntry} /> 147 157 ))} 148 158 {availableActions.length > 0 && 149 159 (showRawInput ? ( 150 - <div className="raw-input-form"> 151 - <select 152 - value={selectedAction} 153 - onChange={(e) => setSelectedAction(e.target.value)} 154 - className="raw-input-action" 155 - > 160 + <div className="RawActionForm"> 161 + <Select value={selectedAction} onChange={(e) => setSelectedAction(e.target.value)}> 156 162 {availableActions.map((action) => ( 157 163 <option key={action} value={action}> 158 164 {action} 159 165 </option> 160 166 ))} 161 - </select> 167 + </Select> 162 168 <textarea 163 169 placeholder="Paste a request payload from a real action" 164 170 value={rawPayload} 165 171 onChange={(e) => setRawPayload(e.target.value)} 166 - className="raw-input-payload" 172 + className="RawActionForm-textarea" 167 173 rows={6} 168 174 /> 169 - <div className="raw-input-buttons"> 170 - <button onClick={handleAddRaw} disabled={!rawPayload.trim()}> 175 + <div className="RawActionForm-buttons"> 176 + <button 177 + className="RawActionForm-submitBtn" 178 + onClick={handleAddRaw} 179 + disabled={!rawPayload.trim()} 180 + > 171 181 Add 172 182 </button> 173 - <button onClick={() => setShowRawInput(false)}>Cancel</button> 183 + <button className="RawActionForm-cancelBtn" onClick={() => setShowRawInput(false)}> 184 + Cancel 185 + </button> 174 186 </div> 175 187 </div> 176 188 ) : ( 177 - <div className="add-raw-btn-wrapper"> 178 - <button className="add-raw-btn" onClick={handleShowRawInput} title="Add action"> 189 + <div className="AddActionButton-wrapper"> 190 + <button className="AddActionButton" onClick={handleShowRawInput} title="Add action"> 179 191 + 180 192 </button> 181 193 </div>
+219
src/client/ui/LivePreview.css
··· 1 + /* LivePreview component styles */ 2 + 3 + .LivePreview-playback { 4 + display: flex; 5 + align-items: center; 6 + gap: 10px; 7 + padding: 8px 12px; 8 + background: var(--surface); 9 + border-bottom: 1px solid var(--border); 10 + } 11 + 12 + .LivePreview-controls { 13 + display: flex; 14 + align-items: center; 15 + gap: 4px; 16 + } 17 + 18 + .LivePreview-controlBtn { 19 + background: transparent; 20 + border: none; 21 + color: var(--text); 22 + width: 30px; 23 + height: 30px; 24 + border-radius: 4px; 25 + cursor: pointer; 26 + display: flex; 27 + align-items: center; 28 + justify-content: center; 29 + transition: all 0.1s; 30 + } 31 + 32 + .LivePreview-controlBtn svg { 33 + width: 16px; 34 + height: 16px; 35 + } 36 + 37 + .LivePreview-controlBtn:hover:not(:disabled) { 38 + background: var(--border); 39 + color: var(--text-bright); 40 + } 41 + 42 + .LivePreview-controlBtn:disabled { 43 + opacity: 0.3; 44 + cursor: not-allowed; 45 + } 46 + 47 + .LivePreview-controlBtn--playing { 48 + color: #ffd54f; 49 + } 50 + 51 + .LivePreview-controlBtn--step { 52 + background: #ffd54f; 53 + color: #000; 54 + animation: livePreviewPulseStep 1.5s ease-in-out infinite; 55 + } 56 + 57 + .LivePreview-controlBtn--step:hover:not(:disabled) { 58 + background: #ffe566; 59 + color: #000; 60 + animation: none; 61 + } 62 + 63 + .LivePreview-controlBtn--step:disabled { 64 + background: transparent; 65 + color: var(--text); 66 + animation: none; 67 + } 68 + 69 + @keyframes livePreviewPulseStep { 70 + 0%, 71 + 100% { 72 + opacity: 1; 73 + } 74 + 50% { 75 + opacity: 0.7; 76 + } 77 + } 78 + 79 + .LivePreview-slider { 80 + flex: 1; 81 + height: 4px; 82 + -webkit-appearance: none; 83 + appearance: none; 84 + background: var(--border); 85 + border-radius: 2px; 86 + outline: none; 87 + } 88 + 89 + .LivePreview-slider::-webkit-slider-thumb { 90 + -webkit-appearance: none; 91 + appearance: none; 92 + width: 14px; 93 + height: 14px; 94 + background: #ffd54f; 95 + border-radius: 50%; 96 + cursor: pointer; 97 + border: none; 98 + } 99 + 100 + .LivePreview-slider::-moz-range-thumb { 101 + width: 14px; 102 + height: 14px; 103 + background: #ffd54f; 104 + border-radius: 50%; 105 + cursor: pointer; 106 + border: none; 107 + } 108 + 109 + .LivePreview-slider:disabled { 110 + opacity: 0.5; 111 + } 112 + 113 + .LivePreview-stepInfo { 114 + font-size: 11px; 115 + color: var(--text-dim); 116 + font-family: var(--font-mono); 117 + min-width: 60px; 118 + text-align: right; 119 + } 120 + 121 + .LivePreview-container { 122 + flex: 1; 123 + padding: 20px; 124 + background: #fff; 125 + color: #111; 126 + overflow: auto; 127 + font-family: -apple-system, BlinkMacSystemFont, sans-serif; 128 + font-size: 16px; 129 + line-height: 1.5; 130 + } 131 + 132 + .LivePreview-container h1 { 133 + font-size: 28px; 134 + color: #000; 135 + margin: 0 0 16px; 136 + } 137 + 138 + .LivePreview-container h2 { 139 + font-size: 22px; 140 + color: #000; 141 + } 142 + 143 + .LivePreview-container h3 { 144 + font-size: 18px; 145 + color: #000; 146 + } 147 + 148 + .LivePreview-container p { 149 + color: #111; 150 + margin: 8px 0; 151 + } 152 + 153 + .LivePreview-container button { 154 + background: #333; 155 + color: #fff; 156 + border: none; 157 + padding: 8px 14px; 158 + border-radius: 4px; 159 + cursor: pointer; 160 + font-size: 14px; 161 + } 162 + 163 + .LivePreview-container button:hover { 164 + background: #444; 165 + } 166 + 167 + .LivePreview-empty { 168 + color: #999; 169 + font-style: italic; 170 + } 171 + 172 + .LivePreview-empty--error { 173 + color: #c0392b; 174 + } 175 + 176 + /* Responsive */ 177 + 178 + @media (max-width: 768px) { 179 + .LivePreview-playback { 180 + padding: 6px 8px; 181 + gap: 6px; 182 + } 183 + 184 + .LivePreview-controls { 185 + gap: 2px; 186 + } 187 + 188 + .LivePreview-controlBtn { 189 + width: 26px; 190 + height: 26px; 191 + } 192 + 193 + .LivePreview-stepInfo { 194 + min-width: 50px; 195 + font-size: 10px; 196 + } 197 + } 198 + 199 + @media (max-width: 480px) { 200 + .LivePreview-slider, 201 + .LivePreview-stepInfo { 202 + display: none; 203 + } 204 + 205 + .LivePreview-playback { 206 + padding: 4px 6px; 207 + gap: 4px; 208 + } 209 + 210 + .LivePreview-controlBtn { 211 + width: 24px; 212 + height: 24px; 213 + } 214 + 215 + .LivePreview-controlBtn svg { 216 + width: 14px; 217 + height: 14px; 218 + } 219 + }
+20 -14
src/client/ui/LivePreview.tsx
··· 1 1 import React, { Suspense, Component, useState, useEffect, type ReactNode } from "react"; 2 2 import type { EntryView, Thenable } from "../runtime/index.ts"; 3 + import "./LivePreview.css"; 3 4 4 5 type PreviewErrorBoundaryProps = { 5 6 children: ReactNode; ··· 22 23 render(): ReactNode { 23 24 if (this.state.error) { 24 25 return ( 25 - <span className="empty error"> 26 + <span className="LivePreview-empty LivePreview-empty--error"> 26 27 Error: {this.state.error.message || String(this.state.error)} 27 28 </span> 28 29 ); ··· 106 107 } 107 108 108 109 return ( 109 - <div className="pane preview-pane"> 110 - <div className="pane-header">preview</div> 111 - <div className="playback-container"> 112 - <div className="playback-controls"> 113 - <button className="control-btn" onClick={handleReset} disabled={isAtStart} title="Reset"> 110 + <div className="Workspace-pane Workspace-pane--preview"> 111 + <div className="Workspace-paneHeader">preview</div> 112 + <div className="LivePreview-playback"> 113 + <div className="LivePreview-controls"> 114 + <button 115 + className="LivePreview-controlBtn" 116 + onClick={handleReset} 117 + disabled={isAtStart} 118 + title="Reset" 119 + > 114 120 <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> 115 121 <path d="M8 1a7 7 0 1 1-7 7h1.5a5.5 5.5 0 1 0 1.6-3.9L6 6H1V1l1.6 1.6A7 7 0 0 1 8 1z" /> 116 122 </svg> 117 123 </button> 118 124 <button 119 - className={`control-btn play-btn${isPlaying ? " playing" : ""}`} 125 + className={`LivePreview-controlBtn${isPlaying ? " LivePreview-controlBtn--playing" : ""}`} 120 126 onClick={handlePlayPause} 121 127 disabled={isAtEnd} 122 128 title={isPlaying ? "Pause" : "Play"} ··· 132 138 )} 133 139 </button> 134 140 <button 135 - className={`control-btn ${!isAtEnd ? "step-btn" : ""}`} 141 + className={`LivePreview-controlBtn${!isAtEnd ? " LivePreview-controlBtn--step" : ""}`} 136 142 onClick={handleStep} 137 143 disabled={isAtEnd} 138 144 title="Step forward" ··· 149 155 </svg> 150 156 </button> 151 157 <button 152 - className="control-btn" 158 + className="LivePreview-controlBtn" 153 159 onClick={handleSkip} 154 160 disabled={isAtEnd} 155 161 title="Skip to end" ··· 166 172 value={cursor} 167 173 onChange={() => {}} 168 174 disabled 169 - className="step-slider" 175 + className="LivePreview-slider" 170 176 /> 171 - <span className="step-info">{statusText}</span> 177 + <span className="LivePreview-stepInfo">{statusText}</span> 172 178 </div> 173 - <div className="preview-container"> 179 + <div className="LivePreview-container"> 174 180 {showPlaceholder ? ( 175 - <span className="empty">{isAtStart ? "Step to begin..." : "Loading..."}</span> 181 + <span className="LivePreview-empty">{isAtStart ? "Step to begin..." : "Loading..."}</span> 176 182 ) : flightPromise ? ( 177 183 <PreviewErrorBoundary> 178 - <Suspense fallback={<span className="empty">Loading...</span>}> 184 + <Suspense fallback={<span className="LivePreview-empty">Loading...</span>}> 179 185 <StreamingContent streamPromise={flightPromise} /> 180 186 </Suspense> 181 187 </PreviewErrorBoundary>
+65
src/client/ui/Select.css
··· 1 + /* Shared Select component styles */ 2 + 3 + .Select { 4 + width: 100%; 5 + -webkit-appearance: none; 6 + appearance: none; 7 + text-overflow: ellipsis; 8 + overflow: hidden; 9 + white-space: nowrap; 10 + background: 11 + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2.5'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") 12 + no-repeat right var(--select-chevron-offset, 8px) center / 12px 12px, 13 + linear-gradient(to bottom, #333, #2a2a2a); 14 + border: 1px solid #555; 15 + border-radius: 4px; 16 + color: #fff; 17 + padding: 5px 28px 5px 10px; 18 + font-size: 13px; 19 + font-weight: 500; 20 + cursor: pointer; 21 + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); 22 + } 23 + 24 + .Select:hover { 25 + border-color: #666; 26 + background: 27 + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23bbb' stroke-width='2.5'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") 28 + no-repeat right var(--select-chevron-offset, 8px) center / 12px 12px, 29 + linear-gradient(to bottom, #3a3a3a, #333); 30 + } 31 + 32 + .Select:focus { 33 + outline: none; 34 + border-color: #ffd54f; 35 + } 36 + 37 + .Select option { 38 + background: #2a2a2a; 39 + color: #e0e0e0; 40 + padding: 8px; 41 + font-size: 13px; 42 + } 43 + 44 + .Select option:hover, 45 + .Select option:checked { 46 + background: #3a3a3a; 47 + color: #ffd54f; 48 + } 49 + 50 + /* Responsive */ 51 + 52 + @media (max-width: 768px) { 53 + .Select { 54 + --select-chevron-offset: 4px; 55 + padding: 4px 20px 4px 6px; 56 + font-size: 16px; 57 + } 58 + } 59 + 60 + @media (max-width: 480px) { 61 + .Select { 62 + --select-chevron-offset: 3px; 63 + padding: 3px 18px 3px 5px; 64 + } 65 + }
+17
src/client/ui/Select.tsx
··· 1 + import type { ChangeEvent, ReactNode } from "react"; 2 + import "./Select.css"; 3 + 4 + type SelectProps = { 5 + value: string; 6 + onChange: (e: ChangeEvent<HTMLSelectElement>) => void; 7 + disabled?: boolean; 8 + children: ReactNode; 9 + }; 10 + 11 + export function Select({ value, onChange, disabled, children }: SelectProps) { 12 + return ( 13 + <select className="Select" value={value} onChange={onChange} disabled={disabled}> 14 + {children} 15 + </select> 16 + ); 17 + }
+157
src/client/ui/TreeView.css
··· 1 + /* TreeView component styles */ 2 + 3 + .FlightTreeView { 4 + flex: 1; 5 + min-height: 0; 6 + padding: 12px; 7 + font-family: var(--font-mono); 8 + font-size: 12px; 9 + line-height: 1.8; 10 + overflow: auto; 11 + background: var(--bg); 12 + } 13 + 14 + .FlightTreeView--inEntry { 15 + padding: 8px; 16 + font-size: 11px; 17 + line-height: 1.6; 18 + } 19 + 20 + .FlightTreeView-output { 21 + margin: 0; 22 + white-space: pre-wrap; 23 + word-break: break-word; 24 + color: var(--text); 25 + } 26 + 27 + /* PendingFallback */ 28 + 29 + .PendingFallback { 30 + display: inline-block; 31 + color: #64b5f6; 32 + background: rgba(100, 181, 246, 0.15); 33 + padding: 2px 8px; 34 + border-radius: 10px; 35 + border: 1px solid rgba(100, 181, 246, 0.4); 36 + font-size: 10px; 37 + font-weight: 500; 38 + letter-spacing: 0.5px; 39 + animation: pendingPulse 2s ease-in-out infinite; 40 + } 41 + 42 + @keyframes pendingPulse { 43 + 0%, 44 + 100% { 45 + opacity: 0.7; 46 + } 47 + 50% { 48 + opacity: 1; 49 + } 50 + } 51 + 52 + /* ErrorFallback */ 53 + 54 + .ErrorFallback { 55 + display: inline-block; 56 + color: #e57373; 57 + background: rgba(229, 115, 115, 0.15); 58 + padding: 2px 8px; 59 + border-radius: 10px; 60 + border: 1px solid rgba(229, 115, 115, 0.4); 61 + font-size: 10px; 62 + font-weight: 500; 63 + letter-spacing: 0.5px; 64 + } 65 + 66 + /* JSXValue types */ 67 + 68 + .JSXValue-null { 69 + color: #5c6370; 70 + font-style: italic; 71 + } 72 + 73 + .JSXValue-undefined { 74 + color: #5c6370; 75 + font-style: italic; 76 + } 77 + 78 + .JSXValue-string { 79 + color: #98c379; 80 + } 81 + 82 + .JSXValue-number { 83 + color: #d19a66; 84 + } 85 + 86 + .JSXValue-boolean { 87 + color: #56b6c2; 88 + } 89 + 90 + .JSXValue-symbol { 91 + color: #c678dd; 92 + } 93 + 94 + .JSXValue-function { 95 + color: #61afef; 96 + font-style: italic; 97 + } 98 + 99 + .JSXValue-circular { 100 + color: #e57373; 101 + font-style: italic; 102 + } 103 + 104 + .JSXValue-date { 105 + color: #e5c07b; 106 + } 107 + 108 + .JSXValue-collection { 109 + color: var(--text-dim); 110 + } 111 + 112 + .JSXValue-iterator { 113 + color: var(--text-dim); 114 + font-style: italic; 115 + } 116 + 117 + .JSXValue-stream { 118 + color: var(--text-dim); 119 + font-style: italic; 120 + } 121 + 122 + .JSXValue-key { 123 + color: #61afef; 124 + } 125 + 126 + .JSXValue-empty { 127 + color: #5c6370; 128 + font-style: italic; 129 + } 130 + 131 + .JSXValue-unknown { 132 + color: var(--text-dim); 133 + } 134 + 135 + /* JSXElement */ 136 + 137 + .JSXElement-tag { 138 + color: #e06c75; 139 + } 140 + 141 + .JSXElement-clientTag { 142 + color: #c678dd; 143 + } 144 + 145 + .JSXElement-reactTag { 146 + color: #e5c07b; 147 + } 148 + 149 + .JSXElement-propName { 150 + color: #61afef; 151 + } 152 + 153 + .JSXElement-children { 154 + margin-left: 16px; 155 + border-left: 1px solid #333; 156 + padding-left: 12px; 157 + }
+54 -45
src/client/ui/TreeView.tsx
··· 1 1 import React, { Suspense, Component, type ReactNode } from "react"; 2 2 import type { Thenable } from "../runtime/index.ts"; 3 + import "./TreeView.css"; 3 4 4 5 // Internal React element type with $$typeof 5 6 type ReactElementInternal = { ··· 28 29 } 29 30 30 31 function PendingFallback(): React.ReactElement { 31 - return <span className="tree-pending">Pending</span>; 32 + return <span className="PendingFallback">Pending</span>; 32 33 } 33 34 34 35 type ErrorFallbackProps = { ··· 37 38 38 39 function ErrorFallback({ error }: ErrorFallbackProps): React.ReactElement { 39 40 const message = error instanceof Error ? error.message : String(error); 40 - return <span className="tree-error">Error: {message}</span>; 41 + return <span className="ErrorFallback">Error: {message}</span>; 41 42 } 42 43 43 44 type ErrorBoundaryProps = { ··· 113 114 114 115 // `ancestors` tracks the current path for cycle detection 115 116 function JSXValue({ value, indent = 0, ancestors = [] }: JSXValueProps): React.ReactElement { 116 - if (value === null) return <span className="tree-null">null</span>; 117 - if (value === undefined) return <span className="tree-undefined">undefined</span>; 117 + if (value === null) return <span className="JSXValue-null">null</span>; 118 + if (value === undefined) return <span className="JSXValue-undefined">undefined</span>; 118 119 119 120 if (typeof value === "string") { 120 121 const display = value.length > 50 ? value.slice(0, 50) + "..." : value; 121 - return <span className="tree-string">"{escapeHtml(display)}"</span>; 122 + return <span className="JSXValue-string">"{escapeHtml(display)}"</span>; 122 123 } 123 124 if (typeof value === "number") { 124 125 const display = Object.is(value, -0) ? "-0" : String(value); 125 - return <span className="tree-number">{display}</span>; 126 + return <span className="JSXValue-number">{display}</span>; 126 127 } 127 128 if (typeof value === "bigint") { 128 - return <span className="tree-number">{String(value)}n</span>; 129 + return <span className="JSXValue-number">{String(value)}n</span>; 129 130 } 130 - if (typeof value === "boolean") return <span className="tree-boolean">{String(value)}</span>; 131 + if (typeof value === "boolean") return <span className="JSXValue-boolean">{String(value)}</span>; 131 132 if (typeof value === "symbol") { 132 - return <span className="tree-symbol">{value.toString()}</span>; 133 + return <span className="JSXValue-symbol">{value.toString()}</span>; 133 134 } 134 135 if (typeof value === "function") { 135 136 return ( 136 - <span className="tree-function"> 137 + <span className="JSXValue-function"> 137 138 [Function: {(value as { name?: string }).name || "anonymous"}] 138 139 </span> 139 140 ); ··· 141 142 142 143 if (typeof value === "object" && value !== null) { 143 144 if (ancestors.includes(value)) { 144 - return <span className="tree-circular">[Circular]</span>; 145 + return <span className="JSXValue-circular">[Circular]</span>; 145 146 } 146 147 } 147 148 ··· 149 150 typeof value === "object" && value !== null ? [...ancestors, value] : ancestors; 150 151 151 152 if (value instanceof Date) { 152 - return <span className="tree-date">Date({value.toISOString()})</span>; 153 + return <span className="JSXValue-date">Date({value.toISOString()})</span>; 153 154 } 154 155 155 156 if (value instanceof Map) { 156 - if (value.size === 0) return <span className="tree-collection">Map(0) {"{}"}</span>; 157 + if (value.size === 0) return <span className="JSXValue-collection">Map(0) {"{}"}</span>; 157 158 const pad = " ".repeat(indent + 1); 158 159 const closePad = " ".repeat(indent); 159 160 return ( 160 161 <> 161 - <span className="tree-collection"> 162 + <span className="JSXValue-collection"> 162 163 Map({value.size}) {"{\n"} 163 164 </span> 164 165 {Array.from(value.entries()).map(([k, v], i) => ( ··· 177 178 } 178 179 179 180 if (value instanceof Set) { 180 - if (value.size === 0) return <span className="tree-collection">Set(0) {"{}"}</span>; 181 + if (value.size === 0) return <span className="JSXValue-collection">Set(0) {"{}"}</span>; 181 182 const pad = " ".repeat(indent + 1); 182 183 const closePad = " ".repeat(indent); 183 184 return ( 184 185 <> 185 - <span className="tree-collection"> 186 + <span className="JSXValue-collection"> 186 187 Set({value.size}) {"{\n"} 187 188 </span> 188 189 {Array.from(value).map((v, i) => ( ··· 201 202 202 203 if (value instanceof FormData) { 203 204 const entries = Array.from(value.entries()); 204 - if (entries.length === 0) return <span className="tree-collection">FormData {"{}"}</span>; 205 + if (entries.length === 0) return <span className="JSXValue-collection">FormData {"{}"}</span>; 205 206 const pad = " ".repeat(indent + 1); 206 207 const closePad = " ".repeat(indent); 207 208 return ( 208 209 <> 209 - <span className="tree-collection">FormData {"{\n"}</span> 210 + <span className="JSXValue-collection">FormData {"{\n"}</span> 210 211 {entries.map(([k, v], i) => ( 211 212 <React.Fragment key={i}> 212 213 {pad} 213 - <span className="tree-key">{k}</span>:{" "} 214 + <span className="JSXValue-key">{k}</span>:{" "} 214 215 <JSXValue value={v} indent={indent + 1} ancestors={nextAncestors} /> 215 216 {i < entries.length - 1 ? "," : ""} 216 217 {"\n"} ··· 224 225 225 226 if (value instanceof Blob) { 226 227 return ( 227 - <span className="tree-collection"> 228 + <span className="JSXValue-collection"> 228 229 Blob({value.size} bytes, "{value.type || "application/octet-stream"}") 229 230 </span> 230 231 ); ··· 236 237 const preview = Array.from(arr.slice(0, 5)).join(", "); 237 238 const suffix = arr.length > 5 ? ", ..." : ""; 238 239 return ( 239 - <span className="tree-collection"> 240 + <span className="JSXValue-collection"> 240 241 {name}({arr.length}) [{preview} 241 242 {suffix}] 242 243 </span> 243 244 ); 244 245 } 245 246 if (value instanceof ArrayBuffer) { 246 - return <span className="tree-collection">ArrayBuffer({value.byteLength} bytes)</span>; 247 + return <span className="JSXValue-collection">ArrayBuffer({value.byteLength} bytes)</span>; 247 248 } 248 249 249 250 if (Array.isArray(value)) { ··· 252 253 253 254 const renderItem = (i: number): React.ReactElement => { 254 255 if (!(i in value)) { 255 - return <span className="tree-empty">empty</span>; 256 + return <span className="JSXValue-empty">empty</span>; 256 257 } 257 258 return <JSXValue value={value[i]} indent={indent + 1} ancestors={nextAncestors} />; 258 259 }; ··· 297 298 const obj = value as Record<string | symbol, unknown>; 298 299 299 300 if (typeof obj.next === "function" && typeof obj[Symbol.iterator] === "function") { 300 - return <span className="tree-iterator">Iterator {"{}"}</span>; 301 + return <span className="JSXValue-iterator">Iterator {"{}"}</span>; 301 302 } 302 303 303 304 if (typeof obj[Symbol.asyncIterator] === "function") { 304 - return <span className="tree-iterator">AsyncIterator {"{}"}</span>; 305 + return <span className="JSXValue-iterator">AsyncIterator {"{}"}</span>; 305 306 } 306 307 307 308 if (value instanceof ReadableStream) { 308 - return <span className="tree-stream">ReadableStream {"{}"}</span>; 309 + return <span className="JSXValue-stream">ReadableStream {"{}"}</span>; 309 310 } 310 311 311 312 if (typeof obj.then === "function") { ··· 340 341 {"{ "} 341 342 {entries.map(([k, v], i) => ( 342 343 <React.Fragment key={k}> 343 - <span className="tree-key">{k}</span>:{" "} 344 + <span className="JSXValue-key">{k}</span>:{" "} 344 345 <JSXValue value={v} indent={indent} ancestors={nextAncestors} /> 345 346 {i < entries.length - 1 ? ", " : ""} 346 347 </React.Fragment> ··· 357 358 {entries.map(([k, v], i) => ( 358 359 <React.Fragment key={k}> 359 360 {pad} 360 - <span className="tree-key">{k}</span>:{" "} 361 + <span className="JSXValue-key">{k}</span>:{" "} 361 362 <JSXValue value={v} indent={indent + 1} ancestors={nextAncestors} /> 362 363 {i < entries.length - 1 ? "," : ""} 363 364 {"\n"} ··· 369 370 ); 370 371 } 371 372 372 - return <span className="tree-unknown">{String(value)}</span>; 373 + return <span className="JSXValue-unknown">{String(value)}</span>; 373 374 } 374 375 375 376 type JSXElementProps = { ··· 384 385 const padInner = " ".repeat(indent + 1); 385 386 386 387 let tagName: string; 387 - let tagClass = "tree-tag"; 388 + let tagClass = "JSXElement-tag"; 388 389 if (typeof type === "string") { 389 390 tagName = type; 390 391 } else if (typeof type === "function") { 391 392 const funcType = type as { displayName?: string; name?: string }; 392 393 tagName = funcType.displayName || funcType.name || "Component"; 393 - tagClass = "tree-client-tag"; 394 + tagClass = "JSXElement-clientTag"; 394 395 } else if (typeof type === "symbol") { 395 396 switch (type) { 396 397 case Symbol.for("react.fragment"): ··· 417 418 default: 418 419 tagName = "Unknown"; 419 420 } 420 - tagClass = "tree-react-tag"; 421 + tagClass = "JSXElement-reactTag"; 421 422 } else if (type && typeof type === "object" && (type as { $$typeof?: symbol }).$$typeof) { 422 423 const lazyType = type as ReactLazy; 423 424 if (lazyType.$$typeof === Symbol.for("react.lazy")) { ··· 434 435 ); 435 436 } 436 437 tagName = "Component"; 437 - tagClass = "tree-client-tag"; 438 + tagClass = "JSXElement-clientTag"; 438 439 } else { 439 440 tagName = "Unknown"; 440 441 } ··· 455 456 {key != null && ( 456 457 <> 457 458 {" "} 458 - <span className="tree-prop-name">key</span>=<span className="tree-string">"{key}"</span> 459 + <span className="JSXElement-propName">key</span>= 460 + <span className="JSXValue-string">"{key}"</span> 459 461 </> 460 462 )} 461 463 {propEntries.map(([k, v]) => ( ··· 473 475 {key != null && ( 474 476 <> 475 477 {" "} 476 - <span className="tree-prop-name">key</span>=<span className="tree-string">"{key}"</span> 478 + <span className="JSXElement-propName">key</span>= 479 + <span className="JSXValue-string">"{key}"</span> 477 480 </> 478 481 )} 479 482 {propEntries.map(([k, v]) => ( ··· 508 511 return ( 509 512 <> 510 513 {" "} 511 - <span className="tree-prop-name">{name}</span>= 512 - <span className="tree-string">"{escapeHtml(value)}"</span> 514 + <span className="JSXElement-propName">{name}</span>= 515 + <span className="JSXValue-string">"{escapeHtml(value)}"</span> 513 516 </> 514 517 ); 515 518 } ··· 519 522 return ( 520 523 <> 521 524 {" "} 522 - <span className="tree-prop-name">{name}</span>={"{"} 525 + <span className="JSXElement-propName">{name}</span>={"{"} 523 526 {"\n"} 524 527 {pad} 525 528 <JSXValue value={value} indent={indent} ancestors={ancestors} /> ··· 535 538 return ( 536 539 <> 537 540 {" "} 538 - <span className="tree-prop-name">{name}</span>={"{["} 541 + <span className="JSXElement-propName">{name}</span>={"{["} 539 542 {"\n"} 540 543 {value.map((v, i) => ( 541 544 <React.Fragment key={i}> ··· 553 556 return ( 554 557 <> 555 558 {" "} 556 - <span className="tree-prop-name">{name}</span>={"{"} 559 + <span className="JSXElement-propName">{name}</span>={"{"} 557 560 <JSXValue value={value} indent={indent} ancestors={ancestors} /> 558 561 {"}"} 559 562 </> ··· 599 602 600 603 type FlightTreeViewProps = { 601 604 flightPromise: Thenable<unknown> | null; 605 + inEntry?: boolean; 602 606 }; 603 607 604 - export function FlightTreeView({ flightPromise }: FlightTreeViewProps): React.ReactElement { 608 + export function FlightTreeView({ 609 + flightPromise, 610 + inEntry, 611 + }: FlightTreeViewProps): React.ReactElement { 612 + const className = inEntry ? "FlightTreeView FlightTreeView--inEntry" : "FlightTreeView"; 613 + 605 614 if (!flightPromise) { 606 615 return ( 607 - <div className="flight-tree"> 608 - <pre className="jsx-output"> 616 + <div className={className}> 617 + <pre className="FlightTreeView-output"> 609 618 <PendingFallback /> 610 619 </pre> 611 620 </div> ··· 613 622 } 614 623 615 624 return ( 616 - <div className="flight-tree"> 617 - <pre className="jsx-output"> 625 + <div className={className}> 626 + <pre className="FlightTreeView-output"> 618 627 <ErrorBoundary> 619 628 <Suspense fallback={<PendingFallback />}> 620 629 <Await promise={flightPromise}>
+97
src/client/ui/Workspace.css
··· 1 + /* Workspace component styles */ 2 + 3 + .Workspace { 4 + flex: 1; 5 + min-height: 0; 6 + display: grid; 7 + grid-template-columns: 50% 50%; 8 + grid-template-rows: 50% 50%; 9 + grid-template-areas: 10 + "server flight" 11 + "client preview"; 12 + overflow: hidden; 13 + } 14 + 15 + .Workspace-pane { 16 + display: flex; 17 + flex-direction: column; 18 + overflow: hidden; 19 + } 20 + 21 + .Workspace-pane--server { 22 + grid-area: server; 23 + border-right: 1px solid var(--border); 24 + border-bottom: 1px solid var(--border); 25 + } 26 + 27 + .Workspace-pane--client { 28 + grid-area: client; 29 + border-right: 1px solid var(--border); 30 + } 31 + 32 + .Workspace-pane--flight { 33 + grid-area: flight; 34 + border-bottom: 1px solid var(--border); 35 + } 36 + 37 + .Workspace-pane--preview { 38 + grid-area: preview; 39 + } 40 + 41 + .Workspace-paneHeader { 42 + padding: 6px 12px; 43 + font-size: 10px; 44 + text-transform: uppercase; 45 + letter-spacing: 1px; 46 + color: var(--text-dim); 47 + flex-shrink: 0; 48 + border-bottom: 1px solid var(--border); 49 + } 50 + 51 + /* WorkspaceLoading states */ 52 + 53 + .WorkspaceLoading-output { 54 + flex: 1; 55 + min-height: 0; 56 + margin: 0; 57 + padding: 12px; 58 + font-family: var(--font-mono); 59 + font-size: 12px; 60 + line-height: 1.6; 61 + overflow: auto; 62 + white-space: pre-wrap; 63 + word-break: break-all; 64 + background: var(--bg); 65 + color: var(--text-dim); 66 + } 67 + 68 + .WorkspaceLoading-preview { 69 + flex: 1; 70 + padding: 20px; 71 + background: #fff; 72 + color: #111; 73 + overflow: auto; 74 + font-family: -apple-system, BlinkMacSystemFont, sans-serif; 75 + font-size: 16px; 76 + line-height: 1.5; 77 + } 78 + 79 + .WorkspaceLoading-empty { 80 + color: var(--text-dim); 81 + font-style: italic; 82 + } 83 + 84 + .WorkspaceLoading-empty--waiting::after { 85 + content: "..."; 86 + animation: workspaceLoadingPulse 1.5s ease-in-out infinite; 87 + } 88 + 89 + @keyframes workspaceLoadingPulse { 90 + 0%, 91 + 100% { 92 + opacity: 0.3; 93 + } 94 + 50% { 95 + opacity: 1; 96 + } 97 + }
+21 -20
src/client/ui/Workspace.tsx
··· 3 3 import { CodeEditor } from "./CodeEditor.tsx"; 4 4 import { FlightLog } from "./FlightLog.tsx"; 5 5 import { LivePreview } from "./LivePreview.tsx"; 6 + import "./Workspace.css"; 6 7 7 8 type WorkspaceProps = { 8 9 initialServerCode: string; ··· 47 48 } 48 49 49 50 return ( 50 - <main> 51 + <main className="Workspace"> 51 52 <CodeEditor 52 53 label="server" 53 54 defaultValue={serverCode} 54 55 onChange={handleServerChange} 55 - className="editor-server" 56 + paneClass="Workspace-pane--server" 56 57 /> 57 58 <CodeEditor 58 59 label="client" 59 60 defaultValue={clientCode} 60 61 onChange={handleClientChange} 61 - className="editor-client" 62 + paneClass="Workspace-pane--client" 62 63 /> 63 64 {session ? ( 64 65 <WorkspaceContent session={session} onReset={reset} key={session.id} /> ··· 72 73 function WorkspaceLoading(): React.ReactElement { 73 74 return ( 74 75 <> 75 - <div className="pane flight-pane"> 76 - <div className="pane-header">flight</div> 77 - <div className="flight-output"> 78 - <span className="empty waiting-dots">Compiling</span> 76 + <div className="Workspace-pane Workspace-pane--flight"> 77 + <div className="Workspace-paneHeader">flight</div> 78 + <div className="WorkspaceLoading-output"> 79 + <span className="WorkspaceLoading-empty WorkspaceLoading-empty--waiting">Compiling</span> 79 80 </div> 80 81 </div> 81 - <div className="pane preview-pane"> 82 - <div className="pane-header">preview</div> 83 - <div className="preview-container"> 84 - <span className="empty waiting-dots">Compiling</span> 82 + <div className="Workspace-pane Workspace-pane--preview"> 83 + <div className="Workspace-paneHeader">preview</div> 84 + <div className="WorkspaceLoading-preview"> 85 + <span className="WorkspaceLoading-empty WorkspaceLoading-empty--waiting">Compiling</span> 85 86 </div> 86 87 </div> 87 88 </> ··· 102 103 if (session.state.status === "error") { 103 104 return ( 104 105 <> 105 - <div className="pane flight-pane"> 106 - <div className="pane-header">flight</div> 107 - <pre className="flight-output error">{session.state.message}</pre> 106 + <div className="Workspace-pane Workspace-pane--flight"> 107 + <div className="Workspace-paneHeader">flight</div> 108 + <pre className="FlightLog-output FlightLog-output--error">{session.state.message}</pre> 108 109 </div> 109 - <div className="pane preview-pane"> 110 - <div className="pane-header">preview</div> 111 - <div className="preview-container"> 112 - <span className="empty error">Compilation error</span> 110 + <div className="Workspace-pane Workspace-pane--preview"> 111 + <div className="Workspace-paneHeader">preview</div> 112 + <div className="LivePreview-container"> 113 + <span className="LivePreview-empty LivePreview-empty--error">Compilation error</span> 113 114 </div> 114 115 </div> 115 116 </> ··· 120 121 121 122 return ( 122 123 <> 123 - <div className="pane flight-pane"> 124 - <div className="pane-header">flight</div> 124 + <div className="Workspace-pane Workspace-pane--flight"> 125 + <div className="Workspace-paneHeader">flight</div> 125 126 <FlightLog 126 127 entries={entries} 127 128 cursor={cursor}