@unpunnyfuns/conduit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +202 -0
  3. package/dist/assert.d.ts +6 -0
  4. package/dist/assert.d.ts.map +1 -0
  5. package/dist/assert.js +8 -0
  6. package/dist/assert.js.map +1 -0
  7. package/dist/cn.d.ts +3 -0
  8. package/dist/cn.d.ts.map +1 -0
  9. package/dist/cn.js +3 -0
  10. package/dist/cn.js.map +1 -0
  11. package/dist/components/Badge.d.ts +6 -0
  12. package/dist/components/Badge.d.ts.map +1 -0
  13. package/dist/components/Badge.js +11 -0
  14. package/dist/components/Badge.js.map +1 -0
  15. package/dist/components/Card.d.ts +20 -0
  16. package/dist/components/Card.d.ts.map +1 -0
  17. package/dist/components/Card.js +24 -0
  18. package/dist/components/Card.js.map +1 -0
  19. package/dist/components/Diagram.d.ts +37 -0
  20. package/dist/components/Diagram.d.ts.map +1 -0
  21. package/dist/components/Diagram.js +92 -0
  22. package/dist/components/Diagram.js.map +1 -0
  23. package/dist/components/EdgeLayer.d.ts +23 -0
  24. package/dist/components/EdgeLayer.d.ts.map +1 -0
  25. package/dist/components/EdgeLayer.js +85 -0
  26. package/dist/components/EdgeLayer.js.map +1 -0
  27. package/dist/components/Lane.d.ts +11 -0
  28. package/dist/components/Lane.d.ts.map +1 -0
  29. package/dist/components/Lane.js +6 -0
  30. package/dist/components/Lane.js.map +1 -0
  31. package/dist/components/icons.d.ts +7 -0
  32. package/dist/components/icons.d.ts.map +1 -0
  33. package/dist/components/icons.js +47 -0
  34. package/dist/components/icons.js.map +1 -0
  35. package/dist/components/pulse.d.ts +15 -0
  36. package/dist/components/pulse.d.ts.map +1 -0
  37. package/dist/components/pulse.js +10 -0
  38. package/dist/components/pulse.js.map +1 -0
  39. package/dist/index.d.ts +14 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +12 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/layout/architecture.d.ts +60 -0
  44. package/dist/layout/architecture.d.ts.map +1 -0
  45. package/dist/layout/architecture.js +88 -0
  46. package/dist/layout/architecture.js.map +1 -0
  47. package/dist/layout/bounds.d.ts +25 -0
  48. package/dist/layout/bounds.d.ts.map +1 -0
  49. package/dist/layout/bounds.js +32 -0
  50. package/dist/layout/bounds.js.map +1 -0
  51. package/dist/layout/congestion.d.ts +18 -0
  52. package/dist/layout/congestion.d.ts.map +1 -0
  53. package/dist/layout/congestion.js +49 -0
  54. package/dist/layout/congestion.js.map +1 -0
  55. package/dist/layout/design.d.ts +98 -0
  56. package/dist/layout/design.d.ts.map +1 -0
  57. package/dist/layout/design.js +92 -0
  58. package/dist/layout/design.js.map +1 -0
  59. package/dist/layout/edges.d.ts +53 -0
  60. package/dist/layout/edges.d.ts.map +1 -0
  61. package/dist/layout/edges.js +848 -0
  62. package/dist/layout/edges.js.map +1 -0
  63. package/dist/layout/errors.d.ts +6 -0
  64. package/dist/layout/errors.d.ts.map +1 -0
  65. package/dist/layout/errors.js +9 -0
  66. package/dist/layout/errors.js.map +1 -0
  67. package/dist/layout/frame.d.ts +35 -0
  68. package/dist/layout/frame.d.ts.map +1 -0
  69. package/dist/layout/frame.js +63 -0
  70. package/dist/layout/frame.js.map +1 -0
  71. package/dist/layout/geometry.d.ts +23 -0
  72. package/dist/layout/geometry.d.ts.map +1 -0
  73. package/dist/layout/geometry.js +22 -0
  74. package/dist/layout/geometry.js.map +1 -0
  75. package/dist/layout/labels.d.ts +12 -0
  76. package/dist/layout/labels.d.ts.map +1 -0
  77. package/dist/layout/labels.js +153 -0
  78. package/dist/layout/labels.js.map +1 -0
  79. package/dist/layout/lanes.d.ts +5 -0
  80. package/dist/layout/lanes.d.ts.map +1 -0
  81. package/dist/layout/lanes.js +13 -0
  82. package/dist/layout/lanes.js.map +1 -0
  83. package/dist/layout/layout.d.ts +42 -0
  84. package/dist/layout/layout.d.ts.map +1 -0
  85. package/dist/layout/layout.js +94 -0
  86. package/dist/layout/layout.js.map +1 -0
  87. package/dist/layout/rank.d.ts +14 -0
  88. package/dist/layout/rank.d.ts.map +1 -0
  89. package/dist/layout/rank.js +78 -0
  90. package/dist/layout/rank.js.map +1 -0
  91. package/dist/layout/scope.d.ts +20 -0
  92. package/dist/layout/scope.d.ts.map +1 -0
  93. package/dist/layout/scope.js +52 -0
  94. package/dist/layout/scope.js.map +1 -0
  95. package/dist/layout/seating.d.ts +27 -0
  96. package/dist/layout/seating.d.ts.map +1 -0
  97. package/dist/layout/seating.js +135 -0
  98. package/dist/layout/seating.js.map +1 -0
  99. package/dist/layout/text.d.ts +12 -0
  100. package/dist/layout/text.d.ts.map +1 -0
  101. package/dist/layout/text.js +148 -0
  102. package/dist/layout/text.js.map +1 -0
  103. package/dist/layout/trace.d.ts +8 -0
  104. package/dist/layout/trace.d.ts.map +1 -0
  105. package/dist/layout/trace.js +51 -0
  106. package/dist/layout/trace.js.map +1 -0
  107. package/dist/layout.d.ts +9 -0
  108. package/dist/layout.d.ts.map +1 -0
  109. package/dist/layout.js +6 -0
  110. package/dist/layout.js.map +1 -0
  111. package/dist/schema/document.d.ts +239 -0
  112. package/dist/schema/document.d.ts.map +1 -0
  113. package/dist/schema/document.js +83 -0
  114. package/dist/schema/document.js.map +1 -0
  115. package/dist/schema/errors.d.ts +20 -0
  116. package/dist/schema/errors.d.ts.map +1 -0
  117. package/dist/schema/errors.js +12 -0
  118. package/dist/schema/errors.js.map +1 -0
  119. package/dist/schema/integrity.d.ts +8 -0
  120. package/dist/schema/integrity.d.ts.map +1 -0
  121. package/dist/schema/integrity.js +63 -0
  122. package/dist/schema/integrity.js.map +1 -0
  123. package/dist/schema/parse.d.ts +6 -0
  124. package/dist/schema/parse.d.ts.map +1 -0
  125. package/dist/schema/parse.js +53 -0
  126. package/dist/schema/parse.js.map +1 -0
  127. package/dist/schema/primitives.d.ts +58 -0
  128. package/dist/schema/primitives.d.ts.map +1 -0
  129. package/dist/schema/primitives.js +44 -0
  130. package/dist/schema/primitives.js.map +1 -0
  131. package/dist/theme.css +63 -0
  132. package/package.json +83 -0
