feat(ui): Steps for servers — route roots, server effects, request/decorator triggers, guards for Python/Java/Kotlin/C#/Go/C
- api/route-roots.ts: the symbol a route runs (references-edge handler, exported page component, or the route itself for an inline handler), shared by steps and screens; the bare Steps tab lists an API's endpoints by router file - api/effects.ts: database / response / queue / email / payments / cache / auth / process / network / storage / device / telemetry, matched on the call as written per language family, with model + read/write and the literal status on a response site - graph/branch-guards.ts: callSitesForFile (the whole member chain), memberTypesInTree, decoratorsForFile, request/decorator triggers with the middleware/guard chain; guard + argument rules for Python, Java, Kotlin, C#, Go and C - steps.ts: classify on the chain before trusting a name match, retarget this.x.y() by declared type, skip test doubles after the effect pre-check, project kind on the wire - viewer: kindWord/kindWords per project kind, endpoint chooser, response boxes labelled by status codes - python.ts: FastAPI detected from a monorepo sub-directory; is-test-file: samples/examples package paths are not tests - tests: ui-steps-api-servers, ui-effects, branch-guards-languages; spec §3.13 Servers paragraph, CHANGELOG, plan doc 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
5e06204deb
commit
950686def4
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
|
||||
import type { MapNodeLayout } from '../../lib/map-model';
|
||||
import { kindWord, type StepNodeInfo } from '../../lib/steps-model';
|
||||
import { kindWord, type ProjectKind, type StepNodeInfo } from '../../lib/steps-model';
|
||||
|
||||
let { data }: NodeProps = $props();
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
data as unknown as {
|
||||
layout: MapNodeLayout;
|
||||
info: StepNodeInfo;
|
||||
project: ProjectKind;
|
||||
selected: boolean;
|
||||
dimmed: boolean;
|
||||
onSelect: (id: string) => void;
|
||||
@@ -73,7 +74,7 @@
|
||||
style={`width:${layout.width}px;height:${layout.height}px`}
|
||||
onclick={() => node.onSelect(info.id)}
|
||||
aria-pressed={node.selected}
|
||||
title={`${info.label} — ${step.anchor ? 'where this picture starts; ' : ''}${kindWord(step.kind)}. ${info.sub}.${cutNote}`}
|
||||
title={`${info.label} — ${step.anchor ? 'where this picture starts; ' : ''}${kindWord(step.kind, node.project, step)}. ${info.sub}.${cutNote}`}
|
||||
>
|
||||
<span class="name"
|
||||
>{#if step.anchor}<span class="mark" aria-hidden="true">●</span>{/if}{info.label}{#if step.cut !== null}<span
|
||||
|
||||
+46
-15
@@ -63,36 +63,66 @@ const HIT_SAMPLES = 24;
|
||||
|
||||
/* ---------------------------------------------------------------- words -- */
|
||||
|
||||
/** A short word for a step's kind, as the panel and the legend say it. */
|
||||
export function kindWord(kind: WireStep['kind']): string {
|
||||
/** What the index is a picture of; the server decides it from the routes (`WireStepsPayload.project`). */
|
||||
export type ProjectKind = WireStepsPayload['project'];
|
||||
|
||||
/**
|
||||
* A short word for a step's kind, as the panel and the legend say it — in the
|
||||
* project's own vocabulary. The same box is a screen in an app, a page in a
|
||||
* web app and an endpoint in an API; a route that leads with an HTTP verb is
|
||||
* an endpoint wherever it is. One place decides, so the legend, the panel
|
||||
* and the tooltip never disagree.
|
||||
*/
|
||||
export function kindWord(kind: WireStep['kind'], project: ProjectKind = 'app', step?: WireStep): string {
|
||||
return kindWords(kind, project, step)[0];
|
||||
}
|
||||
|
||||
/** The singular and the plural, for counts: `1 endpoint`, `3 outside the index`. */
|
||||
export function kindWords(kind: WireStep['kind'], project: ProjectKind = 'app', step?: WireStep): [string, string] {
|
||||
switch (kind) {
|
||||
case 'screen':
|
||||
return 'screen';
|
||||
if (step?.screen?.endpoint) return ['endpoint', 'endpoints'];
|
||||
return project === 'api' ? ['endpoint', 'endpoints'] : project === 'web' ? ['page', 'pages'] : ['screen', 'screens'];
|
||||
case 'trigger':
|
||||
return 'handler';
|
||||
return ['handler', 'handlers'];
|
||||
case 'bridge':
|
||||
return 'native call';
|
||||
return project === 'app' ? ['native call', 'native calls'] : project === 'web' ? ['call to the server', 'calls to the server'] : ['call to another tier', 'calls to another tier'];
|
||||
case 'event':
|
||||
return 'native event';
|
||||
return project === 'app' ? ['native event', 'native events'] : project === 'web' ? ['arrives from the server', 'arrive from the server'] : ['arrives from a queue or bus', 'arrive from a queue or bus'];
|
||||
case 'store':
|
||||
return 'store action';
|
||||
return project === 'api' ? ['data call', 'data calls'] : ['store action', 'store actions'];
|
||||
case 'effect':
|
||||
return 'outside the index';
|
||||
return ['outside the index', 'outside the index'];
|
||||
default:
|
||||
return 'start';
|
||||
return ['start', 'start'];
|
||||
}
|
||||
}
|
||||
|
||||
/** `3 handlers`, `1 endpoint`, `11 outside the index`. */
|
||||
export function countWords(n: number, kind: WireStep['kind'], project: ProjectKind = 'app'): string {
|
||||
const [one, many] = kindWords(kind, project);
|
||||
return `${n} ${n === 1 ? one : many}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* What fires something, in a few characters: `onPress · <Button>`,
|
||||
* `onSubmit · useFormik(…)`, `addListener('onZipComplete')`, `useEffect`.
|
||||
* `onSubmit · useFormik(…)`, `addListener('onZipComplete')`, `useEffect`;
|
||||
* for a server, `POST /users · after authenticate, validate(…)`,
|
||||
* `@Process('email')`, `page load · /blog/[slug]`.
|
||||
*/
|
||||
export function triggerWords(t: WireStepTrigger): string {
|
||||
const after = t.after && t.after.length > 0 ? ` · after ${t.after.join(', ')}` : '';
|
||||
switch (t.kind) {
|
||||
case 'prop':
|
||||
return t.of ? `${t.name} · <${t.of}>` : t.name;
|
||||
case 'option':
|
||||
return t.of ? `${t.name} · ${t.of}(…)` : t.name;
|
||||
case 'request':
|
||||
return `${t.name} ${t.of ?? ''}`.trim() + after;
|
||||
case 'decorator':
|
||||
return `@${t.name}(${t.of ?? ''})` + after;
|
||||
case 'load':
|
||||
return `page load · ${t.of ?? t.name}` + after;
|
||||
default:
|
||||
return t.of ? `${t.name}(${t.of})` : t.name;
|
||||
}
|
||||
@@ -114,7 +144,7 @@ export function stepLabel(step: WireStep): string {
|
||||
}
|
||||
|
||||
/** The second line: what the step is, then where it is. */
|
||||
export function stepSub(step: WireStep): string {
|
||||
export function stepSub(step: WireStep, project: ProjectKind = 'app'): string {
|
||||
const file = step.node ? step.node.file.slice(step.node.file.lastIndexOf('/') + 1) : '';
|
||||
switch (step.kind) {
|
||||
case 'screen':
|
||||
@@ -123,15 +153,16 @@ export function stepSub(step: WireStep): string {
|
||||
// The event before the file: `onPress · <Button> · index.tsx`.
|
||||
return step.trigger ? `${triggerWords(step.trigger)} · ${file}` : `handler · ${file}`;
|
||||
case 'bridge':
|
||||
return `native · ${file}`;
|
||||
return `${project === 'app' ? 'native' : project === 'web' ? 'server' : 'another tier'} · ${file}`;
|
||||
case 'event':
|
||||
return `${step.label} · ${file}`;
|
||||
case 'store':
|
||||
return `store · ${file}`;
|
||||
return `${project === 'api' ? 'data' : 'store'} · ${file}`;
|
||||
case 'effect':
|
||||
return step.sub;
|
||||
default:
|
||||
return step.sub;
|
||||
// The anchor: its file, at the size of a box; the panel prints the whole path.
|
||||
return step.node && step.sub === step.node.file ? file : step.sub;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +187,7 @@ export function buildStepsModel(payload: WireStepsPayload): StepsModel {
|
||||
}
|
||||
for (const step of payload.steps) {
|
||||
counts[step.kind]++;
|
||||
const info: StepNodeInfo = { id: step.id, step, label: stepLabel(step), sub: stepSub(step) };
|
||||
const info: StepNodeInfo = { id: step.id, step, label: stepLabel(step), sub: stepSub(step, payload.project) };
|
||||
nodes.set(step.id, info);
|
||||
modules.push({
|
||||
id: step.id,
|
||||
|
||||
+36
-5
@@ -712,17 +712,28 @@ export interface WireStepSite {
|
||||
when: string;
|
||||
/** What fires THIS site, when it differs from the link's first. */
|
||||
trigger?: WireStepTrigger;
|
||||
/** For a response site: the status code it sends, when literal. */
|
||||
status?: number;
|
||||
}
|
||||
|
||||
/** What fires a step or a link: the event it is written under, and the function that writes it there. */
|
||||
export interface WireStepTrigger {
|
||||
kind: 'prop' | 'option' | 'callback';
|
||||
/** `onPress`, `onSubmit`, `useEffect`, `addListener`. */
|
||||
/**
|
||||
* `prop` / `option` / `callback`: a binding at the call site (JSX attribute,
|
||||
* `on*` key, runs-later argument). `request`: the route a handler serves —
|
||||
* `name` the verb, `of` the path. `decorator`: a decorator on the handler —
|
||||
* `name` its name, `of` its literal argument (`@Process('email')`). `load`:
|
||||
* a page's own load-time work — `of` the page path.
|
||||
*/
|
||||
kind: 'prop' | 'option' | 'callback' | 'request' | 'decorator' | 'load';
|
||||
/** `onPress`, `onSubmit`, `useEffect`, `addListener`, `POST`, `Process`. */
|
||||
name: string;
|
||||
/** `Button` for a prop, `useFormik` for an option, the first string argument for a callback; null when unknown. */
|
||||
of: string | null;
|
||||
/** The function the binding is written in. */
|
||||
in: string;
|
||||
/** What runs before it fires: the middleware / guard chain, in order (`authenticate`, `validate(…)`). */
|
||||
after?: string[];
|
||||
}
|
||||
|
||||
export interface WireStep {
|
||||
@@ -748,9 +759,27 @@ export interface WireStep {
|
||||
events?: string[];
|
||||
/** For a handler: what fires it. */
|
||||
trigger?: WireStepTrigger;
|
||||
screen?: { path: string; component: WireNodeRef | null };
|
||||
/** The calls one function makes into one category, and the function. */
|
||||
effect?: { api: string; apis: string[]; category: string; by: WireNodeRef; line: number };
|
||||
/**
|
||||
* For a screen or an endpoint: its path and the symbol that serves it.
|
||||
* `endpoint` when the route leads with an HTTP verb; `inline` when the
|
||||
* handler is anonymous at the registration site (component is null).
|
||||
*/
|
||||
screen?: { path: string; component: WireNodeRef | null; endpoint: boolean; inline: boolean };
|
||||
/**
|
||||
* The calls one function makes into one category, and the function. A
|
||||
* database call names its model / table and read vs write when the call
|
||||
* says; a response box lists the status codes its sites send.
|
||||
*/
|
||||
effect?: {
|
||||
api: string;
|
||||
apis: string[];
|
||||
category: string;
|
||||
by: WireNodeRef;
|
||||
line: number;
|
||||
model?: string;
|
||||
access?: 'read' | 'write';
|
||||
statuses?: number[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface WireStepLink {
|
||||
@@ -775,6 +804,8 @@ export interface WireStepsPayload {
|
||||
anchor: WireNodeRef;
|
||||
/** Other symbols that share the anchor's name, when it was given by name. */
|
||||
ambiguous: WireNodeRef[];
|
||||
/** An `app` of screens, an `api` of endpoints, or a `web` app with both — the viewer's words follow it. */
|
||||
project: 'app' | 'api' | 'web';
|
||||
steps: WireStep[];
|
||||
links: WireStepLink[];
|
||||
depth: number;
|
||||
|
||||
+165
-44
@@ -21,8 +21,10 @@
|
||||
import KindGlyph from '../components/KindGlyph.svelte';
|
||||
import {
|
||||
canDrawSteps,
|
||||
fetchRoutes,
|
||||
fetchScreens,
|
||||
fetchSteps,
|
||||
type WireRoute,
|
||||
type WireScreen,
|
||||
type WireStepLink,
|
||||
type WireStepsPayload,
|
||||
@@ -35,6 +37,7 @@
|
||||
import {
|
||||
buildStepsModel,
|
||||
kindWord,
|
||||
kindWords,
|
||||
stepNeighbourhood,
|
||||
stepPairId,
|
||||
stepViaText,
|
||||
@@ -62,8 +65,30 @@
|
||||
let viewport = $state<Viewport | undefined>(undefined);
|
||||
const HOVER_REACH = 10;
|
||||
|
||||
/** The chooser's list, when the view opens without an anchor. */
|
||||
/** The chooser's lists, when the view opens without an anchor: the screens of an app, else the endpoints of an API. */
|
||||
let screens = $state<WireScreen[] | null>(null);
|
||||
let routes = $state<WireRoute[] | null>(null);
|
||||
|
||||
/** What the chooser offers: null while reading. */
|
||||
const chooser = $derived.by<'screens' | 'routes' | 'none' | null>(() => {
|
||||
if (screens === null) return null;
|
||||
if (screens.length > 0) return 'screens';
|
||||
if (routes === null) return null;
|
||||
return routes.length > 0 ? 'routes' : 'none';
|
||||
});
|
||||
|
||||
/** Endpoints by the file they are registered in — the router file is how a reader groups them — biggest first, in registration order within. */
|
||||
function routeGroups(list: WireRoute[]): Array<{ file: string; entries: WireRoute[] }> {
|
||||
const byFile = new Map<string, WireRoute[]>();
|
||||
for (const r of list) {
|
||||
const group = byFile.get(r.routeFile) ?? [];
|
||||
group.push(r);
|
||||
byFile.set(r.routeFile, group);
|
||||
}
|
||||
return [...byFile]
|
||||
.map(([file, entries]) => ({ file, entries: [...entries].sort((a, b) => a.routeLine - b.routeLine) }))
|
||||
.sort((a, b) => b.entries.length - a.entries.length || a.file.localeCompare(b.file));
|
||||
}
|
||||
|
||||
const LEGEND_KEY = 'codegraph-ui:steps-legend';
|
||||
let legendOpen = $state(readLegendOpen());
|
||||
@@ -82,7 +107,16 @@
|
||||
}
|
||||
});
|
||||
|
||||
const FIT = { fitViewOptions: { padding: 0.1, maxZoom: 1, minZoom: 0.4 } };
|
||||
/**
|
||||
* The fit. A picture of a few boxes is centred — and the key, bottom left,
|
||||
* would sit on its second row; it is fitted to the right of the key instead.
|
||||
* A picture of many boxes is fitted to the whole stage, as the Screens view's.
|
||||
*/
|
||||
const fitOptions = $derived(
|
||||
model !== null && model.layout.nodes.length <= 24 && legendOpen
|
||||
? { padding: { left: '440px', top: '32px', right: '32px', bottom: '32px' }, maxZoom: 1, minZoom: 0.4 }
|
||||
: { padding: 0.1, maxZoom: 1, minZoom: 0.4 }
|
||||
);
|
||||
const nodeTypes = { step: StepNode };
|
||||
const edgeTypes = { screen: ScreenEdge };
|
||||
const DEPTHS = [4, 6, 8, 10, 12];
|
||||
@@ -107,11 +141,19 @@
|
||||
loading = false;
|
||||
error = null;
|
||||
fetchScreens(controller.signal)
|
||||
.then((next) => {
|
||||
.then(async (next) => {
|
||||
screens = next.routed ? next.screens : [];
|
||||
// No screens: an API's endpoints are its places to start from.
|
||||
if (next.routed) {
|
||||
routes = [];
|
||||
return;
|
||||
}
|
||||
const found = await fetchRoutes({ limit: 300 }, controller.signal);
|
||||
routes = found.routed ? found.entries : [];
|
||||
})
|
||||
.catch(() => {
|
||||
screens = [];
|
||||
screens = screens ?? [];
|
||||
routes = routes ?? [];
|
||||
});
|
||||
return () => controller.abort();
|
||||
}
|
||||
@@ -162,6 +204,7 @@
|
||||
data: {
|
||||
layout: node,
|
||||
info: model.nodes.get(node.id)!,
|
||||
project: payload?.project ?? 'app',
|
||||
selected: selected === node.id,
|
||||
dimmed: neighbours !== null && !neighbours.has(node.id),
|
||||
onSelect: (id: string) => {
|
||||
@@ -333,20 +376,29 @@
|
||||
{:else if !asked}
|
||||
<div class="state chooser">
|
||||
<h2>What happens from where?</h2>
|
||||
<p>
|
||||
Pick a screen and this view draws everything it sets in motion — its handlers, the calls that
|
||||
cross into native code, the events that come back, the state it writes, the requests that leave
|
||||
the app — one box per step, an arrow for every way one leads to the next, and on each arrow the
|
||||
condition under which it happens. Or search a symbol and choose <i>What happens from here</i>.
|
||||
</p>
|
||||
{#if screens === null}
|
||||
<p class="dim">Reading screens…</p>
|
||||
{:else if screens.length === 0}
|
||||
<p class="dim">
|
||||
No screens in this graph. Open a symbol from the search box and follow <i>What happens from here</i>,
|
||||
or link here directly with <span class="mono">#/steps?symbol=<name></span>.
|
||||
{#if chooser === 'routes'}
|
||||
<p>
|
||||
Pick an endpoint and this view draws everything it sets in motion — its handler and what runs
|
||||
before it, the calls into the database, a queue, another service, and every response it can
|
||||
send — one box per step, an arrow for every way one leads to the next, and on each arrow the
|
||||
condition under which it happens. Or search a symbol and choose <i>What happens from here</i>.
|
||||
</p>
|
||||
{:else}
|
||||
<p>
|
||||
Pick a screen and this view draws everything it sets in motion — its handlers, the calls that
|
||||
cross into native code, the events that come back, the state it writes, the requests that leave
|
||||
the app — one box per step, an arrow for every way one leads to the next, and on each arrow the
|
||||
condition under which it happens. Or search a symbol and choose <i>What happens from here</i>.
|
||||
</p>
|
||||
{/if}
|
||||
{#if chooser === null}
|
||||
<p class="dim">Reading {screens === null ? 'screens' : 'endpoints'}…</p>
|
||||
{:else if chooser === 'none'}
|
||||
<p class="dim">
|
||||
No screens or endpoints in this graph. Open a symbol from the search box and follow <i>What happens from here</i>,
|
||||
or link here directly with <span class="mono">#/steps?symbol=<name></span>.
|
||||
</p>
|
||||
{:else if chooser === 'screens' && screens !== null}
|
||||
<div class="chooser-list">
|
||||
{#each [...screens].sort((a, b) => b.outgoing + b.incoming - (a.outgoing + a.incoming) || a.path.localeCompare(b.path)) as screen (screen.id)}
|
||||
<a class="pick mono" href={stepsHref({ anchor: screen.id })}
|
||||
@@ -354,6 +406,17 @@
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if routes !== null}
|
||||
{#each routeGroups(routes) as group (group.file)}
|
||||
<div class="group-h"><span class="mono">{group.file}</span><span class="dim">{group.entries.length}</span></div>
|
||||
<div class="chooser-list">
|
||||
{#each group.entries as route (route.routeId)}
|
||||
<a class="pick mono" href={stepsHref({ anchor: route.routeId })}
|
||||
>{route.url} <span class="dim sans">{route.handler}</span></a
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{:else if error !== null}
|
||||
@@ -370,7 +433,7 @@
|
||||
{nodeTypes}
|
||||
{edgeTypes}
|
||||
fitView
|
||||
{...FIT}
|
||||
fitViewOptions={fitOptions}
|
||||
bind:viewport
|
||||
minZoom={0.2}
|
||||
maxZoom={3}
|
||||
@@ -398,22 +461,58 @@
|
||||
<span class="k-box k-anchor mono"><span class="mark">●</span>start</span>
|
||||
<span>Where the picture starts; each row down is one more step away</span>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="k-box mono">/path</span>
|
||||
<span>A screen, or a handler — a function fired from a tap, an option, a listener; its line says the event</span>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="k-box k-cross mono">⇢ fn</span>
|
||||
<span>The code crosses into native (⇢ a bridge call) or comes back from it (⇠ an event)</span>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="k-box k-store mono">set</span>
|
||||
<span>A store action — a function in a store file</span>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="k-box k-effect mono">api</span>
|
||||
<span>A call that leaves the index: the network, storage, the device, telemetry</span>
|
||||
</div>
|
||||
{#if payload.project === 'api'}
|
||||
<div class="lrow">
|
||||
<span class="k-box mono">POST /x</span>
|
||||
<span>An endpoint — its verb and path — or a handler: a function a request, a job, an event or a schedule fires; its line says which</span>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="k-box k-cross mono">⇢ fn</span>
|
||||
<span>The code crosses a tier: a call into another service or a job put on a queue (⇢), or a job, an event, a message arriving (⇠)</span>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="k-box k-store mono">set</span>
|
||||
<span>A data call — a function in a store or state file</span>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="k-box k-effect mono">db</span>
|
||||
<span>A call that leaves the index: the database, the response, a queue, email, payments, a cache, auth, the network</span>
|
||||
</div>
|
||||
{:else if payload.project === 'web'}
|
||||
<div class="lrow">
|
||||
<span class="k-box mono">/path</span>
|
||||
<span>A page, an endpoint, or a handler — a function an event, a request or a page load fires; its line says which</span>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="k-box k-cross mono">⇢ fn</span>
|
||||
<span>The code crosses to the server (⇢ a request, a server action) or comes back from it (⇠ a push, a stream)</span>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="k-box k-store mono">set</span>
|
||||
<span>A store action — a function in a store file</span>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="k-box k-effect mono">api</span>
|
||||
<span>A call that leaves the index: the network, the database, the response, storage, a queue, email</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="lrow">
|
||||
<span class="k-box mono">/path</span>
|
||||
<span>A screen, or a handler — a function fired from a tap, an option, a listener; its line says the event</span>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="k-box k-cross mono">⇢ fn</span>
|
||||
<span>The code crosses into native (⇢ a bridge call) or comes back from it (⇠ an event)</span>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="k-box k-store mono">set</span>
|
||||
<span>A store action — a function in a store file</span>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="k-box k-effect mono">api</span>
|
||||
<span>A call that leaves the index: the network, storage, the device, telemetry</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="lrow">
|
||||
<svg width="44" height="12" aria-hidden="true"><path d="M2 6 H42" class="k-line" /></svg>
|
||||
<span>Leads to — the plumbing between the two is folded into the line</span>
|
||||
@@ -448,7 +547,7 @@
|
||||
{#if link.sites.length > 1}<span class="dim">{link.sites.length} ways</span>{/if}
|
||||
<span class="when">{@render words(conditionTokens(link.when))}</span>
|
||||
{#if link.label}<span class="dim">{link.label}</span>{/if}
|
||||
{#if link.sites[0]}<span class="mono">{siteWords(link.sites[0])}</span>{/if}
|
||||
{#if link.sites[0]}<span class="mono">{#if link.sites[0].status}<b class="status">{link.sites[0].status}</b> · {/if}{siteWords(link.sites[0])}</span>{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if hoveredInfo.links.length > 5}<div class="dim">+{hoveredInfo.links.length - 5} more</div>{/if}
|
||||
@@ -463,7 +562,7 @@
|
||||
<div class="head">
|
||||
<div>
|
||||
<div class="mono big">{selectedInfo.label}</div>
|
||||
<div class="sub dim">{kindWord(selectedInfo.step.kind)}{#if selectedInfo.step.anchor} · where the picture starts{/if}</div>
|
||||
<div class="sub dim">{kindWord(selectedInfo.step.kind, payload.project, selectedInfo.step)}{#if selectedInfo.step.anchor} · where the picture starts{/if}</div>
|
||||
{#if selectedInfo.step.trigger}
|
||||
<div class="fires"><b class="kw">FIRES FROM</b> {triggerWords(selectedInfo.step.trigger)} <span class="dim">in {selectedInfo.step.trigger.in}</span></div>
|
||||
{/if}
|
||||
@@ -494,7 +593,7 @@
|
||||
<button class="clear" onclick={() => (selected = null)}>clear</button>
|
||||
</div>
|
||||
{#if selectedInfo.step.cut === 'screen'}
|
||||
<p class="dim note">Another screen — a chapter of its own. Start here to see what happens on it, or continue through screens from the summary.</p>
|
||||
<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>
|
||||
{: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>
|
||||
{:else if selectedInfo.step.cut !== null}
|
||||
@@ -511,6 +610,9 @@
|
||||
{#if selectedInfo.step.effect && selectedInfo.step.effect.apis.length > 1}
|
||||
<p class="dim note mono">{selectedInfo.step.effect.apis.join(' · ')}</p>
|
||||
{/if}
|
||||
{#if selectedInfo.step.effect?.category === 'response'}
|
||||
<p class="dim note">The endpoint’s contract as the code has it: each row below is one way it answers, with the condition it answers under.</p>
|
||||
{/if}
|
||||
{#if selectedInfo.step.events && selectedInfo.step.events.length > 1}
|
||||
<p class="dim note mono">⇠ {selectedInfo.step.events.join(' · ')}</p>
|
||||
{/if}
|
||||
@@ -551,9 +653,9 @@
|
||||
{/if}
|
||||
{#if sc.rows.length > 1}<div class="when">{@render words(restTokens(row.rest, sc.common.length > 0))}</div>{/if}
|
||||
{#if href}
|
||||
<a class="site" {href}>{siteWords(row.site)} <span class="dim">· {basename(row.site.file)}:{row.site.line}</span></a>
|
||||
<a class="site" {href}>{#if row.site.status}<b class="status">{row.site.status}</b> · {/if}{siteWords(row.site)} <span class="dim">· {basename(row.site.file)}:{row.site.line}</span></a>
|
||||
{:else}
|
||||
<span class="site">{siteWords(row.site)} <span class="dim">· {basename(row.site.file)}:{row.site.line}</span></span>
|
||||
<span class="site">{#if row.site.status}<b class="status">{row.site.status}</b> · {/if}{siteWords(row.site)} <span class="dim">· {basename(row.site.file)}:{row.site.line}</span></span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
@@ -593,9 +695,9 @@
|
||||
{/if}
|
||||
{#if sc.rows.length > 1}<div class="when">{@render words(restTokens(row.rest, sc.common.length > 0))}</div>{/if}
|
||||
{#if href}
|
||||
<a class="site" {href}>{siteWords(row.site)} <span class="dim">· {basename(row.site.file)}:{row.site.line}</span></a>
|
||||
<a class="site" {href}>{#if row.site.status}<b class="status">{row.site.status}</b> · {/if}{siteWords(row.site)} <span class="dim">· {basename(row.site.file)}:{row.site.line}</span></a>
|
||||
{:else}
|
||||
<span class="site">{siteWords(row.site)} <span class="dim">· {basename(row.site.file)}:{row.site.line}</span></span>
|
||||
<span class="site">{#if row.site.status}<b class="status">{row.site.status}</b> · {/if}{siteWords(row.site)} <span class="dim">· {basename(row.site.file)}:{row.site.line}</span></span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
@@ -638,14 +740,15 @@
|
||||
<p>
|
||||
<label class="opt">
|
||||
<input type="checkbox" checked={payload.through} onchange={(e) => navigate(rewrite({ through: (e.currentTarget as HTMLInputElement).checked }))} />
|
||||
Continue through screens
|
||||
Continue through {kindWords('screen', payload.project)[1]}
|
||||
</label>
|
||||
<span class="dim">— otherwise another screen is drawn as a boundary, and is a click from being the next anchor.</span>
|
||||
<span class="dim">— otherwise another {kindWord('screen', payload.project)} is drawn as a boundary, and is a click from being the next anchor.</span>
|
||||
</p>
|
||||
<p class="counts">
|
||||
{#each ['screen', 'trigger', 'bridge', 'event', 'store', 'effect'] as const as kind (kind)}
|
||||
{#if model.counts[kind] > 0}
|
||||
<span><b>{model.counts[kind]}</b> {kindWord(kind)}{model.counts[kind] === 1 ? '' : 's'}</span>
|
||||
{@const words = kindWords(kind, payload.project)}
|
||||
<span><b>{model.counts[kind]}</b> {model.counts[kind] === 1 ? words[0] : words[1]}</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</p>
|
||||
@@ -665,7 +768,7 @@
|
||||
{/if}
|
||||
<h4>Most connected</h4>
|
||||
{#each [...payload.steps].sort((a, b) => (model.layout.nodes.find((n) => n.id === b.id)?.ports.top.length ?? 0) + (model.layout.nodes.find((n) => n.id === b.id)?.ports.bottom.length ?? 0) - ((model.layout.nodes.find((n) => n.id === a.id)?.ports.top.length ?? 0) + (model.layout.nodes.find((n) => n.id === a.id)?.ports.bottom.length ?? 0))).slice(0, 8) as step (step.id)}
|
||||
<button class="peer mono" onclick={() => (selected = step.id)}>{model.nodes.get(step.id)?.label ?? step.label} <span class="dim sans">{kindWord(step.kind)}</span></button>
|
||||
<button class="peer mono" onclick={() => (selected = step.id)}>{model.nodes.get(step.id)?.label ?? step.label} <span class="dim sans">{kindWord(step.kind, payload.project, step)}</span></button>
|
||||
{/each}
|
||||
{/if}
|
||||
</aside>
|
||||
@@ -730,6 +833,19 @@
|
||||
margin-top: 12px;
|
||||
border-top: 1px solid var(--rule-soft);
|
||||
}
|
||||
/* A router file heading over its endpoints; the list under it keeps its own top rule. */
|
||||
.group-h {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
font-size: 11.5px;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
.group-h + .chooser-list {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.pick {
|
||||
display: block;
|
||||
padding: 7px 8px;
|
||||
@@ -984,6 +1100,11 @@
|
||||
text-decoration: none;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
/* A response's status code leads its row: the number is the fact. */
|
||||
.status {
|
||||
color: var(--ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
a.site:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user