@truedat/dd 8.12.10 → 8.12.11

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.
@@ -0,0 +1,56 @@
1
+ import { fireEvent, screen } from "@testing-library/react";
2
+ import { render } from "@truedat/test/render";
3
+ import LineageGraphNode from "../LineageGraphNode";
4
+
5
+ jest.mock("../LineageResourceNode", () => () => <div>Resource</div>);
6
+ jest.mock("@truedat/core/components/graph/LineageGroupNode", () => () => (
7
+ <div>Group</div>
8
+ ));
9
+
10
+ it.each(["lineageResource", "lineageGroup"])(
11
+ "unpins %s without selecting or expanding the node",
12
+ (type) => {
13
+ const onUnpin = jest.fn();
14
+ const onClick = jest.fn();
15
+ const onDoubleClick = jest.fn();
16
+ const view = render(
17
+ <div
18
+ className="react-flow__node"
19
+ tabIndex={0}
20
+ onClick={onClick}
21
+ onDoubleClick={onDoubleClick}
22
+ role="button"
23
+ >
24
+ <LineageGraphNode type={type} data={{ onUnpin }} />
25
+ </div>,
26
+ );
27
+ const button = screen.getByRole("button", { name: "Unpin focus" });
28
+ fireEvent.click(button);
29
+ fireEvent.doubleClick(button);
30
+ expect(onUnpin).toHaveBeenCalledTimes(1);
31
+ expect(onClick).not.toHaveBeenCalled();
32
+ expect(onDoubleClick).not.toHaveBeenCalled();
33
+ expect(view.container.querySelector(".react-flow__node")).toHaveFocus();
34
+ },
35
+ );
36
+
37
+ it("renders no pin control for an unpinned node", () => {
38
+ render(<LineageGraphNode type="lineageResource" data={{}} />);
39
+ expect(
40
+ screen.queryByRole("button", { name: "Unpin focus" }),
41
+ ).not.toBeInTheDocument();
42
+ });
43
+
44
+ it("keeps Space keyup away from the canvas pan shortcut", () => {
45
+ const onKeyUp = jest.fn();
46
+ render(
47
+ <div onKeyUp={onKeyUp} role="presentation">
48
+ <LineageGraphNode type="lineageResource" data={{ onUnpin: jest.fn() }} />
49
+ </div>,
50
+ );
51
+ fireEvent.keyUp(screen.getByRole("button", { name: "Unpin focus" }), {
52
+ key: " ",
53
+ code: "Space",
54
+ });
55
+ expect(onKeyUp).not.toHaveBeenCalled();
56
+ });
@@ -0,0 +1,338 @@
1
+ import { act, fireEvent, screen, within } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import Graph from "@truedat/core/components/Graph";
4
+ import { render } from "@truedat/test/render";
5
+ import { LineageGraph } from "../LineageGraph";
6
+
7
+ jest.mock("@truedat/core/components/Graph", () =>
8
+ jest.fn(
9
+ ({
10
+ nodes,
11
+ edges,
12
+ topControls,
13
+ onNodeClick,
14
+ onNodeDoubleClick,
15
+ onPaneClick,
16
+ }) => (
17
+ <div>
18
+ {topControls}
19
+ {nodes.map((node) => (
20
+ <div key={node.id}>
21
+ <button
22
+ className={`react-flow__node ${node.className || ""}`}
23
+ data-id={node.id}
24
+ data-selected={node.data.focusSelected}
25
+ data-dimmed={node.data.focusDimmed}
26
+ onClick={(event) => onNodeClick(event, node)}
27
+ onDoubleClick={(event) => onNodeDoubleClick(event, node)}
28
+ type="button"
29
+ >
30
+ {`Node ${node.id}`}
31
+ </button>
32
+ {node.data.onUnpin ? (
33
+ <button
34
+ type="button"
35
+ onClick={node.data.onUnpin}
36
+ >{`Unpin node ${node.id}`}</button>
37
+ ) : null}
38
+ </div>
39
+ ))}
40
+ {edges.map((edge) => (
41
+ <button
42
+ key={edge.id}
43
+ data-focused={edge.data.focusFocused}
44
+ onClick={edge.data.onClick}
45
+ type="button"
46
+ >
47
+ {`Edge ${edge.id}`}
48
+ </button>
49
+ ))}
50
+ <button onClick={onPaneClick} type="button">
51
+ Canvas
52
+ </button>
53
+ </div>
54
+ ),
55
+ ),
56
+ );
57
+
58
+ const resources = ["A", "B", "C"].map((id) => ({ id, name: id }));
59
+ const defaultProps = {
60
+ resources,
61
+ edges: [],
62
+ groups: [],
63
+ cancelPoll: jest.fn(),
64
+ graphId: "graph-1",
65
+ };
66
+ const nodeButton = (id) => screen.getByRole("button", { name: `Node ${id}` });
67
+ const pinA = () => {
68
+ fireEvent.click(nodeButton("A"));
69
+ fireEvent.click(screen.getByRole("button", { name: "Pin focus" }));
70
+ };
71
+ const dock = (container) =>
72
+ container.querySelector(".lineage-graph__dock-context-bar");
73
+ const pinChip = (container) =>
74
+ container.querySelector(".lineage-graph__dock-pin");
75
+
76
+ describe("LineageGraph pinned focus", () => {
77
+ it("shows the new selection without an extra pin chip", () => {
78
+ const { container } = render(<LineageGraph {...defaultProps} />);
79
+ pinA();
80
+ fireEvent.click(nodeButton("B"));
81
+ expect(nodeButton("A")).toHaveClass("lineage-node--pinned");
82
+ expect(nodeButton("B")).toHaveAttribute("data-selected", "true");
83
+ expect(nodeButton("B")).toHaveAttribute("data-dimmed", "false");
84
+ expect(dock(container)).toHaveTextContent("B");
85
+ expect(pinChip(container)).not.toBeInTheDocument();
86
+ expect(
87
+ screen.getByRole("button", { name: "Clear selection for B" }),
88
+ ).toBeInTheDocument();
89
+ fireEvent.mouseDown(document.body);
90
+ fireEvent.click(screen.getByRole("button", { name: "Canvas" }));
91
+ expect(nodeButton("A")).toHaveClass("lineage-node--pinned");
92
+ expect(dock(container)).toHaveTextContent("A");
93
+ expect(pinChip(container)).not.toBeInTheDocument();
94
+ });
95
+
96
+ it.each(["Enter", " "])(
97
+ "selects with %s and restores default focus when unpinning the node",
98
+ (key) => {
99
+ const { container } = render(<LineageGraph {...defaultProps} />);
100
+ pinA();
101
+ fireEvent.keyDown(nodeButton("B"), { key });
102
+ expect(nodeButton("B")).toHaveAttribute("data-selected", "true");
103
+ fireEvent.click(screen.getByRole("button", { name: "Unpin node A" }));
104
+ expect(nodeButton("A")).not.toHaveClass("lineage-node--pinned");
105
+ expect(nodeButton("B")).not.toHaveAttribute("data-selected", "true");
106
+ expect(nodeButton("C")).not.toHaveAttribute("data-dimmed", "true");
107
+ expect(dock(container)).not.toBeInTheDocument();
108
+ },
109
+ );
110
+
111
+ it("reopens the pinned context, replaces pin explicitly and clears only selection", () => {
112
+ const { container } = render(<LineageGraph {...defaultProps} />);
113
+ pinA();
114
+ fireEvent.click(nodeButton("B"));
115
+ fireEvent.click(screen.getByRole("button", { name: "Pin focus" }));
116
+ expect(nodeButton("A")).not.toHaveClass("lineage-node--pinned");
117
+ expect(nodeButton("B")).toHaveClass("lineage-node--pinned");
118
+ expect(pinChip(container)).not.toBeInTheDocument();
119
+ fireEvent.click(
120
+ screen.getByRole("button", { name: "Clear selection for B" }),
121
+ );
122
+ expect(dock(container)).toHaveTextContent("B");
123
+ fireEvent.click(
124
+ within(dock(container)).getByRole("button", { name: "B Table" }),
125
+ );
126
+ expect(screen.getByLabelText("Lineage inspector")).toBeInTheDocument();
127
+ expect(nodeButton("B")).toHaveAttribute("data-selected", "true");
128
+ act(() => jest.runOnlyPendingTimers());
129
+ });
130
+
131
+ it("retains pin through loading but clears it when the graph changes or the node is removed", () => {
132
+ const view = render(<LineageGraph {...defaultProps} />);
133
+ pinA();
134
+ view.rerender(<LineageGraph {...defaultProps} loading resources={[]} />);
135
+ view.rerender(<LineageGraph {...defaultProps} />);
136
+ expect(nodeButton("A")).toHaveClass("lineage-node--pinned");
137
+ view.rerender(<LineageGraph {...defaultProps} graphId="graph-2" />);
138
+ expect(nodeButton("A")).not.toHaveClass("lineage-node--pinned");
139
+ pinA();
140
+ view.rerender(
141
+ <LineageGraph
142
+ {...defaultProps}
143
+ graphId="graph-2"
144
+ resources={resources.slice(1)}
145
+ />,
146
+ );
147
+ expect(
148
+ screen.queryByRole("button", { name: "Unpin focus" }),
149
+ ).not.toBeInTheDocument();
150
+ });
151
+ });
152
+
153
+ it("inspects an unrelated edge without replacing the pinned focus", () => {
154
+ render(
155
+ <LineageGraph
156
+ {...defaultProps}
157
+ edges={[{ id: "BC", source: "B", target: "C" }]}
158
+ />,
159
+ );
160
+ pinA();
161
+ fireEvent.click(screen.getByRole("button", { name: "Edge BC" }));
162
+ expect(nodeButton("A")).toHaveClass("lineage-node--pinned");
163
+ expect(screen.getByRole("button", { name: "Edge BC" })).toHaveAttribute(
164
+ "data-focused",
165
+ "false",
166
+ );
167
+ expect(screen.getByLabelText("Lineage inspector")).toBeInTheDocument();
168
+ });
169
+
170
+ it("keeps pin when selecting a search result or double-clicking another node", () => {
171
+ const { container } = render(<LineageGraph {...defaultProps} />);
172
+ pinA();
173
+ fireEvent.click(screen.getByRole("button", { name: "Search nodes" }));
174
+ fireEvent.change(
175
+ screen.getByRole("textbox", { name: "Search graph nodes" }),
176
+ { target: { value: "B" } },
177
+ );
178
+ act(() => jest.advanceTimersByTime(300));
179
+ fireEvent.click(container.querySelector(".lineage-graph__search-result"));
180
+ expect(nodeButton("B")).toHaveAttribute("data-selected", "true");
181
+ expect(nodeButton("A")).toHaveClass("lineage-node--pinned");
182
+ fireEvent.doubleClick(nodeButton("C"));
183
+ expect(nodeButton("C")).toHaveAttribute("data-selected", "true");
184
+ expect(nodeButton("A")).toHaveClass("lineage-node--pinned");
185
+ });
186
+
187
+ it("keeps a hidden pinned child through collapse and expansion", () => {
188
+ const topology = {
189
+ nodes: [{ id: "G", name: "Group", class: "Group" }, ...resources],
190
+ contains: [{ source: "G", target: "A" }],
191
+ depends: [],
192
+ };
193
+ const { container } = render(
194
+ <LineageGraph
195
+ {...defaultProps}
196
+ topology={topology}
197
+ defaultExpansionDepth={10}
198
+ />,
199
+ );
200
+ pinA();
201
+ fireEvent.click(screen.getByRole("button", { name: "Collapse all" }));
202
+ expect(
203
+ screen.queryByRole("button", { name: "Node A" }),
204
+ ).not.toBeInTheDocument();
205
+ expect(dock(container)).toHaveTextContent("A");
206
+ expect(nodeButton("G")).not.toHaveClass("lineage-node--pinned");
207
+ fireEvent.click(screen.getByRole("button", { name: "Expand all" }));
208
+ expect(nodeButton("A")).toHaveClass("lineage-node--pinned");
209
+ });
210
+
211
+ it.each([
212
+ [false, "A"],
213
+ [true, "A"],
214
+ [true, "nested"],
215
+ ])(
216
+ "opens only the ancestors when centering a hidden node (pinned: %s, node: %s)",
217
+ async (pinned, targetId) => {
218
+ const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
219
+ const topology = {
220
+ nodes: [
221
+ { id: "G", name: "Group", class: "Group" },
222
+ { id: "nested", name: "Nested", class: "Group" },
223
+ { id: "other", name: "Other", class: "Group" },
224
+ ...resources,
225
+ ],
226
+ contains: [
227
+ { source: "G", target: "nested" },
228
+ { source: "nested", target: "A" },
229
+ { source: "other", target: "B" },
230
+ ],
231
+ depends: [],
232
+ };
233
+ const { container } = render(
234
+ <LineageGraph
235
+ {...defaultProps}
236
+ topology={topology}
237
+ defaultExpansionDepth={10}
238
+ />,
239
+ );
240
+ fireEvent.keyDown(nodeButton(targetId), { key: "Enter" });
241
+ if (pinned) {
242
+ fireEvent.click(screen.getByRole("button", { name: "Pin focus" }));
243
+ }
244
+ fireEvent.click(screen.getByRole("button", { name: "Collapse all" }));
245
+ expect(screen.queryByRole("button", { name: "Node A" })).toBeNull();
246
+ expect(screen.queryByRole("button", { name: "Node nested" })).toBeNull();
247
+ const requestKey =
248
+ Graph.mock.calls.at(-1)[0].graphOptions.focusNodeRequestKey;
249
+ if (pinned) {
250
+ fireEvent.click(screen.getByRole("button", { name: "Canvas" }));
251
+ fireEvent.click(
252
+ within(dock(container)).getByRole("button", { name: /^(A|Nested)/ }),
253
+ );
254
+ }
255
+ screen.getByRole("button", { name: "Center in graph" }).focus();
256
+ await user.keyboard("{Enter}");
257
+ expect(nodeButton("nested")).toBeInTheDocument();
258
+ expect(nodeButton(targetId)).toHaveAttribute("data-selected", "true");
259
+ if (pinned)
260
+ expect(nodeButton(targetId)).toHaveClass("lineage-node--pinned");
261
+ if (targetId === "nested") {
262
+ expect(screen.queryByRole("button", { name: "Node A" })).toBeNull();
263
+ }
264
+ expect(nodeButton("other")).toBeInTheDocument();
265
+ expect(screen.queryByRole("button", { name: "Node B" })).toBeNull();
266
+ expect(Graph.mock.calls.at(-1)[0].graphOptions).toMatchObject({
267
+ focusNodeId: targetId,
268
+ focusNodeRequestKey: requestKey + 1,
269
+ });
270
+ fireEvent.click(screen.getByRole("button", { name: "Center in graph" }));
271
+ expect(Graph.mock.calls.at(-1)[0].graphOptions.focusNodeRequestKey).toBe(
272
+ requestKey + 2,
273
+ );
274
+ },
275
+ );
276
+
277
+ it("preserves the logical pin when duplicate fields acquire a new visible representative", () => {
278
+ const originalId = "https://glue.eu.amazonaws.com/#/table/field";
279
+ const aliasId = "glue:/Glue/table/field";
280
+ const topology = {
281
+ nodes: [
282
+ { id: "table", name: "Table", class: "Resource", type: "table" },
283
+ { id: originalId, name: "Field", class: "Resource", type: "column" },
284
+ ],
285
+ contains: [{ source: "table", target: originalId }],
286
+ depends: [],
287
+ };
288
+ const view = render(
289
+ <LineageGraph
290
+ {...defaultProps}
291
+ topology={topology}
292
+ defaultExpansionDepth={10}
293
+ />,
294
+ );
295
+ fireEvent.click(nodeButton(originalId));
296
+ fireEvent.click(screen.getByRole("button", { name: "Pin focus" }));
297
+ view.rerender(
298
+ <LineageGraph
299
+ {...defaultProps}
300
+ defaultExpansionDepth={10}
301
+ topology={{
302
+ ...topology,
303
+ nodes: [
304
+ ...topology.nodes,
305
+ { id: aliasId, name: "Field", class: "Resource", type: "column" },
306
+ ],
307
+ contains: [...topology.contains, { source: "table", target: aliasId }],
308
+ }}
309
+ />,
310
+ );
311
+ expect(nodeButton(aliasId)).toHaveClass("lineage-node--pinned");
312
+ expect(dock(view.container)).toHaveTextContent("Field");
313
+ fireEvent.click(
314
+ within(dock(view.container)).getByRole("button", { name: "Unpin focus" }),
315
+ );
316
+ expect(nodeButton(aliasId)).not.toHaveClass("lineage-node--pinned");
317
+ });
318
+
319
+ it("minimizes to the pinned context on an outside click without a pane click", () => {
320
+ const { container } = render(<LineageGraph {...defaultProps} />);
321
+ pinA();
322
+ expect(
323
+ within(dock(container)).queryByRole("button", { name: "Unpin focus" }),
324
+ ).not.toBeInTheDocument();
325
+ expect(dock(container).closest(".lineage-graph__dock-context")).toHaveClass(
326
+ "lineage-graph__dock-context--pinned",
327
+ );
328
+ fireEvent.click(nodeButton("B"));
329
+ fireEvent.mouseDown(document.body);
330
+ expect(dock(container)).toHaveTextContent("A");
331
+ expect(screen.queryByLabelText("Lineage inspector")).not.toBeInTheDocument();
332
+ fireEvent.click(
333
+ within(dock(container)).getByRole("button", { name: "Unpin focus" }),
334
+ );
335
+ expect(dock(container)).not.toBeInTheDocument();
336
+ expect(nodeButton("A")).not.toHaveClass("lineage-node--pinned");
337
+ expect(nodeButton("B")).not.toHaveAttribute("data-selected", "true");
338
+ });
@@ -233,13 +233,13 @@ describe("<LineageGraphToolbar />", () => {
233
233
  });
234
234
 
235
235
  it("renders the pinned minimized bar with unpin and accent state", () => {
236
- renderToolbar({ pinned: true });
236
+ renderToolbar({ context: null, pinnedContext: defaultProps.context });
237
237
 
238
238
  const unpin = screen.getByRole("button", { name: "Unpin focus" });
239
239
 
240
240
  expect(unpin).toHaveAttribute("aria-pressed", "true");
241
241
  expect(unpin.querySelector(".lucide-pin")).toBeInTheDocument();
242
- expect(screen.getByRole("button", { name: "Search nodes" })).toBeDisabled();
242
+ expect(screen.getByRole("button", { name: "Search nodes" })).toBeEnabled();
243
243
 
244
244
  fireEvent.click(unpin);
245
245
  expect(defaultProps.onUnpin).toHaveBeenCalledTimes(1);
@@ -362,3 +362,55 @@ describe("visibleLegendKeys", () => {
362
362
  ).not.toBeInTheDocument();
363
363
  });
364
364
  });
365
+
366
+ it("shows the current selection without an additional pinned chip", () => {
367
+ const onPinnedContextSelect = jest.fn();
368
+ const rendered = renderToolbar({
369
+ pinnedContext: defaultProps.context,
370
+ context: {
371
+ type: "node",
372
+ node: {
373
+ ...defaultProps.context.node,
374
+ name: "Customers",
375
+ graphNodeId: "customers",
376
+ },
377
+ },
378
+ activePanel: "inspector",
379
+ onPinnedContextSelect,
380
+ });
381
+ expect(screen.getByLabelText("Lineage inspector")).toHaveTextContent(
382
+ "Customers",
383
+ );
384
+ expect(
385
+ rendered.container.querySelector(".lineage-graph__dock-context-bar"),
386
+ ).toHaveTextContent("Customers");
387
+ expect(
388
+ rendered.container.querySelector(".lineage-graph__dock-pin"),
389
+ ).not.toBeInTheDocument();
390
+ expect(
391
+ screen.queryByRole("button", { name: "Unpin focus" }),
392
+ ).not.toBeInTheDocument();
393
+ expect(
394
+ screen.getByRole("button", { name: "Clear selection for Customers" }),
395
+ ).toBeInTheDocument();
396
+ });
397
+
398
+ it("preserves the structure icon in the pinned header without an unpin button", () => {
399
+ const { container } = renderToolbar({
400
+ pinned: true,
401
+ pinnedContext: defaultProps.context,
402
+ activePanel: "inspector",
403
+ });
404
+ const bar = container.querySelector(".lineage-graph__dock-context-bar");
405
+ expect(bar.closest(".lineage-graph__dock-context")).toHaveClass(
406
+ "lineage-graph__dock-context--pinned",
407
+ );
408
+ expect(
409
+ bar.querySelector(
410
+ ".lineage-graph__dock-context-summary-icon .lucide-table-2",
411
+ ),
412
+ ).toBeInTheDocument();
413
+ expect(
414
+ bar.querySelector(".lineage-graph__dock-context-unpin"),
415
+ ).not.toBeInTheDocument();
416
+ });
@@ -344,7 +344,7 @@ describe("<LineageInspectorPanel />", () => {
344
344
  "utf8",
345
345
  );
346
346
 
347
- expect(styles).toContain("--td-lineage-inspector-motion-duration: 180ms;");
347
+ expect(styles).toContain("--td-lineage-inspector-motion-duration: 220ms;");
348
348
  expect(styles).toContain("@media (prefers-reduced-motion: reduce)");
349
349
  expect(styles).toContain(
350
350
  ".lineage-view__header-icon > .icon,\n.lineage-inspector__header-icon > .icon",
@@ -2481,8 +2481,8 @@
2481
2481
  var(--td-lineage-inspector-toolbar-offset) +
2482
2482
  var(--td-lineage-context-control-size) - 1px
2483
2483
  );
2484
- --td-lineage-inspector-motion-duration: 180ms;
2485
- --td-lineage-inspector-motion-easing: cubic-bezier(0.16, 1, 0.3, 1);
2484
+ --td-lineage-inspector-motion-duration: 220ms;
2485
+ --td-lineage-inspector-motion-easing: cubic-bezier(0.22, 0.68, 0, 1);
2486
2486
 
2487
2487
  position: relative;
2488
2488
  display: flex;
@@ -2540,12 +2540,16 @@
2540
2540
  content: "";
2541
2541
  pointer-events: none;
2542
2542
  }
2543
+ .lineage-graph__dock-context--pinned .lineage-graph__dock-context-bar {
2544
+ background: var(--td-graph-accent, #ed5c17);
2545
+ }
2543
2546
  .lineage-graph__dock-context--pinned
2544
2547
  .lineage-graph__dock-context-bar:not(
2545
2548
  .lineage-graph__dock-context-bar--expanded
2546
2549
  ) {
2547
2550
  background: var(--td-graph-accent, #ed5c17);
2548
- box-shadow: 0 5px 12px rgba(237, 92, 23, 0.28);
2551
+ box-shadow: 0 5px 12px
2552
+ color-mix(in srgb, var(--td-graph-accent, #ed5c17) 28%, transparent);
2549
2553
  }
2550
2554
  .lineage-graph__dock-context-bar--edge {
2551
2555
  background: var(--td-graph-edge-metadata, #7c3aed);
@@ -2564,6 +2568,11 @@
2564
2568
  cursor: pointer;
2565
2569
  }
2566
2570
  .lineage-graph__dock-context-reopen {
2571
+ padding: 0;
2572
+ border: 0;
2573
+ background: transparent;
2574
+ color: inherit;
2575
+ font: inherit;
2567
2576
  position: static;
2568
2577
  display: flex;
2569
2578
  height: 100%;
@@ -2572,28 +2581,30 @@
2572
2581
  align-items: center;
2573
2582
  text-align: left;
2574
2583
  }
2584
+ button.lineage-graph__dock-context-reopen {
2585
+ cursor: pointer;
2586
+ }
2575
2587
  .lineage-graph__dock-context-clear,
2576
2588
  .lineage-graph__dock-context-unpin {
2577
2589
  width: var(--td-lineage-context-control-size);
2578
2590
  min-width: var(--td-lineage-context-control-size);
2579
2591
  }
2592
+ @keyframes td-lineage-context-copy-in {
2593
+ from {
2594
+ opacity: 0;
2595
+ }
2596
+ to {
2597
+ opacity: 1;
2598
+ }
2599
+ }
2580
2600
  .lineage-graph__dock-context-clear {
2581
2601
  border-left: 1px solid rgba(255, 255, 255, 0.34);
2582
2602
  }
2583
2603
  .lineage-graph__dock-context-bar--expanded .lineage-graph__dock-context-clear {
2584
2604
  border-left: 0;
2585
2605
  }
2586
- .lineage-graph__dock-context-bar--expanded .lineage-graph__dock-context-unpin {
2587
- height: 100%;
2588
- border-right: 1px solid rgba(255, 255, 255, 0.34);
2589
- }
2590
- .lineage-graph__dock-context--pinned
2591
- .lineage-graph__dock-context-bar--expanded
2592
- .lineage-graph__dock-context-unpin {
2593
- background: var(--td-graph-accent, #ed5c17);
2594
- color: #ffffff;
2595
- }
2596
2606
  .lineage-graph__dock-context-summary {
2607
+ animation: td-lineage-context-copy-in 140ms ease-out;
2597
2608
  display: flex;
2598
2609
  width: 100%;
2599
2610
  height: 100%;
@@ -2614,12 +2625,6 @@
2614
2625
  border: 1px solid transparent;
2615
2626
  border-radius: 6px;
2616
2627
  transition:
2617
- width var(--td-lineage-inspector-motion-duration)
2618
- var(--td-lineage-inspector-motion-easing),
2619
- min-width var(--td-lineage-inspector-motion-duration)
2620
- var(--td-lineage-inspector-motion-easing),
2621
- height var(--td-lineage-inspector-motion-duration)
2622
- var(--td-lineage-inspector-motion-easing),
2623
2628
  border-color var(--td-lineage-inspector-motion-duration)
2624
2629
  var(--td-lineage-inspector-motion-easing),
2625
2630
  background-color var(--td-lineage-inspector-motion-duration)
@@ -2635,7 +2640,7 @@
2635
2640
  flex: 1 1 auto;
2636
2641
  flex-direction: column;
2637
2642
  justify-content: center;
2638
- gap: 3px;
2643
+ gap: 0;
2639
2644
  }
2640
2645
  .lineage-graph__dock-context-label,
2641
2646
  .lineage-graph__dock-context-type {
@@ -2649,8 +2654,6 @@
2649
2654
  .lineage-graph__dock-context-label {
2650
2655
  font-size: 11px;
2651
2656
  font-weight: 700;
2652
- transition: font-size var(--td-lineage-inspector-motion-duration)
2653
- var(--td-lineage-inspector-motion-easing);
2654
2657
  }
2655
2658
  .lineage-graph__dock-context-type {
2656
2659
  max-height: 0;
@@ -2662,8 +2665,6 @@
2662
2665
  text-transform: uppercase;
2663
2666
  transform: translateY(-3px);
2664
2667
  transition:
2665
- max-height var(--td-lineage-inspector-motion-duration)
2666
- var(--td-lineage-inspector-motion-easing),
2667
2668
  opacity var(--td-lineage-inspector-motion-duration)
2668
2669
  var(--td-lineage-inspector-motion-easing),
2669
2670
  transform var(--td-lineage-inspector-motion-duration)
@@ -2678,6 +2679,9 @@
2678
2679
  border-radius: 8px;
2679
2680
  background: rgba(255, 255, 255, 0.1);
2680
2681
  }
2682
+ .lineage-graph__dock-context-bar--expanded .lineage-graph__dock-context-copy {
2683
+ gap: 3px;
2684
+ }
2681
2685
  .lineage-graph__dock-context-bar--expanded .lineage-graph__dock-context-label {
2682
2686
  font-size: 16px;
2683
2687
  font-weight: 800;
@@ -3607,6 +3611,7 @@
3607
3611
  @media (prefers-reduced-motion: reduce) {
3608
3612
  .lineage-graph__tool-panel,
3609
3613
  .lineage-inspector,
3614
+ .lineage-graph__dock-context-summary,
3610
3615
  .lineage-graph__dock-context-clear .icon {
3611
3616
  animation: none;
3612
3617
  }
@@ -4044,3 +4049,55 @@
4044
4049
  .lineage-graph__legend-section {
4045
4050
  flex-shrink: 0;
4046
4051
  }
4052
+
4053
+ .lineage-graph__stage-surface
4054
+ .react-flow__node.lineage-node--pinned
4055
+ > :is(.td-lineage-resource-node, .td-lineage-group-node) {
4056
+ box-shadow: inset 0 0 0 2px var(--td-graph-accent, #ed5c17);
4057
+ opacity: 1;
4058
+
4059
+ > :is(
4060
+ .td-lineage-resource-node__header,
4061
+ .td-lineage-group-node__header,
4062
+ .td-lineage-group-node__absolute-header
4063
+ ) {
4064
+ background: var(--td-lineage-interaction-bg, #fff7ed);
4065
+ box-shadow: none;
4066
+ color: color-mix(in srgb, var(--td-graph-accent, #ed5c17) 70%, #000000);
4067
+
4068
+ :is(
4069
+ .td-lineage-resource-node__label,
4070
+ .td-lineage-resource-node__kind,
4071
+ .td-lineage-group-node__label,
4072
+ .td-lineage-group-node__absolute-label,
4073
+ .td-lineage-group-node__toggle,
4074
+ .td-lineage-group-node__type-icon.icon
4075
+ ) {
4076
+ color: color-mix(in srgb, var(--td-graph-accent, #ed5c17) 70%, #000000);
4077
+ }
4078
+ }
4079
+ }
4080
+
4081
+ .lineage-graph__stage-surface .lineage-graph__node-unpin {
4082
+ position: absolute;
4083
+ top: -10px;
4084
+ left: -10px;
4085
+ z-index: 5;
4086
+ display: inline-flex;
4087
+ width: 24px;
4088
+ height: 24px;
4089
+ align-items: center;
4090
+ justify-content: center;
4091
+ padding: 0;
4092
+ border: 1px solid var(--td-graph-accent, #ed5c17);
4093
+ border-radius: 50%;
4094
+ background: var(--td-graph-accent, #ed5c17);
4095
+ color: #ffffff;
4096
+ cursor: pointer;
4097
+ pointer-events: auto;
4098
+
4099
+ &:focus-visible {
4100
+ outline: 2px solid #0284c7;
4101
+ outline-offset: 2px;
4102
+ }
4103
+ }