mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-22 08:38:08 +02:00
docs: rewrite context-as-code as reviewing-context guide
Move the page from Concepts to Guides and rebuild around an interactive review-loop diagram. Extract pan/zoom + fit-view controls into a shared FlowCanvas wrapper and adopt it across all three docs diagrams.
This commit is contained in:
parent
a1cfb03d73
commit
bb455ed5ce
13 changed files with 1059 additions and 285 deletions
704
docs-site/components/context-review-loop.tsx
Normal file
704
docs-site/components/context-review-loop.tsx
Normal file
|
|
@ -0,0 +1,704 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Handle,
|
||||
MarkerType,
|
||||
type Node,
|
||||
type NodeProps,
|
||||
Position,
|
||||
} from "@xyflow/react";
|
||||
|
||||
import { FlowCanvas } from "./flow-canvas";
|
||||
|
||||
type SourcesNodeData = {
|
||||
variant: "sources";
|
||||
badge: string;
|
||||
title: string;
|
||||
caption: string;
|
||||
items: Array<{ label: string; color: string }>;
|
||||
};
|
||||
|
||||
type IngestNodeData = {
|
||||
variant: "ingest";
|
||||
badge: string;
|
||||
title: string;
|
||||
command: string;
|
||||
caption: string;
|
||||
};
|
||||
|
||||
type DiffFileLine = { kind: "add" | "del" | "ctx" | "hunk"; text: string };
|
||||
type DiffFile = {
|
||||
path: string;
|
||||
accent: string;
|
||||
added: number;
|
||||
removed: number;
|
||||
lines: DiffFileLine[];
|
||||
};
|
||||
|
||||
type DiffNodeData = {
|
||||
variant: "diff";
|
||||
badge: string;
|
||||
title: string;
|
||||
caption: string;
|
||||
branch: string;
|
||||
files: DiffFile[];
|
||||
};
|
||||
|
||||
type ReviewNodeData = {
|
||||
variant: "review";
|
||||
badge: string;
|
||||
title: string;
|
||||
caption: string;
|
||||
checks: string[];
|
||||
};
|
||||
|
||||
type MergedNodeData = {
|
||||
variant: "merged";
|
||||
badge: string;
|
||||
title: string;
|
||||
caption: string;
|
||||
paths: Array<{ label: string; color: string }>;
|
||||
};
|
||||
|
||||
type SourcesNode = Node<SourcesNodeData, "sources">;
|
||||
type IngestNode = Node<IngestNodeData, "ingest">;
|
||||
type DiffNode = Node<DiffNodeData, "diff">;
|
||||
type ReviewNode = Node<ReviewNodeData, "review">;
|
||||
type MergedNode = Node<MergedNodeData, "merged">;
|
||||
type FlowNode = SourcesNode | IngestNode | DiffNode | ReviewNode | MergedNode;
|
||||
|
||||
const NODE_W = 420;
|
||||
const STD_H = 196;
|
||||
const DIFF_H = 472;
|
||||
|
||||
const CENTER_X = 220;
|
||||
const GAP = 72;
|
||||
|
||||
const SOURCES_Y = 16;
|
||||
const INGEST_Y = SOURCES_Y + STD_H + GAP;
|
||||
const DIFF_Y = INGEST_Y + STD_H + GAP;
|
||||
const REVIEW_Y = DIFF_Y + DIFF_H + GAP;
|
||||
const MERGED_Y = REVIEW_Y + STD_H + GAP;
|
||||
const CANVAS_BOTTOM = MERGED_Y + STD_H + 16;
|
||||
|
||||
const FORWARD_STROKE = "#0891b2";
|
||||
const FEEDBACK_STROKE = "#94a3b8";
|
||||
|
||||
const sourcesNode: SourcesNode = {
|
||||
id: "sources",
|
||||
type: "sources",
|
||||
position: { x: CENTER_X, y: SOURCES_Y },
|
||||
data: {
|
||||
variant: "sources",
|
||||
badge: "1 · evidence",
|
||||
title: "Data stack",
|
||||
caption: "Connectors scan warehouses, modeling code, BI tools, and notes.",
|
||||
items: [
|
||||
{ label: "warehouse", color: "#3b82f6" },
|
||||
{ label: "dbt", color: "#f59e0b" },
|
||||
{ label: "Metabase", color: "#f97316" },
|
||||
{ label: "Notion", color: "#10b981" },
|
||||
],
|
||||
},
|
||||
draggable: false,
|
||||
selectable: false,
|
||||
};
|
||||
|
||||
const ingestNode: IngestNode = {
|
||||
id: "ingest",
|
||||
type: "ingest",
|
||||
position: { x: CENTER_X, y: INGEST_Y },
|
||||
data: {
|
||||
variant: "ingest",
|
||||
badge: "2 · run",
|
||||
title: "ktx ingest",
|
||||
command: "ktx ingest --all",
|
||||
caption:
|
||||
"Reconciles new evidence with the accepted YAML and Markdown already on disk.",
|
||||
},
|
||||
draggable: false,
|
||||
selectable: false,
|
||||
};
|
||||
|
||||
const diffNode: DiffNode = {
|
||||
id: "diff",
|
||||
type: "diff",
|
||||
position: { x: CENTER_X, y: DIFF_Y },
|
||||
data: {
|
||||
variant: "diff",
|
||||
badge: "3 · diff",
|
||||
title: "Branch diff",
|
||||
caption: "Every decision lands as a YAML or Markdown line.",
|
||||
branch: "ingest/nightly",
|
||||
files: [
|
||||
{
|
||||
path: "semantic-layer/warehouse/orders.yaml",
|
||||
accent: "#3b82f6",
|
||||
added: 4,
|
||||
removed: 1,
|
||||
lines: [
|
||||
{ kind: "hunk", text: "@@ measures @@" },
|
||||
{ kind: "ctx", text: " - name: revenue" },
|
||||
{ kind: "del", text: " expr: sum(amount)" },
|
||||
{ kind: "add", text: " expr: sum(amount - refund_amount)" },
|
||||
{ kind: "add", text: " - name: net_orders" },
|
||||
{ kind: "add", text: " expr: count(distinct id)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "wiki/global/revenue.md",
|
||||
accent: "#10b981",
|
||||
added: 2,
|
||||
removed: 0,
|
||||
lines: [
|
||||
{ kind: "hunk", text: "@@ Net revenue @@" },
|
||||
{ kind: "add", text: "Excludes refunds and test accounts." },
|
||||
{ kind: "add", text: "sl_refs: [warehouse.orders]" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
draggable: false,
|
||||
selectable: false,
|
||||
};
|
||||
|
||||
const reviewNode: ReviewNode = {
|
||||
id: "review",
|
||||
type: "review",
|
||||
position: { x: CENTER_X, y: REVIEW_Y },
|
||||
data: {
|
||||
variant: "review",
|
||||
badge: "4 · review",
|
||||
title: "PR review",
|
||||
caption: "Analysts approve, edit, or reject like any pull request.",
|
||||
checks: ["joins are safe", "measures match policy", "wiki cites evidence"],
|
||||
},
|
||||
draggable: false,
|
||||
selectable: false,
|
||||
};
|
||||
|
||||
const mergedNode: MergedNode = {
|
||||
id: "merged",
|
||||
type: "merged",
|
||||
position: { x: CENTER_X, y: MERGED_Y },
|
||||
data: {
|
||||
variant: "merged",
|
||||
badge: "5 · merged",
|
||||
title: "Accepted context",
|
||||
caption: "Merged files become the trusted layer agents read at runtime.",
|
||||
paths: [
|
||||
{ label: "semantic-layer/", color: "#3b82f6" },
|
||||
{ label: "wiki/", color: "#10b981" },
|
||||
],
|
||||
},
|
||||
draggable: false,
|
||||
selectable: false,
|
||||
};
|
||||
|
||||
const nodes: FlowNode[] = [
|
||||
sourcesNode,
|
||||
ingestNode,
|
||||
diffNode,
|
||||
reviewNode,
|
||||
mergedNode,
|
||||
];
|
||||
|
||||
const arrowMarker = (color: string) => ({
|
||||
type: MarkerType.ArrowClosed,
|
||||
color,
|
||||
width: 14,
|
||||
height: 14,
|
||||
});
|
||||
|
||||
const forwardLabelStyle = {
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
fill: "var(--color-fd-muted-foreground)",
|
||||
letterSpacing: "0.02em",
|
||||
} as const;
|
||||
|
||||
const forwardLabelBg = {
|
||||
fill: "var(--color-fd-background)",
|
||||
stroke: "var(--color-fd-border)",
|
||||
strokeWidth: 1,
|
||||
} as const;
|
||||
|
||||
const edges = [
|
||||
{
|
||||
id: "sources-ingest",
|
||||
source: "sources",
|
||||
sourceHandle: "bottom",
|
||||
target: "ingest",
|
||||
targetHandle: "top",
|
||||
type: "straight" as const,
|
||||
label: "scan",
|
||||
labelBgPadding: [6, 3] as [number, number],
|
||||
labelBgBorderRadius: 4,
|
||||
labelStyle: forwardLabelStyle,
|
||||
labelBgStyle: forwardLabelBg,
|
||||
style: { stroke: FORWARD_STROKE, strokeWidth: 1.75 },
|
||||
markerEnd: arrowMarker(FORWARD_STROKE),
|
||||
},
|
||||
{
|
||||
id: "ingest-diff",
|
||||
source: "ingest",
|
||||
sourceHandle: "bottom",
|
||||
target: "diff",
|
||||
targetHandle: "top",
|
||||
type: "straight" as const,
|
||||
label: "propose files",
|
||||
labelBgPadding: [6, 3] as [number, number],
|
||||
labelBgBorderRadius: 4,
|
||||
labelStyle: forwardLabelStyle,
|
||||
labelBgStyle: forwardLabelBg,
|
||||
style: { stroke: FORWARD_STROKE, strokeWidth: 1.75 },
|
||||
markerEnd: arrowMarker(FORWARD_STROKE),
|
||||
},
|
||||
{
|
||||
id: "diff-review",
|
||||
source: "diff",
|
||||
sourceHandle: "bottom",
|
||||
target: "review",
|
||||
targetHandle: "top",
|
||||
type: "straight" as const,
|
||||
label: "open PR",
|
||||
labelBgPadding: [6, 3] as [number, number],
|
||||
labelBgBorderRadius: 4,
|
||||
labelStyle: forwardLabelStyle,
|
||||
labelBgStyle: forwardLabelBg,
|
||||
style: { stroke: FORWARD_STROKE, strokeWidth: 1.75 },
|
||||
markerEnd: arrowMarker(FORWARD_STROKE),
|
||||
},
|
||||
{
|
||||
id: "review-merged",
|
||||
source: "review",
|
||||
sourceHandle: "bottom",
|
||||
target: "merged",
|
||||
targetHandle: "top",
|
||||
type: "straight" as const,
|
||||
label: "merge",
|
||||
labelBgPadding: [6, 3] as [number, number],
|
||||
labelBgBorderRadius: 4,
|
||||
labelStyle: forwardLabelStyle,
|
||||
labelBgStyle: forwardLabelBg,
|
||||
style: { stroke: FORWARD_STROKE, strokeWidth: 1.75 },
|
||||
markerEnd: arrowMarker(FORWARD_STROKE),
|
||||
},
|
||||
{
|
||||
id: "merged-sources",
|
||||
source: "merged",
|
||||
sourceHandle: "right",
|
||||
target: "sources",
|
||||
targetHandle: "right",
|
||||
type: "smoothstep" as const,
|
||||
pathOptions: { offset: 64, borderRadius: 18 },
|
||||
style: {
|
||||
stroke: FEEDBACK_STROKE,
|
||||
strokeWidth: 1.5,
|
||||
strokeDasharray: "5 5",
|
||||
},
|
||||
markerEnd: arrowMarker(FEEDBACK_STROKE),
|
||||
},
|
||||
];
|
||||
|
||||
function BadgePill({
|
||||
tone,
|
||||
children,
|
||||
}: {
|
||||
tone: "neutral" | "primary" | "review" | "merged";
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const cls =
|
||||
tone === "primary"
|
||||
? "border-cyan-300/70 bg-cyan-50 text-cyan-800 dark:border-cyan-400/40 dark:bg-cyan-400/15 dark:text-cyan-100"
|
||||
: tone === "review"
|
||||
? "border-fuchsia-300/70 bg-fuchsia-50 text-fuchsia-800 dark:border-fuchsia-400/40 dark:bg-fuchsia-400/15 dark:text-fuchsia-100"
|
||||
: tone === "merged"
|
||||
? "border-emerald-300/70 bg-emerald-50 text-emerald-800 dark:border-emerald-400/40 dark:bg-emerald-400/15 dark:text-emerald-100"
|
||||
: "border-slate-300 bg-slate-50 text-slate-700 dark:border-slate-600/60 dark:bg-slate-700/40 dark:text-slate-200";
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 rounded-sm border px-2 font-mono text-[13px] font-semibold uppercase tracking-[0.06em] ${cls}`}
|
||||
style={{ lineHeight: "20px", paddingBlock: 0 }}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function FlowHandles({ noTop = false }: { noTop?: boolean }) {
|
||||
return (
|
||||
<>
|
||||
{!noTop ? (
|
||||
<Handle
|
||||
id="top"
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="!opacity-0"
|
||||
/>
|
||||
) : null}
|
||||
<Handle
|
||||
id="bottom"
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
className="!opacity-0"
|
||||
/>
|
||||
<Handle
|
||||
id="right"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="!opacity-0"
|
||||
/>
|
||||
<Handle
|
||||
id="right-target"
|
||||
type="target"
|
||||
position={Position.Right}
|
||||
className="!opacity-0"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SourcesHandles() {
|
||||
return (
|
||||
<>
|
||||
<Handle
|
||||
id="bottom"
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
className="!opacity-0"
|
||||
/>
|
||||
<Handle
|
||||
id="right"
|
||||
type="target"
|
||||
position={Position.Right}
|
||||
className="!opacity-0"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SourcesNodeView({ data }: NodeProps<SourcesNode>) {
|
||||
return (
|
||||
<div
|
||||
style={{ width: NODE_W, height: STD_H }}
|
||||
className="flex flex-col rounded-md border border-fd-border bg-fd-card px-4 py-3.5 shadow-sm"
|
||||
>
|
||||
<SourcesHandles />
|
||||
<BadgePill tone="neutral">{data.badge}</BadgePill>
|
||||
<p className="mt-2 text-[19px] font-semibold leading-7 text-fd-foreground">
|
||||
{data.title}
|
||||
</p>
|
||||
<p className="mt-1.5 text-[15px] leading-6 text-fd-muted-foreground">
|
||||
{data.caption}
|
||||
</p>
|
||||
<div className="mt-auto flex flex-wrap gap-1.5 pt-2.5">
|
||||
{data.items.map((item) => (
|
||||
<span
|
||||
key={item.label}
|
||||
className="inline-flex items-center gap-1.5 rounded border border-fd-border bg-fd-background px-2 py-0.5 text-[13px] leading-5 text-fd-muted-foreground"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-1.5 w-1.5 flex-none rounded-full"
|
||||
style={{ background: item.color }}
|
||||
/>
|
||||
{item.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IngestNodeView({ data }: NodeProps<IngestNode>) {
|
||||
return (
|
||||
<div
|
||||
style={{ width: NODE_W, height: STD_H }}
|
||||
className="relative flex flex-col rounded-md border border-cyan-300/40 bg-[#0f1f23] px-4 py-3.5 text-white shadow-sm dark:bg-[#0b181b]"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute inset-y-0 left-0 w-[3px] rounded-l-md"
|
||||
style={{ background: FORWARD_STROKE }}
|
||||
/>
|
||||
<FlowHandles />
|
||||
<div className="flex items-center gap-2.5">
|
||||
<BadgePill tone="primary">{data.badge}</BadgePill>
|
||||
<p className="text-[19px] font-semibold leading-7 text-white">
|
||||
{data.title}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className="mt-2 inline-flex w-fit items-center whitespace-pre rounded-sm border border-cyan-100/15 bg-white/[0.08] px-2 py-0.5 font-mono text-[14px] leading-5 text-cyan-100"
|
||||
style={{
|
||||
fontVariantLigatures: "none",
|
||||
fontFeatureSettings: '"liga" 0, "calt" 0',
|
||||
}}
|
||||
>
|
||||
{`$ ${data.command}`}
|
||||
</span>
|
||||
<p className="mt-2 text-[15px] leading-6 text-cyan-50/80">
|
||||
{data.caption}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DiffLine({ line }: { line: DiffFileLine }) {
|
||||
if (line.kind === "hunk") {
|
||||
return (
|
||||
<div className="bg-fd-muted/40 px-2.5 py-1 font-mono text-[12px] uppercase tracking-[0.06em] text-fd-muted-foreground">
|
||||
{line.text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const symbol = line.kind === "add" ? "+" : line.kind === "del" ? "-" : " ";
|
||||
const cls =
|
||||
line.kind === "add"
|
||||
? "bg-emerald-500/8 text-emerald-700 dark:text-emerald-300"
|
||||
: line.kind === "del"
|
||||
? "bg-rose-500/8 text-rose-700 dark:text-rose-300"
|
||||
: "text-fd-muted-foreground";
|
||||
return (
|
||||
<div
|
||||
className={`flex gap-1.5 px-2.5 py-px font-mono text-[13px] leading-5 ${cls}`}
|
||||
>
|
||||
<span aria-hidden="true" className="w-2.5 flex-none text-center opacity-70">
|
||||
{symbol}
|
||||
</span>
|
||||
<span className="min-w-0 truncate">{line.text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DiffFileBlock({ file }: { file: DiffFile }) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-sm border border-fd-border bg-fd-background">
|
||||
<div
|
||||
className="flex items-center gap-2 border-b border-fd-border px-2.5 py-1.5"
|
||||
style={{ borderTop: `2px solid ${file.accent}` }}
|
||||
>
|
||||
<span
|
||||
className="truncate font-mono text-[14px] font-semibold tracking-tight"
|
||||
style={{ color: file.accent }}
|
||||
>
|
||||
{file.path}
|
||||
</span>
|
||||
<span className="ml-auto flex-none font-mono text-[12px] tabular-nums text-emerald-600 dark:text-emerald-400">
|
||||
+{file.added}
|
||||
</span>
|
||||
{file.removed > 0 ? (
|
||||
<span className="flex-none font-mono text-[12px] tabular-nums text-rose-600 dark:text-rose-400">
|
||||
-{file.removed}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="py-1">
|
||||
{file.lines.map((line, idx) => (
|
||||
<DiffLine key={idx} line={line} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DiffNodeView({ data }: NodeProps<DiffNode>) {
|
||||
return (
|
||||
<div
|
||||
style={{ width: NODE_W, height: DIFF_H }}
|
||||
className="flex flex-col rounded-md border-2 border-fd-primary/45 bg-fd-card px-4 py-3.5 shadow-md"
|
||||
>
|
||||
<FlowHandles />
|
||||
<div className="flex items-center gap-2.5">
|
||||
<BadgePill tone="primary">{data.badge}</BadgePill>
|
||||
<p className="text-[19px] font-semibold leading-7 text-fd-foreground">
|
||||
{data.title}
|
||||
</p>
|
||||
<span
|
||||
className="ml-auto inline-flex w-fit items-center gap-1.5 whitespace-nowrap rounded border border-fd-border bg-fd-background px-2 py-0.5 font-mono text-[13px] leading-5 text-fd-muted-foreground"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="6" cy="6" r="2.25" />
|
||||
<circle cx="6" cy="18" r="2.25" />
|
||||
<circle cx="18" cy="6" r="2.25" />
|
||||
<path d="M6 8.5v7" />
|
||||
<path d="M6 18c0-6 12-6 12-9.5" />
|
||||
</svg>
|
||||
{data.branch}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1.5 text-[15px] leading-6 text-fd-muted-foreground">
|
||||
{data.caption}
|
||||
</p>
|
||||
<div className="mt-2.5 flex min-h-0 flex-1 flex-col gap-2">
|
||||
{data.files.map((file) => (
|
||||
<DiffFileBlock key={file.path} file={file} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReviewNodeView({ data }: NodeProps<ReviewNode>) {
|
||||
return (
|
||||
<div
|
||||
style={{ width: NODE_W, height: STD_H }}
|
||||
className="flex flex-col rounded-md border border-fd-border bg-fd-card px-4 py-3.5 shadow-sm"
|
||||
>
|
||||
<FlowHandles />
|
||||
<div className="flex items-center gap-2.5">
|
||||
<BadgePill tone="review">{data.badge}</BadgePill>
|
||||
<p className="text-[19px] font-semibold leading-7 text-fd-foreground">
|
||||
{data.title}
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-1.5 text-[15px] leading-6 text-fd-muted-foreground">
|
||||
{data.caption}
|
||||
</p>
|
||||
<ul className="mt-auto flex flex-wrap gap-x-4 gap-y-1.5 pt-2.5">
|
||||
{data.checks.map((check) => (
|
||||
<li
|
||||
key={check}
|
||||
className="inline-flex items-center gap-1.5 text-[13px] leading-5 text-fd-muted-foreground"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="h-3.5 w-3.5 flex-none text-fuchsia-500"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="4 12 10 18 20 6" />
|
||||
</svg>
|
||||
<span>{check}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MergedNodeView({ data }: NodeProps<MergedNode>) {
|
||||
return (
|
||||
<div
|
||||
style={{ width: NODE_W, height: STD_H }}
|
||||
className="flex flex-col rounded-md border border-emerald-300/55 bg-fd-card px-4 py-3.5 shadow-sm"
|
||||
>
|
||||
<FlowHandles />
|
||||
<div className="flex items-center gap-2.5">
|
||||
<BadgePill tone="merged">{data.badge}</BadgePill>
|
||||
<p className="text-[19px] font-semibold leading-7 text-fd-foreground">
|
||||
{data.title}
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-1.5 text-[15px] leading-6 text-fd-muted-foreground">
|
||||
{data.caption}
|
||||
</p>
|
||||
<div className="mt-auto flex flex-wrap gap-4 pt-2.5">
|
||||
{data.paths.map((path) => (
|
||||
<span
|
||||
key={path.label}
|
||||
className="inline-flex items-center gap-1.5 font-mono text-[15px] font-semibold tracking-tight"
|
||||
style={{ color: path.color }}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-2 w-2 flex-none rounded-full"
|
||||
style={{ background: path.color }}
|
||||
/>
|
||||
{path.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const nodeTypes = {
|
||||
sources: SourcesNodeView,
|
||||
ingest: IngestNodeView,
|
||||
diff: DiffNodeView,
|
||||
review: ReviewNodeView,
|
||||
merged: MergedNodeView,
|
||||
};
|
||||
|
||||
export function ContextReviewLoop() {
|
||||
return (
|
||||
<section
|
||||
id="review-loop"
|
||||
className="not-prose my-10 w-full max-w-full min-w-0 scroll-mt-24"
|
||||
aria-labelledby="review-loop-title"
|
||||
>
|
||||
<article
|
||||
className="max-w-full min-w-0 overflow-hidden rounded-lg border border-fd-border bg-fd-card shadow-sm"
|
||||
aria-label="The ktx context review loop"
|
||||
>
|
||||
<div className="border-b border-fd-border bg-fd-muted/35 px-5 py-4">
|
||||
<a
|
||||
href="#review-loop"
|
||||
className="group/anchor inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-[0.08em] text-fd-primary transition-colors hover:text-fd-primary/80"
|
||||
>
|
||||
The review loop
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="opacity-0 transition-opacity duration-150 group-hover/anchor:opacity-100 group-focus-visible/anchor:opacity-100"
|
||||
>
|
||||
#
|
||||
</span>
|
||||
</a>
|
||||
<h3
|
||||
id="review-loop-title"
|
||||
className="mt-1 text-base font-semibold tracking-normal text-fd-foreground sm:text-lg"
|
||||
style={{ fontFamily: "var(--font-display)" }}
|
||||
>
|
||||
Every ingest is a diff you can refuse
|
||||
</h3>
|
||||
<p className="mt-2 max-w-3xl text-xs leading-5 text-fd-muted-foreground">
|
||||
Evidence becomes file changes. File changes become a PR. The PR
|
||||
merges into the layer agents will read tomorrow, and what you
|
||||
merged today becomes the baseline for the next run.
|
||||
</p>
|
||||
<p className="mt-2 inline-flex items-center gap-1.5 text-[10.5px] font-medium uppercase tracking-[0.06em] text-fd-muted-foreground">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="inline-block h-px w-6"
|
||||
style={{
|
||||
borderTop: `1.5px dashed ${FEEDBACK_STROKE}`,
|
||||
}}
|
||||
/>
|
||||
dashed line: merged files feed the next ingest
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FlowCanvas
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
canvasStyle={{
|
||||
height: "min(1080px, 160vw)",
|
||||
minHeight: 760,
|
||||
}}
|
||||
translateExtent={[
|
||||
[-160, -120],
|
||||
[CENTER_X + NODE_W + 320, CANVAS_BOTTOM + 120],
|
||||
]}
|
||||
ariaLabel="ktx context review loop diagram"
|
||||
/>
|
||||
</article>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
118
docs-site/components/flow-canvas.tsx
Normal file
118
docs-site/components/flow-canvas.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import {
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Controls,
|
||||
type Edge,
|
||||
type EdgeTypes,
|
||||
type FitViewOptions,
|
||||
type Node,
|
||||
type NodeTypes,
|
||||
type OnInit,
|
||||
ReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
|
||||
type FlowCanvasProps<TNode extends Node, TEdge extends Edge> = {
|
||||
nodes: TNode[];
|
||||
edges: TEdge[];
|
||||
nodeTypes?: NodeTypes;
|
||||
edgeTypes?: EdgeTypes;
|
||||
/** Inline style for the canvas wrapper (height, minHeight, etc.). */
|
||||
canvasStyle: React.CSSProperties;
|
||||
/** Extra class on the canvas wrapper (the `flow-canvas` class is always
|
||||
* applied). Use it to scope per-diagram styles. */
|
||||
className?: string;
|
||||
fitViewOptions?: FitViewOptions<TNode>;
|
||||
maxZoom?: number;
|
||||
translateExtent?: [[number, number], [number, number]];
|
||||
ariaLabel?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_FIT_VIEW = { padding: 0.05 } satisfies FitViewOptions;
|
||||
|
||||
/**
|
||||
* Shared ReactFlow wrapper for docs diagrams.
|
||||
*
|
||||
* Behavior:
|
||||
* - Drag-to-pan, pinch-to-zoom, double-click-to-zoom.
|
||||
* - Scroll wheel passes through to the page (zoomOnScroll/panOnScroll off).
|
||||
* - On mount, the view is fitted and `minZoom` is locked to the fitted zoom
|
||||
* so the user can zoom in but not out beyond the initial framing.
|
||||
* - Nodes are non-draggable, non-selectable, non-focusable — the diagram is
|
||||
* a static read-only artifact.
|
||||
* - Common CSS lives in `global.css` under the `.flow-canvas` selector; the
|
||||
* per-diagram `className` adds anything else specific to that diagram.
|
||||
*/
|
||||
export function FlowCanvas<TNode extends Node, TEdge extends Edge>({
|
||||
nodes,
|
||||
edges,
|
||||
nodeTypes,
|
||||
edgeTypes,
|
||||
canvasStyle,
|
||||
className,
|
||||
fitViewOptions = DEFAULT_FIT_VIEW,
|
||||
maxZoom = 1.5,
|
||||
translateExtent,
|
||||
ariaLabel,
|
||||
}: FlowCanvasProps<TNode, TEdge>) {
|
||||
const [minZoom, setMinZoom] = useState(0.15);
|
||||
const handleInit = useCallback<OnInit<TNode, TEdge>>(
|
||||
(instance) => {
|
||||
requestAnimationFrame(() => {
|
||||
void instance.fitView(fitViewOptions).then(() => {
|
||||
setMinZoom(instance.getZoom());
|
||||
});
|
||||
});
|
||||
},
|
||||
[fitViewOptions],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flow-canvas relative bg-fd-background ${className ?? ""}`}
|
||||
style={canvasStyle}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<div className="pointer-events-none absolute right-2.5 top-2.5 z-10 rounded border border-fd-border/50 bg-white/30 px-1.5 py-px font-mono text-[9.5px] font-medium uppercase tracking-[0.06em] text-fd-muted-foreground shadow-sm backdrop-blur-sm dark:bg-white/10">
|
||||
Drag to pan · ⌘/Ctrl + scroll to zoom
|
||||
</div>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onInit={handleInit}
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
nodesFocusable={false}
|
||||
edgesFocusable={false}
|
||||
elementsSelectable={false}
|
||||
panOnDrag
|
||||
panOnScroll={false}
|
||||
zoomOnScroll={false}
|
||||
zoomOnPinch
|
||||
zoomOnDoubleClick
|
||||
preventScrolling={false}
|
||||
minZoom={minZoom}
|
||||
maxZoom={maxZoom}
|
||||
translateExtent={translateExtent}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background
|
||||
variant={BackgroundVariant.Dots}
|
||||
gap={18}
|
||||
size={1}
|
||||
color="var(--color-fd-border)"
|
||||
/>
|
||||
<Controls
|
||||
showInteractive={false}
|
||||
position="bottom-right"
|
||||
aria-label="Zoom and fit-view controls"
|
||||
/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
type Edge,
|
||||
type EdgeProps,
|
||||
getSmoothStepPath,
|
||||
|
|
@ -11,11 +9,11 @@ import {
|
|||
type Node,
|
||||
type NodeProps,
|
||||
Position,
|
||||
ReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { FlowCanvas } from "./flow-canvas";
|
||||
|
||||
type SourceNodeData = {
|
||||
accent: string;
|
||||
body: string;
|
||||
|
|
@ -538,81 +536,21 @@ export function ProductMechanics() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="mechanics-canvas bg-fd-background"
|
||||
style={{
|
||||
<FlowCanvas
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
canvasStyle={{
|
||||
height: "min(1240px, 170vw)",
|
||||
minHeight: 720,
|
||||
}}
|
||||
>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
fitView
|
||||
fitViewOptions={{ padding: 0.04 }}
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
nodesFocusable={false}
|
||||
edgesFocusable={false}
|
||||
elementsSelectable={false}
|
||||
panOnDrag={false}
|
||||
panOnScroll={false}
|
||||
zoomOnScroll={false}
|
||||
zoomOnPinch={false}
|
||||
zoomOnDoubleClick={false}
|
||||
preventScrolling={false}
|
||||
minZoom={0.2}
|
||||
maxZoom={1.5}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background
|
||||
variant={BackgroundVariant.Dots}
|
||||
gap={18}
|
||||
size={1}
|
||||
color="var(--color-fd-border)"
|
||||
/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
className="mechanics-canvas"
|
||||
fitViewOptions={{ padding: 0.04 }}
|
||||
ariaLabel="ktx ingestion flow diagram"
|
||||
/>
|
||||
</article>
|
||||
<style>{`
|
||||
.mechanics-canvas .react-flow__node {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
width: auto;
|
||||
text-align: left;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
cursor: auto;
|
||||
pointer-events: all !important;
|
||||
}
|
||||
.mechanics-canvas .react-flow__node > * {
|
||||
pointer-events: auto;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
.mechanics-canvas .react-flow__node.selected,
|
||||
.mechanics-canvas .react-flow__node:focus,
|
||||
.mechanics-canvas .react-flow__node:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.mechanics-canvas .react-flow__pane {
|
||||
cursor: default;
|
||||
}
|
||||
.mechanics-canvas .react-flow__handle {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.mechanics-canvas .mechanics-particle {
|
||||
pointer-events: none;
|
||||
filter: drop-shadow(0 0 6px currentColor);
|
||||
|
|
|
|||
|
|
@ -2,17 +2,14 @@
|
|||
|
||||
import { useCallback, useState } from "react";
|
||||
import {
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Handle,
|
||||
MarkerType,
|
||||
type Node,
|
||||
type NodeProps,
|
||||
type OnInit,
|
||||
Position,
|
||||
ReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
|
||||
import { FlowCanvas } from "./flow-canvas";
|
||||
|
||||
type LaneVariant = "manual" | "ktx";
|
||||
|
||||
|
|
@ -472,8 +469,6 @@ const edges = [
|
|||
},
|
||||
];
|
||||
|
||||
type FlowEdge = (typeof edges)[number];
|
||||
|
||||
function AgentNodeView({ data }: NodeProps<AgentNode>) {
|
||||
return (
|
||||
<div
|
||||
|
|
@ -980,15 +975,6 @@ const nodeTypes = {
|
|||
};
|
||||
|
||||
export function SemanticLayerFlow() {
|
||||
const [minZoom, setMinZoom] = useState(0.2);
|
||||
const handleFlowInit = useCallback<OnInit<FlowNode, FlowEdge>>((instance) => {
|
||||
requestAnimationFrame(() => {
|
||||
void instance.fitView(FIT_VIEW_OPTIONS).then(() => {
|
||||
setMinZoom(instance.getZoom());
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="imperative-vs-declarative"
|
||||
|
|
@ -1027,85 +1013,20 @@ export function SemanticLayerFlow() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="sl-flow-canvas relative bg-fd-background"
|
||||
style={{
|
||||
<FlowCanvas
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
canvasStyle={{
|
||||
height: "min(2340px, 290vw)",
|
||||
minHeight: 1780,
|
||||
}}
|
||||
>
|
||||
<div className="pointer-events-none absolute right-2.5 top-2.5 z-10 rounded border border-fd-border/50 bg-white/30 px-1.5 py-px font-mono text-[9.5px] font-medium uppercase tracking-[0.06em] text-fd-muted-foreground shadow-sm backdrop-blur-sm dark:bg-white/10">
|
||||
Drag to pan • ⌘/Ctrl + scroll to zoom
|
||||
</div>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
onInit={handleFlowInit}
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
nodesFocusable={false}
|
||||
edgesFocusable={false}
|
||||
elementsSelectable={false}
|
||||
panOnDrag
|
||||
panOnScroll={false}
|
||||
zoomOnScroll={false}
|
||||
zoomOnPinch
|
||||
zoomOnDoubleClick
|
||||
preventScrolling={false}
|
||||
minZoom={minZoom}
|
||||
maxZoom={1.5}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background
|
||||
variant={BackgroundVariant.Dots}
|
||||
gap={18}
|
||||
size={1}
|
||||
color="var(--color-fd-border)"
|
||||
/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
className="sl-flow-canvas"
|
||||
fitViewOptions={FIT_VIEW_OPTIONS}
|
||||
ariaLabel="Semantic query to SQL flow diagram"
|
||||
/>
|
||||
</article>
|
||||
<style>{`
|
||||
.sl-flow-canvas .react-flow__node {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
width: auto;
|
||||
text-align: left;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
cursor: auto;
|
||||
pointer-events: all !important;
|
||||
}
|
||||
.sl-flow-canvas .react-flow__node > * {
|
||||
pointer-events: auto;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
.sl-flow-canvas .react-flow__node.selected,
|
||||
.sl-flow-canvas .react-flow__node:focus,
|
||||
.sl-flow-canvas .react-flow__node:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.sl-flow-canvas .react-flow__pane {
|
||||
cursor: grab;
|
||||
}
|
||||
.sl-flow-canvas .react-flow__pane:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.sl-flow-canvas .react-flow__handle {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.sl-flow-canvas pre {
|
||||
font-size: 11.5px !important;
|
||||
line-height: 17.5px !important;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue