diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f53b5c..4e1ce71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### New Features -- **A Steps tab in `codegraph ui` — what happens from here.** Pick a screen (or search any symbol and choose *What happens from here*) and the viewer draws everything it sets in motion as typed steps: the handlers wired to its taps and listeners, the calls that cross into native code, the native events that come back, the store actions it writes, and the calls that leave the app into the network, storage, the device or telemetry — one box per step, an arrow for every way one leads to the next, and on each arrow the condition under which it happens. The plumbing between two steps (hooks, helpers, the components in between) is folded into the arrow and listed in the side panel, exactly as the Screens tab folds a tap's chain into one transition — and every call the panel lists says what it passes, read from the source as written (`SecureStore.setItemAsync('userEmail', values.email)`, `axios.post('/auth/login', { email, password })`), so a step is not just *that* something was stored or sent but *what*. Any step is the next anchor, any link opens as a Flow strip, a cap the walk hit is announced on the step it hit it at, and the picture travels in the URL. React Native + Expo apps get the full picture today; any project gets handlers, stores and calls that leave the index. +- **A Steps tab in `codegraph ui` — what happens from here.** Pick a screen (or search any symbol and choose *What happens from here*) and the viewer draws everything it sets in motion as typed steps: the handlers wired to its taps and listeners, the calls that cross into native code, the native events that come back, the store actions it writes, and the calls that leave the app into the network, storage, the device or telemetry — one box per step, an arrow for every way one leads to the next, and on each arrow the condition under which it happens. The plumbing between two steps (hooks, helpers, the components in between) is folded into the arrow and listed in the side panel, exactly as the Screens tab folds a tap's chain into one transition — and every call the panel lists says what it passes, read from the source as written (`SecureStore.setItemAsync('userEmail', values.email)`, `axios.post('/auth/login', { email, password })`), so a step is not just *that* something was stored or sent but *what*. And each handler says what fires it — the JSX prop and its element (`onPress · {#if sc.common.length > 0}
{@render words(commonTokens(sc.common))}
{/if} - {#if link.via.length > 0}
via {viaText(link)}
{/if} + {#if link.via.length > 0}
via {viaText(link)}
{/if} {#if sc.rows.length > 1}
{sc.rows.length} ways
{/if} {#each sc.rows as row (row.site.file + row.site.line)}
1}> @@ -456,7 +456,7 @@ > {#if sc.common.length > 0}
{@render words(commonTokens(sc.common))}
{/if} - {#if link.via.length > 0}
via {viaText(link)}
{/if} + {#if link.via.length > 0}
via {viaText(link)}
{/if} {#if sc.rows.length > 1}
{sc.rows.length} ways
{/if} {#each sc.rows as row (row.site.file + row.site.line)}
1}> @@ -644,6 +644,8 @@ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18); font-size: 12px; pointer-events: none; + /* A long via chain or condition wraps inside the box. */ + overflow-wrap: anywhere; } .tiprow { display: flex; @@ -736,8 +738,10 @@ .kw { font-weight: 600; } + /* The chain a transition travels through — the answer to "where on the screen": read, not dim. */ .via { - font: 400 11px var(--mono); + color: var(--ink-2); + font: 400 11.5px var(--mono); margin-top: 2px; } .ways { diff --git a/ui/src/views/StepsView.svelte b/ui/src/views/StepsView.svelte index 196e7b0..3f890f8 100644 --- a/ui/src/views/StepsView.svelte +++ b/ui/src/views/StepsView.svelte @@ -38,6 +38,7 @@ stepNeighbourhood, stepPairId, stepViaText, + triggerWords, type StepsModel, } from '../lib/steps-model'; @@ -234,7 +235,7 @@ const box = stage.getBoundingClientRect(); hovered = { edge, - x: Math.min(event.clientX - box.left + 14, box.width - 360), + x: Math.min(event.clientX - box.left + 14, box.width - 420), y: event.clientY - box.top + 14, }; } @@ -262,7 +263,7 @@ } hovered = { edge, - x: Math.min(event.clientX - box.left + 14, box.width - 360), + x: Math.min(event.clientX - box.left + 14, box.width - 420), y: event.clientY - box.top + 14, }; } @@ -399,7 +400,7 @@
/path - A screen, or a handler — a function wired to a tap or a listener + A screen, or a handler — a function fired from a tap, an option, a listener; its line says the event
⇢ fn @@ -442,9 +443,10 @@
{nameOf(hoveredInfo.from)} → {nameOf(hoveredInfo.to)}
{#each hoveredInfo.links.slice(0, 5) as link (link.id)}
+ {#if link.trigger}FIRES FROM {triggerWords(link.trigger)} in {link.trigger.in}{/if} + {#if link.via.length > 0}via {stepViaText(link)}{/if} {#if link.sites.length > 1}{link.sites.length} ways{/if} {@render words(conditionTokens(link.when))} - {#if link.via.length > 0}via {stepViaText(link)}{/if} {#if link.label}{link.label}{/if} {#if link.sites[0]}{siteWords(link.sites[0])}{/if}
@@ -462,6 +464,9 @@
{selectedInfo.label}
{kindWord(selectedInfo.step.kind)}{#if selectedInfo.step.anchor} · where the picture starts{/if}
+ {#if selectedInfo.step.trigger} +
FIRES FROM {triggerWords(selectedInfo.step.trigger)} in {selectedInfo.step.trigger.in}
+ {/if} {#if selectedInfo.step.screen?.component} @@ -533,13 +538,17 @@ onfocusout={() => onRowHover(null)} > + {#if link.trigger}
FIRES FROM {triggerWords(link.trigger)} in {link.trigger.in}
{/if} + {#if link.via.length > 0}
via {stepViaText(link)}
{/if} {#if sc.common.length > 0}
{@render words(commonTokens(sc.common))}
{/if} - {#if link.via.length > 0}
via {stepViaText(link)}
{/if} {#if link.label}
{link.label}
{/if} {#if sc.rows.length > 1}
{sc.rows.length} ways
{/if} {#each sc.rows as row (row.site.file + row.site.line)} {@const href = siteHref(link, row.site, fallback)}
1}> + {#if row.site.trigger && triggerWords(row.site.trigger) !== (link.trigger ? triggerWords(link.trigger) : '')} +
FIRES FROM {triggerWords(row.site.trigger)}
+ {/if} {#if sc.rows.length > 1}
{@render words(restTokens(row.rest, sc.common.length > 0))}
{/if} {#if href}
{siteWords(row.site)} · {basename(row.site.file)}:{row.site.line} @@ -571,13 +580,17 @@ onfocusout={() => onRowHover(null)} > + {#if link.trigger}
FIRES FROM {triggerWords(link.trigger)} in {link.trigger.in}
{/if} + {#if link.via.length > 0}
via {stepViaText(link)}
{/if} {#if sc.common.length > 0}
{@render words(commonTokens(sc.common))}
{/if} - {#if link.via.length > 0}
via {stepViaText(link)}
{/if} {#if link.label}
{link.label}
{/if} {#if sc.rows.length > 1}
{sc.rows.length} ways
{/if} {#each sc.rows as row (row.site.file + row.site.line)} {@const href = siteHref(link, row.site, fallback)}
1}> + {#if row.site.trigger && triggerWords(row.site.trigger) !== (link.trigger ? triggerWords(link.trigger) : '')} +
FIRES FROM {triggerWords(row.site.trigger)}
+ {/if} {#if sc.rows.length > 1}
{@render words(restTokens(row.rest, sc.common.length > 0))}
{/if} {#if href} {siteWords(row.site)} · {basename(row.site.file)}:{row.site.line} @@ -809,13 +822,21 @@ .tip { position: absolute; z-index: 5; - width: 340px; + width: 400px; padding: 8px 10px; border: 1px solid var(--ink); background: var(--paper); box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18); font-size: 12px; pointer-events: none; + /* A call with its arguments is one long token: it wraps inside the box. */ + overflow-wrap: anywhere; + } + .tip .mono, + .tip .when, + .tip .via, + .tip .fires { + overflow-wrap: anywhere; } .tiprow { display: flex; @@ -930,8 +951,19 @@ .kw { font-weight: 600; } + /* The chain a hop travels through — the answer to "where on the screen": read, not dim. */ .via { - font: 400 11px var(--mono); + color: var(--ink-2); + font: 400 11.5px var(--mono); + margin-top: 2px; + } + .via.dim { + color: var(--ink-3); + font-size: 11px; + } + .fires { + color: var(--ink); + font: 400 11.5px var(--mono); margin-top: 2px; } .ways {