From 99361496c964e9d1541d5ebb458751953d740919 Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Fri, 16 Oct 2026 13:28:14 -0400 Subject: [PATCH 01/10] fix(website): clamp info-tip bubbles inside the viewport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bubble is horizontally centered on its (i) toggle, so a toggle near the viewport edge (e.g. the I/O card's tooltip on the homepage) opened half outside the screen and clipped. max-width bounds the width, not the position. On open — hover, tap or keyboard — the bubble now shifts horizontally to stay 12px inside the viewport, and re-clamps on resize. The shift is measured from the bubble's real rect after layout and removed on close. --- .changeset/tooltip-viewport-clamp.md | 5 ++++ website/site/src/components/InfoTip.astro | 33 +++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 .changeset/tooltip-viewport-clamp.md diff --git a/.changeset/tooltip-viewport-clamp.md b/.changeset/tooltip-viewport-clamp.md new file mode 100644 index 000000000..13b44b5c4 --- /dev/null +++ b/.changeset/tooltip-viewport-clamp.md @@ -0,0 +1,5 @@ +--- +"nostream-website": patch +--- + +Info-tip bubbles no longer clip at the viewport edge: the tooltip is centered on its (i) toggle, so near an edge it overflowed. On open (hover, tap or keyboard) the bubble now shifts horizontally to stay inside the viewport, and re-clamps on resize. diff --git a/website/site/src/components/InfoTip.astro b/website/site/src/components/InfoTip.astro index a088e93f9..4d0ef0b16 100644 --- a/website/site/src/components/InfoTip.astro +++ b/website/site/src/components/InfoTip.astro @@ -124,9 +124,42 @@ const id = `tip-${Astro.props['detail'] ? '' : ''}${Math.random().toString(36).s }) // Keep aria-expanded truthful for screen readers: the toggle event fires on // the
element (not the summary), including programmatic closes. + // While open, also clamp the bubble inside the viewport: the bubble is + // centered on its (i) toggle, so near a viewport edge it would otherwise + // clip (CSS max-width bounds the width, not the position). + const clamp = (details: Element) => { + const bubble = details.querySelector('.tip-bubble') + if (!bubble) return + bubble.style.transform = '' + const pad = 12 + const rect = bubble.getBoundingClientRect() + const overshootLeft = pad - rect.left + const overshootRight = rect.right - (window.innerWidth - pad) + if (overshootLeft > 0) { + bubble.style.transform = `translateX(-50%) translateX(${overshootLeft}px)` + } else if (overshootRight > 0) { + bubble.style.transform = `translateX(-50%) translateX(${-overshootRight}px)` + } + } document.querySelectorAll('details.info-tip').forEach((d) => { + // Hover path: the bubble opens via CSS without a toggle event, so clamp + // when the pointer reaches the toggle (bubble rect exists before the + // opacity transition finishes, so measurement is valid). + d.addEventListener('pointerenter', () => clamp(d)) d.addEventListener('toggle', () => { d.querySelector('summary')?.setAttribute('aria-expanded', d.hasAttribute('open') ? 'true' : 'false') + if (d.hasAttribute('open')) { + // The bubble lays out on the same frame the attribute flips; measure + // after layout so getBoundingClientRect sees the real rect. + requestAnimationFrame(() => clamp(d)) + } else { + const bubble = d.querySelector('.tip-bubble') + if (bubble) bubble.style.transform = '' + } }) }) + // Re-clamp when the viewport moves under an open bubble (rotation, resize). + window.addEventListener('resize', () => { + document.querySelectorAll('details.info-tip[open]').forEach(clamp) + }, { passive: true }) From 1df91593848750fb72463f564c457ee051dcb814 Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Fri, 16 Oct 2026 13:36:06 -0400 Subject: [PATCH 02/10] fix(website): re-clamp hovered bubbles on close and resize Two Greptile P2s, both valid states the first clamp pass missed: - Tapping a tooltip closed it while the pointer stayed over it; :hover keeps the bubble visible but the close path cleared the shift, so an edge bubble jumped outside the viewport until re-hover. Re-clamp instead of clearing when :hover still matches. - The resize re-clamp matched only [open] bubbles; hover-only ones kept a stale shift across viewport changes. Match :hover too. --- website/site/src/components/InfoTip.astro | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/website/site/src/components/InfoTip.astro b/website/site/src/components/InfoTip.astro index 4d0ef0b16..732cefde8 100644 --- a/website/site/src/components/InfoTip.astro +++ b/website/site/src/components/InfoTip.astro @@ -152,14 +152,19 @@ const id = `tip-${Astro.props['detail'] ? '' : ''}${Math.random().toString(36).s // The bubble lays out on the same frame the attribute flips; measure // after layout so getBoundingClientRect sees the real rect. requestAnimationFrame(() => clamp(d)) + } else if (d.matches(':hover')) { + // Tap-close while the pointer stays over the tooltip: :hover keeps + // the bubble visible, so re-clamp instead of clearing the shift. + clamp(d) } else { const bubble = d.querySelector('.tip-bubble') if (bubble) bubble.style.transform = '' } }) }) - // Re-clamp when the viewport moves under an open bubble (rotation, resize). + // Re-clamp when the viewport moves under a visible bubble (rotation, + // resize) — hover-opened bubbles have no [open], so match both. window.addEventListener('resize', () => { - document.querySelectorAll('details.info-tip[open]').forEach(clamp) + document.querySelectorAll('details.info-tip[open], details.info-tip:hover').forEach(clamp) }, { passive: true }) From 53e8af86b76e895acc1853687cfab51b9778552b Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Fri, 16 Oct 2026 15:22:38 -0400 Subject: [PATCH 03/10] fix(website): clamp against the rendered box, not the untransformed rect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bubble is centered by the stylesheet's translateX(-50%), so its rendered box spans anchor-w/2 .. anchor+w/2 — the first pass measured the untransformed rect and compared its raw edges, under-shooting by w/2: at an anchor 120px from the left edge a 260px bubble rendered from -10px (clipped) and the clamp never fired. The shift now starts from the rendered position; verified across 7 anchor/width/viewport cases including the reported one. --- website/site/src/components/InfoTip.astro | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/website/site/src/components/InfoTip.astro b/website/site/src/components/InfoTip.astro index 732cefde8..a62f5de73 100644 --- a/website/site/src/components/InfoTip.astro +++ b/website/site/src/components/InfoTip.astro @@ -130,11 +130,17 @@ const id = `tip-${Astro.props['detail'] ? '' : ''}${Math.random().toString(36).s const clamp = (details: Element) => { const bubble = details.querySelector('.tip-bubble') if (!bubble) return + // Measure the UNTRANSFORMED rect (CSS reset), then reason about the + // RENDERED position: the stylesheet centers the bubble with + // translateX(-50%), so its rendered box spans [anchor - w/2, + // anchor + w/2] where anchor is the untransformed rect's left edge. bubble.style.transform = '' const pad = 12 const rect = bubble.getBoundingClientRect() - const overshootLeft = pad - rect.left - const overshootRight = rect.right - (window.innerWidth - pad) + const renderedLeft = rect.left - rect.width / 2 + const renderedRight = rect.left + rect.width / 2 + const overshootLeft = pad - renderedLeft + const overshootRight = renderedRight - (window.innerWidth - pad) if (overshootLeft > 0) { bubble.style.transform = `translateX(-50%) translateX(${overshootLeft}px)` } else if (overshootRight > 0) { From 528055a2cf46b61d708a268fd3b1261ba6b89529 Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Fri, 16 Oct 2026 15:34:11 -0400 Subject: [PATCH 04/10] fix(website): serve preview assets at their requested path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workers static assets are keyed by file path: the preview worker serves dist/ at the root of its URL, so the built tree must exist AT /preview// for the HTML's base-prefixed references to resolve. They did not — every asset request (/preview/845/_astro/*.css, /media/*.mp4) fell through the SPA fallback and returned the index page as text/html, which is why the preview rendered unstyled and read as broken even though the page HTML and its inline script were correct. After build + apply-base, the workflow now copies the tree into dist/preview// (excluding the copy itself), so asset requests hit real files. Verified locally: dist/preview/845/_astro/index.*.css and index.html both exist; ~2x dist size, negligible for previews. --- .github/workflows/deploy-website-preview.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/deploy-website-preview.yml b/.github/workflows/deploy-website-preview.yml index f1fe09030..8ea72e33c 100644 --- a/.github/workflows/deploy-website-preview.yml +++ b/.github/workflows/deploy-website-preview.yml @@ -82,6 +82,15 @@ jobs: run: | pnpm run build node scripts/apply-base.mjs + # The assets manifest is keyed by file path: the worker serves dist/ + # at the root of its URL, but the preview is requested under + # /preview//... — so the built tree must also EXIST at that path + # or every asset request falls through to the SPA-fallback + # index.html (text/html for .css/.mp4 requests — the broken preview). + # Copy the build into dist/preview// (excluding the copy itself). + n="${{ github.event.pull_request.number }}" + mkdir -p "dist/preview/$n" + find dist -mindepth 1 -maxdepth 1 ! -name preview -exec cp -r {} "dist/preview/$n/" \; - name: Mark the preview comment as rendering if: steps.guard.outputs.skip == 'false' && github.event.action != 'closed' From 8e930bdd44b1fe8ef1decfe41683bb93ed34aa4a Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Fri, 16 Oct 2026 15:55:02 -0400 Subject: [PATCH 05/10] fix(website): delegate tooltip behavior at the document level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real-browser tracing on the preview caught a tip whose per-element listeners had silently not attached — no error, no markup difference, nothing to observe from outside. Attach everything at the document instead: toggle in the capture phase (it does not bubble), pointerover delegation for the hover path, click/keydown as before. Every current and future .info-tip is covered regardless of node replacement. --- website/site/src/components/InfoTip.astro | 66 ++++++++++++----------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/website/site/src/components/InfoTip.astro b/website/site/src/components/InfoTip.astro index a62f5de73..066bdd0d3 100644 --- a/website/site/src/components/InfoTip.astro +++ b/website/site/src/components/InfoTip.astro @@ -108,55 +108,45 @@ const id = `tip-${Astro.props['detail'] ? '' : ''}${Math.random().toString(36).s diff --git a/website/site/src/pages/community.astro b/website/site/src/pages/community.astro index 47bf9c532..fb6614b7b 100644 --- a/website/site/src/pages/community.astro +++ b/website/site/src/pages/community.astro @@ -133,10 +133,7 @@ const external = (href: string) => href.startsWith('http')

Ricardo started nostream and still leads it, reviewing changes, cutting releases and steering the roadmap for relay operators. He also created{' '} - - agentic schematics - - , the catalog behind our Get started build spec. + agentic schematics, the catalog behind our Get started build spec.

) } @@ -163,6 +160,10 @@ const external = (href: string) => href.startsWith('http')
+
+

02 Participate

+

Ways in. Pick yours.

+
{ channels.map((c, i) => ( @@ -186,7 +187,7 @@ const external = (href: string) => href.startsWith('http')
-

02 Guides from the community

+

03 Guides from the community

Written by people who run relays.

    diff --git a/website/site/src/pages/get-started.astro b/website/site/src/pages/get-started.astro index 789433987..1d3b3e59b 100644 --- a/website/site/src/pages/get-started.astro +++ b/website/site/src/pages/get-started.astro @@ -33,7 +33,7 @@ const agents = ['Claude Code', 'Cursor', 'Codex', 'OpenCode', 'Pi'] Your relay, built by your agent. -
    +
    @@ -70,6 +70,10 @@ const agents = ['Claude Code', 'Cursor', 'Codex', 'OpenCode', 'Pi']

    01 What the schematic builds

    The upstream deploy path, phase by phase.

    +

    + Numbered as in the schematic: it also runs P1 (image discovery), P3 (optional settings overrides) and P6 + (optional WebSocket smoke test) — checks and choices your agent makes on its own. +

      { @@ -205,6 +209,13 @@ const agents = ['Claude Code', 'Cursor', 'Codex', 'OpenCode', 'Pi'] font-size: 14px; } + .phase-note { + max-width: 640px; + margin: 0; + color: var(--fg-2); + font-size: 14px; + } + .more { margin-top: 28px; font-size: 15px; diff --git a/website/site/src/styles/global.css b/website/site/src/styles/global.css index 28a0dbf5b..1b5cd64cc 100644 --- a/website/site/src/styles/global.css +++ b/website/site/src/styles/global.css @@ -168,7 +168,7 @@ video { .section { position: relative; - padding: clamp(80px, 11vw, 140px) 0; + padding: clamp(56px, 7vw, 96px) 0; } .section-head { From bf890690481783429cfac74beb0549cf249427d4 Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Fri, 16 Oct 2026 17:23:34 -0400 Subject: [PATCH 07/10] fix(website): floor grid tracks so mobile never overflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The I/O cell's nowrap command (and any wide nowrap child) forced the grid track's auto minimum past the viewport on phones — the page panned sideways at 390px. minmax(0, 1fr) on the single-column grids (bento, rail, gallery) lets children scroll internally instead. --- website/site/src/components/home/Audience.astro | 1 + website/site/src/components/home/Console.astro | 1 + website/site/src/components/home/Pillars.astro | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/website/site/src/components/home/Audience.astro b/website/site/src/components/home/Audience.astro index b9d21af0c..fa0154edc 100644 --- a/website/site/src/components/home/Audience.astro +++ b/website/site/src/components/home/Audience.astro @@ -77,6 +77,7 @@ const audiences = [ .rail { position: relative; display: grid; + grid-template-columns: minmax(0, 1fr); gap: 18px; } diff --git a/website/site/src/components/home/Console.astro b/website/site/src/components/home/Console.astro index 39dcb64c0..d7871ea60 100644 --- a/website/site/src/components/home/Console.astro +++ b/website/site/src/components/home/Console.astro @@ -151,6 +151,7 @@ const capabilities = [ .gallery { display: grid; + grid-template-columns: minmax(0, 1fr); gap: 20px 16px; } diff --git a/website/site/src/components/home/Pillars.astro b/website/site/src/components/home/Pillars.astro index 9c67c475b..c136a4f93 100644 --- a/website/site/src/components/home/Pillars.astro +++ b/website/site/src/components/home/Pillars.astro @@ -89,6 +89,10 @@ const pillars = [ .bento { display: grid; gap: 16px; + /* Grid tracks default to min-width:auto, so the nowrap .cmd in the I/O + cell forces the track (and the page) wider than the viewport on + mobile. Floor the track at 0 and let the cmd scroll inside itself. */ + grid-template-columns: minmax(0, 1fr); } @media (min-width: 900px) { From 5784c2965ae7ebd93d5c61f8b98f14719feed1bd Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Fri, 16 Oct 2026 17:27:24 -0400 Subject: [PATCH 08/10] fix(website): seat the hero headline in the clear above the film MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At 1084px the 54px headline collided with the film frame's title bar. Smaller type, higher anchor — the frame chrome starts below it now. --- website/site/src/components/home/Hero.astro | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/website/site/src/components/home/Hero.astro b/website/site/src/components/home/Hero.astro index 67282dcae..cf7087d18 100644 --- a/website/site/src/components/home/Hero.astro +++ b/website/site/src/components/home/Hero.astro @@ -45,16 +45,18 @@ import { VERSION } from '../../data/site' /* The headline used to live only inside the rendered film — invisible to search and screen readers. It is real text now, echoing the film's - second scene; the film keeps telling the full story beside it. */ + second scene; the film keeps telling the full story beside it. The + film frame's own chrome starts lower, so the headline sits in the + clear above it. */ .hero-h1 { position: absolute; z-index: 3; left: var(--pad-x); - top: clamp(28px, 7vh, 72px); + top: clamp(12px, 2.2vh, 26px); max-width: min(560px, calc(100% - 2 * var(--pad-x))); margin: 0; - font-size: clamp(30px, 4.4vw, 54px); - line-height: 1.08; + font-size: clamp(24px, 3vw, 38px); + line-height: 1.12; letter-spacing: -0.02em; pointer-events: none; } From 4239d76b0a76e7065b17404ad81612a0b3d90139 Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Fri, 16 Oct 2026 17:32:34 -0400 Subject: [PATCH 09/10] chore(website): extend the changeset for the review fixes --- .changeset/tooltip-viewport-clamp.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.changeset/tooltip-viewport-clamp.md b/.changeset/tooltip-viewport-clamp.md index 13b44b5c4..32190580a 100644 --- a/.changeset/tooltip-viewport-clamp.md +++ b/.changeset/tooltip-viewport-clamp.md @@ -3,3 +3,7 @@ --- Info-tip bubbles no longer clip at the viewport edge: the tooltip is centered on its (i) toggle, so near an edge it overflowed. On open (hover, tap or keyboard) the bubble now shifts horizontally to stay inside the viewport, and re-clamps on resize. + +Tooltip behavior is delegated at the document level, so every tip is covered even when its per-element listeners would have silently failed to attach. + +Site polish from an external review: the community participate cards have their own section heading, the hero headline is real DOM text with a pause control on the film, the console screenshots are labeled as real first-run console data, the honest-advertising copy matches what its warning shows, the nav's active-path comparison strips the deploy base, the get-started phase list explains the skipped numbers, and single-column grids no longer let nowrap content push the page wider than a phone viewport. The hero film is re-rendered at 1080p for the current 25-NIP list. From ae2f9e0d7b62a6c5012b9e5bc7ce1a1463336949 Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Fri, 16 Oct 2026 17:43:25 -0400 Subject: [PATCH 10/10] fix(website): close the drawer on hash CTA; sync the film label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review findings: the mobile drawer's same-page #build link scrolled with the drawer still covering the target — close it and reset the menu button first. And the film toggle now derives its label/aria-pressed from real playback state (play/pause events + initial sync), so under prefers-reduced-motion it offers Play instead of starting the film. --- website/site/src/components/Nav.astro | 10 +++++ website/site/src/components/home/Hero.astro | 46 ++++++++++++--------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/website/site/src/components/Nav.astro b/website/site/src/components/Nav.astro index 6eb532efa..8888e3b18 100644 --- a/website/site/src/components/Nav.astro +++ b/website/site/src/components/Nav.astro @@ -63,6 +63,16 @@ const isActive = (href: string) => current === href || current.startsWith(`${hre menu.setAttribute('aria-expanded', String(open)) menu.setAttribute('aria-label', open ? 'Close menu' : 'Open menu') }) + + // Same-page hash links from the mobile drawer (the Get-started CTA on + // /get-started) scroll under the still-open drawer — close it first. + document.querySelectorAll('.drawer a[href^="#"]').forEach((a) => { + a.addEventListener('click', () => { + nav?.classList.remove('open') + menu?.setAttribute('aria-expanded', 'false') + menu?.setAttribute('aria-label', 'Open menu') + }) + })