fix(web): mesh-stream wheel-scroll trap on landing page
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:
@@ -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)]"
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user