beadcyte 0.4.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 (108) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/LICENSE +661 -0
  3. package/README.md +386 -0
  4. package/THIRD_PARTY_NOTICES.md +56 -0
  5. package/bin/beadcyte.mjs +60 -0
  6. package/package.json +77 -0
  7. package/src/changelog-cli.mjs +105 -0
  8. package/src/changelog.mjs +196 -0
  9. package/src/cli.mjs +577 -0
  10. package/src/estimator.mjs +314 -0
  11. package/src/format.mjs +22 -0
  12. package/src/history-walk.mjs +170 -0
  13. package/src/index.mjs +5 -0
  14. package/src/mutate.mjs +193 -0
  15. package/src/projects.mjs +120 -0
  16. package/src/provenance.mjs +75 -0
  17. package/src/review-hours.mjs +117 -0
  18. package/src/roster-path.mjs +24 -0
  19. package/src/scheduler.mjs +424 -0
  20. package/src/serve.mjs +411 -0
  21. package/src/server-state.mjs +105 -0
  22. package/src/ship.mjs +178 -0
  23. package/src/stage-waits.mjs +69 -0
  24. package/src/start.mjs +111 -0
  25. package/src/stop.mjs +66 -0
  26. package/src/velocity.mjs +209 -0
  27. package/src/web/App.vue +691 -0
  28. package/src/web/app.css +54 -0
  29. package/src/web/assets/favicon.svg +12 -0
  30. package/src/web/avatar.ts +53 -0
  31. package/src/web/bead-detail.ts +240 -0
  32. package/src/web/changelog-view.ts +41 -0
  33. package/src/web/components/BeadDrawer.vue +1883 -0
  34. package/src/web/components/BeadSubGraph.vue +326 -0
  35. package/src/web/components/BeadSubGraphOverlay.vue +192 -0
  36. package/src/web/components/BeadTooltip.vue +516 -0
  37. package/src/web/components/BeadcyteMark.vue +64 -0
  38. package/src/web/components/BeadsGantt.vue +2125 -0
  39. package/src/web/components/BeadsGrid.vue +468 -0
  40. package/src/web/components/BeadsIncytes.vue +567 -0
  41. package/src/web/components/BeadsMine.vue +325 -0
  42. package/src/web/components/BeadsTable.vue +335 -0
  43. package/src/web/components/ChangelogOverlay.vue +198 -0
  44. package/src/web/components/ContextMenu.vue +386 -0
  45. package/src/web/components/ControlsPanel.vue +476 -0
  46. package/src/web/components/CostTrend.vue +206 -0
  47. package/src/web/components/FilterPopover.vue +245 -0
  48. package/src/web/components/GroupProgress.vue +274 -0
  49. package/src/web/components/LoadMeter.vue +144 -0
  50. package/src/web/components/MineRow.vue +28 -0
  51. package/src/web/components/OptionsMenu.vue +825 -0
  52. package/src/web/components/PriorityChip.vue +105 -0
  53. package/src/web/components/ScoreStrip.vue +131 -0
  54. package/src/web/components/SearchPalette.vue +210 -0
  55. package/src/web/components/ShipTrend.vue +510 -0
  56. package/src/web/components/ShortcutsOverlay.vue +164 -0
  57. package/src/web/components/Term.vue +177 -0
  58. package/src/web/components/Toast.vue +50 -0
  59. package/src/web/components/TriageMeters.vue +426 -0
  60. package/src/web/components/TypeChip.vue +96 -0
  61. package/src/web/components/Walkthrough.vue +209 -0
  62. package/src/web/components/WhatIfPanel.vue +206 -0
  63. package/src/web/components/WipBullets.vue +191 -0
  64. package/src/web/components/filter-option.ts +9 -0
  65. package/src/web/composables/url-codec.ts +136 -0
  66. package/src/web/composables/useBeadTooltip.ts +148 -0
  67. package/src/web/composables/useKeyboard.ts +97 -0
  68. package/src/web/composables/useLiveRefresh.ts +69 -0
  69. package/src/web/composables/useTheme.ts +125 -0
  70. package/src/web/composables/useUrlState.ts +208 -0
  71. package/src/web/controls-scope.ts +83 -0
  72. package/src/web/cost.ts +251 -0
  73. package/src/web/dep-headings.ts +62 -0
  74. package/src/web/economics.ts +440 -0
  75. package/src/web/env.d.ts +85 -0
  76. package/src/web/frontier.ts +208 -0
  77. package/src/web/gantt-viewport.ts +99 -0
  78. package/src/web/highlights.ts +124 -0
  79. package/src/web/index.html +46 -0
  80. package/src/web/insights.ts +107 -0
  81. package/src/web/keybindings.ts +200 -0
  82. package/src/web/load-meter.ts +72 -0
  83. package/src/web/main.ts +20 -0
  84. package/src/web/markdown.ts +14 -0
  85. package/src/web/mine.ts +137 -0
  86. package/src/web/mutations.ts +21 -0
  87. package/src/web/person.ts +102 -0
  88. package/src/web/projects-text.ts +15 -0
  89. package/src/web/projects.ts +188 -0
  90. package/src/web/refresh.ts +47 -0
  91. package/src/web/search.ts +50 -0
  92. package/src/web/shortcuts.ts +113 -0
  93. package/src/web/status-filter.ts +48 -0
  94. package/src/web/store.ts +1378 -0
  95. package/src/web/style-audit.mjs +346 -0
  96. package/src/web/styles-alt.css +111 -0
  97. package/src/web/styles-ported.css +270 -0
  98. package/src/web/subgraph.ts +362 -0
  99. package/src/web/table.ts +201 -0
  100. package/src/web/theme.ts +88 -0
  101. package/src/web/tokens.css +168 -0
  102. package/src/web/triage.ts +914 -0
  103. package/src/web/view-model.ts +717 -0
  104. package/src/web/walkthrough.ts +133 -0
  105. package/src/web/watchlist.ts +47 -0
  106. package/src/web/whatif.ts +291 -0
  107. package/src/web/window.ts +73 -0
  108. package/src/web/wip.ts +83 -0
