|
73 | 73 | font-weight: 500; |
74 | 74 | letter-spacing: 0.01em; |
75 | 75 | } |
76 | | - #live { |
| 76 | + .livedot { |
77 | 77 | width: 7px; |
78 | 78 | height: 7px; |
79 | 79 | border-radius: 50%; |
80 | 80 | background: var(--faint); |
81 | 81 | transition: background 0.3s; |
82 | 82 | } |
83 | | - #live.on { |
| 83 | + .livedot.on { |
84 | 84 | background: #4caf78; |
85 | 85 | } |
86 | 86 | #sessionList { |
|
407 | 407 | color: var(--text); |
408 | 408 | } |
409 | 409 |
|
| 410 | + /* phone widths: the sidebar becomes an off-canvas drawer behind a slim |
| 411 | + top bar; the stream keeps the full width */ |
| 412 | + .topbar { |
| 413 | + display: none; |
| 414 | + } |
| 415 | + #scrim { |
| 416 | + display: none; |
| 417 | + } |
| 418 | + @media (max-width: 700px) { |
| 419 | + #app { |
| 420 | + flex-direction: column; |
| 421 | + } |
| 422 | + .topbar { |
| 423 | + display: flex; |
| 424 | + align-items: center; |
| 425 | + gap: 4px; |
| 426 | + flex: none; |
| 427 | + padding: 8px 10px; |
| 428 | + background: var(--panel); |
| 429 | + border-bottom: 0.5px solid var(--border); |
| 430 | + } |
| 431 | + .topbar .brand { |
| 432 | + padding: 0; |
| 433 | + } |
| 434 | + .topbar .menu { |
| 435 | + position: relative; |
| 436 | + font-size: 17px; |
| 437 | + line-height: 1; |
| 438 | + color: var(--muted); |
| 439 | + background: none; |
| 440 | + border: none; |
| 441 | + border-radius: 8px; |
| 442 | + padding: 6px 9px; |
| 443 | + cursor: pointer; |
| 444 | + font-family: inherit; |
| 445 | + } |
| 446 | + .topbar .menu:hover { |
| 447 | + color: var(--text); |
| 448 | + background: var(--hover); |
| 449 | + } |
| 450 | + .topbar .menu .dot { |
| 451 | + position: absolute; |
| 452 | + top: 3px; |
| 453 | + right: 4px; |
| 454 | + width: 7px; |
| 455 | + height: 7px; |
| 456 | + border-radius: 50%; |
| 457 | + background: var(--accent); |
| 458 | + display: none; |
| 459 | + } |
| 460 | + .topbar .menu .dot.show { |
| 461 | + display: block; |
| 462 | + } |
| 463 | + aside { |
| 464 | + position: fixed; |
| 465 | + top: 0; |
| 466 | + bottom: 0; |
| 467 | + left: 0; |
| 468 | + z-index: 30; |
| 469 | + width: min(280px, 84vw); |
| 470 | + transform: translateX(-105%); |
| 471 | + transition: transform 0.2s ease; |
| 472 | + } |
| 473 | + body.nav-open aside { |
| 474 | + transform: none; |
| 475 | + box-shadow: 0 0 32px rgba(0, 0, 0, 0.25); |
| 476 | + } |
| 477 | + #scrim { |
| 478 | + display: block; |
| 479 | + position: fixed; |
| 480 | + inset: 0; |
| 481 | + z-index: 25; |
| 482 | + background: rgba(0, 0, 0, 0.35); |
| 483 | + opacity: 0; |
| 484 | + pointer-events: none; |
| 485 | + transition: opacity 0.2s; |
| 486 | + } |
| 487 | + body.nav-open #scrim { |
| 488 | + opacity: 1; |
| 489 | + pointer-events: auto; |
| 490 | + } |
| 491 | + main { |
| 492 | + min-height: 0; |
| 493 | + } |
| 494 | + .session-head { |
| 495 | + padding: 12px 16px 10px; |
| 496 | + } |
| 497 | + #stream { |
| 498 | + padding: 16px 14px 120px; |
| 499 | + } |
| 500 | + #onboard { |
| 501 | + padding: 40px 18px; |
| 502 | + } |
| 503 | + .card-title { |
| 504 | + min-width: 0; |
| 505 | + flex: 0 1 auto; |
| 506 | + white-space: nowrap; |
| 507 | + overflow: hidden; |
| 508 | + text-overflow: ellipsis; |
| 509 | + } |
| 510 | + } |
| 511 | + /* narrow or touch: hover-revealed actions must stay reachable */ |
| 512 | + @media (max-width: 700px), (hover: none) { |
| 513 | + .card-head .act { |
| 514 | + opacity: 1; |
| 515 | + } |
| 516 | + .sess .x { |
| 517 | + opacity: 1; |
| 518 | + } |
| 519 | + .sess-title { |
| 520 | + padding-right: 44px; |
| 521 | + } |
| 522 | + .sess .dot { |
| 523 | + right: 32px; |
| 524 | + } |
| 525 | + } |
| 526 | + |
410 | 527 | #toast { |
411 | 528 | position: fixed; |
412 | 529 | left: 50%; |
|
435 | 552 | </head> |
436 | 553 | <body> |
437 | 554 | <div id="app"> |
| 555 | + <header class="topbar"> |
| 556 | + <button class="menu" id="menuBtn" aria-label="Show sessions"> |
| 557 | + ☰<span class="dot" id="menuDot"></span> |
| 558 | + </button> |
| 559 | + <div class="brand"><span class="livedot"></span>sideshow</div> |
| 560 | + </header> |
438 | 561 | <aside> |
439 | | - <div class="brand"><span id="live"></span>sideshow</div> |
| 562 | + <div class="brand"><span class="livedot"></span>sideshow</div> |
440 | 563 | <div id="sessionList"></div> |
441 | 564 | <div class="aside-foot"> |
442 | 565 | <a href="/guide" target="_blank">design guide</a> · |
@@ -479,6 +602,7 @@ <h2>or try it yourself</h2> |
479 | 602 | </div> |
480 | 603 | </main> |
481 | 604 | </div> |
| 605 | + <div id="scrim"></div> |
482 | 606 | <div id="toast" role="status" aria-live="polite"></div> |
483 | 607 | <script> |
484 | 608 | const $ = (id) => document.getElementById(id); |
@@ -520,6 +644,15 @@ <h2>or try it yourself</h2> |
520 | 644 | return s.title || s.agent + " session"; |
521 | 645 | } |
522 | 646 |
|
| 647 | + function setLive(on) { |
| 648 | + for (const el of document.querySelectorAll(".livedot")) el.classList.toggle("on", on); |
| 649 | + } |
| 650 | + |
| 651 | + // --- mobile drawer --- |
| 652 | + |
| 653 | + $("menuBtn").onclick = () => document.body.classList.toggle("nav-open"); |
| 654 | + $("scrim").onclick = () => document.body.classList.remove("nav-open"); |
| 655 | + |
523 | 656 | // --- sidebar --- |
524 | 657 |
|
525 | 658 | function renderSidebar() { |
@@ -577,6 +710,7 @@ <h2>or try it yourself</h2> |
577 | 710 | } |
578 | 711 | $("onboard").hidden = state.sessions.length > 0; |
579 | 712 | $("sessionView").hidden = state.sessions.length === 0; |
| 713 | + $("menuDot").classList.toggle("show", state.unread.size > 0); |
580 | 714 | } |
581 | 715 |
|
582 | 716 | async function refreshSessions() { |
@@ -621,6 +755,7 @@ <h2>or try it yourself</h2> |
621 | 755 | async function select(id) { |
622 | 756 | state.selected = id; |
623 | 757 | state.unread.delete(id); |
| 758 | + document.body.classList.remove("nav-open"); |
624 | 759 | renderSidebar(); |
625 | 760 | renderSessionHead(); |
626 | 761 | const stream = $("stream"); |
@@ -818,8 +953,8 @@ <h2>or try it yourself</h2> |
818 | 953 |
|
819 | 954 | function connect() { |
820 | 955 | const es = new EventSource("/api/events"); |
821 | | - es.onopen = () => $("live").classList.add("on"); |
822 | | - es.onerror = () => $("live").classList.remove("on"); |
| 956 | + es.onopen = () => setLive(true); |
| 957 | + es.onerror = () => setLive(false); |
823 | 958 | es.onmessage = async (ev) => { |
824 | 959 | const e = JSON.parse(ev.data); |
825 | 960 | if (e.type.startsWith("session-")) { |
|
0 commit comments