feat(ui): double-click a step to start the picture there; double-click a screen for what happens on it
- StepsView / StepNode: a double-click on any step with a symbol re-anchors the Steps picture on it (the panel's Start here) — an endpoint or another screen drawn as a boundary opens as its own chapter in one gesture; detected in the view's click path (two clicks on one box within 400 ms) since the flow canvas does not reliably pass dblclick on, with ondblclick kept - ScreensView / ScreenNode: a double-click on a screen (or an origin) opens its Steps picture - a boundary's panel says it is not entered instead of "nothing leaves this step"; tooltips and the boundary notes mention the gesture - spec §3.13, CHANGELOG Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
This commit is contained in:
co-authored by
Claude Fable 5
parent
dff4e50e98
commit
435a7fd37a
@@ -25,6 +25,8 @@
|
||||
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);
|
||||
@@ -54,12 +56,13 @@
|
||||
class:unreached={info.unreached}
|
||||
style={`width:${layout.width}px;height:${layout.height}px`}
|
||||
onclick={() => node.onSelect(info.id)}
|
||||
ondblclick={() => node.onOpen?.(info.id)}
|
||||
aria-pressed={node.selected}
|
||||
title={info.origin
|
||||
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>
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
* ellipsis, and its tooltip says which cap.
|
||||
*
|
||||
* Hidden handles along the top and bottom, one per port the layout decided
|
||||
* (`directional` ports), exactly as the screen box.
|
||||
* (`directional` ports), exactly as the screen box. A click selects the step;
|
||||
* a double-click starts the picture there (the panel's *Start here →*) — an
|
||||
* endpoint or another screen reached as a boundary opens as its own chapter.
|
||||
*/
|
||||
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
|
||||
import type { MapNodeLayout } from '../../lib/map-model';
|
||||
@@ -28,6 +30,8 @@
|
||||
selected: boolean;
|
||||
dimmed: boolean;
|
||||
onSelect: (id: string) => void;
|
||||
/** Re-anchor the picture on this step — a double-click; absent for a step with no symbol. */
|
||||
onStart?: (id: string) => void;
|
||||
}
|
||||
);
|
||||
const layout = $derived(node.layout);
|
||||
@@ -75,8 +79,9 @@
|
||||
class:anchor={step.anchor}
|
||||
style={`width:${layout.width}px;height:${layout.height}px`}
|
||||
onclick={() => node.onSelect(info.id)}
|
||||
ondblclick={() => node.onStart?.(info.id)}
|
||||
aria-pressed={node.selected}
|
||||
title={`${info.label} — ${step.anchor ? 'where this picture starts; ' : ''}${kindWord(step.kind, node.project, step)}. ${info.sub}.${cutNote}`}
|
||||
title={`${info.label} — ${step.anchor ? 'where this picture starts; ' : ''}${kindWord(step.kind, node.project, step)}. ${info.sub}.${cutNote}${node.onStart && !step.anchor ? ' Double-click to start here.' : ''}`}
|
||||
>
|
||||
<span class="name"
|
||||
>{#if step.anchor}<span class="mark" aria-hidden="true">●</span>{/if}{info.label}{#if step.cut !== null}<span
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
import KindGlyph from '../components/KindGlyph.svelte';
|
||||
import { fetchScreens, type WireScreensPayload, type WireScreenLink } from '../lib/api';
|
||||
import { live } from '../lib/live.svelte';
|
||||
import { symbolHref, fileHref, stepsHref } from '../lib/navigation';
|
||||
import { symbolHref, fileHref, navigate, stepsHref } from '../lib/navigation';
|
||||
import { isEdgeVisible, type MapEdgeLayout } from '../lib/map-model';
|
||||
import { commonTokens, conditionTokens, restTokens, scenarios, whenWords, type WordToken } from '../lib/conditions';
|
||||
import {
|
||||
@@ -71,6 +71,9 @@
|
||||
});
|
||||
|
||||
const FIT = { fitViewOptions: { padding: 0.1, maxZoom: 1, minZoom: 0.4 } };
|
||||
/** Two clicks on one box closer than this are a double-click. */
|
||||
const DOUBLE_CLICK_MS = 400;
|
||||
let lastClick: { id: string; at: number } | null = null;
|
||||
const nodeTypes = { screen: ScreenNode };
|
||||
const edgeTypes = { screen: ScreenEdge };
|
||||
|
||||
@@ -133,10 +136,22 @@
|
||||
selected: selected === node.id,
|
||||
dimmed: neighbours !== null && !neighbours.has(node.id),
|
||||
onSelect: (id: string) => {
|
||||
// Two clicks on the same box within a beat are a double-click: what
|
||||
// happens from here. Read here rather than off the DOM's `dblclick`,
|
||||
// which the flow canvas does not always pass on.
|
||||
const now = performance.now();
|
||||
if (lastClick !== null && lastClick.id === id && now - lastClick.at < DOUBLE_CLICK_MS) {
|
||||
lastClick = null;
|
||||
navigate(stepsHref({ anchor: id }));
|
||||
return;
|
||||
}
|
||||
lastClick = { id, at: now };
|
||||
selected = selected === id ? null : id;
|
||||
hovered = null;
|
||||
panelHot = null;
|
||||
},
|
||||
// Double-click: what happens from here — the screen's (or an origin's) Steps picture.
|
||||
onOpen: (id: string) => navigate(stepsHref({ anchor: id })),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -118,6 +118,17 @@
|
||||
: { padding: 0.1, maxZoom: 1, minZoom: 0.4 }
|
||||
);
|
||||
const nodeTypes = { step: StepNode };
|
||||
|
||||
/** Two clicks on one box closer than this are a double-click. */
|
||||
const DOUBLE_CLICK_MS = 400;
|
||||
let lastClick: { id: string; at: number } | null = null;
|
||||
/** Start the picture at a step — the panel's *Start here →*. False for a step with no symbol, or the anchor. */
|
||||
function startHere(id: string): boolean {
|
||||
const step = model?.nodes.get(id)?.step;
|
||||
if (!step || !step.node || step.anchor) return false;
|
||||
navigate(stepsHref({ anchor: step.node.id }));
|
||||
return true;
|
||||
}
|
||||
const edgeTypes = { screen: ScreenEdge };
|
||||
const DEPTHS = [4, 6, 8, 10, 12];
|
||||
|
||||
@@ -208,10 +219,23 @@
|
||||
selected: selected === node.id,
|
||||
dimmed: neighbours !== null && !neighbours.has(node.id),
|
||||
onSelect: (id: string) => {
|
||||
// Two clicks on the same box within a beat are a double-click:
|
||||
// the picture starts there. Read here rather than off the DOM's
|
||||
// `dblclick`, which the flow canvas does not always pass on.
|
||||
const now = performance.now();
|
||||
if (lastClick !== null && lastClick.id === id && now - lastClick.at < DOUBLE_CLICK_MS) {
|
||||
lastClick = null;
|
||||
if (startHere(id)) return;
|
||||
}
|
||||
lastClick = { id, at: now };
|
||||
selected = selected === id ? null : id;
|
||||
hovered = null;
|
||||
panelHot = null;
|
||||
},
|
||||
// Double-click: the picture starts here — an endpoint or another
|
||||
// screen drawn as a boundary opens as its own chapter. An effect has
|
||||
// no symbol to start from.
|
||||
...(model.nodes.get(node.id)?.step.node && !model.nodes.get(node.id)?.step.anchor ? { onStart: startHere } : {}),
|
||||
},
|
||||
}));
|
||||
});
|
||||
@@ -593,9 +617,9 @@
|
||||
<button class="clear" onclick={() => (selected = null)}>clear</button>
|
||||
</div>
|
||||
{#if selectedInfo.step.cut === 'screen'}
|
||||
<p class="dim note">Another {kindWord('screen', payload.project, selectedInfo.step)} — a chapter of its own. Start here to see what happens on it, or continue through {kindWords('screen', payload.project)[1]} from the summary.</p>
|
||||
<p class="dim note">Another {kindWord('screen', payload.project, selectedInfo.step)} — a chapter of its own. Start here (or double-click its box) to see what happens on it, or continue through {kindWords('screen', payload.project)[1]} from the summary.</p>
|
||||
{:else if selectedInfo.step.cut === 'component'}
|
||||
<p class="dim note">The event lands in a component of another screen — a picture of its own. Start here to see it, or continue through screens from the summary.</p>
|
||||
<p class="dim note">The event lands in a component of another screen — a picture of its own. Start here (or double-click its box) to see it, or continue through screens from the summary.</p>
|
||||
{:else if selectedInfo.step.cut !== null}
|
||||
<p class="dim note">
|
||||
The walk was cut at this step ({selectedInfo.step.cut === 'depth'
|
||||
@@ -666,7 +690,11 @@
|
||||
<h4>Leads to <span class="dim">{lists.leadsTo.length}</span></h4>
|
||||
{#if lists.leadsTo.length === 0}
|
||||
<p class="dim">
|
||||
{selectedInfo.step.kind === 'effect' ? 'Outside the index: the graph cannot follow it further.' : 'Nothing the walk follows leaves this step.'}
|
||||
{selectedInfo.step.kind === 'effect'
|
||||
? 'Outside the index: the graph cannot follow it further.'
|
||||
: selectedInfo.step.cut === 'screen' || selectedInfo.step.cut === 'component'
|
||||
? 'Not entered — a boundary. Start here for its own picture, or continue through from the summary.'
|
||||
: 'Nothing the walk follows leaves this step.'}
|
||||
</p>
|
||||
{/if}
|
||||
{#each lists.leadsTo as link (link.id)}
|
||||
|
||||
Reference in New Issue
Block a user