package/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Coldtea AI
4
+ Copyright (c) 2026 unpunnyfuns
5
+
6
+ Portions of this software are derived from pr-lens
7
+ (https://github.com/coldteadotai/pr-lens), licensed under the MIT License.
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # @unpunnyfuns/conduit
2
+
3
+ Lane-based architecture diagrams for React, with live content in every card.
4
+
5
+ A port of [pr-lens](https://github.com/coldteadotai/pr-lens)'s layout engine
6
+ with a React + Tailwind v4 renderer. Hand-author a document — lanes, nodes,
7
+ edges — and get a deterministic diagram whose cards can host any React
8
+ content, such as a chart.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @unpunnyfuns/conduit
14
+ ```
15
+
16
+ Peers: `react ^19`, `react-dom ^19`, `tailwindcss ^4`.
17
+
18
+ In your Tailwind CSS entry:
19
+
20
+ ```css
21
+ @import "tailwindcss";
22
+ @import "@unpunnyfuns/conduit/theme.css";
23
+ @source "../node_modules/@unpunnyfuns/conduit";
24
+ ```
25
+
26
+ The `@source` line is required: the components use Tailwind utilities and
27
+ Tailwind only emits the classes it can see.
28
+
29
+ ## Structure
30
+
31
+ | Path | Contents |
32
+ | -------------------------------- | --------------------------------------------------------------------------------------- |
33
+ | `@unpunnyfuns/conduit` | `Diagram`, `Card`, `LaneBand`, `EdgeLayer`, `Badge`, `KindIcon`, `parseDocument`, types |
34
+ | `@unpunnyfuns/conduit/layout` | `layout()` — the pure engine, no React |
35
+ | `@unpunnyfuns/conduit/theme.css` | `--color-conduit-*` tokens with light and `.dark` values |
36
+
37
+ ## Usage
38
+
39
+ ```tsx
40
+ import { Diagram, parseDocument } from "@unpunnyfuns/conduit";
41
+
42
+ const doc = parseDocument({
43
+ version: 1,
44
+ title: "Data ingress",
45
+ lanes: [
46
+ { id: "sources", label: "External sources", order: 0 },
47
+ { id: "ingest", label: "Ingestion", order: 1 },
48
+ ],
49
+ nodes: [
50
+ { id: "partner-api", label: "Partner API", kind: "external", lane: "sources", row: 0 },
51
+ { id: "kafka", label: "Kafka ingest", kind: "queue", lane: "ingest", row: 0, size: "chart" },
52
+ ],
53
+ edges: [{ id: "api-to-kafka", from: "partner-api", to: "kafka", kind: "http", animated: true }],
54
+ });
55
+
56
+ export const Ingress = () => (
57
+ <Diagram doc={doc} onNodeClick={(id) => console.log(id)}>
58
+ {(node) => (node.size === "chart" ? <Throughput source={node.id} /> : null)}
59
+ </Diagram>
60
+ );
61
+ ```
62
+
63
+ ### Document
64
+
65
+ ```ts
66
+ type ConduitDocumentInput = {
67
+ version: 1;
68
+ title: string;
69
+ summary?: string;
70
+ direction?: "right" | "down";
71
+ lanes: { id; label; subtitle?; order?; status?; summary? }[];
72
+ nodes: {
73
+ id;
74
+ label;
75
+ kind: NodeKind;
76
+ lane;
77
+ row?: number;
78
+ size?: "compact" | "chart" | number;
79
+ status?: Status;
80
+ badges?: { label; tone? }[];
81
+ group?;
82
+ subtitle?;
83
+ summary?;
84
+ }[];
85
+ edges?: {
86
+ id;
87
+ from;
88
+ to;
89
+ kind: EdgeKind;
90
+ label?;
91
+ emphasis?: "normal" | "hero" | "muted";
92
+ animated?;
93
+ status?;
94
+ summary?;
95
+ }[];
96
+ views?: {
97
+ id;
98
+ title;
99
+ summary?;
100
+ scope?: { kind: "all" } | { kind: "selection"; lanes?; nodes?; edges? };
101
+ children?;
102
+ }[];
103
+ };
104
+
105
+ type Status = "neutral" | "positive" | "caution" | "critical";
106
+ ```
107
+
108
+ Two nodes with the same `row` in one lane sit side by side; a third throws
109
+ `ROW_OVERFULL`. Status affects colour only. Omit `row` to derive it from
110
+ edges. `size: "chart"` reserves a body slot; a number is a height in
111
+ pixels.
112
+
113
+ `direction: "down"` draws lanes as horizontal bands with cards flowing left
114
+ to right; `row` then means column. Default `"right"`.
115
+
116
+ ### `Diagram` props
117
+
118
+ | Prop | Type | Notes |
119
+ | ---------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------- |
120
+ | `doc` | `ConduitDocument` | from `parseDocument` |
121
+ | `view` | `string` | id of a view to draw |
122
+ | `selected` | `string[]` | ids to keep lit; everything else dims |
123
+ | `highlight` | `"neighbours" \| "upstream" \| "downstream" \| "both"` | what a selection lights; traced edges draw at hero weight; default `"neighbours"` |
124
+ | `onNodeClick`, `onEdgeClick` | `(id) => void` | cards become buttons when set |
125
+ | `cardHeights` | `{ compact?; withSubtitle?; chart? }` | defaults 52 / 62 / 140 |
126
+ | `className` | `string` | applied to the scroll viewport |
127
+ | `fit` | `boolean` | scale the canvas down to fit; default `false` |
128
+ | `children` | `(node) => ReactNode` | body slot of every card that has one |
129
+ | `edgeState` | `Record<string, EdgeState>` | live overlay by edge id; never relayouts |
130
+
131
+ The root is a scroll viewport; size it with `className`. With `fit` the
132
+ canvas scales down to the viewport width (never up).
133
+
134
+ `layout()` runs during render and throws `ConduitLayoutError`
135
+ (`UNKNOWN_VIEW`, `ROW_OVERFULL`, `NOTHING_TO_RENDER`) for a document it
136
+ cannot draw; wrap `Diagram` in an error boundary if documents are
137
+ user-supplied.
138
+
139
+ ### Live state
140
+
141
+ `edgeState` flips a hop between live, idle, done, stale and down without
142
+ touching the document or triggering a relayout. Per edge, with
143
+ `state = edgeState[edge.id]`:
144
+
145
+ | `state.level` | pulses | tone | stroke |
146
+ | ------------- | ------------------------------------------------------------- | ---------- | -------------------------------- |
147
+ | absent | document `animated` | document | as today |
148
+ | `live` | pulses on; period from `rate` when given, else the edge's own | document | as today |
149
+ | `idle` | off | document | as today |
150
+ | `done` | off | `positive` | as today |
151
+ | `stale` | off | `caution` | as today |
152
+ | `down` | off | `critical` | dashed (`stroke-dasharray: 5 4`) |
153
+
154
+ State tone replaces the document's `status` tone while set — a `critical`
155
+ edge marked `stale` shows caution until the state clears.
156
+
157
+ ```tsx
158
+ import { Diagram, type EdgeState } from "@unpunnyfuns/conduit";
159
+
160
+ const edgeState: Record<string, EdgeState> = { "kafka-to-validator": { level: "down" } };
161
+ <Diagram doc={doc} edgeState={edgeState} />;
162
+ ```
163
+
164
+ ### Layout without React
165
+
166
+ ```ts
167
+ import { layout } from "@unpunnyfuns/conduit/layout";
168
+
169
+ const { width, height, nodes, edges, atlas } = layout(doc, { view: "storage" });
170
+ ```
171
+
172
+ Same document, same geometry, every time.
173
+
174
+ To trace from a selection:
175
+
176
+ ```ts
177
+ import { traceFrom } from "@unpunnyfuns/conduit/layout";
178
+
179
+ const traced = traceFrom(doc.edges, ["warehouse"], "upstream");
180
+ const litNodeIds = [...traced.nodes];
181
+ ```
182
+
183
+ `traceFrom` walks whatever edge list you give it — pass `doc.edges` for the whole
184
+ document, or the `edges` from a `layout()` call to match a view's scope, which is
185
+ what `Diagram` does.
186
+
187
+ ## Development
188
+
189
+ ```bash
190
+ npm test
191
+ npm run example
192
+ npm run build
193
+ ```
194
+
195
+ `npm test` runs the engine tests in Node and the component tests in
196
+ Chromium; `npm run example` starts the Vite playground.
197
+
198
+ Developing requires Node ≥ 22.12 (Vitest 5); the published package runs on Node ≥ 20.11.
199
+
200
+ ## License
201
+
202
+ MIT. Layout engine derived from pr-lens, © Coldtea AI.
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Ends a switch over a closed set. Adding a variant then fails to compile at
3
+ * every consumer instead of silently falling through at runtime.
4
+ */
5
+ export declare const assertNever: (value: never, message?: string) => never;
6
+ //# sourceMappingURL=assert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assert.d.ts","sourceRoot":"","sources":["../src/assert.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,WAAW,UAAW,KAAK,uBAAkC,KAEzE,CAAC"}
package/dist/assert.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Ends a switch over a closed set. Adding a variant then fails to compile at
3
+ * every consumer instead of silently falling through at runtime.
4
+ */
5
+ export const assertNever = (value, message = "Unhandled variant") => {
6
+ throw new Error(`${message}: ${JSON.stringify(value)}`);
7
+ };
8
+ //# sourceMappingURL=assert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assert.js","sourceRoot":"","sources":["../src/assert.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAY,EAAE,OAAO,GAAG,mBAAmB,EAAS,EAAE;IAChF,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;AAC1D,CAAC,CAAC"}
package/dist/cn.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ /** Joins class names, dropping falsy parts. */
2
+ export declare const cn: (...parts: readonly (string | false | null | undefined)[]) => string;
3
+ //# sourceMappingURL=cn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cn.d.ts","sourceRoot":"","sources":["../src/cn.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,eAAO,MAAM,EAAE,aAAc,SAAS,CAAC,MAAM,GAAG,KAAK,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,KAAG,MACiB,CAAC"}
package/dist/cn.js ADDED
@@ -0,0 +1,3 @@
1
+ /** Joins class names, dropping falsy parts. */
2
+ export const cn = (...parts) => parts.filter((part) => typeof part === "string" && part.length > 0).join(" ");
3
+ //# sourceMappingURL=cn.js.map
package/dist/cn.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cn.js","sourceRoot":"","sources":["../src/cn.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,KAAqD,EAAU,EAAE,CACrF,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { Status } from "../schema/primitives.js";
2
+ export declare const Badge: ({ label, tone }: {
3
+ label: string;
4
+ tone: Status;
5
+ }) => import("react").JSX.Element;
6
+ //# sourceMappingURL=Badge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Badge.d.ts","sourceRoot":"","sources":["../../src/components/Badge.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAWtD,eAAO,MAAM,KAAK,oBAAqB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,gCAUrE,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from "../cn.js";
3
+ import { BADGE_HEIGHT } from "../layout/design.js";
4
+ const TONE = {
5
+ neutral: "bg-conduit-chip border-conduit-card-border text-conduit-muted",
6
+ positive: "bg-conduit-positive-fill border-conduit-positive-border text-conduit-positive-text",
7
+ caution: "bg-conduit-caution-fill border-conduit-caution-border text-conduit-caution-text",
8
+ critical: "bg-conduit-critical-fill border-conduit-critical-border text-conduit-critical-text",
9
+ };
10
+ export const Badge = ({ label, tone }) => (_jsx("span", { className: cn("inline-flex items-center rounded-full border px-[9px] text-[8.5px] font-bold uppercase leading-none tracking-[0.06em] whitespace-nowrap", TONE[tone]), style: { height: BADGE_HEIGHT }, children: label }));
11
+ //# sourceMappingURL=Badge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Badge.js","sourceRoot":"","sources":["../../src/components/Badge.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AAC9B,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,IAAI,GAA2B;IACnC,OAAO,EAAE,+DAA+D;IACxE,QAAQ,EAAE,oFAAoF;IAC9F,OAAO,EAAE,iFAAiF;IAC1F,QAAQ,EAAE,oFAAoF;CAC/F,CAAC;AAEF,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,EAAE,KAAK,EAAE,IAAI,EAAmC,EAAE,EAAE,CAAC,CACzE,eACE,SAAS,EAAE,EAAE,CACX,yIAAyI,EACzI,IAAI,CAAC,IAAI,CAAC,CACX,EACD,KAAK,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,YAE9B,KAAK,GACD,CACR,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ConduitNode } from "../schema/document.js";
3
+ import type { Box } from "../layout/geometry.js";
4
+ export type CardProps = {
5
+ id?: string;
6
+ node: ConduitNode;
7
+ box: Box;
8
+ /** Height of the title block the layout reserved; the body slot gets the rest. */
9
+ headerHeight: number;
10
+ dimmed?: boolean;
11
+ selected?: boolean;
12
+ onClick?: () => void;
13
+ children?: ReactNode;
14
+ };
15
+ /**
16
+ * One card. Absolutely positioned from its layout box so that a chart
17
+ * mounting in the body changes nothing around it.
18
+ */
19
+ export declare const Card: ({ id, node, box, headerHeight, dimmed, selected, onClick, children, }: CardProps) => import("react").JSX.Element;
20
+ //# sourceMappingURL=Card.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Card.d.ts","sourceRoot":"","sources":["../../src/components/Card.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGzD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,uBAAuB,CAAC;AAcjD,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,WAAW,CAAC;IAClB,GAAG,EAAE,GAAG,CAAC;IACT,kFAAkF;IAClF,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,IAAI,0EASd,SAAS,gCAuDX,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../cn.js";
3
+ import { Badge } from "./Badge.js";
4
+ import { KindIcon } from "./icons.js";
5
+ const BORDER = {
6
+ neutral: "border-conduit-card-border",
7
+ positive: "border-conduit-positive-border",
8
+ caution: "border-conduit-caution-border",
9
+ critical: "border-conduit-critical-border",
10
+ };
11
+ /** Below this width a card's title steps down a size, as upstream did. */
12
+ const SMALL_TITLE_BELOW = 200;
13
+ /**
14
+ * One card. Absolutely positioned from its layout box so that a chart
15
+ * mounting in the body changes nothing around it.
16
+ */
17
+ export const Card = ({ id, node, box, headerHeight, dimmed = false, selected = false, onClick, children, }) => {
18
+ const hasBody = box.height > headerHeight;
19
+ const label = node.subtitle === undefined ? node.label : `${node.label}, ${node.subtitle}`;
20
+ const className = cn("absolute flex flex-col rounded-[10px] border bg-conduit-card text-left shadow-[0_1px_2px_var(--color-conduit-shadow)] transition-opacity", BORDER[node.status], dimmed && "opacity-45", selected && "ring-2 ring-conduit-fg/40");
21
+ const style = { left: box.x, top: box.y, width: box.width, height: box.height };
22
+ return (_jsxs("div", { role: "group", "aria-label": label, "data-conduit-node": id, className: className, style: style, children: [onClick !== undefined && (_jsx("button", { type: "button", "aria-label": label, onClick: onClick, className: "absolute inset-x-0 top-0 cursor-pointer rounded-[10px] bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-conduit-fg/40", style: { height: headerHeight } })), node.badges.length > 0 && (_jsx("div", { className: "absolute -top-2 right-[7px] flex gap-1.5", children: node.badges.map((badge) => (_jsx(Badge, { label: badge.label, tone: badge.tone }, badge.label))) })), _jsxs("div", { className: "flex shrink-0 items-center gap-[10px] px-[14px]", style: { height: headerHeight }, children: [_jsx("span", { className: "flex size-[26px] shrink-0 items-center justify-center rounded-[7px] bg-conduit-chip", children: _jsx(KindIcon, { kind: node.kind }) }), _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: cn("truncate font-semibold text-conduit-fg", box.width >= SMALL_TITLE_BELOW ? "text-[13px]" : "text-[11.5px]"), children: node.label }), node.subtitle !== undefined && (_jsx("span", { className: "truncate font-mono text-[9.5px] text-conduit-muted", children: node.subtitle }))] })] }), hasBody && _jsx("div", { className: "min-h-0 flex-1 px-[14px] pb-[12px]", children: children })] }));
23
+ };
24
+ //# sourceMappingURL=Card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Card.js","sourceRoot":"","sources":["../../src/components/Card.tsx"],"names":[],"mappings":";AAGA,OAAO,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AAE9B,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,MAAM,MAAM,GAA2B;IACrC,OAAO,EAAE,4BAA4B;IACrC,QAAQ,EAAE,gCAAgC;IAC1C,OAAO,EAAE,+BAA+B;IACxC,QAAQ,EAAE,gCAAgC;CAC3C,CAAC;AAEF,0EAA0E;AAC1E,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAc9B;;;GAGG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,EACnB,EAAE,EACF,IAAI,EACJ,GAAG,EACH,YAAY,EACZ,MAAM,GAAG,KAAK,EACd,QAAQ,GAAG,KAAK,EAChB,OAAO,EACP,QAAQ,GACE,EAAE,EAAE;IACd,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,GAAG,YAAY,CAAC;IAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC;IAC3F,MAAM,SAAS,GAAG,EAAE,CAClB,0IAA0I,EAC1I,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EACnB,MAAM,IAAI,YAAY,EACtB,QAAQ,IAAI,2BAA2B,CACxC,CAAC;IACF,MAAM,KAAK,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;IAEhF,OAAO,CACL,eAAK,IAAI,EAAC,OAAO,gBAAa,KAAK,uBAAqB,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,aAC3F,OAAO,KAAK,SAAS,IAAI,CACxB,iBACE,IAAI,EAAC,QAAQ,gBACD,KAAK,EACjB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAC,wJAAwJ,EAClK,KAAK,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,GAC/B,CACH,EACA,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,CACzB,cAAK,SAAS,EAAC,0CAA0C,YACtD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAC1B,KAAC,KAAK,IAAmB,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,IAAjD,KAAK,CAAC,KAAK,CAA0C,CAClE,CAAC,GACE,CACP,EACD,eACE,SAAS,EAAC,iDAAiD,EAC3D,KAAK,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,aAE/B,eAAM,SAAS,EAAC,qFAAqF,YACnG,KAAC,QAAQ,IAAC,IAAI,EAAE,IAAI,CAAC,IAAI,GAAI,GACxB,EACP,gBAAM,SAAS,EAAC,uBAAuB,aACrC,eACE,SAAS,EAAE,EAAE,CACX,wCAAwC,EACxC,GAAG,CAAC,KAAK,IAAI,iBAAiB,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,eAAe,CACjE,YAEA,IAAI,CAAC,KAAK,GACN,EACN,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,CAC9B,eAAM,SAAS,EAAC,oDAAoD,YACjE,IAAI,CAAC,QAAQ,GACT,CACR,IACI,IACH,EACL,OAAO,IAAI,cAAK,SAAS,EAAC,oCAAoC,YAAE,QAAQ,GAAO,IAC5E,CACP,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,37 @@
1
+ import { type ReactNode } from "react";
2
+ import type { ConduitDocument, ConduitNode } from "../schema/document.js";
3
+ import { type CardHeights } from "../layout/design.js";
4
+ import { type Highlight } from "../layout/trace.js";
5
+ import type { EdgeState } from "./pulse.js";
6
+ export type DiagramProps = {
7
+ doc: ConduitDocument;
8
+ /** Id of a view to draw. Omitted draws the whole document. */
9
+ view?: string;
10
+ /** Ids of lanes, nodes or edges to keep lit; everything else dims. */
11
+ selected?: readonly string[];
12
+ /** What a selection lights: neighbours (default), everything upstream, downstream, or both. */
13
+ highlight?: Highlight;
14
+ onNodeClick?: (id: string) => void;
15
+ onEdgeClick?: (id: string) => void;
16
+ cardHeights?: Partial<CardHeights>;
17
+ /** Applied to the scroll viewport; size it with `h-[…]` / `max-h-[…]` etc. */
18
+ className?: string;
19
+ /** Scales the canvas down to the viewport's width. Never scales up. */
20
+ fit?: boolean;
21
+ /** Fills the body slot of every card that has one. */
22
+ children?: (node: ConduitNode) => ReactNode;
23
+ /** Live overlay by edge id; changes never relayout. */
24
+ edgeState?: Readonly<Record<string, EdgeState>>;
25
+ };
26
+ /**
27
+ * The diagram: lanes underneath, one svg of edges, cards on top. Layout is
28
+ * pure and memoised on its inputs, so a chart re-rendering inside a card
29
+ * never triggers a relayout.
30
+ *
31
+ * The root (`role="figure"`) is a scroll viewport; the fixed-size drawing
32
+ * itself is `[data-conduit-canvas]` inside it, scaled down to the viewport's
33
+ * width when `fit` is set. Atlas coordinates from `layout()` are relative to
34
+ * the canvas, not the viewport.
35
+ */
36
+ export declare const Diagram: ({ doc, view, selected, highlight, onNodeClick, onEdgeClick, cardHeights, className, fit, children, edgeState, }: DiagramProps) => import("react").JSX.Element;
37
+ //# sourceMappingURL=Diagram.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Diagram.d.ts","sourceRoot":"","sources":["../../src/components/Diagram.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAwC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC7E,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAE1E,OAAO,EAAwB,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAE7E,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAI/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,eAAe,CAAC;IACrB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B,+FAA+F;IAC/F,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,WAAW,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IACnC,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,sDAAsD;IACtD,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,SAAS,CAAC;IAC5C,uDAAuD;IACvD,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;CACjD,CAAC;AAKF;;;;;;;;;GASG;AACH,eAAO,MAAM,OAAO,oHAYjB,YAAY,gCA0Jd,CAAC"}
@@ -0,0 +1,92 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { cn } from "../cn.js";
4
+ import { DEFAULT_CARD_HEIGHTS } from "../layout/design.js";
5
+ import { layout } from "../layout/layout.js";
6
+ import { traceFrom } from "../layout/trace.js";
7
+ import { Card } from "./Card.js";
8
+ import { EdgeLayer } from "./EdgeLayer.js";
9
+ import { LaneBand } from "./Lane.js";
10
+ const headerHeightOf = (node, heights) => node.subtitle === undefined ? heights.compact : heights.withSubtitle;
11
+ /**
12
+ * The diagram: lanes underneath, one svg of edges, cards on top. Layout is
13
+ * pure and memoised on its inputs, so a chart re-rendering inside a card
14
+ * never triggers a relayout.
15
+ *
16
+ * The root (`role="figure"`) is a scroll viewport; the fixed-size drawing
17
+ * itself is `[data-conduit-canvas]` inside it, scaled down to the viewport's
18
+ * width when `fit` is set. Atlas coordinates from `layout()` are relative to
19
+ * the canvas, not the viewport.
20
+ */
21
+ export const Diagram = ({ doc, view, selected, highlight = "neighbours", onNodeClick, onEdgeClick, cardHeights, className, fit = false, children, edgeState, }) => {
22
+ // Deps are the scalars, not `cardHeights` itself: a caller passing a fresh
23
+ // object literal every render must not defeat the memo below it.
24
+ const compactHeight = cardHeights?.compact;
25
+ const withSubtitleHeight = cardHeights?.withSubtitle;
26
+ const chartHeight = cardHeights?.chart;
27
+ const heights = useMemo(() => ({
28
+ compact: compactHeight ?? DEFAULT_CARD_HEIGHTS.compact,
29
+ withSubtitle: withSubtitleHeight ?? DEFAULT_CARD_HEIGHTS.withSubtitle,
30
+ chart: chartHeight ?? DEFAULT_CARD_HEIGHTS.chart,
31
+ }), [compactHeight, withSubtitleHeight, chartHeight]);
32
+ const { laid, nodeLabel } = useMemo(() => {
33
+ const drawn = layout(doc, { view, cardHeights: heights });
34
+ const labels = new Map(drawn.nodes.map(({ node }) => [node.id, node.label]));
35
+ return { laid: drawn, nodeLabel: labels };
36
+ }, [doc, view, heights]);
37
+ const viewportRef = useRef(null);
38
+ const [viewportWidth, setViewportWidth] = useState(0);
39
+ useEffect(() => {
40
+ if (!fit)
41
+ return;
42
+ const node = viewportRef.current;
43
+ if (node === null || typeof ResizeObserver === "undefined")
44
+ return;
45
+ const update = () => setViewportWidth(node.clientWidth);
46
+ update();
47
+ const observer = new ResizeObserver(update);
48
+ observer.observe(node);
49
+ return () => observer.disconnect();
50
+ }, [fit]);
51
+ const scale = fit && viewportWidth > 0 ? Math.min(1, viewportWidth / laid.width) : 1;
52
+ const lit = useMemo(() => {
53
+ if (selected === undefined || selected.length === 0)
54
+ return undefined;
55
+ const trace = traceFrom(laid.edges.map(({ edge }) => edge), selected, highlight);
56
+ const ids = new Set(selected);
57
+ const nodes = new Set(laid.nodes.filter(({ node }) => trace.nodes.has(node.id)).map(({ node }) => node.id));
58
+ const edges = new Set(laid.edges.filter(({ edge }) => trace.edges.has(edge.id)).map(({ edge }) => edge.id));
59
+ const lanesWithLitNode = new Set();
60
+ for (const { node } of laid.nodes)
61
+ if (nodes.has(node.id))
62
+ lanesWithLitNode.add(node.lane);
63
+ const lanes = new Set(laid.lanes
64
+ .filter(({ lane }) => ids.has(lane.id) || lanesWithLitNode.has(lane.id))
65
+ .map(({ lane }) => lane.id));
66
+ return { nodes, edges, lanes };
67
+ }, [selected, highlight, laid]);
68
+ const emphasised = lit === undefined || highlight === "neighbours" ? undefined : lit.edges;
69
+ const dimmedEdges = useMemo(() => new Set(lit === undefined
70
+ ? []
71
+ : laid.edges.filter(({ edge }) => !lit.edges.has(edge.id)).map(({ edge }) => edge.id)), [lit, laid]);
72
+ return (_jsx("div", { role: "figure", "aria-label": doc.title, ref: viewportRef, className: cn("relative max-w-full overflow-auto", className), children: _jsxs("div", { "data-conduit-canvas": true, className: "relative rounded-xl bg-conduit-bg bg-[radial-gradient(var(--color-conduit-dot)_1px,transparent_1px)] bg-[size:18px_18px] text-conduit-fg", style: {
73
+ width: laid.width,
74
+ height: laid.height,
75
+ ...(scale < 1
76
+ ? {
77
+ transform: `scale(${scale})`,
78
+ transformOrigin: "top left",
79
+ marginRight: -(laid.width * (1 - scale)),
80
+ marginBottom: -(laid.height * (1 - scale)),
81
+ }
82
+ : {}),
83
+ }, children: [laid.lanes.map(({ lane, box }) => (_jsx(LaneBand, { id: lane.id, lane: lane, box: box, dimmed: lit !== undefined && !lit.lanes.has(lane.id) }, lane.id))), _jsx(EdgeLayer, { width: laid.width, height: laid.height, edges: laid.edges, dimmedIds: dimmedEdges, emphasisedIds: emphasised, onEdgeClick: onEdgeClick, edgeState: edgeState }), laid.nodes.map(({ node, box }) => {
84
+ const headerHeight = headerHeightOf(node, heights);
85
+ const hasBody = box.height > headerHeight;
86
+ return (_jsx(Card, { id: node.id, node: node, box: box, headerHeight: headerHeight, dimmed: lit !== undefined && !lit.nodes.has(node.id), selected: selected?.includes(node.id) ?? false, onClick: onNodeClick === undefined ? undefined : () => onNodeClick(node.id), children: hasBody ? children?.(node) : null }, node.id));
87
+ }), _jsx("ul", { className: "sr-only", children: laid.edges.map(({ edge }) => {
88
+ const text = `${nodeLabel.get(edge.from) ?? edge.from} → ${nodeLabel.get(edge.to) ?? edge.to}, ${edge.kind}`;
89
+ return (_jsx("li", { children: onEdgeClick === undefined ? (text) : (_jsx("button", { type: "button", onClick: () => onEdgeClick(edge.id), children: text })) }, edge.id));
90
+ }) })] }) }));
91
+ };
92
+ //# sourceMappingURL=Diagram.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Diagram.js","sourceRoot":"","sources":["../../src/components/Diagram.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { PlacedEdge } from "../layout/layout.js";
2
+ import { type EdgeState } from "./pulse.js";
3
+ export type EdgeLayerProps = {
4
+ width: number;
5
+ height: number;
6
+ edges: readonly PlacedEdge[];
7
+ dimmedIds: ReadonlySet<string>;
8
+ /**
9
+ * Edges drawn at hero weight without changing their pulses, e.g. a traced path.
10
+ * An edge the document marks `muted` stays muted.
11
+ */
12
+ emphasisedIds?: ReadonlySet<string>;
13
+ onEdgeClick?: (id: string) => void;
14
+ /** Live overlay by edge id. Absent ids follow the document; see the mapping in the README. */
15
+ edgeState?: Readonly<Record<string, EdgeState>>;
16
+ };
17
+ /**
18
+ * Every edge of the diagram in one svg under the cards. Lines pass behind
19
+ * cards; pills are opaque so they read wherever they land, and are drawn
20
+ * last so they pass in front of everything in this layer.
21
+ */
22
+ export declare const EdgeLayer: ({ width, height, edges, dimmedIds, emphasisedIds, onEdgeClick, edgeState, }: EdgeLayerProps) => import("react").JSX.Element;
23
+ //# sourceMappingURL=EdgeLayer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EdgeLayer.d.ts","sourceRoot":"","sources":["../../src/components/EdgeLayer.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAoC,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AA+G9E,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IAC7B,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC/B;;;OAGG;IACH,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,8FAA8F;IAC9F,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;CACjD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,SAAS,gFAQnB,cAAc,gCAgGhB,CAAC"}
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from "react";
3
+ import { cn } from "../cn.js";
4
+ import { HERO_PULSE_COUNT, HERO_PULSE_DURATION, PILL_TEXT_SIZE, PULSE_DURATION, } from "../layout/design.js";
5
+ import { coord } from "../layout/geometry.js";
6
+ import { pulseDurationFor } from "./pulse.js";
7
+ import { STATUSES } from "../schema/primitives.js";
8
+ const STROKE = {
9
+ neutral: "stroke-conduit-edge",
10
+ positive: "stroke-conduit-positive",
11
+ caution: "stroke-conduit-caution",
12
+ critical: "stroke-conduit-critical",
13
+ };
14
+ const FILL = {
15
+ neutral: "fill-conduit-edge",
16
+ positive: "fill-conduit-positive",
17
+ caution: "fill-conduit-caution",
18
+ critical: "fill-conduit-critical",
19
+ };
20
+ const PILL_TEXT = {
21
+ neutral: "fill-conduit-muted",
22
+ positive: "fill-conduit-positive-text",
23
+ caution: "fill-conduit-caution-text",
24
+ critical: "fill-conduit-critical-text",
25
+ };
26
+ const PULSE_RADIUS = 2.6;
27
+ const TRAIN_RADIUS = 3;
28
+ const toneFor = (level, base) => {
29
+ switch (level) {
30
+ case "down":
31
+ return "critical";
32
+ case "stale":
33
+ return "caution";
34
+ case "done":
35
+ return "positive";
36
+ default:
37
+ return base;
38
+ }
39
+ };
40
+ /**
41
+ * The travelling pulse: the mark that says a connection carries traffic
42
+ * rather than merely existing. A dot running behind the drawing's clock says
43
+ * so with a negative `begin`, so it is never seen waiting at the origin.
44
+ */
45
+ const Pulses = ({ id, path, tone, hero, duration, }) => {
46
+ const count = hero ? HERO_PULSE_COUNT : 1;
47
+ return (_jsx("g", { "data-pulse": id, className: "motion-reduce:hidden", children: Array.from({ length: count }, (_, index) => {
48
+ const behind = ((duration / count) * index) % duration;
49
+ return (_jsx("circle", { r: hero ? TRAIN_RADIUS : PULSE_RADIUS, className: FILL[tone], children: _jsx("animateMotion", { dur: `${coord(duration)}s`, begin: behind === 0 ? undefined : `${coord(behind - duration)}s`, repeatCount: "indefinite", path: path }) }, index));
50
+ }) }));
51
+ };
52
+ const Pill = ({ text, box, tone, }) => (_jsxs("g", { children: [_jsx("rect", { className: "fill-conduit-pill stroke-conduit-pill-border stroke-[1]", x: box.x, y: box.y, width: box.width, height: box.height, rx: box.height / 2 }), _jsx("text", { className: cn("font-semibold", PILL_TEXT[tone]), style: { fontSize: PILL_TEXT_SIZE }, x: box.x + box.width / 2, y: box.y + box.height / 2 + 3.5, textAnchor: "middle", children: text })] }));
53
+ const EMPTY = new Set();
54
+ const NO_STATE = {};
55
+ /**
56
+ * Every edge of the diagram in one svg under the cards. Lines pass behind
57
+ * cards; pills are opaque so they read wherever they land, and are drawn
58
+ * last so they pass in front of everything in this layer.
59
+ */
60
+ export const EdgeLayer = ({ width, height, edges, dimmedIds, emphasisedIds = EMPTY, onEdgeClick, edgeState = NO_STATE, }) => {
61
+ const uid = useId().replace(/:/g, "");
62
+ const marker = (tone) => `${uid}-mk-${tone}`;
63
+ return (_jsxs("svg", { className: "pointer-events-none absolute inset-0 overflow-visible", width: width, height: height, viewBox: `0 0 ${width} ${height}`, "aria-hidden": "true", children: [_jsx("defs", { children: STATUSES.map((tone) => (_jsx("marker", { id: marker(tone), viewBox: "0 0 10 10", refX: 8, refY: 5, markerWidth: 6.5, markerHeight: 6.5, orient: "auto-start-reverse", children: _jsx("path", { d: "M0,0 L10,5 L0,10 z", className: FILL[tone] }) }, tone))) }), edges.map(({ edge, path, tone }) => {
64
+ const state = edgeState[edge.id];
65
+ const level = state?.level;
66
+ const liveTone = toneFor(level, tone);
67
+ const pulsing = level === undefined ? edge.animated : level === "live";
68
+ const heroPulses = edge.emphasis === "hero";
69
+ const duration = level === "live" && state?.rate !== undefined
70
+ ? pulseDurationFor(state.rate)
71
+ : heroPulses
72
+ ? HERO_PULSE_DURATION
73
+ : PULSE_DURATION;
74
+ const hero = heroPulses || (emphasisedIds.has(edge.id) && edge.emphasis !== "muted");
75
+ const dimmed = dimmedIds.has(edge.id) || edge.emphasis === "muted";
76
+ return (_jsxs("g", { "data-edge-group": edge.id, "data-edge-state": level, className: cn("transition-opacity", dimmed && "opacity-45"), children: [hero && (_jsx("path", { d: path, className: cn("fill-none stroke-[7] opacity-[0.14]", STROKE[liveTone]) })), _jsx("path", { "data-edge": edge.id, d: path, className: cn("fill-none", STROKE[liveTone], hero ? "stroke-[2.25]" : "stroke-[1.5]", level === "down" && "[stroke-dasharray:5_4]"), markerEnd: `url(#${marker(liveTone)})` }), pulsing && (_jsx(Pulses, { id: edge.id, path: path, tone: liveTone, hero: heroPulses, duration: duration })), onEdgeClick !== undefined && (_jsx("path", { "data-edge-hit": edge.id, d: path, className: "pointer-events-auto cursor-pointer fill-none stroke-transparent stroke-[12]", onClick: () => onEdgeClick(edge.id) }))] }, edge.id));
77
+ }), edges.map(({ edge, label, tone }) => {
78
+ if (label === undefined)
79
+ return null;
80
+ const level = edgeState[edge.id]?.level;
81
+ const liveTone = toneFor(level, tone);
82
+ return _jsx(Pill, { text: label.text, box: label.box, tone: liveTone }, `${edge.id}-label`);
83
+ })] }));
84
+ };
85
+ //# sourceMappingURL=EdgeLayer.js.map