@@ -0,0 +1,326 @@
1
+ <script setup lang="ts">
2
+ // BeadSubGraph.vue — mini-DAG for the drawer. Two pictures, one renderer:
3
+ //
4
+ // - "neighbourhood" (default): the focal bead's transitive blockers
5
+ // (leftward, up to N hops) and dependents (rightward), one column per
6
+ // hop, the focal bead in the middle.
7
+ // - "children": every child of an epic, one column per dependency depth
8
+ // within that set, chains stacked as bands, isolated children listed
9
+ // below them (bp-67g.59).
10
+ //
11
+ // The layouts live in ../subgraph.ts so they can be tested without a DOM;
12
+ // this file paints nodes — id, truncated title, status, priority, coloured
13
+ // by status — and cubic-bezier edges between them. Clicking a node switches
14
+ // the drawer to that bead; hovering one floats the same BeadTooltip the Gantt
15
+ // uses (bp-67g.42), so an id on a node can be read as a title, status and
16
+ // assignee without leaving the picture.
17
+
18
+ import { ArrowRight } from "@lucide/vue";
19
+ import { computed, ref, watch } from "vue";
20
+ import type { Bead } from "../store";
21
+ import { useBeadTooltip } from "../composables/useBeadTooltip";
22
+ import BeadTooltip from "./BeadTooltip.vue";
23
+ import {
24
+ DEFAULT_MAX_HOPS,
25
+ GEOMETRY,
26
+ layoutChildren,
27
+ layoutNeighbourhood,
28
+ } from "../subgraph";
29
+ import type { GraphEdge, GraphNode } from "../subgraph";
30
+
31
+ const props = defineProps<{
32
+ rootBead: Bead;
33
+ beads: Bead[];
34
+ maxHops?: number;
35
+ /** Which picture to draw; see the header comment. */
36
+ mode?: "neighbourhood" | "children";
37
+ /** Children mode: isolated children per row. See ChildrenLayoutOptions. */
38
+ isolatedColumns?: number;
39
+ /** Children mode: the child to draw as the focal (root-styled) node. */
40
+ focalId?: string;
41
+ }>();
42
+
43
+ const emit = defineEmits<{
44
+ select: [id: string];
45
+ }>();
46
+
47
+ const NODE_W = GEOMETRY.nodeW;
48
+ const NODE_H = GEOMETRY.nodeH;
49
+ // The hit area runs this far past the node box on every side: the marks are
50
+ // small, so the target is bigger than the mark. Half the row gap, so two
51
+ // stacked nodes' hit areas meet without overlapping.
52
+ const HIT_PAD = GEOMETRY.rowGap / 2;
53
+
54
+ // ── node hover (bp-67g.42) ───────────────────────────────────────────────
55
+ // The same hover machine as the Gantt's bars. This host sits in a column
56
+ // that scrolls — the drawer body, or this element's own overflow — so a
57
+ // scroll dismisses the panel rather than leaving it fixed over nothing, and
58
+ // the panel is kept inside this element's horizontal extent.
59
+ const rootEl = ref<HTMLElement | null>(null);
60
+ const {
61
+ props: tipProps,
62
+ enter: tipEnter,
63
+ cancelLeave: tipStay,
64
+ scheduleLeave: tipLeave,
65
+ dismiss: tipDismiss,
66
+ toggleWatch: tipWatch,
67
+ } = useBeadTooltip({
68
+ dismissOnScroll: true,
69
+ bounds: () => {
70
+ const r = rootEl.value?.getBoundingClientRect();
71
+ return r ? { left: r.left, right: r.right } : null;
72
+ },
73
+ });
74
+
75
+ function onNodeEnter(n: GraphNode, e: MouseEvent) {
76
+ // The band is the node box, not the padded hit area, so the panel sits
77
+ // just under the mark.
78
+ const box = (e.currentTarget as Element | null)?.querySelector(".node-box") ?? null;
79
+ tipEnter(n.id, e, box);
80
+ }
81
+
82
+ function onNodeClick(id: string) {
83
+ tipDismiss();
84
+ emit("select", id);
85
+ }
86
+
87
+ // Details on the panel does what clicking the node does: the drawer moves.
88
+ function onDetails(id: string) {
89
+ tipDismiss();
90
+ emit("select", id);
91
+ }
92
+
93
+ const maxHops = computed(() => props.maxHops ?? DEFAULT_MAX_HOPS);
94
+
95
+ const graph = computed(() =>
96
+ props.mode === "children"
97
+ ? layoutChildren(props.rootBead, props.beads, {
98
+ isolatedColumns: props.isolatedColumns ?? 1,
99
+ focalId: props.focalId,
100
+ })
101
+ : layoutNeighbourhood(props.rootBead, props.beads, maxHops.value),
102
+ );
103
+
104
+ // A new picture under the pointer — a node click moved the drawer — has no
105
+ // node to leave, so mouseleave never fires; close the panel explicitly.
106
+ watch(graph, () => tipDismiss());
107
+
108
+ // A neighbourhood always contains its root, so one node means no edges. A
109
+ // children graph with one node is a real, if lonely, child and is drawn.
110
+ const isEmpty = computed(() =>
111
+ props.mode === "children"
112
+ ? graph.value.nodes.length === 0
113
+ : graph.value.nodes.length <= 1,
114
+ );
115
+ const emptyText = computed(() =>
116
+ props.mode === "children"
117
+ ? "No children."
118
+ : `No dependencies within ${maxHops.value} hops.`,
119
+ );
120
+
121
+ function edgePath(e: GraphEdge): string {
122
+ const dx = e.x2 - e.x1;
123
+ const cx1 = e.x1 + Math.max(20, dx * 0.4);
124
+ const cx2 = e.x2 - Math.max(20, dx * 0.4);
125
+ return `M ${e.x1} ${e.y1} C ${cx1} ${e.y1}, ${cx2} ${e.y2}, ${e.x2} ${e.y2}`;
126
+ }
127
+
128
+ /**
129
+ * The curve's last control point shares the endpoint's y, so it always
130
+ * arrives horizontally — the terminal icon needs no rotation here.
131
+ */
132
+ function edgeTerminal(e: GraphEdge) {
133
+ return { x: e.x2, y: e.y2 };
134
+ }
135
+
136
+ function nodeClass(n: GraphNode): string {
137
+ return `status-${n.bead.status} is-${n.role}`;
138
+ }
139
+
140
+ function truncate(s: string, n: number): string {
141
+ if (!s) return "";
142
+ return s.length > n ? s.slice(0, n - 1) + "…" : s;
143
+ }
144
+ </script>
145
+
146
+ <template>
147
+ <div ref="rootEl" class="bead-subgraph">
148
+ <div v-if="isEmpty" class="empty">{{ emptyText }}</div>
149
+ <!-- The canvas is padded by the hit pad on every side: an outer <svg>
150
+ clips to its own bounds, so without it the nodes along the edges
151
+ would lose the part of their hit area that hangs past the graph. -->
152
+ <svg
153
+ v-else
154
+ class="subgraph-svg"
155
+ :viewBox="`${-HIT_PAD} ${-HIT_PAD} ${graph.width + 2 * HIT_PAD} ${graph.height + 2 * HIT_PAD}`"
156
+ :width="graph.width + 2 * HIT_PAD"
157
+ :height="graph.height + 2 * HIT_PAD"
158
+ >
159
+ <g class="edges">
160
+ <template v-for="e in graph.edges" :key="`term-${e.fromId}-${e.toId}`">
161
+ <!-- Terminal icon instead of a <marker>: currentColor inside a
162
+ marker resolves against <defs>, not the referencing path, so
163
+ a marker can't take the edge's colour. -->
164
+ <ArrowRight
165
+ class="edge-terminal-icon"
166
+ :x="edgeTerminal(e).x - 15"
167
+ :y="edgeTerminal(e).y - 6.5"
168
+ />
169
+ </template>
170
+ <path
171
+ v-for="e in graph.edges"
172
+ :key="`${e.fromId}->${e.toId}`"
173
+ class="edge"
174
+ :d="edgePath(e)"
175
+ />
176
+ </g>
177
+
178
+ <g class="nodes">
179
+ <g
180
+ v-for="n in graph.nodes"
181
+ :key="n.id"
182
+ class="node"
183
+ :class="nodeClass(n)"
184
+ :transform="`translate(${n.x} ${n.y})`"
185
+ @click="onNodeClick(n.id)"
186
+ @mouseenter="onNodeEnter(n, $event)"
187
+ @mouseleave="tipLeave"
188
+ >
189
+ <!-- Hit area first, so the box and its text paint over it. -->
190
+ <rect
191
+ class="node-hit"
192
+ :x="-HIT_PAD"
193
+ :y="-HIT_PAD"
194
+ :width="NODE_W + 2 * HIT_PAD"
195
+ :height="NODE_H + 2 * HIT_PAD"
196
+ />
197
+ <rect
198
+ class="node-box"
199
+ :width="NODE_W"
200
+ :height="NODE_H"
201
+ rx="3"
202
+ />
203
+ <text class="node-id" x="8" y="14">{{ n.bead.id }}</text>
204
+ <text class="node-title" x="8" y="28">
205
+ {{ truncate(n.bead.title, 18) }}
206
+ </text>
207
+ <text class="node-status" :x="NODE_W - 8" y="14" text-anchor="end">
208
+ {{ n.bead.status.replace(/_/g, " ") }}
209
+ </text>
210
+ <text class="node-pri" :x="NODE_W - 8" y="28" text-anchor="end">
211
+ P{{ n.bead.priority ?? 4 }}
212
+ </text>
213
+ </g>
214
+ </g>
215
+ </svg>
216
+ <BeadTooltip
217
+ v-bind="tipProps"
218
+ @details="onDetails"
219
+ @watch="tipWatch"
220
+ @tooltip-enter="tipStay"
221
+ @tooltip-leave="tipLeave"
222
+ />
223
+ </div>
224
+ </template>
225
+
226
+ <style>
227
+ .bead-subgraph {
228
+ overflow: auto;
229
+ padding: 8px 0;
230
+ }
231
+
232
+ .bead-subgraph .empty {
233
+ color: var(--c-dim);
234
+ font-style: italic;
235
+ font-size: 12px;
236
+ padding: 12px 0;
237
+ }
238
+
239
+ .subgraph-svg {
240
+ display: block;
241
+ }
242
+
243
+ /* One colour for the whole edge layer: the path paints with `stroke`, the
244
+ terminal icon with currentColor, so both are set here. The glyph is a step
245
+ brighter than the hairline it terminates — a stroked arrow at this size
246
+ needs more contrast than the filled triangle it replaced. */
247
+ .bead-subgraph .edges {
248
+ color: var(--c-dim);
249
+ }
250
+
251
+ .bead-subgraph .edge {
252
+ fill: none;
253
+ stroke: var(--c-border);
254
+ stroke-width: 1.5;
255
+ stroke-opacity: 0.7;
256
+ }
257
+
258
+ .bead-subgraph .edge-terminal-icon {
259
+ width: 13px;
260
+ height: 13px;
261
+ stroke-width: 2.5;
262
+ pointer-events: none;
263
+ }
264
+
265
+ .bead-subgraph .node {
266
+ cursor: pointer;
267
+ }
268
+
269
+ /* Invisible, but a target: a transparent fill still counts as painted for
270
+ pointer-events, so the pointer is caught this far outside the box. */
271
+ .bead-subgraph .node .node-hit {
272
+ fill: transparent;
273
+ stroke: none;
274
+ pointer-events: all;
275
+ }
276
+
277
+ .bead-subgraph .node .node-box {
278
+ fill: var(--c-panel);
279
+ stroke: var(--c-border);
280
+ stroke-width: 1;
281
+ }
282
+
283
+ .bead-subgraph .node.is-root .node-box {
284
+ fill: var(--c-accent-wash-strong);
285
+ stroke: var(--c-accent);
286
+ stroke-width: 1.5;
287
+ }
288
+
289
+ .bead-subgraph .node.status-closed .node-box {
290
+ stroke: var(--c-shipped);
291
+ }
292
+ .bead-subgraph .node.status-in_progress .node-box {
293
+ stroke: var(--c-in-progress);
294
+ }
295
+ .bead-subgraph .node.status-deferred .node-box {
296
+ stroke: var(--c-closed-noship);
297
+ fill-opacity: 0.6;
298
+ }
299
+
300
+ .bead-subgraph .node:hover .node-box {
301
+ fill: var(--c-accent-wash);
302
+ stroke: var(--c-accent);
303
+ }
304
+
305
+ .bead-subgraph .node-id {
306
+ fill: var(--c-accent);
307
+ font-family: ui-monospace, monospace;
308
+ font-size: 10px;
309
+ }
310
+
311
+ .bead-subgraph .node-title {
312
+ fill: var(--c-text);
313
+ font-size: 11px;
314
+ }
315
+
316
+ .bead-subgraph .node-status {
317
+ fill: var(--c-dim);
318
+ font-size: 9px;
319
+ text-transform: lowercase;
320
+ }
321
+
322
+ .bead-subgraph .node-pri {
323
+ fill: var(--c-dim);
324
+ font-size: 9px;
325
+ }
326
+ </style>
@@ -0,0 +1,192 @@
1
+ <script setup lang="ts">
2
+ // BeadSubGraphOverlay.vue — an epic's children graph at full viewport.
3
+ //
4
+ // The drawer is a narrow column, and an epic with a few dozen children has
5
+ // more graph than fits in it. This is the same picture the drawer's
6
+ // Subgraph section draws — BeadSubGraph in children mode, beads inside the
7
+ // epic only — given the whole window. Clicking a node does what it does in
8
+ // the drawer: the drawer moves to that bead, and this closes.
9
+ //
10
+ // Escape is taken in the capture phase and stopped there, ahead of App.vue's
11
+ // document handler, so one press closes this and not the drawer under it.
12
+
13
+ import { computed, onMounted, onUnmounted, ref } from "vue";
14
+ import { X } from "@lucide/vue";
15
+ import type { Bead } from "../store";
16
+ import { GEOMETRY } from "../subgraph";
17
+ import BeadSubGraph from "./BeadSubGraph.vue";
18
+
19
+ const props = defineProps<{
20
+ epic: Bead;
21
+ beads: Bead[];
22
+ childCount: number;
23
+ }>();
24
+
25
+ const emit = defineEmits<{
26
+ close: [];
27
+ select: [id: string];
28
+ }>();
29
+
30
+ function onKeydown(e: KeyboardEvent) {
31
+ if (e.key !== "Escape") return;
32
+ e.preventDefault();
33
+ e.stopImmediatePropagation();
34
+ emit("close");
35
+ }
36
+
37
+ // How many isolated children fit side by side in the room we have. The
38
+ // drawer lists them in one column because it scrolls vertically for free;
39
+ // here the width is the whole window, and forty unconnected children as a
40
+ // forty-row column would leave most of it empty.
41
+ const bodyEl = ref<HTMLElement | null>(null);
42
+ const bodyWidth = ref(0);
43
+ let observer: ResizeObserver | null = null;
44
+ const isolatedColumns = computed(() =>
45
+ Math.max(
46
+ 1,
47
+ Math.floor(
48
+ (bodyWidth.value + GEOMETRY.colGap) / (GEOMETRY.nodeW + GEOMETRY.colGap),
49
+ ),
50
+ ),
51
+ );
52
+
53
+ onMounted(() => {
54
+ document.addEventListener("keydown", onKeydown, true);
55
+ if (bodyEl.value) {
56
+ bodyWidth.value = bodyEl.value.clientWidth;
57
+ observer = new ResizeObserver((entries) => {
58
+ bodyWidth.value = entries[0]?.contentRect.width ?? 0;
59
+ });
60
+ observer.observe(bodyEl.value);
61
+ }
62
+ });
63
+ onUnmounted(() => {
64
+ document.removeEventListener("keydown", onKeydown, true);
65
+ observer?.disconnect();
66
+ });
67
+ </script>
68
+
69
+ <template>
70
+ <Teleport to="body">
71
+ <div
72
+ class="subgraph-overlay"
73
+ role="dialog"
74
+ aria-modal="true"
75
+ :aria-label="`${props.epic.id} — all children`"
76
+ @click.self="emit('close')"
77
+ >
78
+ <div class="subgraph-overlay-panel">
79
+ <header class="subgraph-overlay-head">
80
+ <code class="id">{{ props.epic.id }}</code>
81
+ <span class="title">{{ props.epic.title }}</span>
82
+ <span class="hint">
83
+ all {{ props.childCount }} children · blocks edges between them
84
+ </span>
85
+ <button
86
+ class="close"
87
+ type="button"
88
+ title="Close (Esc)"
89
+ aria-label="Close"
90
+ @click="emit('close')"
91
+ >
92
+ <X class="icon" />
93
+ </button>
94
+ </header>
95
+ <div ref="bodyEl" class="subgraph-overlay-body">
96
+ <BeadSubGraph
97
+ :root-bead="props.epic"
98
+ :beads="props.beads"
99
+ mode="children"
100
+ :isolated-columns="isolatedColumns"
101
+ @select="(id) => emit('select', id)"
102
+ />
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </Teleport>
107
+ </template>
108
+
109
+ <style>
110
+ /* Above the drawer (500), below the search palette (1000): the palette is
111
+ summoned from anywhere and should still land on top. */
112
+ .subgraph-overlay {
113
+ position: fixed;
114
+ inset: 0;
115
+ z-index: 900;
116
+ background: var(--c-scrim);
117
+ padding: 24px;
118
+ display: flex;
119
+ }
120
+
121
+ .subgraph-overlay-panel {
122
+ flex: 1;
123
+ min-width: 0;
124
+ display: flex;
125
+ flex-direction: column;
126
+ background: var(--c-panel);
127
+ border: 1px solid var(--c-border);
128
+ border-radius: var(--radius);
129
+ box-shadow: var(--shadow-pop);
130
+ color: var(--c-text);
131
+ font-size: 12px;
132
+ overflow: hidden;
133
+ }
134
+
135
+ .subgraph-overlay-head {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 10px;
139
+ padding: 10px 14px;
140
+ border-bottom: 1px solid var(--c-border);
141
+ }
142
+
143
+ .subgraph-overlay-head .id {
144
+ color: var(--c-accent);
145
+ font-family: var(--font-mono);
146
+ font-size: 12px;
147
+ }
148
+
149
+ .subgraph-overlay-head .title {
150
+ font-weight: 600;
151
+ overflow: hidden;
152
+ text-overflow: ellipsis;
153
+ white-space: nowrap;
154
+ }
155
+
156
+ .subgraph-overlay-head .hint {
157
+ color: var(--c-dim);
158
+ white-space: nowrap;
159
+ }
160
+
161
+ .subgraph-overlay-head .close {
162
+ margin-left: auto;
163
+ display: inline-flex;
164
+ align-items: center;
165
+ padding: 4px;
166
+ border: 1px solid transparent;
167
+ border-radius: var(--radius-sm);
168
+ background: transparent;
169
+ color: var(--c-dim);
170
+ cursor: pointer;
171
+ }
172
+
173
+ .subgraph-overlay-head .close:hover {
174
+ background: var(--c-dim-wash);
175
+ color: var(--c-text);
176
+ }
177
+
178
+ .subgraph-overlay-head .close .icon {
179
+ width: 16px;
180
+ height: 16px;
181
+ }
182
+
183
+ /* The graph scrolls inside the panel; the panel itself never does. Padding
184
+ sits on the scroller so the edges of a wide graph are not flush with the
185
+ border when scrolled to either end. */
186
+ .subgraph-overlay-body {
187
+ flex: 1;
188
+ min-height: 0;
189
+ overflow: auto;
190
+ padding: 8px 14px;
191
+ }
192
+ </style>