Files
codegraph/ui/src/components/screens/ScreenNode.svelte
T
Colby McHenryandClaude Fable 5 77dc0ad2eb fix(ui): a double-click on a box no longer zooms the canvas
The flow canvas zooms on any double-click that reaches its pane, including one bubbling up from a step or screen box. A box's double-click is a navigation, not a zoom: it is stopped at the box in the capture phase (the delegated handler runs at the root, after the pane), so the picture keeps its fit while the pane's own double-click still zooms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-29 11:21:42 -05:00

151 lines
4.4 KiB
Svelte

<script lang="ts">
/**
* One screen on the Screens view: its path, and the component that renders
* it. An origin — a function that navigates but belongs to no screen (a
* store action after login) — draws dashed, so it reads as a trigger rather
* than a place. The entry screen (`/`) carries a mark.
*
* Hidden handles along the top and bottom, one per port, exactly as the
* Map's module box does — except that here a side may hold both kinds. The
* layout routed every transition (`directional` ports): a return trip
* leaves the TOP of this box and arrives at the BOTTOM of the screen it
* returns to, so the line runs around the boxes instead of through them.
* This only draws the ports the layout decided.
*/
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
import type { MapNodeLayout } from '../../lib/map-model';
import type { ScreenNodeInfo } from '../../lib/screens-model';
let { data }: NodeProps = $props();
const node = $derived(
data as unknown as {
layout: MapNodeLayout;
info: ScreenNodeInfo;
selected: boolean;
dimmed: boolean;
onSelect: (id: string) => void;
/** Open what happens from this screen (the Steps picture) — a double-click. */
onOpen?: (id: string) => void;
}
);
const layout = $derived(node.layout);
const info = $derived(node.info);
function portStyle(index: number, total: number): string {
return `left:${((index + 1) / (total + 1)) * 100}%`;
}
</script>
{#each layout.ports.top as port, i (`${port.type}:${port.id}`)}
<Handle
type={port.type}
id={`${port.type === 'source' ? 's' : 't'}:${port.id}`}
position={Position.Top}
style={portStyle(i, layout.ports.top.length)}
isConnectable={false}
/>
{/each}
<button
class="snode"
class:sel={node.selected}
class:dimmed={node.dimmed}
class:origin={info.origin}
class:entry={info.entry}
class:unreached={info.unreached}
style={`width:${layout.width}px;height:${layout.height}px`}
onclick={() => node.onSelect(info.id)}
ondblclickcapture={(e) => {
// The flow canvas zooms on a double-click that reaches its pane; a
// double-click on a box is a navigation, not a zoom — stop it here,
// at the target, before it bubbles. The pane's own double-click keeps zooming.
e.stopPropagation();
node.onOpen?.(info.id);
}}
aria-pressed={node.selected}
title={(info.origin
? `${info.label} navigates, but no screen reaches it within the walk. In ${info.sub}.`
: `${info.label} rendered by ${info.sub}${info.entry ? '. The entry screen.' : ''}${
info.unreached ? '. No transition in the graph reaches it from the entry.' : ''
}`) + (node.onOpen ? ' Double-click for what happens here.' : '')}
>
<span class="name">{#if info.entry}<span class="mark" aria-hidden="true">●</span>{/if}{info.label}</span>
<span class="sub">{info.sub}</span>
</button>
{#each layout.ports.bottom as port, i (`${port.type}:${port.id}`)}
<Handle
type={port.type}
id={`${port.type === 'source' ? 's' : 't'}:${port.id}`}
position={Position.Bottom}
style={portStyle(i, layout.ports.bottom.length)}
isConnectable={false}
/>
{/each}
<style>
.snode {
display: flex;
flex-direction: column;
justify-content: center;
gap: 1px;
box-sizing: border-box;
padding: 0 9px;
border: 1px solid var(--ink);
border-radius: 0;
background: var(--paper);
text-align: left;
cursor: pointer;
font: inherit;
color: var(--ink);
transition: background 90ms linear;
}
.snode:hover,
.snode.sel {
border-width: 2px;
padding: 0 8px;
background: var(--press);
}
.snode.dimmed {
border-color: var(--ink-4);
color: var(--ink-4);
}
.snode.dimmed .sub {
color: var(--ink-4);
}
.snode.origin {
border-style: dashed;
border-color: var(--ink-3);
}
.snode.unreached {
border-color: var(--ink-4);
color: var(--ink-2);
}
.snode:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
.name {
font: 500 13px var(--mono);
line-height: 15px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mark {
color: var(--accent);
margin-right: 5px;
font-size: 9px;
vertical-align: 1px;
}
.sub {
font: 400 11px var(--sans);
line-height: 13px;
color: var(--ink-3);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>