fix(web): mesh-stream wheel-scroll trap on landing page
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

The demo-dashboard embedded MeshStream with a fixed min-h-[480px] grid
+ overflow-y-auto on the message <ol>. Browsers capture every wheel
event that fires over a scrollable container — so hovering the demo
section froze page scroll until the user moved the cursor off.

Landing demo has only 6 messages, never needs internal scroll. The
fixed viewport only makes sense in the live dashboard where envelope
count can exceed the box.

Added `scrollable?: boolean` prop to MeshStream (default false).
- demo-dashboard (landing): no prop → intrinsic height, no overflow,
  wheel events propagate to the page
- live-stream-panel (/dashboard/meshes/[id]/live): scrollable → keeps
  the chat-style fixed viewport with scroll

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-05 22:01:06 +01:00
parent cbd5f14c6e
commit 701516bc8b
2 changed files with 21 additions and 2 deletions

View File

@@ -121,6 +121,13 @@ export interface MeshStreamProps {
emptyLabel?: string; emptyLabel?: string;
/** footer content (stats / progress bar / timers) */ /** footer content (stats / progress bar / timers) */
footer?: React.ReactNode; footer?: React.ReactNode;
/**
* When true (live dashboard), the message list gets a fixed viewport
* with overflow-y-auto — standard chat UI. When false (landing demo),
* the list grows intrinsically so wheel events pass through to the
* page scroll instead of being captured by the list.
*/
scrollable?: boolean;
} }
export const MeshStream = ({ export const MeshStream = ({
@@ -130,6 +137,7 @@ export const MeshStream = ({
peersHint, peersHint,
emptyLabel = "Waiting for messages…", emptyLabel = "Waiting for messages…",
footer, footer,
scrollable = false,
}: MeshStreamProps) => { }: MeshStreamProps) => {
const [focusedPeer, setFocusedPeer] = useState<string | null>(null); const [focusedPeer, setFocusedPeer] = useState<string | null>(null);
const [hoveredKey, setHoveredKey] = useState<string | null>(null); const [hoveredKey, setHoveredKey] = useState<string | null>(null);
@@ -140,7 +148,12 @@ export const MeshStream = ({
: messages; : messages;
return ( return (
<div className="grid min-h-[480px] grid-cols-1 md:grid-cols-[220px_1fr]"> <div
className={
"grid grid-cols-1 md:grid-cols-[220px_1fr] " +
(scrollable ? "min-h-[480px]" : "")
}
>
{/* peers sidebar */} {/* peers sidebar */}
<aside <aside
className="border-b border-[var(--cm-border)] bg-[var(--cm-bg-elevated)]/20 p-4 md:border-b-0 md:border-r" className="border-b border-[var(--cm-border)] bg-[var(--cm-bg-elevated)]/20 p-4 md:border-b-0 md:border-r"
@@ -239,7 +252,12 @@ export const MeshStream = ({
: "all peers · E2E encrypted"} : "all peers · E2E encrypted"}
</span> </span>
</div> </div>
<ol className="flex-1 space-y-3 overflow-y-auto p-4"> <ol
className={
"space-y-3 p-4 " +
(scrollable ? "flex-1 overflow-y-auto" : "")
}
>
{filtered.length === 0 && ( {filtered.length === 0 && (
<li <li
className="py-8 text-center text-[13px] text-[var(--cm-fg-tertiary)]" className="py-8 text-center text-[13px] text-[var(--cm-fg-tertiary)]"

View File

@@ -114,6 +114,7 @@ export const LiveStreamPanel = ({ meshId }: { meshId: string }) => {
channelLabel="live-stream" channelLabel="live-stream"
emptyLabel={emptyLabel} emptyLabel={emptyLabel}
footer={footer} footer={footer}
scrollable
/> />
</div> </div>
); );