@braidlabs/console 0.1.1
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.
- package/README.md +131 -0
- package/package.json +43 -0
- package/src/app/console-app.d.ts +17 -0
- package/src/app/console-app.js +48 -0
- package/src/app/console-app.js.map +1 -0
- package/src/app/main.d.ts +1 -0
- package/src/app/main.js +32 -0
- package/src/app/main.js.map +1 -0
- package/src/index.d.ts +38 -0
- package/src/index.js +30 -0
- package/src/index.js.map +1 -0
- package/src/lib/access-panel.d.ts +21 -0
- package/src/lib/access-panel.js +35 -0
- package/src/lib/access-panel.js.map +1 -0
- package/src/lib/client.d.ts +94 -0
- package/src/lib/client.js +109 -0
- package/src/lib/client.js.map +1 -0
- package/src/lib/draft.d.ts +45 -0
- package/src/lib/draft.js +85 -0
- package/src/lib/draft.js.map +1 -0
- package/src/lib/integration-panel.d.ts +13 -0
- package/src/lib/integration-panel.js +85 -0
- package/src/lib/integration-panel.js.map +1 -0
- package/src/lib/integration.d.ts +36 -0
- package/src/lib/integration.js +148 -0
- package/src/lib/integration.js.map +1 -0
- package/src/lib/registry-console.d.ts +46 -0
- package/src/lib/registry-console.js +88 -0
- package/src/lib/registry-console.js.map +1 -0
- package/src/lib/registry-editor.d.ts +26 -0
- package/src/lib/registry-editor.js +133 -0
- package/src/lib/registry-editor.js.map +1 -0
- package/src/lib/styles.d.ts +27 -0
- package/src/lib/styles.js +470 -0
- package/src/lib/styles.js.map +1 -0
- package/src/lib/topology-graph.d.ts +47 -0
- package/src/lib/topology-graph.js +124 -0
- package/src/lib/topology-graph.js.map +1 -0
- package/src/lib/topology.d.ts +86 -0
- package/src/lib/topology.js +122 -0
- package/src/lib/topology.js.map +1 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The console's styles, as a string injected once at mount.
|
|
3
|
+
*
|
|
4
|
+
* Two constraints drive every choice here, both from being a *library* that mounts inside someone
|
|
5
|
+
* else's page:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Every rule is scoped under `.braid-console`.** No resets, no bare element selectors, no
|
|
8
|
+
* `:root`. A library that styles `body` or `*` vandalizes its host, and the damage shows up in
|
|
9
|
+
* a part of the app nobody connects to this component.
|
|
10
|
+
* 2. **Everything themable is a custom property on the scope class**, so a host restyles by
|
|
11
|
+
* setting variables rather than escalating specificity. Dark mode follows
|
|
12
|
+
* `prefers-color-scheme`, and a host that knows better can force it with `data-theme` — which
|
|
13
|
+
* must win in *both* directions, since an admin shell may be dark inside a light OS.
|
|
14
|
+
*
|
|
15
|
+
* Shipped as a string rather than a `.css` import so the library has no build-tool requirements:
|
|
16
|
+
* a consumer bundling this needs no CSS loader, no `?inline` suffix, and no side-effect import
|
|
17
|
+
* they can forget.
|
|
18
|
+
*
|
|
19
|
+
* **The palette is Fluent's neutral ramp and brand blue**, because the console's likeliest home is
|
|
20
|
+
* an internal admin surface sitting beside Azure, Entra, or an in-house portal built to match one.
|
|
21
|
+
* A tool that looks foreign next to the tools it is used with reads as untrusted regardless of
|
|
22
|
+
* what it does. The ramp is expressed as tokens rather than literals precisely so a shop with a
|
|
23
|
+
* different system overrides eight variables instead of forking the stylesheet.
|
|
24
|
+
*/
|
|
25
|
+
export const CONSOLE_STYLES = `
|
|
26
|
+
.braid-console {
|
|
27
|
+
--bc-bg: #ffffff;
|
|
28
|
+
--bc-surface: #faf9f8;
|
|
29
|
+
--bc-surface-alt: #f3f2f1;
|
|
30
|
+
--bc-border: #e1dfdd;
|
|
31
|
+
--bc-border-strong: #c8c6c4;
|
|
32
|
+
--bc-text: #201f1e;
|
|
33
|
+
--bc-muted: #605e5c;
|
|
34
|
+
--bc-subtle: #8a8886;
|
|
35
|
+
--bc-accent: #0f6cbd;
|
|
36
|
+
--bc-accent-hover: #115ea3;
|
|
37
|
+
--bc-accent-soft: #eff6fc;
|
|
38
|
+
--bc-warn-bg: #fff4ce;
|
|
39
|
+
--bc-warn-text: #6a4b16;
|
|
40
|
+
--bc-warn-border: #f2d98c;
|
|
41
|
+
--bc-ok: #0e700e;
|
|
42
|
+
--bc-ok-soft: #dff6dd;
|
|
43
|
+
--bc-err: #a4262c;
|
|
44
|
+
--bc-err-soft: #fde7e9;
|
|
45
|
+
--bc-radius: 4px;
|
|
46
|
+
--bc-gap: 12px;
|
|
47
|
+
--bc-shadow: 0 1.6px 3.6px rgba(0,0,0,0.10), 0 0.3px 0.9px rgba(0,0,0,0.07);
|
|
48
|
+
--bc-font: "Segoe UI", "Segoe UI Web (West European)", ui-sans-serif, system-ui, -apple-system, Roboto, sans-serif;
|
|
49
|
+
--bc-mono: ui-monospace, "Cascadia Mono", SFMono-Regular, Consolas, Menlo, monospace;
|
|
50
|
+
|
|
51
|
+
color: var(--bc-text);
|
|
52
|
+
background: var(--bc-bg);
|
|
53
|
+
font-family: var(--bc-font);
|
|
54
|
+
font-size: 14px;
|
|
55
|
+
line-height: 1.4;
|
|
56
|
+
container-type: inline-size;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@media (prefers-color-scheme: dark) {
|
|
60
|
+
.braid-console {
|
|
61
|
+
--bc-bg: #1b1a19;
|
|
62
|
+
--bc-surface: #252423;
|
|
63
|
+
--bc-surface-alt: #292827;
|
|
64
|
+
--bc-border: #3b3a39;
|
|
65
|
+
--bc-border-strong: #484644;
|
|
66
|
+
--bc-text: #f3f2f1;
|
|
67
|
+
--bc-muted: #c8c6c4;
|
|
68
|
+
--bc-subtle: #a19f9d;
|
|
69
|
+
--bc-accent: #479ef5;
|
|
70
|
+
--bc-accent-hover: #62abf5;
|
|
71
|
+
--bc-accent-soft: #082338;
|
|
72
|
+
--bc-warn-bg: #3a2f0b;
|
|
73
|
+
--bc-warn-text: #fce100;
|
|
74
|
+
--bc-warn-border: #6b5a12;
|
|
75
|
+
--bc-ok: #54b054;
|
|
76
|
+
--bc-ok-soft: #052505;
|
|
77
|
+
--bc-err: #f1707b;
|
|
78
|
+
--bc-err-soft: #3b1216;
|
|
79
|
+
--bc-shadow: 0 1.6px 3.6px rgba(0,0,0,0.4), 0 0.3px 0.9px rgba(0,0,0,0.3);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* An explicit host choice wins over the OS in both directions. */
|
|
84
|
+
.braid-console[data-theme='light'] {
|
|
85
|
+
--bc-bg: #ffffff; --bc-surface: #faf9f8; --bc-surface-alt: #f3f2f1;
|
|
86
|
+
--bc-border: #e1dfdd; --bc-border-strong: #c8c6c4;
|
|
87
|
+
--bc-text: #201f1e; --bc-muted: #605e5c; --bc-subtle: #8a8886;
|
|
88
|
+
--bc-accent: #0f6cbd; --bc-accent-hover: #115ea3; --bc-accent-soft: #eff6fc;
|
|
89
|
+
--bc-warn-bg: #fff4ce; --bc-warn-text: #6a4b16; --bc-warn-border: #f2d98c;
|
|
90
|
+
--bc-ok: #0e700e; --bc-ok-soft: #dff6dd; --bc-err: #a4262c; --bc-err-soft: #fde7e9;
|
|
91
|
+
--bc-shadow: 0 1.6px 3.6px rgba(0,0,0,0.10), 0 0.3px 0.9px rgba(0,0,0,0.07);
|
|
92
|
+
}
|
|
93
|
+
.braid-console[data-theme='dark'] {
|
|
94
|
+
--bc-bg: #1b1a19; --bc-surface: #252423; --bc-surface-alt: #292827;
|
|
95
|
+
--bc-border: #3b3a39; --bc-border-strong: #484644;
|
|
96
|
+
--bc-text: #f3f2f1; --bc-muted: #c8c6c4; --bc-subtle: #a19f9d;
|
|
97
|
+
--bc-accent: #479ef5; --bc-accent-hover: #62abf5; --bc-accent-soft: #082338;
|
|
98
|
+
--bc-warn-bg: #3a2f0b; --bc-warn-text: #fce100; --bc-warn-border: #6b5a12;
|
|
99
|
+
--bc-ok: #54b054; --bc-ok-soft: #052505; --bc-err: #f1707b; --bc-err-soft: #3b1216;
|
|
100
|
+
--bc-shadow: 0 1.6px 3.6px rgba(0,0,0,0.4), 0 0.3px 0.9px rgba(0,0,0,0.3);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.braid-console *, .braid-console *::before, .braid-console *::after { box-sizing: border-box; }
|
|
104
|
+
|
|
105
|
+
/* --- shell -------------------------------------------------------------- */
|
|
106
|
+
|
|
107
|
+
/* The chrome an operator reads before anything else: what this is, which gateway, how fresh. */
|
|
108
|
+
.braid-console__shell { display: flex; flex-direction: column; gap: 0; }
|
|
109
|
+
|
|
110
|
+
.braid-console__titlebar {
|
|
111
|
+
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
|
112
|
+
padding: 0 0 10px;
|
|
113
|
+
}
|
|
114
|
+
.braid-console__producttitle { font-size: 20px; font-weight: 600; margin: 0; letter-spacing: -0.01em; }
|
|
115
|
+
.braid-console__breadcrumb { color: var(--bc-subtle); font-size: 13px; display: flex; align-items: center; gap: 6px; }
|
|
116
|
+
.braid-console__breadcrumb code { font-family: var(--bc-mono); font-size: 12px; color: var(--bc-muted); }
|
|
117
|
+
|
|
118
|
+
/* Fluent's command bar: actions live on one line above the content, left-aligned, icon-led. */
|
|
119
|
+
.braid-console__commandbar {
|
|
120
|
+
display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
|
|
121
|
+
border-top: 1px solid var(--bc-border); border-bottom: 1px solid var(--bc-border);
|
|
122
|
+
padding: 6px 0; margin-bottom: var(--bc-gap);
|
|
123
|
+
}
|
|
124
|
+
.braid-console__command {
|
|
125
|
+
font: inherit; font-size: 13px; color: inherit; background: transparent; border: 1px solid transparent;
|
|
126
|
+
border-radius: var(--bc-radius); padding: 5px 10px; cursor: pointer;
|
|
127
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
128
|
+
}
|
|
129
|
+
.braid-console__command:hover { background: var(--bc-surface-alt); }
|
|
130
|
+
.braid-console__command:disabled { opacity: 0.4; cursor: not-allowed; background: transparent; }
|
|
131
|
+
.braid-console__command:focus-visible { outline: 2px solid var(--bc-accent); outline-offset: 1px; }
|
|
132
|
+
.braid-console__commandicon { font-size: 13px; line-height: 1; color: var(--bc-accent); }
|
|
133
|
+
.braid-console__commandsep { width: 1px; align-self: stretch; background: var(--bc-border); margin: 2px 4px; }
|
|
134
|
+
|
|
135
|
+
/* Tabs (Fluent "pivot"): an underline on the selected one, never a filled pill. */
|
|
136
|
+
.braid-console__tabs { display: flex; align-items: center; gap: 2px; }
|
|
137
|
+
.braid-console__tab {
|
|
138
|
+
font: inherit; font-size: 14px; color: var(--bc-muted); background: transparent;
|
|
139
|
+
border: 0; border-bottom: 2px solid transparent; cursor: pointer;
|
|
140
|
+
padding: 7px 10px; display: inline-flex; align-items: center; gap: 6px;
|
|
141
|
+
}
|
|
142
|
+
.braid-console__tab:hover { color: var(--bc-text); }
|
|
143
|
+
.braid-console__tab[aria-selected='true'] { color: var(--bc-text); font-weight: 600; border-bottom-color: var(--bc-accent); }
|
|
144
|
+
.braid-console__tab:focus-visible { outline: 2px solid var(--bc-accent); outline-offset: -2px; }
|
|
145
|
+
.braid-console__tabcount {
|
|
146
|
+
font-size: 11px; font-weight: 600; background: var(--bc-surface-alt); color: var(--bc-muted);
|
|
147
|
+
border-radius: 9px; padding: 0 6px; font-variant-numeric: tabular-nums;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* --- summary strip ------------------------------------------------------ */
|
|
151
|
+
|
|
152
|
+
/* Four numbers an operator wants before scrolling: scale, composition, and what is gated. */
|
|
153
|
+
.braid-console__stats { display: flex; gap: 1px; background: var(--bc-border); border: 1px solid var(--bc-border);
|
|
154
|
+
border-radius: var(--bc-radius); overflow: hidden; margin-bottom: var(--bc-gap); flex-wrap: wrap; }
|
|
155
|
+
.braid-console__stat { background: var(--bc-bg); padding: 10px 14px; flex: 1 1 auto; min-width: 8rem; }
|
|
156
|
+
.braid-console__statvalue { font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums; display: block; line-height: 1.2; }
|
|
157
|
+
.braid-console__statlabel { font-size: 12px; color: var(--bc-muted); }
|
|
158
|
+
.braid-console__stat--warn .braid-console__statvalue { color: var(--bc-warn-text); }
|
|
159
|
+
|
|
160
|
+
.braid-console__header {
|
|
161
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: var(--bc-gap);
|
|
162
|
+
padding-bottom: var(--bc-gap);
|
|
163
|
+
}
|
|
164
|
+
.braid-console__title { font-size: 15px; font-weight: 600; margin: 0; }
|
|
165
|
+
.braid-console__count { color: var(--bc-muted); font-variant-numeric: tabular-nums; font-size: 13px; }
|
|
166
|
+
.braid-console__spacer { margin-left: auto; }
|
|
167
|
+
|
|
168
|
+
.braid-console__search {
|
|
169
|
+
font: inherit; color: inherit; background: var(--bc-bg);
|
|
170
|
+
border: 1px solid var(--bc-border-strong); border-radius: var(--bc-radius);
|
|
171
|
+
padding: 6px 10px; min-width: 15rem;
|
|
172
|
+
}
|
|
173
|
+
.braid-console__search::placeholder { color: var(--bc-subtle); }
|
|
174
|
+
.braid-console__search:hover { border-color: var(--bc-muted); }
|
|
175
|
+
/* Fluent's field focus: the border thickens on the accent edge rather than a ring floating outside. */
|
|
176
|
+
.braid-console__search:focus-visible { outline: none; border-color: var(--bc-accent); box-shadow: inset 0 -2px 0 0 var(--bc-accent); }
|
|
177
|
+
|
|
178
|
+
.braid-console__notice {
|
|
179
|
+
display: flex; gap: 8px; align-items: flex-start;
|
|
180
|
+
background: var(--bc-warn-bg); color: var(--bc-warn-text);
|
|
181
|
+
border: 1px solid var(--bc-warn-border); border-left: 3px solid var(--bc-warn-text);
|
|
182
|
+
border-radius: var(--bc-radius); padding: 9px 12px; margin-bottom: var(--bc-gap); font-size: 13px;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* --- table -------------------------------------------------------------- */
|
|
186
|
+
|
|
187
|
+
.braid-console__table { width: 100%; border-collapse: collapse; }
|
|
188
|
+
.braid-console__table th {
|
|
189
|
+
text-align: left; font-size: 12px; font-weight: 600; color: var(--bc-muted);
|
|
190
|
+
padding: 8px 12px; border-bottom: 1px solid var(--bc-border-strong);
|
|
191
|
+
position: sticky; top: 0; background: var(--bc-bg); z-index: 1;
|
|
192
|
+
}
|
|
193
|
+
.braid-console__table td { padding: 10px 12px; border-bottom: 1px solid var(--bc-border); vertical-align: top; }
|
|
194
|
+
.braid-console__table tbody tr:hover { background: var(--bc-surface); }
|
|
195
|
+
/* A 2px accent bar on the hovered row, the way a Fluent DetailsList marks the active one. */
|
|
196
|
+
.braid-console__table tbody tr { box-shadow: inset 2px 0 0 0 transparent; }
|
|
197
|
+
.braid-console__table tbody tr:hover { box-shadow: inset 2px 0 0 0 var(--bc-accent); }
|
|
198
|
+
|
|
199
|
+
.braid-console__id { font-weight: 600; }
|
|
200
|
+
.braid-console__desc { color: var(--bc-muted); margin-top: 2px; font-size: 13px; }
|
|
201
|
+
.braid-console__mono { font-family: var(--bc-mono); font-size: 12.5px; color: var(--bc-muted); }
|
|
202
|
+
.braid-console__patterns { display: flex; flex-direction: column; gap: 2px; }
|
|
203
|
+
|
|
204
|
+
.braid-console__tag {
|
|
205
|
+
display: inline-block; font-size: 11px; padding: 1px 7px; margin: 4px 4px 0 0;
|
|
206
|
+
background: var(--bc-surface-alt); border: 1px solid var(--bc-border); border-radius: 3px; color: var(--bc-muted);
|
|
207
|
+
}
|
|
208
|
+
.braid-console__badge {
|
|
209
|
+
display: inline-block; font-size: 11.5px; padding: 1px 7px; font-weight: 600;
|
|
210
|
+
border-radius: 3px; border: 1px solid var(--bc-border);
|
|
211
|
+
}
|
|
212
|
+
.braid-console__badge--loadable { color: var(--bc-ok); border-color: transparent; background: var(--bc-ok-soft); }
|
|
213
|
+
.braid-console__badge--gated { color: var(--bc-muted); background: var(--bc-surface-alt); border-color: transparent; }
|
|
214
|
+
|
|
215
|
+
.braid-console__empty { padding: 40px 10px; text-align: center; color: var(--bc-muted); }
|
|
216
|
+
.braid-console__error {
|
|
217
|
+
border: 1px solid var(--bc-err); border-left: 3px solid var(--bc-err);
|
|
218
|
+
background: var(--bc-err-soft); color: var(--bc-text);
|
|
219
|
+
border-radius: var(--bc-radius); padding: 14px 16px;
|
|
220
|
+
}
|
|
221
|
+
.braid-console__error h3 { margin: 0 0 4px; font-size: 14px; color: var(--bc-err); }
|
|
222
|
+
.braid-console__retry {
|
|
223
|
+
font: inherit; color: inherit; background: var(--bc-bg); cursor: pointer; margin-top: 10px;
|
|
224
|
+
border: 1px solid var(--bc-border-strong); border-radius: var(--bc-radius); padding: 5px 12px;
|
|
225
|
+
}
|
|
226
|
+
.braid-console__retry:hover { background: var(--bc-surface-alt); }
|
|
227
|
+
|
|
228
|
+
/* --- topology graph ----------------------------------------------------- */
|
|
229
|
+
|
|
230
|
+
.braid-console__graphwrap { display: flex; gap: var(--bc-gap); align-items: flex-start; }
|
|
231
|
+
.braid-console__graphmain { flex: 1 1 auto; min-width: 0; overflow-x: auto; }
|
|
232
|
+
|
|
233
|
+
.braid-console__collabels {
|
|
234
|
+
display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
|
|
235
|
+
font-size: 11px; font-weight: 600; color: var(--bc-subtle);
|
|
236
|
+
text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 6px;
|
|
237
|
+
}
|
|
238
|
+
.braid-console__collabels span:nth-child(2) { text-align: center; }
|
|
239
|
+
.braid-console__collabels span:nth-child(3) { text-align: right; }
|
|
240
|
+
|
|
241
|
+
.braid-console__graph { width: 100%; min-width: 480px; display: block; overflow: visible; }
|
|
242
|
+
|
|
243
|
+
.braid-console__edge { fill: none; stroke: var(--bc-border-strong); stroke-width: 1.25; transition: stroke 0.12s, opacity 0.12s; }
|
|
244
|
+
.braid-console__edge--served-by { stroke-dasharray: 3 3; }
|
|
245
|
+
.braid-console__edge.is-lit { stroke: var(--bc-accent); stroke-width: 2; }
|
|
246
|
+
.braid-console__edge.is-dim { opacity: 0.2; }
|
|
247
|
+
|
|
248
|
+
.braid-console__node { cursor: pointer; transition: opacity 0.12s; }
|
|
249
|
+
.braid-console__node.is-dim { opacity: 0.3; }
|
|
250
|
+
.braid-console__nodebox { fill: var(--bc-bg); stroke: var(--bc-border-strong); stroke-width: 1; }
|
|
251
|
+
.braid-console__node:hover .braid-console__nodebox { stroke: var(--bc-accent); }
|
|
252
|
+
.braid-console__node:focus-visible { outline: none; }
|
|
253
|
+
.braid-console__node:focus-visible .braid-console__nodebox { stroke: var(--bc-accent); stroke-width: 2; }
|
|
254
|
+
.braid-console__node.is-selected .braid-console__nodebox { stroke: var(--bc-accent); stroke-width: 2; fill: var(--bc-accent-soft); }
|
|
255
|
+
|
|
256
|
+
.braid-console__node--fragment .braid-console__nodebox { fill: var(--bc-surface-alt); }
|
|
257
|
+
/* A shared route is the one state worth colouring: more than one fragment lands on this page. */
|
|
258
|
+
.braid-console__node--route.is-shared .braid-console__nodebox { stroke: var(--bc-warn-border); border-left: 2px solid var(--bc-warn-text); }
|
|
259
|
+
.braid-console__node--route.is-shared .braid-console__nodecount { fill: var(--bc-warn-text); font-weight: 700; }
|
|
260
|
+
|
|
261
|
+
.braid-console__nodelabel { font-family: var(--bc-mono); font-size: 11.5px; fill: var(--bc-text); pointer-events: none; }
|
|
262
|
+
.braid-console__nodecount { font-size: 11px; fill: var(--bc-subtle); pointer-events: none; font-variant-numeric: tabular-nums; }
|
|
263
|
+
.braid-console__nodegate { fill: var(--bc-subtle); pointer-events: none; }
|
|
264
|
+
|
|
265
|
+
.braid-console__graphnote {
|
|
266
|
+
font-size: 12.5px; color: var(--bc-muted); margin: var(--bc-gap) 0 0;
|
|
267
|
+
padding: 8px 10px; background: var(--bc-surface); border-radius: var(--bc-radius);
|
|
268
|
+
border-left: 2px solid var(--bc-border-strong);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* --- detail panel ------------------------------------------------------- */
|
|
272
|
+
|
|
273
|
+
.braid-console__detail {
|
|
274
|
+
flex: 0 0 260px; border: 1px solid var(--bc-border); border-radius: var(--bc-radius);
|
|
275
|
+
background: var(--bc-surface); padding: 12px; font-size: 13px;
|
|
276
|
+
}
|
|
277
|
+
.braid-console__detail--empty { color: var(--bc-muted); }
|
|
278
|
+
.braid-console__detailhint { font-size: 12px; color: var(--bc-subtle); margin: 8px 0 0; line-height: 1.45; }
|
|
279
|
+
.braid-console__detailhead { display: flex; align-items: center; gap: 8px; }
|
|
280
|
+
.braid-console__detailclose {
|
|
281
|
+
margin-left: auto; font: inherit; background: transparent; border: 0; color: var(--bc-muted);
|
|
282
|
+
cursor: pointer; border-radius: 3px; padding: 2px 6px; line-height: 1;
|
|
283
|
+
}
|
|
284
|
+
.braid-console__detailclose:hover { background: var(--bc-surface-alt); color: var(--bc-text); }
|
|
285
|
+
.braid-console__detailtitle { font-size: 15px; margin: 6px 0 4px; font-family: var(--bc-mono); word-break: break-all; }
|
|
286
|
+
.braid-console__detaildesc { color: var(--bc-muted); margin: 0 0 6px; line-height: 1.45; }
|
|
287
|
+
.braid-console__detailwarn { color: var(--bc-warn-text); margin: 8px 0 0; font-size: 12.5px; line-height: 1.45; }
|
|
288
|
+
|
|
289
|
+
.braid-console__kind {
|
|
290
|
+
font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600;
|
|
291
|
+
border-radius: 3px; padding: 2px 6px; background: var(--bc-surface-alt); color: var(--bc-muted);
|
|
292
|
+
}
|
|
293
|
+
.braid-console__kind--route { color: var(--bc-accent); background: var(--bc-accent-soft); }
|
|
294
|
+
|
|
295
|
+
.braid-console__props { display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; margin: 10px 0 0; }
|
|
296
|
+
.braid-console__props dt { color: var(--bc-subtle); font-size: 12px; }
|
|
297
|
+
.braid-console__props dd { margin: 0; }
|
|
298
|
+
|
|
299
|
+
.braid-console__detailsection { margin-top: 12px; border-top: 1px solid var(--bc-border); padding-top: 10px; }
|
|
300
|
+
.braid-console__detailsection h4 { margin: 0 0 6px; font-size: 12px; color: var(--bc-subtle); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
301
|
+
.braid-console__peerlist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
|
|
302
|
+
|
|
303
|
+
/* --- integration dialog -------------------------------------------------- */
|
|
304
|
+
|
|
305
|
+
.braid-console__scrim {
|
|
306
|
+
position: fixed; inset: 0; z-index: 40; display: flex; align-items: center; justify-content: center;
|
|
307
|
+
background: rgba(0,0,0,0.4); padding: 24px;
|
|
308
|
+
}
|
|
309
|
+
.braid-console__dialog {
|
|
310
|
+
background: var(--bc-bg); border: 1px solid var(--bc-border); border-radius: 6px;
|
|
311
|
+
box-shadow: 0 6.4px 14.4px rgba(0,0,0,0.13), 0 1.2px 3.6px rgba(0,0,0,0.11);
|
|
312
|
+
width: min(720px, 100%); max-height: 100%; overflow-y: auto; padding: 16px;
|
|
313
|
+
}
|
|
314
|
+
.braid-console__dialoghead { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
|
315
|
+
.braid-console__dialogtitle { margin: 0; font-size: 16px; font-weight: 600; }
|
|
316
|
+
.braid-console__dialogtitle code { font-family: var(--bc-mono); font-size: 15px; }
|
|
317
|
+
.braid-console__dialoghead .braid-console__detailclose { margin-left: auto; }
|
|
318
|
+
|
|
319
|
+
.braid-console__code {
|
|
320
|
+
margin: 10px 0 0; padding: 12px; overflow-x: auto;
|
|
321
|
+
background: var(--bc-surface-alt); border: 1px solid var(--bc-border); border-radius: var(--bc-radius);
|
|
322
|
+
font-family: var(--bc-mono); font-size: 12.5px; line-height: 1.55; white-space: pre;
|
|
323
|
+
}
|
|
324
|
+
.braid-console__code:focus-visible { outline: 2px solid var(--bc-accent); outline-offset: 1px; }
|
|
325
|
+
|
|
326
|
+
.braid-console__dialogfoot { display: flex; align-items: center; gap: 12px; margin-top: 12px; flex-wrap: wrap; }
|
|
327
|
+
.braid-console__dialogfoot .braid-console__hint { font-size: 12px; color: var(--bc-subtle); }
|
|
328
|
+
.braid-console__dialogfoot code { font-family: var(--bc-mono); }
|
|
329
|
+
.braid-console__dialogfoot kbd {
|
|
330
|
+
font-family: var(--bc-mono); font-size: 11px; border: 1px solid var(--bc-border-strong);
|
|
331
|
+
border-bottom-width: 2px; border-radius: 3px; padding: 0 4px; background: var(--bc-bg);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* --- editor ------------------------------------------------------------- */
|
|
335
|
+
|
|
336
|
+
.braid-console__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
337
|
+
.braid-console__card { border: 1px solid var(--bc-border); border-radius: var(--bc-radius); background: var(--bc-bg); }
|
|
338
|
+
.braid-console__card:hover { border-color: var(--bc-border-strong); }
|
|
339
|
+
.braid-console__cardhead { display: flex; align-items: center; gap: var(--bc-gap); padding: 10px 12px; flex-wrap: wrap; }
|
|
340
|
+
|
|
341
|
+
.braid-console__card.is-open { border-color: var(--bc-accent); box-shadow: var(--bc-shadow); }
|
|
342
|
+
|
|
343
|
+
.braid-console__disclose {
|
|
344
|
+
font: inherit; color: inherit; background: transparent; cursor: pointer;
|
|
345
|
+
display: inline-flex; align-items: center; gap: 8px;
|
|
346
|
+
border: 1px solid transparent; border-radius: var(--bc-radius); padding: 3px 8px 3px 4px; margin-left: -4px;
|
|
347
|
+
}
|
|
348
|
+
/* The row is a control, so it says so on hover rather than only when the pointer finds the glyph. */
|
|
349
|
+
.braid-console__disclose:hover { background: var(--bc-surface-alt); border-color: var(--bc-border); }
|
|
350
|
+
.braid-console__disclose:focus-visible { outline: 2px solid var(--bc-accent); outline-offset: 2px; }
|
|
351
|
+
|
|
352
|
+
.braid-console__caret {
|
|
353
|
+
display: inline-block; color: var(--bc-muted); font-size: 11px;
|
|
354
|
+
transition: transform 0.12s ease; transform-origin: 50% 50%;
|
|
355
|
+
}
|
|
356
|
+
.braid-console__card.is-open .braid-console__caret { transform: rotate(90deg); }
|
|
357
|
+
|
|
358
|
+
.braid-console__cardmeta { font-size: 12px; color: var(--bc-subtle); }
|
|
359
|
+
|
|
360
|
+
.braid-console__ghost, .braid-console__primary {
|
|
361
|
+
font: inherit; font-size: 13px; cursor: pointer; border-radius: var(--bc-radius); padding: 5px 12px;
|
|
362
|
+
border: 1px solid var(--bc-border-strong); background: var(--bc-bg); color: inherit;
|
|
363
|
+
}
|
|
364
|
+
.braid-console__ghost:hover { background: var(--bc-surface-alt); }
|
|
365
|
+
.braid-console__primary { background: var(--bc-accent); border-color: var(--bc-accent); color: #fff; font-weight: 600; }
|
|
366
|
+
.braid-console__primary:hover { background: var(--bc-accent-hover); border-color: var(--bc-accent-hover); }
|
|
367
|
+
.braid-console__ghost:disabled, .braid-console__primary:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
368
|
+
.braid-console__ghost:focus-visible, .braid-console__primary:focus-visible { outline: 2px solid var(--bc-accent); outline-offset: 2px; }
|
|
369
|
+
|
|
370
|
+
.braid-console__badge--error { color: var(--bc-err); background: var(--bc-err-soft); border-color: transparent; }
|
|
371
|
+
|
|
372
|
+
.braid-console__findings { list-style: none; margin: 0; padding: 0 12px 10px; display: flex; flex-direction: column; gap: 5px; }
|
|
373
|
+
.braid-console__finding { font-size: 12.5px; padding: 4px 0 4px 10px; border-left: 2px solid var(--bc-border); line-height: 1.45; }
|
|
374
|
+
.braid-console__finding--error { border-left-color: var(--bc-err); color: var(--bc-err); }
|
|
375
|
+
.braid-console__finding--warning { border-left-color: var(--bc-warn-border); color: var(--bc-warn-text); }
|
|
376
|
+
|
|
377
|
+
.braid-console__fields {
|
|
378
|
+
display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
379
|
+
gap: var(--bc-gap); padding: 12px; border-top: 1px solid var(--bc-border); background: var(--bc-surface);
|
|
380
|
+
}
|
|
381
|
+
.braid-console__field { display: flex; flex-direction: column; gap: 4px; }
|
|
382
|
+
.braid-console__fieldlabel { font-size: 12px; font-weight: 600; color: var(--bc-muted); display: flex; align-items: center; gap: 6px; }
|
|
383
|
+
.braid-console__field input, .braid-console__field select {
|
|
384
|
+
font: inherit; font-size: 13px; color: inherit; background: var(--bc-bg);
|
|
385
|
+
border: 1px solid var(--bc-border-strong); border-radius: var(--bc-radius); padding: 6px 9px; width: 100%;
|
|
386
|
+
}
|
|
387
|
+
.braid-console__field input:focus-visible, .braid-console__field select:focus-visible {
|
|
388
|
+
outline: none; border-color: var(--bc-accent); box-shadow: inset 0 -2px 0 0 var(--bc-accent);
|
|
389
|
+
}
|
|
390
|
+
.braid-console__field input:disabled { opacity: 0.6; }
|
|
391
|
+
|
|
392
|
+
.braid-console__owner {
|
|
393
|
+
font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.04em;
|
|
394
|
+
border: 1px solid var(--bc-border); border-radius: 3px; padding: 1px 5px; color: var(--bc-muted);
|
|
395
|
+
}
|
|
396
|
+
.braid-console__owner--gateway { color: var(--bc-warn-text); border-color: var(--bc-warn-border); background: var(--bc-warn-bg); }
|
|
397
|
+
|
|
398
|
+
.braid-console__bar {
|
|
399
|
+
position: sticky; bottom: 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
|
400
|
+
margin-top: var(--bc-gap); padding: 12px; background: var(--bc-surface);
|
|
401
|
+
border: 1px solid var(--bc-border); border-radius: var(--bc-radius); box-shadow: var(--bc-shadow);
|
|
402
|
+
}
|
|
403
|
+
.braid-console__barstate { color: var(--bc-muted); font-size: 13px; }
|
|
404
|
+
.braid-console__barstate--error { color: var(--bc-err); font-weight: 600; }
|
|
405
|
+
|
|
406
|
+
.braid-console__notice--ok { background: var(--bc-ok-soft); color: var(--bc-ok); border-color: var(--bc-ok); border-left-color: var(--bc-ok); }
|
|
407
|
+
|
|
408
|
+
.braid-console__diff {
|
|
409
|
+
font-family: var(--bc-mono); font-size: 12.5px; padding: 12px; margin-top: var(--bc-gap);
|
|
410
|
+
border: 1px solid var(--bc-border); border-radius: var(--bc-radius); background: var(--bc-surface);
|
|
411
|
+
overflow-x: auto;
|
|
412
|
+
}
|
|
413
|
+
.braid-console__diffline { padding: 2px 0; }
|
|
414
|
+
.braid-console__diffline--add { color: var(--bc-ok); }
|
|
415
|
+
.braid-console__diffline--remove { color: var(--bc-err); }
|
|
416
|
+
.braid-console__diffield { padding: 2px 0 2px 16px; color: var(--bc-muted); }
|
|
417
|
+
|
|
418
|
+
/* --- access matrix ------------------------------------------------------ */
|
|
419
|
+
|
|
420
|
+
.braid-console__access {
|
|
421
|
+
margin-top: var(--bc-gap); padding: 12px;
|
|
422
|
+
border: 1px solid var(--bc-border); border-radius: var(--bc-radius); background: var(--bc-surface);
|
|
423
|
+
}
|
|
424
|
+
.braid-console__accesshead { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
|
|
425
|
+
.braid-console__matrix { margin-top: 8px; background: var(--bc-bg); }
|
|
426
|
+
.braid-console__matrix th, .braid-console__matrix td { padding: 6px 10px; }
|
|
427
|
+
.braid-console__matrix th { white-space: nowrap; position: static; }
|
|
428
|
+
.braid-console__remove { border: 0; padding: 0 4px; margin-left: 4px; font-size: 13px; line-height: 1; }
|
|
429
|
+
|
|
430
|
+
.braid-console__mark { font-family: var(--bc-mono); }
|
|
431
|
+
.braid-console__mark--allowed { color: var(--bc-ok); }
|
|
432
|
+
.braid-console__mark--denied { color: var(--bc-muted); }
|
|
433
|
+
.braid-console__mark--absent { color: var(--bc-muted); opacity: 0.6; }
|
|
434
|
+
|
|
435
|
+
/* Visually hidden, still read aloud — the symbols carry meaning that colour alone would not. */
|
|
436
|
+
.braid-console__sronly {
|
|
437
|
+
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
438
|
+
overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* Container query, not a media query: the library's width is its host's business, not the
|
|
442
|
+
viewport's. A console in a 400px sidebar should stack even on a wide screen. */
|
|
443
|
+
@container (max-width: 860px) {
|
|
444
|
+
.braid-console__graphwrap { flex-direction: column; }
|
|
445
|
+
.braid-console__detail { flex: 1 1 auto; width: 100%; }
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
@container (max-width: 640px) {
|
|
449
|
+
.braid-console__table thead { display: none; }
|
|
450
|
+
.braid-console__table tr { display: block; padding: 8px 0; border-bottom: 1px solid var(--bc-border); }
|
|
451
|
+
.braid-console__table td { display: block; border: 0; padding: 2px 12px; }
|
|
452
|
+
.braid-console__stats { flex-direction: column; }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* Motion is decoration here; the information is in position and colour, which do not move. */
|
|
456
|
+
@media (prefers-reduced-motion: reduce) {
|
|
457
|
+
.braid-console__edge, .braid-console__node, .braid-console__caret { transition: none; }
|
|
458
|
+
}
|
|
459
|
+
`;
|
|
460
|
+
const STYLE_ELEMENT_ID = 'braid-console-styles';
|
|
461
|
+
/** Injects the stylesheet once per document. Safe to call from every mounted instance. */
|
|
462
|
+
export function ensureStyles(doc = document) {
|
|
463
|
+
if (doc.getElementById(STYLE_ELEMENT_ID))
|
|
464
|
+
return;
|
|
465
|
+
const style = doc.createElement('style');
|
|
466
|
+
style.id = STYLE_ELEMENT_ID;
|
|
467
|
+
style.textContent = CONSOLE_STYLES;
|
|
468
|
+
doc.head.append(style);
|
|
469
|
+
}
|
|
470
|
+
//# sourceMappingURL=styles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.js","sourceRoot":"","sources":["../../../../../libs/braid-console/src/lib/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkb7B,CAAC;AAEF,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AAEhD,0FAA0F;AAC1F,MAAM,UAAU,YAAY,CAAC,MAAgB,QAAQ;IACnD,IAAI,GAAG,CAAC,cAAc,CAAC,gBAAgB,CAAC;QAAE,OAAO;IAEjD,MAAM,KAAK,GAAG,GAAG,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IACzC,KAAK,CAAC,EAAE,GAAG,gBAAgB,CAAC;IAC5B,KAAK,CAAC,WAAW,GAAG,cAAc,CAAC;IACnC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACzB,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { DiscoveryEntry } from '@braidlabs/gateway';
|
|
2
|
+
import { type Topology, type TopologyNode } from './topology.js';
|
|
3
|
+
export interface TopologyGraphProps {
|
|
4
|
+
entries: readonly DiscoveryEntry[];
|
|
5
|
+
/** Notified when the selection changes, so a host can drive its own detail pane. */
|
|
6
|
+
onSelect?: (entry: DiscoveryEntry | null) => void;
|
|
7
|
+
/** Asked to show the embed snippet for a fragment. */
|
|
8
|
+
onEmbed?: (entry: DiscoveryEntry) => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The composition topology, drawn as three columns: routes → fragments → origins.
|
|
12
|
+
*
|
|
13
|
+
* **Laid out, not simulated.** A force-directed graph looks impressive and answers no question: it
|
|
14
|
+
* settles somewhere different every load, so an operator cannot learn its shape, point at it in a
|
|
15
|
+
* review, or compare two screenshots. Columns are stable, and the only thing a reader has to
|
|
16
|
+
* understand is that edges go left to right. It also means no layout library, which is what keeps
|
|
17
|
+
* this inside the console's size budget.
|
|
18
|
+
*
|
|
19
|
+
* Rendered as inline SVG with the console's own custom properties, so it themes with everything
|
|
20
|
+
* else and needs no canvas, no measurement pass, and no resize observer.
|
|
21
|
+
*/
|
|
22
|
+
export declare function TopologyGraph({ entries, onSelect, onEmbed }: TopologyGraphProps): import("react").JSX.Element;
|
|
23
|
+
interface PlacedNode {
|
|
24
|
+
node: TopologyNode;
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
}
|
|
28
|
+
interface PlacedEdge {
|
|
29
|
+
key: string;
|
|
30
|
+
d: string;
|
|
31
|
+
kind: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Places nodes in columns and routes edges as horizontal cubic curves.
|
|
35
|
+
*
|
|
36
|
+
* Columns are centred against the tallest one so short columns do not sit at the top with a gulf
|
|
37
|
+
* beneath them. Edges leave the right face of one node and enter the left face of the next, with
|
|
38
|
+
* control points at the midpoint — the standard flow-diagram curve, which reads as a connection
|
|
39
|
+
* without implying direction the way an arrowhead would.
|
|
40
|
+
*/
|
|
41
|
+
export declare function layoutTopology(topology: Topology): {
|
|
42
|
+
nodes: PlacedNode[];
|
|
43
|
+
edges: PlacedEdge[];
|
|
44
|
+
width: number;
|
|
45
|
+
height: number;
|
|
46
|
+
};
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
3
|
+
import { buildTopology, coTenants, edgeKey, neighborhood, } from './topology.js';
|
|
4
|
+
/**
|
|
5
|
+
* The composition topology, drawn as three columns: routes → fragments → origins.
|
|
6
|
+
*
|
|
7
|
+
* **Laid out, not simulated.** A force-directed graph looks impressive and answers no question: it
|
|
8
|
+
* settles somewhere different every load, so an operator cannot learn its shape, point at it in a
|
|
9
|
+
* review, or compare two screenshots. Columns are stable, and the only thing a reader has to
|
|
10
|
+
* understand is that edges go left to right. It also means no layout library, which is what keeps
|
|
11
|
+
* this inside the console's size budget.
|
|
12
|
+
*
|
|
13
|
+
* Rendered as inline SVG with the console's own custom properties, so it themes with everything
|
|
14
|
+
* else and needs no canvas, no measurement pass, and no resize observer.
|
|
15
|
+
*/
|
|
16
|
+
export function TopologyGraph({ entries, onSelect, onEmbed }) {
|
|
17
|
+
const topology = useMemo(() => buildTopology(entries), [entries]);
|
|
18
|
+
const [selected, setSelected] = useState(null);
|
|
19
|
+
const [hovered, setHovered] = useState(null);
|
|
20
|
+
const active = hovered ?? selected;
|
|
21
|
+
const lit = useMemo(() => neighborhood(topology, active), [topology, active]);
|
|
22
|
+
const layout = useMemo(() => layoutTopology(topology), [topology]);
|
|
23
|
+
function select(node) {
|
|
24
|
+
const next = node && node.id !== selected ? node.id : null;
|
|
25
|
+
setSelected(next);
|
|
26
|
+
const entry = next && node?.kind === 'fragment' ? (node.entry ?? null) : null;
|
|
27
|
+
onSelect?.(entry);
|
|
28
|
+
}
|
|
29
|
+
if (topology.fragments.length === 0) {
|
|
30
|
+
return _jsx("p", { className: "braid-console__empty", children: "Nothing is registered, so there is no topology to draw." });
|
|
31
|
+
}
|
|
32
|
+
const selectedNode = topology.nodes.find((node) => node.id === selected) ?? null;
|
|
33
|
+
return (_jsxs("div", { className: "braid-console__graphwrap", children: [_jsxs("div", { className: "braid-console__graphmain", children: [_jsxs("div", { className: "braid-console__collabels", "aria-hidden": "true", children: [_jsx("span", { children: "Routes" }), _jsx("span", { children: "Fragments" }), !topology.originsUnknown && _jsx("span", { children: "Origins" })] }), _jsxs("svg", { className: "braid-console__graph", viewBox: `0 0 ${layout.width} ${layout.height}`, role: "group", "aria-label": "Composition topology: routes, the fragments they compose, and the origins serving them", style: { height: `${layout.height}px` }, children: [_jsx("g", { className: "braid-console__edges", children: layout.edges.map((edge) => (_jsx("path", { d: edge.d, className: `braid-console__edge braid-console__edge--${edge.kind}${active ? (lit.edges.has(edge.key) ? ' is-lit' : ' is-dim') : ''}` }, edge.key))) }), layout.nodes.map((placed) => {
|
|
34
|
+
const dimmed = active && !lit.nodes.has(placed.node.id);
|
|
35
|
+
return (_jsxs("g", { className: `braid-console__node braid-console__node--${placed.node.kind}${dimmed ? ' is-dim' : ''}${placed.node.id === selected ? ' is-selected' : ''}${placed.node.shared ? ' is-shared' : ''}`, transform: `translate(${placed.x}, ${placed.y})`, tabIndex: 0, role: "button", "aria-pressed": placed.node.id === selected, "aria-label": describe(placed.node, topology), onMouseEnter: () => setHovered(placed.node.id), onMouseLeave: () => setHovered(null), onFocus: () => setHovered(placed.node.id), onBlur: () => setHovered(null), onClick: () => select(placed.node), onKeyDown: (event) => {
|
|
36
|
+
if (event.key !== 'Enter' && event.key !== ' ')
|
|
37
|
+
return;
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
select(placed.node);
|
|
40
|
+
}, children: [_jsx("rect", { className: "braid-console__nodebox", width: NODE_W, height: NODE_H, rx: "4" }), _jsx("text", { className: "braid-console__nodelabel", x: "10", y: NODE_H / 2 + 4, children: truncate(placed.node.label) }), placed.node.kind !== 'fragment' && (_jsx("text", { className: "braid-console__nodecount", x: NODE_W - 10, y: NODE_H / 2 + 4, textAnchor: "end", children: placed.node.degree })), placed.node.kind === 'fragment' && placed.node.entry?.loadable === false && (_jsx("circle", { className: "braid-console__nodegate", cx: NODE_W - 12, cy: NODE_H / 2, r: "3.5" }))] }, placed.node.id));
|
|
41
|
+
})] }), topology.unrouted.length > 0 && (_jsxs("p", { className: "braid-console__graphnote", children: [_jsx("strong", { children: topology.unrouted.length }), " unbound", ' ', topology.unrouted.length === 1 ? 'fragment' : 'fragments', " (", topology.unrouted.map((n) => n.label).join(', '), ") \u2014 widgets with no pierce pattern, placed by whichever host asks for them. No route claims them, so nothing here can honestly draw one."] })), topology.originsUnknown && (_jsx("p", { className: "braid-console__graphnote", children: "This listing does not publish endpoints, so origins are not shown. That is a production gateway behaving correctly, not missing data." }))] }), _jsx(TopologyDetail, { node: selectedNode, topology: topology, onClear: () => select(null), onEmbed: onEmbed })] }));
|
|
42
|
+
}
|
|
43
|
+
function TopologyDetail({ node, topology, onClear, onEmbed, }) {
|
|
44
|
+
if (!node) {
|
|
45
|
+
return (_jsxs("aside", { className: "braid-console__detail braid-console__detail--empty", children: [_jsx("p", { children: "Select a node to see what it touches." }), _jsx("p", { className: "braid-console__detailhint", children: "A route lit with more than one fragment is a composed page: those fragments share a document and a blast radius." })] }));
|
|
46
|
+
}
|
|
47
|
+
const peers = node.kind === 'fragment' ? coTenants(topology, node.id) : [];
|
|
48
|
+
const connected = topology.edges
|
|
49
|
+
.filter((edge) => edge.from === node.id || edge.to === node.id)
|
|
50
|
+
.map((edge) => (edge.from === node.id ? edge.to : edge.from));
|
|
51
|
+
return (_jsxs("aside", { className: "braid-console__detail", children: [_jsxs("div", { className: "braid-console__detailhead", children: [_jsx("span", { className: `braid-console__kind braid-console__kind--${node.kind}`, children: node.kind }), _jsx("button", { type: "button", className: "braid-console__detailclose", onClick: onClear, "aria-label": "Clear selection", children: "\u2715" })] }), _jsx("h3", { className: "braid-console__detailtitle", children: node.label }), node.entry && (_jsxs(_Fragment, { children: [node.entry.title !== node.entry.id && _jsx("p", { className: "braid-console__detaildesc", children: node.entry.title }), node.entry.description && _jsx("p", { className: "braid-console__detaildesc", children: node.entry.description }), _jsxs("dl", { className: "braid-console__props", children: [_jsx("dt", { children: "Adapter" }), _jsx("dd", { className: "braid-console__mono", children: node.entry.adapter }), _jsx("dt", { children: "Mount" }), _jsx("dd", { className: "braid-console__mono", children: node.entry.mount }), node.entry.contractVersion && (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Contract" }), _jsxs("dd", { className: "braid-console__mono", children: ["v", node.entry.contractVersion] })] })), _jsx("dt", { children: "Access" }), _jsx("dd", { children: _jsx("span", { className: `braid-console__badge braid-console__badge--${node.entry.loadable ? 'loadable' : 'gated'}`, children: node.entry.loadable ? 'loadable' : 'gated' }) })] })] })), node.kind === 'fragment' && node.entry && onEmbed && (_jsx("div", { className: "braid-console__detailsection", children: _jsx("button", { type: "button", className: "braid-console__ghost", onClick: () => onEmbed(node.entry), children: "Embed this fragment" }) })), node.kind === 'fragment' && (_jsxs("div", { className: "braid-console__detailsection", children: [_jsx("h4", { children: "Shares a page with" }), peers.length === 0 ? (_jsx("p", { className: "braid-console__detaildesc", children: "Nothing. This fragment is alone on every route it pierces." })) : (_jsx("ul", { className: "braid-console__peerlist", children: peers.map((peer) => (_jsx("li", { className: "braid-console__mono", children: label(topology, peer) }, peer))) }))] })), node.kind !== 'fragment' && (_jsxs("div", { className: "braid-console__detailsection", children: [_jsx("h4", { children: node.kind === 'route' ? 'Composes' : 'Serves' }), _jsx("ul", { className: "braid-console__peerlist", children: connected.map((id) => (_jsx("li", { className: "braid-console__mono", children: label(topology, id) }, id))) }), node.kind === 'route' && node.shared && (_jsx("p", { className: "braid-console__detailwarn", children: "More than one fragment pierces this pattern. Intentional when the page composes both; a mistake when one pattern is wider than intended." }))] }))] }));
|
|
52
|
+
}
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Layout
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
const NODE_W = 150;
|
|
57
|
+
const NODE_H = 26;
|
|
58
|
+
const ROW_GAP = 10;
|
|
59
|
+
const COL_GAP = 96;
|
|
60
|
+
const PAD = 8;
|
|
61
|
+
/**
|
|
62
|
+
* Places nodes in columns and routes edges as horizontal cubic curves.
|
|
63
|
+
*
|
|
64
|
+
* Columns are centred against the tallest one so short columns do not sit at the top with a gulf
|
|
65
|
+
* beneath them. Edges leave the right face of one node and enter the left face of the next, with
|
|
66
|
+
* control points at the midpoint — the standard flow-diagram curve, which reads as a connection
|
|
67
|
+
* without implying direction the way an arrowhead would.
|
|
68
|
+
*/
|
|
69
|
+
export function layoutTopology(topology) {
|
|
70
|
+
const columns = topology.originsUnknown
|
|
71
|
+
? [topology.routes, topology.fragments]
|
|
72
|
+
: [topology.routes, topology.fragments, topology.origins];
|
|
73
|
+
const tallest = Math.max(1, ...columns.map((column) => column.length));
|
|
74
|
+
const height = PAD * 2 + tallest * NODE_H + (tallest - 1) * ROW_GAP;
|
|
75
|
+
const width = columns.length * NODE_W + (columns.length - 1) * COL_GAP;
|
|
76
|
+
const placed = [];
|
|
77
|
+
const position = new Map();
|
|
78
|
+
columns.forEach((column, columnIndex) => {
|
|
79
|
+
const span = column.length * NODE_H + Math.max(0, column.length - 1) * ROW_GAP;
|
|
80
|
+
const top = (height - span) / 2;
|
|
81
|
+
const x = columnIndex * (NODE_W + COL_GAP);
|
|
82
|
+
column.forEach((node, rowIndex) => {
|
|
83
|
+
const y = top + rowIndex * (NODE_H + ROW_GAP);
|
|
84
|
+
placed.push({ node, x, y });
|
|
85
|
+
position.set(node.id, { x, y });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
const edges = [];
|
|
89
|
+
for (const edge of topology.edges) {
|
|
90
|
+
const from = position.get(edge.from);
|
|
91
|
+
const to = position.get(edge.to);
|
|
92
|
+
if (!from || !to)
|
|
93
|
+
continue;
|
|
94
|
+
const x1 = from.x + NODE_W;
|
|
95
|
+
const y1 = from.y + NODE_H / 2;
|
|
96
|
+
const x2 = to.x;
|
|
97
|
+
const y2 = to.y + NODE_H / 2;
|
|
98
|
+
const mid = (x1 + x2) / 2;
|
|
99
|
+
edges.push({
|
|
100
|
+
key: edgeKey(edge),
|
|
101
|
+
kind: edge.kind,
|
|
102
|
+
d: `M ${x1} ${y1} C ${mid} ${y1}, ${mid} ${y2}, ${x2} ${y2}`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return { nodes: placed, edges, width, height };
|
|
106
|
+
}
|
|
107
|
+
function describe(node, topology) {
|
|
108
|
+
if (node.kind === 'fragment') {
|
|
109
|
+
const peers = coTenants(topology, node.id).length;
|
|
110
|
+
return `Fragment ${node.label}${peers > 0 ? `, shares a page with ${peers} other` : ''}`;
|
|
111
|
+
}
|
|
112
|
+
if (node.kind === 'route') {
|
|
113
|
+
return `Route ${node.label}, composes ${node.degree} fragment${node.degree === 1 ? '' : 's'}`;
|
|
114
|
+
}
|
|
115
|
+
return `Origin ${node.label}, serves ${node.degree} fragment${node.degree === 1 ? '' : 's'}`;
|
|
116
|
+
}
|
|
117
|
+
function label(topology, nodeId) {
|
|
118
|
+
return topology.nodes.find((node) => node.id === nodeId)?.label ?? nodeId;
|
|
119
|
+
}
|
|
120
|
+
/** Keeps a long pattern from spilling out of its box; the full value is in the detail panel. */
|
|
121
|
+
function truncate(value, max = 20) {
|
|
122
|
+
return value.length <= max ? value : `${value.slice(0, max - 1)}…`;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=topology-graph.js.map
|