@memberjunction/ng-hierarchy-tree 0.0.0 → 6.1.0-edge.4

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,1393 @@
1
+ import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { RunView, CompositeKey } from '@memberjunction/core';
5
+ import { UUIDsEqual, NormalizeUUID } from '@memberjunction/global';
6
+ import { BaseAngularComponent } from '@memberjunction/ng-base-types';
7
+ import * as d3 from 'd3';
8
+ import { CancelableHierarchyNodeEvent } from '../events/hierarchy-tree.events';
9
+ import * as i0 from "@angular/core";
10
+ import * as i1 from "@angular/forms";
11
+ const _c0 = ["svgContainer"];
12
+ const _c1 = ["svgElement"];
13
+ const _forTrack0 = ($index, $item) => $item.ID;
14
+ const _forTrack1 = ($index, $item) => $item.key;
15
+ function HierarchyTreeComponent_Conditional_1_Conditional_1_Conditional_3_Template(rf, ctx) { if (rf & 1) {
16
+ const _r4 = i0.ɵɵgetCurrentView();
17
+ i0.ɵɵelementStart(0, "button", 23);
18
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_Conditional_1_Conditional_1_Conditional_3_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r2.clearSearch()); });
19
+ i0.ɵɵelement(1, "i", 24);
20
+ i0.ɵɵelementEnd();
21
+ } }
22
+ function HierarchyTreeComponent_Conditional_1_Conditional_1_Conditional_4_Template(rf, ctx) { if (rf & 1) {
23
+ i0.ɵɵelementStart(0, "span", 22);
24
+ i0.ɵɵtext(1);
25
+ i0.ɵɵelementEnd();
26
+ } if (rf & 2) {
27
+ const ctx_r2 = i0.ɵɵnextContext(3);
28
+ i0.ɵɵadvance();
29
+ i0.ɵɵtextInterpolate1("", ctx_r2.MatchingNodeCount, " found");
30
+ } }
31
+ function HierarchyTreeComponent_Conditional_1_Conditional_1_Template(rf, ctx) { if (rf & 1) {
32
+ const _r2 = i0.ɵɵgetCurrentView();
33
+ i0.ɵɵelementStart(0, "div", 16);
34
+ i0.ɵɵelement(1, "i", 19);
35
+ i0.ɵɵelementStart(2, "input", 20);
36
+ i0.ɵɵtwoWayListener("ngModelChange", function HierarchyTreeComponent_Conditional_1_Conditional_1_Template_input_ngModelChange_2_listener($event) { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(2); i0.ɵɵtwoWayBindingSet(ctx_r2.SearchQuery, $event) || (ctx_r2.SearchQuery = $event); return i0.ɵɵresetView($event); });
37
+ i0.ɵɵlistener("input", function HierarchyTreeComponent_Conditional_1_Conditional_1_Template_input_input_2_listener() { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.onSearchInput()); });
38
+ i0.ɵɵelementEnd();
39
+ i0.ɵɵconditionalCreate(3, HierarchyTreeComponent_Conditional_1_Conditional_1_Conditional_3_Template, 2, 0, "button", 21);
40
+ i0.ɵɵconditionalCreate(4, HierarchyTreeComponent_Conditional_1_Conditional_1_Conditional_4_Template, 2, 1, "span", 22);
41
+ i0.ɵɵelementEnd();
42
+ } if (rf & 2) {
43
+ const ctx_r2 = i0.ɵɵnextContext(2);
44
+ i0.ɵɵadvance(2);
45
+ i0.ɵɵtwoWayProperty("ngModel", ctx_r2.SearchQuery);
46
+ i0.ɵɵadvance();
47
+ i0.ɵɵconditional(ctx_r2.SearchQuery ? 3 : -1);
48
+ i0.ɵɵadvance();
49
+ i0.ɵɵconditional(ctx_r2.SearchQuery && ctx_r2.MatchingNodeCount > 0 ? 4 : -1);
50
+ } }
51
+ function HierarchyTreeComponent_Conditional_1_Conditional_2_Template(rf, ctx) { if (rf & 1) {
52
+ const _r5 = i0.ɵɵgetCurrentView();
53
+ i0.ɵɵelementStart(0, "div", 17);
54
+ i0.ɵɵelement(1, "i", 25);
55
+ i0.ɵɵelementStart(2, "span");
56
+ i0.ɵɵtext(3, "Focusing subtree: ");
57
+ i0.ɵɵelementStart(4, "strong");
58
+ i0.ɵɵtext(5);
59
+ i0.ɵɵelementEnd()();
60
+ i0.ɵɵelementStart(6, "button", 26);
61
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_Conditional_1_Conditional_2_Template_button_click_6_listener() { i0.ɵɵrestoreView(_r5); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.resetFocus()); });
62
+ i0.ɵɵelement(7, "i", 27);
63
+ i0.ɵɵtext(8, " View Full Hierarchy ");
64
+ i0.ɵɵelementEnd()();
65
+ } if (rf & 2) {
66
+ const ctx_r2 = i0.ɵɵnextContext(2);
67
+ i0.ɵɵadvance(5);
68
+ i0.ɵɵtextInterpolate(ctx_r2.FocusedNode.Name);
69
+ } }
70
+ function HierarchyTreeComponent_Conditional_1_Conditional_3_Template(rf, ctx) { if (rf & 1) {
71
+ const _r6 = i0.ɵɵgetCurrentView();
72
+ i0.ɵɵelementStart(0, "div", 18)(1, "button", 28);
73
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_Conditional_1_Conditional_3_Template_button_click_1_listener() { i0.ɵɵrestoreView(_r6); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.expandAll()); });
74
+ i0.ɵɵelement(2, "i", 29);
75
+ i0.ɵɵelementEnd();
76
+ i0.ɵɵelementStart(3, "button", 30);
77
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_Conditional_1_Conditional_3_Template_button_click_3_listener() { i0.ɵɵrestoreView(_r6); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.collapseAll()); });
78
+ i0.ɵɵelement(4, "i", 31);
79
+ i0.ɵɵelementEnd();
80
+ i0.ɵɵelement(5, "div", 32);
81
+ i0.ɵɵelementStart(6, "button", 33);
82
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_Conditional_1_Conditional_3_Template_button_click_6_listener() { i0.ɵɵrestoreView(_r6); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.zoomIn()); });
83
+ i0.ɵɵelement(7, "i", 34);
84
+ i0.ɵɵelementEnd();
85
+ i0.ɵɵelementStart(8, "button", 35);
86
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_Conditional_1_Conditional_3_Template_button_click_8_listener() { i0.ɵɵrestoreView(_r6); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.zoomOut()); });
87
+ i0.ɵɵelement(9, "i", 36);
88
+ i0.ɵɵelementEnd();
89
+ i0.ɵɵelementStart(10, "button", 37);
90
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_Conditional_1_Conditional_3_Template_button_click_10_listener() { i0.ɵɵrestoreView(_r6); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.onFitToViewClick()); });
91
+ i0.ɵɵelement(11, "i", 38);
92
+ i0.ɵɵelementEnd();
93
+ i0.ɵɵelement(12, "div", 32);
94
+ i0.ɵɵelementStart(13, "button", 39);
95
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_Conditional_1_Conditional_3_Template_button_click_13_listener() { i0.ɵɵrestoreView(_r6); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.refresh()); });
96
+ i0.ɵɵelement(14, "i", 40);
97
+ i0.ɵɵelementEnd()();
98
+ } }
99
+ function HierarchyTreeComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
100
+ i0.ɵɵelementStart(0, "div", 3);
101
+ i0.ɵɵconditionalCreate(1, HierarchyTreeComponent_Conditional_1_Conditional_1_Template, 5, 3, "div", 16);
102
+ i0.ɵɵconditionalCreate(2, HierarchyTreeComponent_Conditional_1_Conditional_2_Template, 9, 1, "div", 17);
103
+ i0.ɵɵconditionalCreate(3, HierarchyTreeComponent_Conditional_1_Conditional_3_Template, 15, 0, "div", 18);
104
+ i0.ɵɵelementEnd();
105
+ } if (rf & 2) {
106
+ const ctx_r2 = i0.ɵɵnextContext();
107
+ i0.ɵɵadvance();
108
+ i0.ɵɵconditional(ctx_r2.Config.ShowSearch ? 1 : -1);
109
+ i0.ɵɵadvance();
110
+ i0.ɵɵconditional(ctx_r2.FocusedNode ? 2 : -1);
111
+ i0.ɵɵadvance();
112
+ i0.ɵɵconditional(ctx_r2.Config.ShowToolbar ? 3 : -1);
113
+ } }
114
+ function HierarchyTreeComponent_Conditional_4_Template(rf, ctx) { if (rf & 1) {
115
+ i0.ɵɵelementStart(0, "div", 5);
116
+ i0.ɵɵelement(1, "div", 41);
117
+ i0.ɵɵelementStart(2, "span");
118
+ i0.ɵɵtext(3, "Loading hierarchy...");
119
+ i0.ɵɵelementEnd()();
120
+ } }
121
+ function HierarchyTreeComponent_Conditional_5_Template(rf, ctx) { if (rf & 1) {
122
+ const _r7 = i0.ɵɵgetCurrentView();
123
+ i0.ɵɵelementStart(0, "div", 6);
124
+ i0.ɵɵelement(1, "i", 42);
125
+ i0.ɵɵelementStart(2, "span");
126
+ i0.ɵɵtext(3);
127
+ i0.ɵɵelementEnd();
128
+ i0.ɵɵelementStart(4, "button", 43);
129
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_Conditional_5_Template_button_click_4_listener() { i0.ɵɵrestoreView(_r7); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.loadData()); });
130
+ i0.ɵɵtext(5, "Retry");
131
+ i0.ɵɵelementEnd()();
132
+ } if (rf & 2) {
133
+ const ctx_r2 = i0.ɵɵnextContext();
134
+ i0.ɵɵadvance(3);
135
+ i0.ɵɵtextInterpolate(ctx_r2.ErrorMessage);
136
+ } }
137
+ function HierarchyTreeComponent_Conditional_6_Template(rf, ctx) { if (rf & 1) {
138
+ i0.ɵɵelementStart(0, "div", 7);
139
+ i0.ɵɵelement(1, "i", 44);
140
+ i0.ɵɵelementStart(2, "span");
141
+ i0.ɵɵtext(3, "No hierarchy records found.");
142
+ i0.ɵɵelementEnd()();
143
+ } }
144
+ function HierarchyTreeComponent_For_17_Conditional_0_Conditional_9_Template(rf, ctx) { if (rf & 1) {
145
+ i0.ɵɵelementStart(0, "div", 52);
146
+ i0.ɵɵtext(1);
147
+ i0.ɵɵelementEnd();
148
+ } if (rf & 2) {
149
+ const node_r9 = i0.ɵɵnextContext(2).$implicit;
150
+ i0.ɵɵadvance();
151
+ i0.ɵɵtextInterpolate(node_r9.Subtitle);
152
+ } }
153
+ function HierarchyTreeComponent_For_17_Conditional_0_Conditional_11_Template(rf, ctx) { if (rf & 1) {
154
+ const _r10 = i0.ɵɵgetCurrentView();
155
+ i0.ɵɵelementStart(0, "button", 57);
156
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_For_17_Conditional_0_Conditional_11_Template_button_click_0_listener($event) { i0.ɵɵrestoreView(_r10); const node_r9 = i0.ɵɵnextContext(2).$implicit; const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.toggleNodeExpansion(node_r9, $event)); });
157
+ i0.ɵɵelement(1, "i", 58);
158
+ i0.ɵɵelementStart(2, "span");
159
+ i0.ɵɵtext(3);
160
+ i0.ɵɵelementEnd()();
161
+ } if (rf & 2) {
162
+ const node_r9 = i0.ɵɵnextContext(2).$implicit;
163
+ i0.ɵɵclassProp("collapsed", !node_r9.IsExpanded);
164
+ i0.ɵɵproperty("title", node_r9.IsExpanded ? "Collapse sub-branch" : "Expand " + node_r9.DirectChildCount + " children");
165
+ i0.ɵɵadvance();
166
+ i0.ɵɵclassProp("fa-chevron-down", node_r9.IsExpanded)("fa-chevron-right", !node_r9.IsExpanded);
167
+ i0.ɵɵadvance(2);
168
+ i0.ɵɵtextInterpolate(node_r9.DirectChildCount);
169
+ } }
170
+ function HierarchyTreeComponent_For_17_Conditional_0_Conditional_12_Template(rf, ctx) { if (rf & 1) {
171
+ i0.ɵɵelementStart(0, "span", 55);
172
+ i0.ɵɵtext(1, "Leaf");
173
+ i0.ɵɵelementEnd();
174
+ } }
175
+ function HierarchyTreeComponent_For_17_Conditional_0_Conditional_13_Template(rf, ctx) { if (rf & 1) {
176
+ const _r11 = i0.ɵɵgetCurrentView();
177
+ i0.ɵɵelementStart(0, "div", 56)(1, "button", 59);
178
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_For_17_Conditional_0_Conditional_13_Template_button_click_1_listener($event) { i0.ɵɵrestoreView(_r11); const node_r9 = i0.ɵɵnextContext(2).$implicit; const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.onNodeAction(node_r9, "focus", $event)); });
179
+ i0.ɵɵelement(2, "i", 25);
180
+ i0.ɵɵelementEnd();
181
+ i0.ɵɵelementStart(3, "button", 60);
182
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_For_17_Conditional_0_Conditional_13_Template_button_click_3_listener($event) { i0.ɵɵrestoreView(_r11); const node_r9 = i0.ɵɵnextContext(2).$implicit; const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.onNodeAction(node_r9, "open", $event)); });
183
+ i0.ɵɵelement(4, "i", 61);
184
+ i0.ɵɵelementEnd()();
185
+ } }
186
+ function HierarchyTreeComponent_For_17_Conditional_0_Template(rf, ctx) { if (rf & 1) {
187
+ const _r8 = i0.ɵɵgetCurrentView();
188
+ i0.ɵɵnamespaceSVG();
189
+ i0.ɵɵelementStart(0, "foreignObject", 45);
190
+ i0.ɵɵnamespaceHTML();
191
+ i0.ɵɵelementStart(1, "div", 46);
192
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_For_17_Conditional_0_Template_div_click_1_listener($event) { i0.ɵɵrestoreView(_r8); const node_r9 = i0.ɵɵnextContext().$implicit; const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.onNodeClick(node_r9, $event)); })("dblclick", function HierarchyTreeComponent_For_17_Conditional_0_Template_div_dblclick_1_listener($event) { i0.ɵɵrestoreView(_r8); const node_r9 = i0.ɵɵnextContext().$implicit; const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.onNodeDoubleClick(node_r9, $event)); });
193
+ i0.ɵɵelement(2, "div", 47);
194
+ i0.ɵɵelementStart(3, "div", 48)(4, "div", 49);
195
+ i0.ɵɵelement(5, "i");
196
+ i0.ɵɵelementEnd();
197
+ i0.ɵɵelementStart(6, "div", 50)(7, "div", 51);
198
+ i0.ɵɵtext(8);
199
+ i0.ɵɵelementEnd();
200
+ i0.ɵɵconditionalCreate(9, HierarchyTreeComponent_For_17_Conditional_0_Conditional_9_Template, 2, 1, "div", 52);
201
+ i0.ɵɵelementEnd()();
202
+ i0.ɵɵelementStart(10, "div", 53);
203
+ i0.ɵɵconditionalCreate(11, HierarchyTreeComponent_For_17_Conditional_0_Conditional_11_Template, 4, 8, "button", 54)(12, HierarchyTreeComponent_For_17_Conditional_0_Conditional_12_Template, 2, 0, "span", 55);
204
+ i0.ɵɵconditionalCreate(13, HierarchyTreeComponent_For_17_Conditional_0_Conditional_13_Template, 5, 0, "div", 56);
205
+ i0.ɵɵelementEnd()()();
206
+ } if (rf & 2) {
207
+ const node_r9 = i0.ɵɵnextContext().$implicit;
208
+ const ctx_r2 = i0.ɵɵnextContext();
209
+ i0.ɵɵattribute("x", node_r9.x)("y", node_r9.y)("width", ctx_r2.Config.NodeWidth || 230)("height", ctx_r2.Config.NodeHeight || 90);
210
+ i0.ɵɵadvance();
211
+ i0.ɵɵclassProp("selected", node_r9.IsSelected)("highlighted", node_r9.IsHighlighted)("focus-root", node_r9.IsFocusRoot);
212
+ i0.ɵɵadvance();
213
+ i0.ɵɵstyleProp("background", node_r9.Color || ctx_r2.Config.DefaultColor);
214
+ i0.ɵɵadvance(2);
215
+ i0.ɵɵstyleProp("color", node_r9.Color || ctx_r2.Config.DefaultColor);
216
+ i0.ɵɵadvance();
217
+ i0.ɵɵclassMap(node_r9.Icon || ctx_r2.Config.DefaultIcon);
218
+ i0.ɵɵadvance(2);
219
+ i0.ɵɵproperty("title", node_r9.Name);
220
+ i0.ɵɵadvance();
221
+ i0.ɵɵtextInterpolate(node_r9.Name);
222
+ i0.ɵɵadvance();
223
+ i0.ɵɵconditional(node_r9.Subtitle ? 9 : -1);
224
+ i0.ɵɵadvance(2);
225
+ i0.ɵɵconditional(node_r9.DirectChildCount > 0 ? 11 : 12);
226
+ i0.ɵɵadvance(2);
227
+ i0.ɵɵconditional(ctx_r2.AllowNodeActions ? 13 : -1);
228
+ } }
229
+ function HierarchyTreeComponent_For_17_Template(rf, ctx) { if (rf & 1) {
230
+ i0.ɵɵconditionalCreate(0, HierarchyTreeComponent_For_17_Conditional_0_Template, 14, 21, ":svg:foreignObject", 45);
231
+ } if (rf & 2) {
232
+ const node_r9 = ctx.$implicit;
233
+ i0.ɵɵconditional(node_r9.x != null && node_r9.y != null ? 0 : -1);
234
+ } }
235
+ function HierarchyTreeComponent_Conditional_19_Conditional_7_Template(rf, ctx) { if (rf & 1) {
236
+ i0.ɵɵelementStart(0, "p", 67);
237
+ i0.ɵɵtext(1);
238
+ i0.ɵɵelementEnd();
239
+ } if (rf & 2) {
240
+ const ctx_r2 = i0.ɵɵnextContext(2);
241
+ i0.ɵɵadvance();
242
+ i0.ɵɵtextInterpolate(ctx_r2.SelectedNode.Subtitle);
243
+ } }
244
+ function HierarchyTreeComponent_Conditional_19_Conditional_27_For_5_Template(rf, ctx) { if (rf & 1) {
245
+ i0.ɵɵelementStart(0, "div", 82)(1, "dt", 83);
246
+ i0.ɵɵtext(2);
247
+ i0.ɵɵelementEnd();
248
+ i0.ɵɵelementStart(3, "dd", 84);
249
+ i0.ɵɵtext(4);
250
+ i0.ɵɵelementEnd()();
251
+ } if (rf & 2) {
252
+ const attr_r13 = ctx.$implicit;
253
+ i0.ɵɵadvance(2);
254
+ i0.ɵɵtextInterpolate(attr_r13.label);
255
+ i0.ɵɵadvance();
256
+ i0.ɵɵproperty("title", attr_r13.value);
257
+ i0.ɵɵadvance();
258
+ i0.ɵɵtextInterpolate(attr_r13.value);
259
+ } }
260
+ function HierarchyTreeComponent_Conditional_19_Conditional_27_Template(rf, ctx) { if (rf & 1) {
261
+ i0.ɵɵelementStart(0, "div", 74)(1, "h5", 80);
262
+ i0.ɵɵtext(2, "Attributes");
263
+ i0.ɵɵelementEnd();
264
+ i0.ɵɵelementStart(3, "dl", 81);
265
+ i0.ɵɵrepeaterCreate(4, HierarchyTreeComponent_Conditional_19_Conditional_27_For_5_Template, 5, 3, "div", 82, _forTrack1);
266
+ i0.ɵɵelementEnd()();
267
+ } if (rf & 2) {
268
+ const ctx_r2 = i0.ɵɵnextContext(2);
269
+ i0.ɵɵadvance(4);
270
+ i0.ɵɵrepeater(ctx_r2.getRecordAttributeEntries(ctx_r2.SelectedNode.Record));
271
+ } }
272
+ function HierarchyTreeComponent_Conditional_19_Conditional_34_Template(rf, ctx) { if (rf & 1) {
273
+ const _r14 = i0.ɵɵgetCurrentView();
274
+ i0.ɵɵelementStart(0, "button", 85);
275
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_Conditional_19_Conditional_34_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r14); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.toggleNodeExpansion(ctx_r2.SelectedNode)); });
276
+ i0.ɵɵelement(1, "i", 58);
277
+ i0.ɵɵelementStart(2, "span");
278
+ i0.ɵɵtext(3);
279
+ i0.ɵɵelementEnd()();
280
+ } if (rf & 2) {
281
+ const ctx_r2 = i0.ɵɵnextContext(2);
282
+ i0.ɵɵadvance();
283
+ i0.ɵɵclassProp("fa-folder-open", ctx_r2.SelectedNode.IsExpanded)("fa-folder", !ctx_r2.SelectedNode.IsExpanded);
284
+ i0.ɵɵadvance(2);
285
+ i0.ɵɵtextInterpolate(ctx_r2.SelectedNode.IsExpanded ? "Collapse" : "Expand");
286
+ } }
287
+ function HierarchyTreeComponent_Conditional_19_Template(rf, ctx) { if (rf & 1) {
288
+ const _r12 = i0.ɵɵgetCurrentView();
289
+ i0.ɵɵelementStart(0, "div", 62)(1, "div", 63)(2, "div", 64);
290
+ i0.ɵɵelement(3, "i");
291
+ i0.ɵɵelementEnd();
292
+ i0.ɵɵelementStart(4, "div", 65)(5, "h4", 66);
293
+ i0.ɵɵtext(6);
294
+ i0.ɵɵelementEnd();
295
+ i0.ɵɵconditionalCreate(7, HierarchyTreeComponent_Conditional_19_Conditional_7_Template, 2, 1, "p", 67);
296
+ i0.ɵɵelementEnd()();
297
+ i0.ɵɵelementStart(8, "button", 68);
298
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_Conditional_19_Template_button_click_8_listener() { i0.ɵɵrestoreView(_r12); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.closeDetailsDrawer()); });
299
+ i0.ɵɵelement(9, "i", 24);
300
+ i0.ɵɵelementEnd()();
301
+ i0.ɵɵelementStart(10, "div", 69)(11, "div", 70)(12, "div", 71)(13, "span", 72);
302
+ i0.ɵɵtext(14);
303
+ i0.ɵɵelementEnd();
304
+ i0.ɵɵelementStart(15, "span", 73);
305
+ i0.ɵɵtext(16, "Hierarchy Level");
306
+ i0.ɵɵelementEnd()();
307
+ i0.ɵɵelementStart(17, "div", 71)(18, "span", 72);
308
+ i0.ɵɵtext(19);
309
+ i0.ɵɵelementEnd();
310
+ i0.ɵɵelementStart(20, "span", 73);
311
+ i0.ɵɵtext(21, "Direct Children");
312
+ i0.ɵɵelementEnd()();
313
+ i0.ɵɵelementStart(22, "div", 71)(23, "span", 72);
314
+ i0.ɵɵtext(24);
315
+ i0.ɵɵelementEnd();
316
+ i0.ɵɵelementStart(25, "span", 73);
317
+ i0.ɵɵtext(26, "Total Subtree");
318
+ i0.ɵɵelementEnd()()();
319
+ i0.ɵɵconditionalCreate(27, HierarchyTreeComponent_Conditional_19_Conditional_27_Template, 6, 0, "div", 74);
320
+ i0.ɵɵelementStart(28, "div", 75)(29, "button", 76);
321
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_Conditional_19_Template_button_click_29_listener() { i0.ɵɵrestoreView(_r12); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.navigateToRecord(ctx_r2.SelectedNode)); });
322
+ i0.ɵɵelement(30, "i", 61);
323
+ i0.ɵɵelementStart(31, "span");
324
+ i0.ɵɵtext(32, "Open Record");
325
+ i0.ɵɵelementEnd()();
326
+ i0.ɵɵelementStart(33, "div", 77);
327
+ i0.ɵɵconditionalCreate(34, HierarchyTreeComponent_Conditional_19_Conditional_34_Template, 4, 5, "button", 78);
328
+ i0.ɵɵelementStart(35, "button", 79);
329
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_Conditional_19_Template_button_click_35_listener() { i0.ɵɵrestoreView(_r12); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.setFocusRoot(ctx_r2.SelectedNode.ID)); });
330
+ i0.ɵɵelement(36, "i", 25);
331
+ i0.ɵɵelementStart(37, "span");
332
+ i0.ɵɵtext(38, "Focus Tree");
333
+ i0.ɵɵelementEnd()()()()();
334
+ } if (rf & 2) {
335
+ const ctx_r2 = i0.ɵɵnextContext();
336
+ i0.ɵɵadvance(2);
337
+ i0.ɵɵstyleProp("color", ctx_r2.SelectedNode.Color || ctx_r2.Config.DefaultColor);
338
+ i0.ɵɵadvance();
339
+ i0.ɵɵclassMap(ctx_r2.SelectedNode.Icon || ctx_r2.Config.DefaultIcon);
340
+ i0.ɵɵadvance(2);
341
+ i0.ɵɵproperty("title", ctx_r2.SelectedNode.Name);
342
+ i0.ɵɵadvance();
343
+ i0.ɵɵtextInterpolate(ctx_r2.SelectedNode.Name);
344
+ i0.ɵɵadvance();
345
+ i0.ɵɵconditional(ctx_r2.SelectedNode.Subtitle ? 7 : -1);
346
+ i0.ɵɵadvance(7);
347
+ i0.ɵɵtextInterpolate(ctx_r2.SelectedNode.Depth);
348
+ i0.ɵɵadvance(5);
349
+ i0.ɵɵtextInterpolate(ctx_r2.SelectedNode.DirectChildCount);
350
+ i0.ɵɵadvance(5);
351
+ i0.ɵɵtextInterpolate(ctx_r2.SelectedNode.TotalDescendantCount);
352
+ i0.ɵɵadvance(3);
353
+ i0.ɵɵconditional(ctx_r2.SelectedNode.Record && ctx_r2.getRecordAttributeEntries(ctx_r2.SelectedNode.Record).length > 0 ? 27 : -1);
354
+ i0.ɵɵadvance(7);
355
+ i0.ɵɵconditional(ctx_r2.SelectedNode.DirectChildCount > 0 ? 34 : -1);
356
+ } }
357
+ /**
358
+ * Modern, interactive, canvas-based visual hierarchy and organizational chart component.
359
+ *
360
+ * Automatically generates a dynamic, zoomable tree visualization for any MemberJunction entity
361
+ * with a self-referencing parent foreign key (such as Corporate Subsidiaries, Product Categories,
362
+ * Committee Governance structures, Tag Taxonomies, and Management Lines).
363
+ *
364
+ * Features:
365
+ * - **Metadata Driven**: Auto-derives primary keys (including composite keys) and parent fields from MJ metadata.
366
+ * - **Interactive Canvas**: D3-powered smooth pan, zoom, auto-fit, and mini-toolbar controls.
367
+ * - **Collapsible Subtrees**: Interactive `[+]` / `[-]` badges with direct and total descendant counts.
368
+ * - **Live Search & Path Expansion**: Instant keyword search that highlights matches and auto-expands ancestor branches.
369
+ * - **Subtree Focus**: Isolate any node as the root of the view with 1-click return to full hierarchy.
370
+ * - **Full Design Token Theming**: Uses `--mj-*` surface, border, typography, and accent tokens.
371
+ * - **Extensive Cancelable Events**: Before/After events for expansion, collapse, reparenting, and navigation.
372
+ *
373
+ * @example
374
+ * ```html
375
+ * <mj-hierarchy-tree
376
+ * [Config]="{
377
+ * EntityName: 'MJ_BizApps_Common: Organizations',
378
+ * ParentField: 'ParentID',
379
+ * SubtitleField: 'OrganizationType',
380
+ * DefaultIcon: 'fa-solid fa-building'
381
+ * }"
382
+ * (Navigate)="onNavigate($event)">
383
+ * </mj-hierarchy-tree>
384
+ * ```
385
+ */
386
+ export class HierarchyTreeComponent extends BaseAngularComponent {
387
+ cdr;
388
+ constructor(cdr) {
389
+ super();
390
+ this.cdr = cdr;
391
+ }
392
+ svgContainerRef;
393
+ svgRef;
394
+ /**
395
+ * Declarative configuration defining the target entity and visual layout properties.
396
+ */
397
+ Config;
398
+ /**
399
+ * Optional pre-loaded data array. If provided, the component skips the automated
400
+ * `RunView` query and constructs the tree directly from these items.
401
+ */
402
+ Data;
403
+ /**
404
+ * Optional active / selected record ID. When provided, the node is highlighted and all ancestors
405
+ * are expanded so it is visible in full tree context.
406
+ */
407
+ ActiveRecordID;
408
+ /**
409
+ * Primary key ID to focus as the subtree root.
410
+ */
411
+ FocusRecordID;
412
+ /**
413
+ * Controls whether action buttons (Open Record, Focus, Add Child) appear on node cards.
414
+ */
415
+ AllowNodeActions = true;
416
+ /**
417
+ * Emitted when a node is clicked.
418
+ */
419
+ NodeClick = new EventEmitter();
420
+ /**
421
+ * Emitted when a node is double-clicked.
422
+ */
423
+ NodeDoubleClick = new EventEmitter();
424
+ /**
425
+ * Emitted when a node is selected / set as active.
426
+ */
427
+ NodeSelect = new EventEmitter();
428
+ /**
429
+ * Emitted before a node expands its children. Cancelable.
430
+ */
431
+ BeforeNodeExpand = new EventEmitter();
432
+ /**
433
+ * Emitted after a node has expanded its children.
434
+ */
435
+ AfterNodeExpand = new EventEmitter();
436
+ /**
437
+ * Emitted before a node collapses its children. Cancelable.
438
+ */
439
+ BeforeNodeCollapse = new EventEmitter();
440
+ /**
441
+ * Emitted after a node has collapsed its children.
442
+ */
443
+ AfterNodeCollapse = new EventEmitter();
444
+ /**
445
+ * Emitted before a node is reparented. Cancelable.
446
+ */
447
+ BeforeReparent = new EventEmitter();
448
+ /**
449
+ * Emitted after a node has been reparented.
450
+ */
451
+ AfterReparent = new EventEmitter();
452
+ /**
453
+ * Emitted when an action button (e.g. Open, Add Child, Focus) is invoked on a node.
454
+ */
455
+ NodeAction = new EventEmitter();
456
+ /**
457
+ * Optional persisted zoom scale factor to restore.
458
+ */
459
+ ZoomLevel;
460
+ /**
461
+ * Emitted when the user zooms in or out on the hierarchy canvas.
462
+ */
463
+ ZoomChange = new EventEmitter();
464
+ /**
465
+ * Emitted when the user requests navigation to a full entity record.
466
+ */
467
+ Navigate = new EventEmitter();
468
+ // --- State ---
469
+ Loading = false;
470
+ ErrorMessage = null;
471
+ SearchQuery = '';
472
+ MatchingNodeCount = 0;
473
+ FocusedNode = null;
474
+ SelectedNode = null;
475
+ ShowDetailsDrawer = false;
476
+ /** Flattened list of all loaded hierarchy nodes for search and lookup. */
477
+ AllNodes = [];
478
+ nodeMap = new Map();
479
+ /** The root nodes of the active view. */
480
+ RootNodes = [];
481
+ /** D3 zoom behavior and SVG group selection */
482
+ zoomBehavior = null;
483
+ svgSelection = null;
484
+ gSelection = null;
485
+ currentZoomTransform = d3.zoomIdentity;
486
+ resizeObserver = null;
487
+ entityInfo = null;
488
+ previousConfigState = {};
489
+ ngOnInit() {
490
+ this.initializeConfig();
491
+ this.log('[HierarchyTree:ngOnInit] Initializing with Config:', this.Config);
492
+ if (this.Config) {
493
+ this.previousConfigState = {
494
+ EntityName: this.Config.EntityName,
495
+ ExtraFilter: this.Config.ExtraFilter,
496
+ OrderBy: this.Config.OrderBy,
497
+ MaxRows: this.Config.MaxRows,
498
+ ParentField: this.Config.ParentField
499
+ };
500
+ }
501
+ this.loadData();
502
+ }
503
+ ngAfterViewInit() {
504
+ this.log('[HierarchyTree:ngAfterViewInit] Initializing D3 Zoom & ResizeObserver. svgRef:', !!this.svgRef?.nativeElement, 'containerRect:', this.svgContainerRef?.nativeElement?.getBoundingClientRect());
505
+ this.initD3Zoom();
506
+ this.setupResizeObserver();
507
+ if (this.AllNodes.length > 0) {
508
+ this.log('[HierarchyTree:ngAfterViewInit] AllNodes already loaded (' + this.AllNodes.length + '), triggering renderTree');
509
+ this.renderTree();
510
+ }
511
+ }
512
+ ngOnChanges(changes) {
513
+ this.log('[HierarchyTree:ngOnChanges] Changes detected:', Object.keys(changes));
514
+ if (changes['Config'] && !changes['Config'].isFirstChange()) {
515
+ this.initializeConfig();
516
+ if (this.hasDataConfigChanged(this.Config)) {
517
+ this.log('[HierarchyTree:ngOnChanges] Data config changed, triggering loadData');
518
+ this.loadData();
519
+ }
520
+ else {
521
+ const activeId = this.ActiveRecordID || this.Config?.ActiveRecordID;
522
+ this.log('[HierarchyTree:ngOnChanges] Config changed but data identical, updating active node:', activeId);
523
+ this.updateActiveNodeSelection(activeId);
524
+ }
525
+ }
526
+ else if (changes['ActiveRecordID'] && !changes['ActiveRecordID'].isFirstChange()) {
527
+ this.updateActiveNodeSelection(this.ActiveRecordID);
528
+ }
529
+ else if (changes['Data'] && !changes['Data'].isFirstChange()) {
530
+ this.buildTreeFromData(this.Data || []);
531
+ }
532
+ else if (changes['FocusRecordID'] && !changes['FocusRecordID'].isFirstChange()) {
533
+ this.setFocusRoot(this.FocusRecordID);
534
+ }
535
+ else if (changes['ZoomLevel'] && !changes['ZoomLevel'].isFirstChange() && this.ZoomLevel !== undefined) {
536
+ this.setZoomLevel(this.ZoomLevel, true);
537
+ }
538
+ }
539
+ hasDataConfigChanged(newConfig) {
540
+ if (!newConfig)
541
+ return false;
542
+ const prev = this.previousConfigState;
543
+ const hasChanged = prev.EntityName !== newConfig.EntityName ||
544
+ prev.ExtraFilter !== newConfig.ExtraFilter ||
545
+ prev.OrderBy !== newConfig.OrderBy ||
546
+ prev.MaxRows !== newConfig.MaxRows ||
547
+ prev.ParentField !== newConfig.ParentField;
548
+ if (hasChanged) {
549
+ this.previousConfigState = {
550
+ EntityName: newConfig.EntityName,
551
+ ExtraFilter: newConfig.ExtraFilter,
552
+ OrderBy: newConfig.OrderBy,
553
+ MaxRows: newConfig.MaxRows,
554
+ ParentField: newConfig.ParentField
555
+ };
556
+ }
557
+ return hasChanged;
558
+ }
559
+ updateActiveNodeSelection(activeId) {
560
+ if (!activeId)
561
+ return;
562
+ const activeNode = this.nodeMap.get(NormalizeUUID(activeId));
563
+ if (activeNode && (!this.SelectedNode || !UUIDsEqual(this.SelectedNode.ID, activeNode.ID))) {
564
+ for (const n of this.AllNodes) {
565
+ n.IsSelected = UUIDsEqual(n.ID, activeNode.ID);
566
+ }
567
+ this.SelectedNode = activeNode;
568
+ this.expandAncestors(activeNode);
569
+ this.renderTree();
570
+ }
571
+ }
572
+ ngOnDestroy() {
573
+ if (this.resizeObserver) {
574
+ this.resizeObserver.disconnect();
575
+ }
576
+ }
577
+ // --- Configuration Initialization ---
578
+ initializeConfig() {
579
+ if (!this.Config)
580
+ return;
581
+ this.Config = {
582
+ DefaultIcon: 'fa-solid fa-sitemap',
583
+ // Design token, not a literal — the accent is bound to [style.background]/[style.color],
584
+ // so a var() resolves at paint time and follows the active theme. The fallback keeps the
585
+ // original rendering wherever the token stylesheet isn't loaded.
586
+ DefaultColor: 'var(--mj-brand-primary, #38bdf8)',
587
+ Orientation: 'top-to-bottom',
588
+ NodeStyle: 'card',
589
+ AllowDragDropReparent: false,
590
+ AllowNodeActions: true,
591
+ ShowSearch: true,
592
+ ShowToolbar: true,
593
+ NodeWidth: 230,
594
+ NodeHeight: 90,
595
+ SiblingSpacing: 40,
596
+ LevelSpacing: 75,
597
+ Height: '100%',
598
+ MinHeight: '500px',
599
+ NavigateOnNodeClick: true,
600
+ AutoRootFocus: true,
601
+ Verbose: false,
602
+ ...this.Config
603
+ };
604
+ }
605
+ log(message, ...args) {
606
+ if (this.Config?.Verbose) {
607
+ console.log(message, ...args);
608
+ }
609
+ }
610
+ // --- Data Loading & Tree Construction ---
611
+ /**
612
+ * Loads records from the server via `RunView` using entity metadata.
613
+ */
614
+ async loadData() {
615
+ if (!this.Config?.EntityName)
616
+ return;
617
+ this.Loading = true;
618
+ this.ErrorMessage = null;
619
+ this.cdr?.detectChanges();
620
+ try {
621
+ // ProviderToUse, not `new Metadata()` — this is an L1 widget, so it must read metadata
622
+ // from whatever provider the host bound via @Input() Provider (BaseAngularComponent
623
+ // falls back to the ambient one). Binding the global provider here would silently query
624
+ // the wrong database wherever the component is hosted against a non-default connection.
625
+ const configEntityLower = this.Config.EntityName.toLowerCase();
626
+ const configEntityStripped = this.Config.EntityName.replace(/^mj[:_\s]+/i, '').trim().toLowerCase();
627
+ this.entityInfo = this.ProviderToUse.Entities.find((e) => {
628
+ const eNameLower = e.Name.toLowerCase();
629
+ const eNameStripped = e.Name.replace(/^mj[:_\s]+/i, '').trim().toLowerCase();
630
+ return eNameLower === configEntityLower || eNameStripped === configEntityStripped;
631
+ }) || null;
632
+ const targetEntityName = this.entityInfo ? this.entityInfo.Name : this.Config.EntityName;
633
+ this.log('[HierarchyTree:loadData] Starting load for:', targetEntityName, {
634
+ configuredEntity: this.Config.EntityName,
635
+ resolvedEntity: this.entityInfo?.Name,
636
+ activeRecordId: this.ActiveRecordID || this.Config?.ActiveRecordID,
637
+ focusRecordId: this.FocusRecordID || this.Config?.FocusRecordID,
638
+ extraFilter: this.Config.ExtraFilter
639
+ });
640
+ if (this.Data) {
641
+ this.log('[HierarchyTree:loadData] Using provided Data prop:', this.Data.length, 'items');
642
+ this.buildTreeFromData(this.Data);
643
+ return;
644
+ }
645
+ const rv = RunView.FromMetadataProvider(this.ProviderToUse);
646
+ const defaultOrderBy = `${this.resolveNameFieldName()} ASC`;
647
+ const rvParams = {
648
+ EntityName: targetEntityName,
649
+ ExtraFilter: this.Config.ExtraFilter || '',
650
+ OrderBy: this.Config.OrderBy || defaultOrderBy,
651
+ ResultType: 'simple',
652
+ MaxRows: this.Config.MaxRows ?? 0
653
+ };
654
+ this.log('[HierarchyTree:loadData] Calling RunView with params:', rvParams);
655
+ const result = await rv.RunView(rvParams);
656
+ this.log('[HierarchyTree:loadData] RunView response:', {
657
+ success: result.Success,
658
+ rowCount: result.RowCount,
659
+ resultsLength: result.Results?.length,
660
+ errorMessage: result.ErrorMessage,
661
+ sampleRow: result.Results?.[0]
662
+ });
663
+ if (!result.Success) {
664
+ this.ErrorMessage = result.ErrorMessage || 'Failed to load hierarchy data.';
665
+ return;
666
+ }
667
+ const rawItems = result.Results || [];
668
+ this.buildTreeFromData(rawItems);
669
+ }
670
+ catch (err) {
671
+ this.ErrorMessage = err instanceof Error ? err.message : 'Error loading hierarchy tree.';
672
+ console.error('[HierarchyTree:loadData] Error:', err);
673
+ }
674
+ finally {
675
+ this.Loading = false;
676
+ this.cdr?.detectChanges();
677
+ setTimeout(() => {
678
+ this.renderTree();
679
+ this.fitToScreen(false);
680
+ }, 80);
681
+ }
682
+ }
683
+ /**
684
+ * Helper to retrieve a property value from a record item in a case-insensitive manner.
685
+ */
686
+ getItemValue(item, fieldName) {
687
+ if (!item || !fieldName)
688
+ return undefined;
689
+ if (item[fieldName] !== undefined)
690
+ return item[fieldName];
691
+ const lower = fieldName.toLowerCase();
692
+ for (const [k, v] of Object.entries(item)) {
693
+ if (k.toLowerCase() === lower)
694
+ return v;
695
+ }
696
+ return undefined;
697
+ }
698
+ /**
699
+ * Constructs the in-memory tree nodes, computes composite keys, descendant counts, and depths.
700
+ */
701
+ buildTreeFromData(items) {
702
+ this.nodeMap.clear();
703
+ this.AllNodes = [];
704
+ const parentFieldName = this.resolveParentFieldName();
705
+ const nameFieldName = this.resolveNameFieldName();
706
+ const subtitleFieldName = this.Config?.SubtitleField;
707
+ const iconFieldName = this.Config?.IconField;
708
+ const colorFieldName = this.Config?.ColorField;
709
+ this.log(`[HierarchyTree:buildTreeFromData] Input items: ${items.length}, parentField="${parentFieldName}", nameField="${nameFieldName}"`);
710
+ // 1. Create flat nodes
711
+ for (const item of items) {
712
+ const { pk, id } = this.extractPrimaryKey(item);
713
+ const parentVal = this.getItemValue(item, parentFieldName);
714
+ const parentId = parentVal != null && String(parentVal).trim() !== '' ? String(parentVal) : null;
715
+ const nameVal = this.getItemValue(item, nameFieldName) ?? this.getItemValue(item, 'Name') ?? 'Unnamed';
716
+ const name = String(nameVal);
717
+ const subtitleVal = this.getItemValue(item, subtitleFieldName);
718
+ const subtitle = subtitleVal != null ? String(subtitleVal) : undefined;
719
+ const iconVal = this.getItemValue(item, iconFieldName);
720
+ const icon = iconVal != null ? String(iconVal) : this.Config.DefaultIcon;
721
+ const colorVal = this.getItemValue(item, colorFieldName);
722
+ const color = colorVal != null ? String(colorVal) : this.Config.DefaultColor;
723
+ const node = {
724
+ ID: id,
725
+ PrimaryKey: pk,
726
+ Name: name,
727
+ Subtitle: subtitle || undefined,
728
+ Icon: icon,
729
+ Color: color,
730
+ ParentID: parentId,
731
+ DirectChildCount: 0,
732
+ TotalDescendantCount: 0,
733
+ Depth: 0,
734
+ IsExpanded: true,
735
+ IsSelected: false,
736
+ IsHighlighted: false,
737
+ IsFocusRoot: false,
738
+ Record: item,
739
+ Children: []
740
+ };
741
+ this.nodeMap.set(NormalizeUUID(id), node);
742
+ this.AllNodes.push(node);
743
+ }
744
+ // 2. Build parent-child relationships and detect cycles
745
+ const roots = [];
746
+ for (const node of this.AllNodes) {
747
+ if (node.ParentID && this.nodeMap.has(NormalizeUUID(node.ParentID))) {
748
+ const parentNode = this.nodeMap.get(NormalizeUUID(node.ParentID));
749
+ if (!this.wouldCreateCycle(node, parentNode)) {
750
+ parentNode.Children.push(node);
751
+ }
752
+ else {
753
+ console.warn(`[HierarchyTree] Circular reference detected between ${node.ID} and ${parentNode.ID}. Promoting to root.`);
754
+ roots.push(node);
755
+ }
756
+ }
757
+ else {
758
+ roots.push(node);
759
+ }
760
+ }
761
+ // 3. Compute counts and depths
762
+ for (const root of roots) {
763
+ this.computeSubtreeMetrics(root, 0);
764
+ }
765
+ // 4. Preserve _allChildren for collapse/expand
766
+ for (const node of this.AllNodes) {
767
+ node._allChildren = [...node.Children];
768
+ }
769
+ this.RootNodes = roots;
770
+ this.log(`[HierarchyTree:buildTreeFromData] Built ${this.AllNodes.length} nodes, ${roots.length} root(s):`, roots.map(r => ({ Name: r.Name, ID: r.ID, Children: r.Children.length, TotalDescendants: r.TotalDescendantCount })));
771
+ // 5. Apply active record selection & auto-expand ancestors
772
+ const activeId = this.ActiveRecordID || this.Config?.ActiveRecordID;
773
+ if (activeId) {
774
+ const activeNode = this.nodeMap.get(NormalizeUUID(activeId));
775
+ if (activeNode) {
776
+ activeNode.IsSelected = true;
777
+ activeNode.IsHighlighted = true;
778
+ this.SelectedNode = activeNode;
779
+ // Expand all ancestors so the active node is visible in full context
780
+ let curr = activeNode.ParentID ? this.nodeMap.get(NormalizeUUID(activeNode.ParentID)) : undefined;
781
+ while (curr) {
782
+ curr.IsExpanded = true;
783
+ curr.Children = [...(curr._allChildren || [])];
784
+ curr = curr.ParentID ? this.nodeMap.get(NormalizeUUID(curr.ParentID)) : undefined;
785
+ }
786
+ // If AutoRootFocus is enabled and multiple roots exist, focus on the active root tree
787
+ if (this.Config.AutoRootFocus !== false && roots.length > 1 && !this.FocusRecordID && !this.Config.FocusRecordID) {
788
+ let rootAncestor = activeNode;
789
+ while (rootAncestor.ParentID && this.nodeMap.has(NormalizeUUID(rootAncestor.ParentID))) {
790
+ rootAncestor = this.nodeMap.get(NormalizeUUID(rootAncestor.ParentID));
791
+ }
792
+ rootAncestor.IsFocusRoot = true;
793
+ this.FocusedNode = rootAncestor;
794
+ }
795
+ }
796
+ }
797
+ // 6. Apply initial expand depth if configured (and not explicitly expanded by active node)
798
+ if (this.Config.InitialExpandDepth && this.Config.InitialExpandDepth > 0 && !activeId) {
799
+ for (const node of this.AllNodes) {
800
+ if (node.Depth >= this.Config.InitialExpandDepth) {
801
+ node.IsExpanded = false;
802
+ node.Children = [];
803
+ }
804
+ }
805
+ }
806
+ if (this.FocusRecordID || this.Config?.FocusRecordID) {
807
+ this.setFocusRoot(this.FocusRecordID || this.Config?.FocusRecordID);
808
+ }
809
+ }
810
+ resolveParentFieldName() {
811
+ if (this.Config?.ParentField)
812
+ return this.Config.ParentField;
813
+ if (this.entityInfo) {
814
+ // Check self-referencing foreign keys
815
+ const selfFk = this.entityInfo.Fields.find((f) => f.RelatedEntity && f.RelatedEntity.toLowerCase() === this.entityInfo.Name.toLowerCase());
816
+ if (selfFk)
817
+ return selfFk.Name;
818
+ }
819
+ return 'ParentID';
820
+ }
821
+ resolveNameFieldName() {
822
+ if (this.Config?.NameField)
823
+ return this.Config.NameField;
824
+ if (this.entityInfo?.NameField)
825
+ return this.entityInfo.NameField.Name;
826
+ return 'Name';
827
+ }
828
+ extractPrimaryKey(item) {
829
+ const pk = new CompositeKey();
830
+ if (this.entityInfo && this.entityInfo.PrimaryKeys.length > 0) {
831
+ for (const k of this.entityInfo.PrimaryKeys) {
832
+ pk.KeyValuePairs.push({
833
+ FieldName: k.Name,
834
+ Value: this.getItemValue(item, k.Name)
835
+ });
836
+ }
837
+ }
838
+ else {
839
+ pk.KeyValuePairs.push({
840
+ FieldName: 'ID',
841
+ Value: this.getItemValue(item, 'ID') ?? this.getItemValue(item, 'id') ?? ''
842
+ });
843
+ }
844
+ const id = pk.KeyValuePairs.length === 1 ? String(pk.KeyValuePairs[0].Value) : pk.ToURLSegment();
845
+ return { pk, id };
846
+ }
847
+ wouldCreateCycle(node, proposedParent) {
848
+ let curr = proposedParent;
849
+ while (curr) {
850
+ if (UUIDsEqual(curr.ID, node.ID))
851
+ return true;
852
+ curr = curr.ParentID ? this.nodeMap.get(NormalizeUUID(curr.ParentID)) : undefined;
853
+ }
854
+ return false;
855
+ }
856
+ computeSubtreeMetrics(node, depth) {
857
+ node.Depth = depth;
858
+ node.DirectChildCount = node.Children.length;
859
+ let descendantCount = node.Children.length;
860
+ for (const child of node.Children) {
861
+ descendantCount += this.computeSubtreeMetrics(child, depth + 1);
862
+ }
863
+ node.TotalDescendantCount = descendantCount;
864
+ return descendantCount;
865
+ }
866
+ // --- D3 Layout & Visual Rendering ---
867
+ initD3Zoom() {
868
+ if (!this.svgRef?.nativeElement)
869
+ return;
870
+ this.svgSelection = d3.select(this.svgRef.nativeElement);
871
+ this.gSelection = this.svgSelection.select('g.mj-hierarchy-canvas');
872
+ this.zoomBehavior = d3.zoom()
873
+ .scaleExtent([0.1, 4.0])
874
+ .on('zoom', (event) => {
875
+ this.currentZoomTransform = event.transform;
876
+ if (this.gSelection) {
877
+ this.gSelection.attr('transform', event.transform.toString());
878
+ }
879
+ if (event.sourceEvent != null) {
880
+ this.ZoomLevel = event.transform.k;
881
+ this.ZoomChange.emit(event.transform.k);
882
+ }
883
+ });
884
+ this.svgSelection.call(this.zoomBehavior);
885
+ }
886
+ setupResizeObserver() {
887
+ if (!this.svgContainerRef?.nativeElement)
888
+ return;
889
+ this.resizeObserver = new ResizeObserver((entries) => {
890
+ for (const entry of entries) {
891
+ if (entry.contentRect.width > 50 && entry.contentRect.height > 50) {
892
+ if (this.AllNodes.length > 0) {
893
+ this.renderTree();
894
+ }
895
+ }
896
+ }
897
+ });
898
+ this.resizeObserver.observe(this.svgContainerRef.nativeElement);
899
+ }
900
+ /**
901
+ * Computes the D3 hierarchy tree layout and updates the SVG rendering.
902
+ */
903
+ renderTree(preserveTransform = false) {
904
+ if (!this.svgRef?.nativeElement || !this.gSelection) {
905
+ if (this.svgRef?.nativeElement) {
906
+ this.initD3Zoom();
907
+ }
908
+ if (!this.gSelection) {
909
+ setTimeout(() => this.renderTree(preserveTransform), 30);
910
+ return;
911
+ }
912
+ }
913
+ // Reset positions across all nodes
914
+ for (const n of this.AllNodes) {
915
+ n.x = undefined;
916
+ n.y = undefined;
917
+ }
918
+ const isVertical = this.Config.Orientation !== 'left-to-right';
919
+ const nodeWidth = this.Config.NodeWidth || 230;
920
+ const nodeHeight = this.Config.NodeHeight || 90;
921
+ const siblingGap = this.Config.SiblingSpacing || 40;
922
+ const levelGap = this.Config.LevelSpacing || 75;
923
+ const activeRoots = this.FocusedNode ? [this.FocusedNode] : this.RootNodes;
924
+ this.log(`[HierarchyTree:renderTree] Starting render for ${activeRoots.length} active root(s):`, activeRoots.map(r => r.Name));
925
+ if (activeRoots.length === 0) {
926
+ this.gSelection.selectAll('*').remove();
927
+ return;
928
+ }
929
+ // 1. Create root hierarchy
930
+ let rootHierarchy;
931
+ if (activeRoots.length === 1) {
932
+ rootHierarchy = d3.hierarchy(activeRoots[0], (d) => (d.IsExpanded ? d.Children : []));
933
+ }
934
+ else {
935
+ // Multiple roots: wrap under a synthetic virtual root
936
+ const virtualRoot = {
937
+ ID: '__virtual_root__',
938
+ PrimaryKey: new CompositeKey(),
939
+ Name: 'Root',
940
+ DirectChildCount: activeRoots.length,
941
+ TotalDescendantCount: activeRoots.length,
942
+ Depth: -1,
943
+ IsExpanded: true,
944
+ IsSelected: false,
945
+ IsHighlighted: false,
946
+ IsFocusRoot: false,
947
+ Children: activeRoots
948
+ };
949
+ rootHierarchy = d3.hierarchy(virtualRoot, (d) => (d.IsExpanded ? d.Children : []));
950
+ }
951
+ // 2. Compute tree layout
952
+ const treeLayout = d3.tree()
953
+ .nodeSize(isVertical ? [nodeWidth + siblingGap, nodeHeight + levelGap] : [nodeHeight + siblingGap, nodeWidth + levelGap])
954
+ .separation((a, b) => (a.parent === b.parent ? 1.05 : 1.2));
955
+ const treeData = treeLayout(rootHierarchy);
956
+ // Filter out virtual root if multi-root
957
+ const nodes = treeData.descendants().filter((d) => d.data.ID !== '__virtual_root__');
958
+ const links = treeData.links().filter((d) => d.source.data.ID !== '__virtual_root__');
959
+ // Update positions on node data (centered on D3 node point)
960
+ for (const n of nodes) {
961
+ n.data.x = isVertical ? n.x - nodeWidth / 2 : n.y;
962
+ n.data.y = isVertical ? n.y : n.x - nodeHeight / 2;
963
+ }
964
+ this.log(`[HierarchyTree:renderTree] Tree layout computed: ${nodes.length} nodes, ${links.length} links. Node positions:`, nodes.map(n => ({ Name: n.data.Name, x: n.data.x, y: n.data.y })));
965
+ // 3. Render Links
966
+ const linkPathGen = (d) => {
967
+ const sx = isVertical ? d.source.data.x + nodeWidth / 2 : d.source.data.x + nodeWidth;
968
+ const sy = isVertical ? d.source.data.y + nodeHeight : d.source.data.y + nodeHeight / 2;
969
+ const tx = isVertical ? d.target.data.x + nodeWidth / 2 : d.target.data.x;
970
+ const ty = isVertical ? d.target.data.y : d.target.data.y + nodeHeight / 2;
971
+ if (isVertical) {
972
+ const midY = (sy + ty) / 2;
973
+ return `M ${sx} ${sy} C ${sx} ${midY}, ${tx} ${midY}, ${tx} ${ty}`;
974
+ }
975
+ else {
976
+ const midX = (sx + tx) / 2;
977
+ return `M ${sx} ${sy} C ${midX} ${sy}, ${midX} ${ty}, ${tx} ${ty}`;
978
+ }
979
+ };
980
+ const linkGroup = this.gSelection.select('g.mj-links-group');
981
+ const linkSelection = linkGroup.selectAll('path.mj-tree-link')
982
+ .data(links, (d) => `${d.source.data.ID}->${d.target.data.ID}`);
983
+ linkSelection.exit().remove();
984
+ linkSelection.enter()
985
+ .append('path')
986
+ .attr('class', 'mj-tree-link')
987
+ .merge(linkSelection)
988
+ .attr('d', linkPathGen)
989
+ .attr('stroke', (d) => (d.target.data.IsHighlighted ? 'var(--mj-brand-primary, #38bdf8)' : 'var(--mj-border-default, #223254)'))
990
+ .attr('stroke-width', (d) => (d.target.data.IsHighlighted ? 2.5 : 1.5))
991
+ .attr('fill', 'none');
992
+ this.AllNodes = [...this.AllNodes];
993
+ this.cdr?.markForCheck();
994
+ this.cdr?.detectChanges();
995
+ if (!preserveTransform) {
996
+ this.fitToScreen(true);
997
+ }
998
+ }
999
+ // --- Interactive Actions ---
1000
+ /**
1001
+ * Toggles the expand/collapse state of a node, firing cancelable Before and After events.
1002
+ */
1003
+ toggleNodeExpansion(node, event) {
1004
+ if (event)
1005
+ event.stopPropagation();
1006
+ if (node.IsExpanded) {
1007
+ const beforeEvt = new CancelableHierarchyNodeEvent(node, event);
1008
+ this.BeforeNodeCollapse.emit(beforeEvt);
1009
+ if (beforeEvt.IsCanceled)
1010
+ return;
1011
+ node.IsExpanded = false;
1012
+ node.Children = [];
1013
+ this.AfterNodeCollapse.emit({ Node: node, OriginalEvent: event });
1014
+ }
1015
+ else {
1016
+ const beforeEvt = new CancelableHierarchyNodeEvent(node, event);
1017
+ this.BeforeNodeExpand.emit(beforeEvt);
1018
+ if (beforeEvt.IsCanceled)
1019
+ return;
1020
+ node.IsExpanded = true;
1021
+ node.Children = [...(node._allChildren || [])];
1022
+ this.AfterNodeExpand.emit({ Node: node, OriginalEvent: event });
1023
+ }
1024
+ this.renderTree(true);
1025
+ }
1026
+ closeDetailsDrawer() {
1027
+ this.ShowDetailsDrawer = false;
1028
+ this.cdr?.markForCheck();
1029
+ }
1030
+ openDetailsDrawer() {
1031
+ this.ShowDetailsDrawer = true;
1032
+ this.cdr?.markForCheck();
1033
+ }
1034
+ onCanvasBackgroundClick(event) {
1035
+ const target = event.target;
1036
+ if (target?.closest('.mj-details-drawer') || target?.closest('.mj-node-card') || target?.closest('.mj-tool-btn')) {
1037
+ return;
1038
+ }
1039
+ }
1040
+ onNodeClick(node, event) {
1041
+ event.stopPropagation();
1042
+ this.SelectedNode = node;
1043
+ this.ShowDetailsDrawer = true;
1044
+ for (const n of this.AllNodes) {
1045
+ n.IsSelected = UUIDsEqual(n.ID, node.ID);
1046
+ }
1047
+ this.NodeClick.emit({ Node: node, OriginalEvent: event });
1048
+ this.NodeSelect.emit({ Node: node, OriginalEvent: event });
1049
+ this.cdr?.markForCheck();
1050
+ }
1051
+ onNodeDoubleClick(node, event) {
1052
+ event.stopPropagation();
1053
+ this.NodeDoubleClick.emit({ Node: node, OriginalEvent: event });
1054
+ this.navigateToRecord(node, event);
1055
+ }
1056
+ onNodeAction(node, action, event) {
1057
+ event.stopPropagation();
1058
+ this.NodeAction.emit({ Node: node, Action: action });
1059
+ switch (action) {
1060
+ case 'open':
1061
+ this.navigateToRecord(node, event);
1062
+ break;
1063
+ case 'focus':
1064
+ this.setFocusRoot(node.ID);
1065
+ break;
1066
+ case 'reset-focus':
1067
+ this.resetFocus();
1068
+ break;
1069
+ case 'add-child':
1070
+ // Handled via custom subscriber or event
1071
+ break;
1072
+ }
1073
+ }
1074
+ navigateToRecord(node, event) {
1075
+ const entityName = this.entityInfo ? this.entityInfo.Name : this.Config.EntityName;
1076
+ const navEvent = {
1077
+ Kind: 'record',
1078
+ EntityName: entityName,
1079
+ PrimaryKey: node.PrimaryKey,
1080
+ OpenInNewTab: event ? event.ctrlKey || event.metaKey : false
1081
+ };
1082
+ this.Navigate.emit(navEvent);
1083
+ }
1084
+ getRecordAttributeEntries(record) {
1085
+ const entries = [];
1086
+ const skipFields = new Set([
1087
+ 'id', 'parentid', 'parentproductcategoryid', 'parentcategoryid',
1088
+ 'companyid', '__mj_version', 'createdat', 'updatedat', 'name'
1089
+ ]);
1090
+ for (const [k, v] of Object.entries(record)) {
1091
+ if (skipFields.has(k.toLowerCase()) || k.startsWith('__mj_') || v == null)
1092
+ continue;
1093
+ const strVal = typeof v === 'object' ? JSON.stringify(v) : String(v);
1094
+ if (strVal.trim() === '')
1095
+ continue;
1096
+ entries.push({
1097
+ key: k,
1098
+ label: k.replace(/([A-Z])/g, ' $1').trim(),
1099
+ value: strVal
1100
+ });
1101
+ }
1102
+ return entries.slice(0, 8);
1103
+ }
1104
+ // --- Search & Highlight ---
1105
+ onSearchInput() {
1106
+ const q = this.SearchQuery.trim().toLowerCase();
1107
+ if (!q) {
1108
+ for (const n of this.AllNodes) {
1109
+ n.IsHighlighted = false;
1110
+ }
1111
+ this.MatchingNodeCount = 0;
1112
+ this.renderTree(true);
1113
+ return;
1114
+ }
1115
+ let count = 0;
1116
+ let firstMatch = null;
1117
+ for (const n of this.AllNodes) {
1118
+ const matches = n.Name.toLowerCase().includes(q) || (n.Subtitle && n.Subtitle.toLowerCase().includes(q));
1119
+ n.IsHighlighted = !!matches;
1120
+ if (matches) {
1121
+ count++;
1122
+ if (!firstMatch)
1123
+ firstMatch = n;
1124
+ this.expandAncestors(n);
1125
+ }
1126
+ }
1127
+ this.MatchingNodeCount = count;
1128
+ this.renderTree(true);
1129
+ if (firstMatch && firstMatch.x != null && firstMatch.y != null) {
1130
+ this.centerOnNode(firstMatch);
1131
+ }
1132
+ }
1133
+ clearSearch() {
1134
+ this.SearchQuery = '';
1135
+ this.onSearchInput();
1136
+ }
1137
+ centerOnNode(node) {
1138
+ if (!this.svgSelection || !this.zoomBehavior || !this.svgContainerRef?.nativeElement)
1139
+ return;
1140
+ if (node.x == null || node.y == null)
1141
+ return;
1142
+ const rect = this.svgContainerRef.nativeElement.getBoundingClientRect();
1143
+ const width = rect.width > 50 ? rect.width : (this.svgContainerRef.nativeElement.clientWidth || 800);
1144
+ const height = rect.height > 50 ? rect.height : (this.svgContainerRef.nativeElement.clientHeight || 500);
1145
+ const rightMargin = (this.ShowDetailsDrawer && this.SelectedNode) ? 340 : 0;
1146
+ const visibleWidth = Math.max(width - rightMargin, 200);
1147
+ const currentScale = this.currentZoomTransform?.k || this.ZoomLevel || 1;
1148
+ const nodeWidth = this.Config.NodeWidth || 230;
1149
+ const nodeHeight = this.Config.NodeHeight || 90;
1150
+ const nodeCenterX = node.x + nodeWidth / 2;
1151
+ const nodeCenterY = node.y + nodeHeight / 2;
1152
+ const tx = visibleWidth / 2 - nodeCenterX * currentScale;
1153
+ const ty = height / 2 - nodeCenterY * currentScale;
1154
+ const target = d3.zoomIdentity.translate(tx, ty).scale(currentScale);
1155
+ this.svgSelection.interrupt().transition().duration(400).ease(d3.easeCubicOut).call(this.zoomBehavior.transform, target);
1156
+ }
1157
+ expandAncestors(node) {
1158
+ let curr = node.ParentID ? this.nodeMap.get(NormalizeUUID(node.ParentID)) : undefined;
1159
+ while (curr) {
1160
+ curr.IsExpanded = true;
1161
+ curr.Children = [...(curr._allChildren || [])];
1162
+ curr = curr.ParentID ? this.nodeMap.get(NormalizeUUID(curr.ParentID)) : undefined;
1163
+ }
1164
+ }
1165
+ // --- Subtree Focus ---
1166
+ setFocusRoot(recordId) {
1167
+ if (!recordId) {
1168
+ this.resetFocus();
1169
+ return;
1170
+ }
1171
+ const target = this.nodeMap.get(NormalizeUUID(recordId));
1172
+ if (!target)
1173
+ return;
1174
+ for (const n of this.AllNodes)
1175
+ n.IsFocusRoot = false;
1176
+ target.IsFocusRoot = true;
1177
+ this.FocusedNode = target;
1178
+ this.renderTree();
1179
+ this.onFitToViewClick();
1180
+ }
1181
+ resetFocus() {
1182
+ for (const n of this.AllNodes)
1183
+ n.IsFocusRoot = false;
1184
+ this.FocusedNode = null;
1185
+ this.renderTree();
1186
+ this.onFitToViewClick();
1187
+ }
1188
+ // --- Toolbar Actions / Verbs ---
1189
+ expandAll() {
1190
+ for (const n of this.AllNodes) {
1191
+ n.IsExpanded = true;
1192
+ n.Children = [...(n._allChildren || [])];
1193
+ }
1194
+ this.renderTree(true);
1195
+ }
1196
+ collapseAll() {
1197
+ for (const n of this.AllNodes) {
1198
+ if (n.Depth > 0) {
1199
+ n.IsExpanded = false;
1200
+ n.Children = [];
1201
+ }
1202
+ }
1203
+ this.renderTree(true);
1204
+ }
1205
+ zoomIn() {
1206
+ if (!this.svgSelection || !this.zoomBehavior)
1207
+ return;
1208
+ const currentScale = this.currentZoomTransform?.k || 1;
1209
+ const targetScale = Math.min(currentScale * 1.25, 4.0);
1210
+ this.setZoomLevel(targetScale, true);
1211
+ this.ZoomLevel = targetScale;
1212
+ this.ZoomChange.emit(targetScale);
1213
+ }
1214
+ zoomOut() {
1215
+ if (!this.svgSelection || !this.zoomBehavior)
1216
+ return;
1217
+ const currentScale = this.currentZoomTransform?.k || 1;
1218
+ const targetScale = Math.max(currentScale * 0.8, 0.1);
1219
+ this.setZoomLevel(targetScale, true);
1220
+ this.ZoomLevel = targetScale;
1221
+ this.ZoomChange.emit(targetScale);
1222
+ }
1223
+ setZoomLevel(scale, animated = true) {
1224
+ if (!this.svgSelection || !this.zoomBehavior || !scale)
1225
+ return;
1226
+ const container = this.svgContainerRef?.nativeElement;
1227
+ const rect = container?.getBoundingClientRect();
1228
+ const width = rect?.width && rect.width > 50 ? rect.width : (container?.clientWidth || 800);
1229
+ const height = rect?.height && rect.height > 50 ? rect.height : (container?.clientHeight || 500);
1230
+ const current = this.currentZoomTransform || d3.zoomIdentity;
1231
+ const cx = width / 2;
1232
+ const cy = height / 2;
1233
+ const newX = cx - (cx - current.x) * (scale / (current.k || 1));
1234
+ const newY = cy - (cy - current.y) * (scale / (current.k || 1));
1235
+ const target = d3.zoomIdentity.translate(newX, newY).scale(scale);
1236
+ if (animated) {
1237
+ this.svgSelection.interrupt().transition().duration(250).ease(d3.easeCubicOut).call(this.zoomBehavior.transform, target);
1238
+ }
1239
+ else {
1240
+ this.svgSelection.interrupt().call(this.zoomBehavior.transform, target);
1241
+ }
1242
+ }
1243
+ resetZoom() {
1244
+ if (!this.svgSelection || !this.zoomBehavior)
1245
+ return;
1246
+ this.onFitToViewClick();
1247
+ }
1248
+ onFitToViewClick() {
1249
+ this.fitToScreen(false, true);
1250
+ setTimeout(() => {
1251
+ this.fitToScreen(false, true);
1252
+ }, 320);
1253
+ }
1254
+ fitToScreen(immediate = false, forceAutoScale = false) {
1255
+ if (!this.svgSelection || !this.zoomBehavior || !this.svgContainerRef?.nativeElement)
1256
+ return;
1257
+ const visibleNodes = this.AllNodes.filter((n) => n.x != null && n.y != null);
1258
+ if (visibleNodes.length === 0)
1259
+ return;
1260
+ const nodeWidth = this.Config.NodeWidth || 230;
1261
+ const nodeHeight = this.Config.NodeHeight || 90;
1262
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
1263
+ for (const n of visibleNodes) {
1264
+ minX = Math.min(minX, n.x);
1265
+ maxX = Math.max(maxX, n.x + nodeWidth);
1266
+ minY = Math.min(minY, n.y);
1267
+ maxY = Math.max(maxY, n.y + nodeHeight);
1268
+ }
1269
+ const treeWidth = Math.max(maxX - minX, 1);
1270
+ const treeHeight = Math.max(maxY - minY, 1);
1271
+ const treeCenterX = (minX + maxX) / 2;
1272
+ const treeCenterY = (minY + maxY) / 2;
1273
+ const rect = this.svgContainerRef.nativeElement.getBoundingClientRect();
1274
+ const width = rect.width > 50 ? rect.width : (this.svgContainerRef.nativeElement.clientWidth || 800);
1275
+ const height = rect.height > 50 ? rect.height : (this.svgContainerRef.nativeElement.clientHeight || 500);
1276
+ const rightMargin = (this.ShowDetailsDrawer && this.SelectedNode) ? 340 : 0;
1277
+ const visibleWidth = Math.max(width - rightMargin, 200);
1278
+ const padding = 40;
1279
+ const availWidth = Math.max(visibleWidth - padding * 2, 100);
1280
+ const availHeight = Math.max(height - padding * 2, 100);
1281
+ const autoScale = Math.max(Math.min(availWidth / treeWidth, availHeight / treeHeight, 1.05), 0.15);
1282
+ const scale = (forceAutoScale || !this.ZoomLevel) ? autoScale : this.ZoomLevel;
1283
+ if (forceAutoScale) {
1284
+ this.ZoomLevel = autoScale;
1285
+ this.ZoomChange.emit(autoScale);
1286
+ }
1287
+ const tx = visibleWidth / 2 - treeCenterX * scale;
1288
+ const ty = height / 2 - treeCenterY * scale;
1289
+ const targetTransform = d3.zoomIdentity.translate(tx, ty).scale(scale);
1290
+ this.log(`[HierarchyTree:fitToScreen] Container=${width}x${height} (visible=${visibleWidth}), visibleNodes=${visibleNodes.length}, bounds=[${minX.toFixed(1)}, ${minY.toFixed(1)}] to [${maxX.toFixed(1)}, ${maxY.toFixed(1)}] (${treeWidth.toFixed(1)}x${treeHeight.toFixed(1)}), autoScale=${autoScale.toFixed(3)}, usingScale=${scale.toFixed(3)}, translate=(${tx.toFixed(1)}, ${ty.toFixed(1)})`);
1291
+ if (immediate) {
1292
+ this.svgSelection.interrupt().call(this.zoomBehavior.transform, targetTransform);
1293
+ }
1294
+ else {
1295
+ this.svgSelection.interrupt().transition().duration(300).ease(d3.easeCubicOut).call(this.zoomBehavior.transform, targetTransform);
1296
+ }
1297
+ }
1298
+ exportAsSVG() {
1299
+ return this.svgRef?.nativeElement ? this.svgRef.nativeElement.outerHTML : '';
1300
+ }
1301
+ async refresh() {
1302
+ await this.loadData();
1303
+ }
1304
+ static ɵfac = function HierarchyTreeComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || HierarchyTreeComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
1305
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: HierarchyTreeComponent, selectors: [["mj-hierarchy-tree"]], viewQuery: function HierarchyTreeComponent_Query(rf, ctx) { if (rf & 1) {
1306
+ i0.ɵɵviewQuery(_c0, 5)(_c1, 5);
1307
+ } if (rf & 2) {
1308
+ let _t;
1309
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.svgContainerRef = _t.first);
1310
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.svgRef = _t.first);
1311
+ } }, inputs: { Config: "Config", Data: "Data", ActiveRecordID: "ActiveRecordID", FocusRecordID: "FocusRecordID", AllowNodeActions: "AllowNodeActions", ZoomLevel: "ZoomLevel" }, outputs: { NodeClick: "NodeClick", NodeDoubleClick: "NodeDoubleClick", NodeSelect: "NodeSelect", BeforeNodeExpand: "BeforeNodeExpand", AfterNodeExpand: "AfterNodeExpand", BeforeNodeCollapse: "BeforeNodeCollapse", AfterNodeCollapse: "AfterNodeCollapse", BeforeReparent: "BeforeReparent", AfterReparent: "AfterReparent", NodeAction: "NodeAction", ZoomChange: "ZoomChange", Navigate: "Navigate" }, features: [i0.ɵɵInheritDefinitionFeature, i0.ɵɵNgOnChangesFeature], decls: 20, vars: 9, consts: [["svgContainer", ""], ["svgElement", ""], [1, "mj-hierarchy-wrapper"], [1, "mj-hierarchy-toolbar"], [1, "mj-canvas-container", 3, "click"], [1, "mj-state-overlay"], [1, "mj-state-overlay", "mj-error-state"], [1, "mj-state-overlay", "mj-empty-state"], [1, "mj-hierarchy-svg"], ["id", "mj-tree-grid", "width", "24", "height", "24", "patternUnits", "userSpaceOnUse"], ["cx", "12", "cy", "12", "r", "1", "fill", "var(--mj-border-subtle, #1e293b)", "opacity", "0.4"], ["width", "100%", "height", "100%", "fill", "url(#mj-tree-grid)"], [1, "mj-hierarchy-canvas"], [1, "mj-links-group"], [1, "mj-nodes-group"], ["aria-label", "Node details panel", 1, "mj-details-drawer"], [1, "mj-search-box"], [1, "mj-focus-indicator"], [1, "mj-toolbar-actions"], [1, "fa-solid", "fa-magnifying-glass", "mj-search-icon"], ["type", "text", "placeholder", "Search hierarchy...", 1, "mj-search-input", 3, "ngModelChange", "input", "ngModel"], ["type", "button", "title", "Clear search", 1, "mj-clear-btn"], [1, "mj-match-badge"], ["type", "button", "title", "Clear search", 1, "mj-clear-btn", 3, "click"], [1, "fa-solid", "fa-xmark"], [1, "fa-solid", "fa-crosshairs"], ["type", "button", "title", "Show entire hierarchy", 1, "mj-reset-focus-btn", 3, "click"], [1, "fa-solid", "fa-rotate-left"], ["type", "button", "title", "Expand all subtrees", 1, "mj-tool-btn", 3, "click"], [1, "fa-solid", "fa-folder-open"], ["type", "button", "title", "Collapse all subtrees", 1, "mj-tool-btn", 3, "click"], [1, "fa-solid", "fa-folder"], [1, "mj-tool-divider"], ["type", "button", "title", "Zoom in", 1, "mj-tool-btn", 3, "click"], [1, "fa-solid", "fa-plus"], ["type", "button", "title", "Zoom out", 1, "mj-tool-btn", 3, "click"], [1, "fa-solid", "fa-minus"], ["type", "button", "title", "Fit hierarchy to view", 1, "mj-tool-btn", 3, "click"], [1, "fa-solid", "fa-expand"], ["type", "button", "title", "Reload hierarchy data", 1, "mj-tool-btn", 3, "click"], [1, "fa-solid", "fa-rotate-right"], [1, "mj-spinner"], [1, "fa-solid", "fa-circle-exclamation", "mj-error-icon"], ["type", "button", 1, "mj-retry-btn", 3, "click"], [1, "fa-solid", "fa-sitemap", "mj-empty-icon"], [1, "mj-node-foreign-object"], [1, "mj-node-card", 3, "click", "dblclick"], [1, "mj-card-accent"], [1, "mj-node-header"], [1, "mj-node-icon"], [1, "mj-node-title-group"], [1, "mj-node-name", 3, "title"], [1, "mj-node-subtitle"], [1, "mj-node-footer"], ["type", "button", 1, "mj-expand-badge", 3, "collapsed", "title"], [1, "mj-leaf-indicator"], [1, "mj-node-actions"], ["type", "button", 1, "mj-expand-badge", 3, "click", "title"], [1, "fa-solid"], ["type", "button", "title", "Focus on this subtree", 1, "mj-node-action-btn", 3, "click"], ["type", "button", "title", "Open record in Explorer", 1, "mj-node-action-btn", 3, "click"], [1, "fa-solid", "fa-arrow-up-right-from-square"], [1, "mj-drawer-header"], [1, "mj-drawer-title-group"], [1, "mj-drawer-icon"], [1, "mj-drawer-titles"], [1, "mj-drawer-name", 3, "title"], [1, "mj-drawer-subtitle"], ["type", "button", "title", "Close details", 1, "mj-drawer-close-btn", 3, "click"], [1, "mj-drawer-body"], [1, "mj-drawer-stats"], [1, "mj-stat-card"], [1, "mj-stat-num"], [1, "mj-stat-label"], [1, "mj-drawer-section"], [1, "mj-drawer-actions"], ["type", "button", "title", "Open this record in Explorer", 1, "mj-drawer-btn-primary", 3, "click"], [1, "mj-drawer-btn-row"], ["type", "button", 1, "mj-drawer-btn-secondary"], ["type", "button", "title", "Focus hierarchy on this subtree", 1, "mj-drawer-btn-secondary", 3, "click"], [1, "mj-section-heading"], [1, "mj-attr-list"], [1, "mj-attr-row"], [1, "mj-attr-dt"], [1, "mj-attr-dd", 3, "title"], ["type", "button", 1, "mj-drawer-btn-secondary", 3, "click"]], template: function HierarchyTreeComponent_Template(rf, ctx) { if (rf & 1) {
1312
+ const _r1 = i0.ɵɵgetCurrentView();
1313
+ i0.ɵɵelementStart(0, "div", 2);
1314
+ i0.ɵɵconditionalCreate(1, HierarchyTreeComponent_Conditional_1_Template, 4, 3, "div", 3);
1315
+ i0.ɵɵelementStart(2, "div", 4, 0);
1316
+ i0.ɵɵlistener("click", function HierarchyTreeComponent_Template_div_click_2_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onCanvasBackgroundClick($event)); });
1317
+ i0.ɵɵconditionalCreate(4, HierarchyTreeComponent_Conditional_4_Template, 4, 0, "div", 5)(5, HierarchyTreeComponent_Conditional_5_Template, 6, 1, "div", 6)(6, HierarchyTreeComponent_Conditional_6_Template, 4, 0, "div", 7);
1318
+ i0.ɵɵnamespaceSVG();
1319
+ i0.ɵɵelementStart(7, "svg", 8, 1)(9, "defs")(10, "pattern", 9);
1320
+ i0.ɵɵelement(11, "circle", 10);
1321
+ i0.ɵɵelementEnd()();
1322
+ i0.ɵɵelement(12, "rect", 11);
1323
+ i0.ɵɵelementStart(13, "g", 12);
1324
+ i0.ɵɵelement(14, "g", 13);
1325
+ i0.ɵɵelementStart(15, "g", 14);
1326
+ i0.ɵɵrepeaterCreate(16, HierarchyTreeComponent_For_17_Template, 1, 1, null, null, _forTrack0);
1327
+ i0.ɵɵelementEnd()()();
1328
+ i0.ɵɵnamespaceHTML();
1329
+ i0.ɵɵelementStart(18, "aside", 15);
1330
+ i0.ɵɵconditionalCreate(19, HierarchyTreeComponent_Conditional_19_Template, 39, 12);
1331
+ i0.ɵɵelementEnd()()();
1332
+ } if (rf & 2) {
1333
+ i0.ɵɵstyleProp("height", ctx.Config.Height || "100%")("min-height", ctx.Config.MinHeight || "500px");
1334
+ i0.ɵɵadvance();
1335
+ i0.ɵɵconditional(ctx.Config.ShowToolbar || ctx.Config.ShowSearch ? 1 : -1);
1336
+ i0.ɵɵadvance(3);
1337
+ i0.ɵɵconditional(ctx.Loading ? 4 : ctx.ErrorMessage ? 5 : ctx.AllNodes.length === 0 ? 6 : -1);
1338
+ i0.ɵɵadvance(12);
1339
+ i0.ɵɵrepeater(ctx.AllNodes);
1340
+ i0.ɵɵadvance(2);
1341
+ i0.ɵɵclassProp("open", ctx.SelectedNode != null && ctx.ShowDetailsDrawer);
1342
+ i0.ɵɵadvance();
1343
+ i0.ɵɵconditional(ctx.SelectedNode ? 19 : -1);
1344
+ } }, dependencies: [CommonModule, FormsModule, i1.DefaultValueAccessor, i1.NgControlStatus, i1.NgModel], styles: ["[_nghost-%COMP%] {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: 500px;\n position: relative;\n box-sizing: border-box;\n font-family: inherit;\n flex: 1;\n}\n\n.mj-hierarchy-wrapper[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: inherit;\n flex: 1;\n background: var(--mj-bg-surface-sunken, #080d1a);\n border: 1px solid var(--mj-border-default, #1e293b);\n border-radius: 12px;\n overflow: hidden;\n position: relative;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);\n}\n\n\n\n.mj-hierarchy-toolbar[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n background: var(--mj-bg-surface, #0f172a);\n border-bottom: 1px solid var(--mj-border-default, #1e293b);\n gap: 12px;\n z-index: 10;\n flex-wrap: wrap;\n}\n\n\n\n.mj-search-box[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n background: var(--mj-bg-surface-sunken, #080d1a);\n border: 1px solid var(--mj-border-default, #1e293b);\n border-radius: 8px;\n padding: 4px 10px;\n min-width: 240px;\n max-width: 340px;\n flex: 1;\n transition: border-color 0.18s ease;\n}\n\n.mj-search-box[_ngcontent-%COMP%]:focus-within {\n border-color: var(--mj-brand-primary, #38bdf8);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 18%, transparent);\n}\n\n.mj-search-icon[_ngcontent-%COMP%] {\n font-size: 12px;\n color: var(--mj-text-muted, #64748b);\n}\n\n.mj-search-input[_ngcontent-%COMP%] {\n background: transparent;\n border: none;\n outline: none;\n color: var(--mj-text-primary, #f8fafc);\n font-size: 12.5px;\n width: 100%;\n}\n\n.mj-search-input[_ngcontent-%COMP%]::placeholder {\n color: var(--mj-text-muted, #64748b);\n}\n\n.mj-clear-btn[_ngcontent-%COMP%] {\n background: transparent;\n border: none;\n color: var(--mj-text-muted, #64748b);\n cursor: pointer;\n font-size: 11px;\n padding: 2px 4px;\n border-radius: 4px;\n}\n\n.mj-clear-btn[_ngcontent-%COMP%]:hover {\n color: var(--mj-text-primary, #f8fafc);\n}\n\n.mj-match-badge[_ngcontent-%COMP%] {\n font-size: 11px;\n font-weight: 700;\n padding: 2px 7px;\n border-radius: 9999px;\n background: color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 15%, transparent);\n color: var(--mj-brand-primary, #38bdf8);\n white-space: nowrap;\n}\n\n\n\n.mj-focus-indicator[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n color: var(--mj-brand-primary, #38bdf8);\n background: color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 10%, transparent);\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 30%, transparent);\n padding: 4px 10px;\n border-radius: 6px;\n}\n\n.mj-reset-focus-btn[_ngcontent-%COMP%] {\n background: var(--mj-brand-primary, #38bdf8);\n color: var(--mj-text-inverse);\n border: none;\n border-radius: 4px;\n padding: 3px 8px;\n font-size: 11px;\n font-weight: 700;\n cursor: pointer;\n margin-left: 6px;\n transition: opacity 0.15s ease;\n}\n\n.mj-reset-focus-btn[_ngcontent-%COMP%]:hover {\n opacity: 0.9;\n}\n\n\n\n.mj-toolbar-actions[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 4px;\n margin-left: auto;\n}\n\n.mj-tool-btn[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n height: 30px;\n background: transparent;\n border: 1px solid var(--mj-border-subtle, #1e293b);\n border-radius: 6px;\n color: var(--mj-text-secondary, #94a3b8);\n font-size: 12px;\n cursor: pointer;\n transition: all 0.18s ease;\n}\n\n.mj-tool-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-elevated, #1e293b);\n color: var(--mj-text-primary, #f8fafc);\n border-color: var(--mj-border-default, #334155);\n}\n\n.mj-tool-divider[_ngcontent-%COMP%] {\n width: 1px;\n height: 18px;\n background: var(--mj-border-subtle, #1e293b);\n margin: 0 4px;\n}\n\n\n\n.mj-canvas-container[_ngcontent-%COMP%] {\n flex: 1;\n position: relative;\n overflow: hidden;\n width: 100%;\n height: 100%;\n min-height: 450px;\n cursor: grab;\n}\n\n.mj-canvas-container[_ngcontent-%COMP%]:active {\n cursor: grabbing;\n}\n\n.mj-hierarchy-svg[_ngcontent-%COMP%] {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n display: block;\n}\n\n\n\n.mj-state-overlay[_ngcontent-%COMP%] {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n background: var(--mj-bg-overlay);\n backdrop-filter: blur(4px);\n z-index: 20;\n color: var(--mj-text-secondary, #94a3b8);\n font-size: 13px;\n}\n\n.mj-spinner[_ngcontent-%COMP%] {\n width: 32px;\n height: 32px;\n border: 3px solid color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 20%, transparent);\n border-top-color: var(--mj-brand-primary, #38bdf8);\n border-radius: 50%;\n animation: _ngcontent-%COMP%_mj-spin 0.8s linear infinite;\n}\n\n@keyframes _ngcontent-%COMP%_mj-spin {\n to { transform: rotate(360deg); }\n}\n\n.mj-error-icon[_ngcontent-%COMP%], .mj-empty-icon[_ngcontent-%COMP%] {\n font-size: 28px;\n color: var(--mj-text-muted, #64748b);\n}\n\n.mj-retry-btn[_ngcontent-%COMP%] {\n background: var(--mj-brand-primary, #38bdf8);\n color: var(--mj-text-inverse);\n border: none;\n padding: 6px 14px;\n font-size: 12px;\n font-weight: 700;\n border-radius: 6px;\n cursor: pointer;\n}\n\n\n\n.mj-node-foreign-object[_ngcontent-%COMP%] {\n overflow: visible;\n}\n\n\n\n.mj-node-card[_ngcontent-%COMP%] {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n background: var(--mj-bg-surface-card, #111a2e);\n border: 1px solid var(--mj-border-default, #223254);\n border-radius: 10px;\n padding: 8px 12px;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);\n position: relative;\n cursor: pointer;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n user-select: none;\n}\n\n.mj-node-card[_ngcontent-%COMP%]:hover {\n border-color: var(--mj-brand-primary, #38bdf8);\n transform: translateY(-2px);\n box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5), 0 0 12px color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 20%, transparent);\n}\n\n.mj-node-card.selected[_ngcontent-%COMP%] {\n border-color: var(--mj-brand-primary, #38bdf8);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 40%, transparent), 0 6px 20px rgba(0, 0, 0, 0.6);\n}\n\n.mj-node-card.highlighted[_ngcontent-%COMP%] {\n border-color: var(--mj-status-warning);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-status-warning) 40%, transparent), 0 6px 20px rgba(0, 0, 0, 0.6);\n}\n\n.mj-node-card.focus-root[_ngcontent-%COMP%] {\n border-color: var(--mj-status-success);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-status-success) 40%, transparent);\n}\n\n\n\n.mj-card-accent[_ngcontent-%COMP%] {\n position: absolute;\n top: 0;\n left: 8px;\n right: 8px;\n height: 3px;\n border-radius: 3px 3px 0 0;\n opacity: 0.85;\n}\n\n\n\n.mj-node-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-top: 4px;\n}\n\n.mj-node-icon[_ngcontent-%COMP%] {\n width: 28px;\n height: 28px;\n border-radius: 6px;\n background: rgba(255, 255, 255, 0.05);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 13px;\n flex-shrink: 0;\n}\n\n.mj-node-title-group[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n min-width: 0;\n flex: 1;\n}\n\n.mj-node-name[_ngcontent-%COMP%] {\n font-size: 12.5px;\n font-weight: 700;\n color: var(--mj-text-primary, #f8fafc);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n line-height: 1.3;\n}\n\n.mj-node-subtitle[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--mj-text-muted, #94a3b8);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n line-height: 1.3;\n}\n\n\n\n.mj-node-footer[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-top: 6px;\n border-top: 1px solid rgba(255, 255, 255, 0.06);\n}\n\n\n\n.mj-expand-badge[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n background: var(--mj-bg-surface-elevated, #1a2744);\n border: 1px solid var(--mj-border-default, #223254);\n border-radius: 9999px;\n padding: 2px 8px;\n color: var(--mj-text-secondary, #94a3b8);\n font-size: 11px;\n font-weight: 700;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.mj-expand-badge[_ngcontent-%COMP%]:hover {\n background: var(--mj-brand-primary, #38bdf8);\n color: var(--mj-text-inverse);\n border-color: var(--mj-brand-primary, #38bdf8);\n}\n\n.mj-expand-badge.collapsed[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 15%, transparent);\n color: var(--mj-brand-primary, #38bdf8);\n border-color: color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 35%, transparent);\n}\n\n.mj-leaf-indicator[_ngcontent-%COMP%] {\n font-size: 10px;\n color: var(--mj-text-muted, #64748b);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n padding-left: 2px;\n}\n\n\n\n.mj-node-actions[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.mj-node-action-btn[_ngcontent-%COMP%] {\n width: 22px;\n height: 22px;\n border-radius: 4px;\n background: transparent;\n border: 1px solid transparent;\n color: var(--mj-text-muted, #64748b);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 10.5px;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.mj-node-action-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-elevated, #1a2744);\n border-color: var(--mj-border-default, #223254);\n color: var(--mj-brand-primary, #38bdf8);\n}\n\n\n\n\n\n.mj-details-drawer[_ngcontent-%COMP%] {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 330px;\n max-width: 85%;\n background: var(--mj-bg-surface, #0f172a);\n border-left: 1px solid var(--mj-border-default, #1e293b);\n box-shadow: -6px 0 24px rgba(0, 0, 0, 0.45);\n z-index: 30;\n display: flex;\n flex-direction: column;\n transform: translateX(100%);\n transition: transform 0.26s cubic-bezier(0.16, 1, 0.3, 1);\n pointer-events: none;\n user-select: text;\n}\n\n.mj-details-drawer.open[_ngcontent-%COMP%] {\n transform: translateX(0);\n pointer-events: auto;\n}\n\n.mj-drawer-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n padding: 16px 18px;\n background: var(--mj-bg-surface-elevated, #151f36);\n border-bottom: 1px solid var(--mj-border-default, #1e293b);\n gap: 12px;\n}\n\n.mj-drawer-title-group[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 0;\n flex: 1;\n}\n\n.mj-drawer-icon[_ngcontent-%COMP%] {\n width: 36px;\n height: 36px;\n border-radius: 8px;\n background: rgba(255, 255, 255, 0.05);\n border: 1px solid rgba(255, 255, 255, 0.08);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n flex-shrink: 0;\n}\n\n.mj-drawer-titles[_ngcontent-%COMP%] {\n min-width: 0;\n flex: 1;\n}\n\n.mj-drawer-name[_ngcontent-%COMP%] {\n margin: 0;\n font-size: 15px;\n font-weight: 700;\n color: var(--mj-text-primary, #f8fafc);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n line-height: 1.3;\n}\n\n.mj-drawer-subtitle[_ngcontent-%COMP%] {\n margin: 2px 0 0 0;\n font-size: 12px;\n color: var(--mj-text-muted, #94a3b8);\n line-height: 1.3;\n}\n\n.mj-drawer-close-btn[_ngcontent-%COMP%] {\n background: transparent;\n border: 1px solid transparent;\n border-radius: 6px;\n width: 28px;\n height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-muted, #94a3b8);\n cursor: pointer;\n font-size: 14px;\n transition: all 0.15s ease;\n flex-shrink: 0;\n}\n\n.mj-drawer-close-btn[_ngcontent-%COMP%]:hover {\n background: rgba(255, 255, 255, 0.08);\n color: var(--mj-text-primary, #f8fafc);\n border-color: var(--mj-border-default, #223254);\n}\n\n.mj-drawer-body[_ngcontent-%COMP%] {\n flex: 1;\n overflow-y: auto;\n padding: 18px;\n display: flex;\n flex-direction: column;\n gap: 18px;\n}\n\n.mj-drawer-stats[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 8px;\n}\n\n.mj-stat-card[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface-sunken, #080d1a);\n border: 1px solid var(--mj-border-default, #1e293b);\n border-radius: 8px;\n padding: 8px 6px;\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n}\n\n.mj-stat-num[_ngcontent-%COMP%] {\n font-size: 16px;\n font-weight: 700;\n color: var(--mj-brand-primary, #38bdf8);\n line-height: 1.2;\n}\n\n.mj-stat-label[_ngcontent-%COMP%] {\n font-size: 10px;\n font-weight: 600;\n color: var(--mj-text-muted, #64748b);\n text-transform: uppercase;\n letter-spacing: 0.4px;\n margin-top: 2px;\n}\n\n.mj-drawer-section[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.mj-section-heading[_ngcontent-%COMP%] {\n margin: 0;\n font-size: 11px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.6px;\n color: var(--mj-text-muted, #64748b);\n}\n\n.mj-attr-list[_ngcontent-%COMP%] {\n margin: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n background: var(--mj-bg-surface-sunken, #080d1a);\n border: 1px solid var(--mj-border-default, #1e293b);\n border-radius: 8px;\n padding: 10px 12px;\n}\n\n.mj-attr-row[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n border-bottom: 1px solid rgba(255, 255, 255, 0.04);\n padding-bottom: 4px;\n}\n\n.mj-attr-row[_ngcontent-%COMP%]:last-child {\n border-bottom: none;\n padding-bottom: 0;\n}\n\n.mj-attr-dt[_ngcontent-%COMP%] {\n color: var(--mj-text-muted, #94a3b8);\n font-weight: 500;\n}\n\n.mj-attr-dd[_ngcontent-%COMP%] {\n margin: 0;\n color: var(--mj-text-primary, #f8fafc);\n font-weight: 600;\n text-align: right;\n max-width: 170px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.mj-drawer-actions[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: auto;\n padding-top: 12px;\n}\n\n.mj-drawer-btn-primary[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n background: var(--mj-brand-primary, #38bdf8);\n color: var(--mj-text-inverse);\n border: none;\n border-radius: 8px;\n padding: 9px 16px;\n font-size: 13px;\n font-weight: 700;\n cursor: pointer;\n transition: all 0.18s ease;\n box-shadow: 0 2px 8px color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 30%, transparent);\n}\n\n.mj-drawer-btn-primary[_ngcontent-%COMP%]:hover {\n background: var(--mj-brand-primary-hover);\n box-shadow: 0 4px 14px color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 45%, transparent);\n transform: translateY(-1px);\n}\n\n.mj-drawer-btn-row[_ngcontent-%COMP%] {\n display: flex;\n gap: 8px;\n}\n\n.mj-drawer-btn-secondary[_ngcontent-%COMP%] {\n flex: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n background: var(--mj-bg-surface-elevated, #1a2744);\n border: 1px solid var(--mj-border-default, #223254);\n border-radius: 8px;\n padding: 7px 12px;\n color: var(--mj-text-primary, #f8fafc);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.mj-drawer-btn-secondary[_ngcontent-%COMP%]:hover {\n background: var(--mj-border-default, #223254);\n border-color: var(--mj-brand-primary, #38bdf8);\n color: var(--mj-brand-primary, #38bdf8);\n}"] });
1345
+ }
1346
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(HierarchyTreeComponent, [{
1347
+ type: Component,
1348
+ args: [{ selector: 'mj-hierarchy-tree', standalone: true, imports: [CommonModule, FormsModule], template: "<div class=\"mj-hierarchy-wrapper\" [style.height]=\"Config.Height || '100%'\" [style.min-height]=\"Config.MinHeight || '500px'\">\n <!-- Top Controls Bar -->\n @if (Config.ShowToolbar || Config.ShowSearch) {\n <div class=\"mj-hierarchy-toolbar\">\n <!-- Search Box -->\n @if (Config.ShowSearch) {\n <div class=\"mj-search-box\">\n <i class=\"fa-solid fa-magnifying-glass mj-search-icon\"></i>\n <input\n type=\"text\"\n class=\"mj-search-input\"\n placeholder=\"Search hierarchy...\"\n [(ngModel)]=\"SearchQuery\"\n (input)=\"onSearchInput()\"\n />\n @if (SearchQuery) {\n <button type=\"button\" class=\"mj-clear-btn\" (click)=\"clearSearch()\" title=\"Clear search\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n }\n @if (SearchQuery && MatchingNodeCount > 0) {\n <span class=\"mj-match-badge\">{{ MatchingNodeCount }} found</span>\n }\n </div>\n }\n\n <!-- Focus Subtree Indicator -->\n @if (FocusedNode) {\n <div class=\"mj-focus-indicator\">\n <i class=\"fa-solid fa-crosshairs\"></i>\n <span>Focusing subtree: <strong>{{ FocusedNode.Name }}</strong></span>\n <button type=\"button\" class=\"mj-reset-focus-btn\" (click)=\"resetFocus()\" title=\"Show entire hierarchy\">\n <i class=\"fa-solid fa-rotate-left\"></i> View Full Hierarchy\n </button>\n </div>\n }\n\n <!-- Toolbar Action Buttons -->\n @if (Config.ShowToolbar) {\n <div class=\"mj-toolbar-actions\">\n <button type=\"button\" class=\"mj-tool-btn\" (click)=\"expandAll()\" title=\"Expand all subtrees\">\n <i class=\"fa-solid fa-folder-open\"></i>\n </button>\n <button type=\"button\" class=\"mj-tool-btn\" (click)=\"collapseAll()\" title=\"Collapse all subtrees\">\n <i class=\"fa-solid fa-folder\"></i>\n </button>\n <div class=\"mj-tool-divider\"></div>\n <button type=\"button\" class=\"mj-tool-btn\" (click)=\"zoomIn()\" title=\"Zoom in\">\n <i class=\"fa-solid fa-plus\"></i>\n </button>\n <button type=\"button\" class=\"mj-tool-btn\" (click)=\"zoomOut()\" title=\"Zoom out\">\n <i class=\"fa-solid fa-minus\"></i>\n </button>\n <button type=\"button\" class=\"mj-tool-btn\" (click)=\"onFitToViewClick()\" title=\"Fit hierarchy to view\">\n <i class=\"fa-solid fa-expand\"></i>\n </button>\n <div class=\"mj-tool-divider\"></div>\n <button type=\"button\" class=\"mj-tool-btn\" (click)=\"refresh()\" title=\"Reload hierarchy data\">\n <i class=\"fa-solid fa-rotate-right\"></i>\n </button>\n </div>\n }\n </div>\n }\n\n <!-- SVG Canvas Container -->\n <div #svgContainer class=\"mj-canvas-container\" (click)=\"onCanvasBackgroundClick($event)\">\n @if (Loading) {\n <div class=\"mj-state-overlay\">\n <div class=\"mj-spinner\"></div>\n <span>Loading hierarchy...</span>\n </div>\n } @else if (ErrorMessage) {\n <div class=\"mj-state-overlay mj-error-state\">\n <i class=\"fa-solid fa-circle-exclamation mj-error-icon\"></i>\n <span>{{ ErrorMessage }}</span>\n <button type=\"button\" class=\"mj-retry-btn\" (click)=\"loadData()\">Retry</button>\n </div>\n } @else if (AllNodes.length === 0) {\n <div class=\"mj-state-overlay mj-empty-state\">\n <i class=\"fa-solid fa-sitemap mj-empty-icon\"></i>\n <span>No hierarchy records found.</span>\n </div>\n }\n\n <svg #svgElement class=\"mj-hierarchy-svg\">\n <defs>\n <pattern id=\"mj-tree-grid\" width=\"24\" height=\"24\" patternUnits=\"userSpaceOnUse\">\n <circle cx=\"12\" cy=\"12\" r=\"1\" fill=\"var(--mj-border-subtle, #1e293b)\" opacity=\"0.4\" />\n </pattern>\n </defs>\n\n <!-- Background grid -->\n <rect width=\"100%\" height=\"100%\" fill=\"url(#mj-tree-grid)\" />\n\n <!-- Main Canvas Group (Zoomable & Pannable) -->\n <g class=\"mj-hierarchy-canvas\">\n <!-- Connective Link Curves -->\n <g class=\"mj-links-group\"></g>\n\n <!-- Interactive Node Cards via ForeignObject -->\n <g class=\"mj-nodes-group\">\n @for (node of AllNodes; track node.ID) {\n @if (node.x != null && node.y != null) {\n <foreignObject\n [attr.x]=\"node.x\"\n [attr.y]=\"node.y\"\n [attr.width]=\"Config.NodeWidth || 230\"\n [attr.height]=\"Config.NodeHeight || 90\"\n class=\"mj-node-foreign-object\"\n >\n <div\n class=\"mj-node-card\"\n [class.selected]=\"node.IsSelected\"\n [class.highlighted]=\"node.IsHighlighted\"\n [class.focus-root]=\"node.IsFocusRoot\"\n (click)=\"onNodeClick(node, $event)\"\n (dblclick)=\"onNodeDoubleClick(node, $event)\"\n >\n <!-- Accent Top Border -->\n <div class=\"mj-card-accent\" [style.background]=\"node.Color || Config.DefaultColor\"></div>\n\n <!-- Node Card Header -->\n <div class=\"mj-node-header\">\n <div class=\"mj-node-icon\" [style.color]=\"node.Color || Config.DefaultColor\">\n <i [class]=\"node.Icon || Config.DefaultIcon\"></i>\n </div>\n <div class=\"mj-node-title-group\">\n <div class=\"mj-node-name\" [title]=\"node.Name\">{{ node.Name }}</div>\n @if (node.Subtitle) {\n <div class=\"mj-node-subtitle\">{{ node.Subtitle }}</div>\n }\n </div>\n </div>\n\n <!-- Node Card Footer & Actions -->\n <div class=\"mj-node-footer\">\n <!-- Subtree Expansion Pill -->\n @if (node.DirectChildCount > 0) {\n <button\n type=\"button\"\n class=\"mj-expand-badge\"\n [class.collapsed]=\"!node.IsExpanded\"\n (click)=\"toggleNodeExpansion(node, $event)\"\n [title]=\"node.IsExpanded ? 'Collapse sub-branch' : 'Expand ' + node.DirectChildCount + ' children'\"\n >\n <i class=\"fa-solid\" [class.fa-chevron-down]=\"node.IsExpanded\" [class.fa-chevron-right]=\"!node.IsExpanded\"></i>\n <span>{{ node.DirectChildCount }}</span>\n </button>\n } @else {\n <span class=\"mj-leaf-indicator\">Leaf</span>\n }\n\n <!-- Quick Action Buttons -->\n @if (AllowNodeActions) {\n <div class=\"mj-node-actions\">\n <button\n type=\"button\"\n class=\"mj-node-action-btn\"\n (click)=\"onNodeAction(node, 'focus', $event)\"\n title=\"Focus on this subtree\"\n >\n <i class=\"fa-solid fa-crosshairs\"></i>\n </button>\n <button\n type=\"button\"\n class=\"mj-node-action-btn\"\n (click)=\"onNodeAction(node, 'open', $event)\"\n title=\"Open record in Explorer\"\n >\n <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\n </button>\n </div>\n }\n </div>\n </div>\n </foreignObject>\n }\n }\n </g>\n </g>\n </svg>\n\n <!-- Slide-In Node Details Drawer -->\n <aside\n class=\"mj-details-drawer\"\n [class.open]=\"SelectedNode != null && ShowDetailsDrawer\"\n aria-label=\"Node details panel\"\n >\n @if (SelectedNode) {\n <div class=\"mj-drawer-header\">\n <div class=\"mj-drawer-title-group\">\n <div class=\"mj-drawer-icon\" [style.color]=\"SelectedNode.Color || Config.DefaultColor\">\n <i [class]=\"SelectedNode.Icon || Config.DefaultIcon\"></i>\n </div>\n <div class=\"mj-drawer-titles\">\n <h4 class=\"mj-drawer-name\" [title]=\"SelectedNode.Name\">{{ SelectedNode.Name }}</h4>\n @if (SelectedNode.Subtitle) {\n <p class=\"mj-drawer-subtitle\">{{ SelectedNode.Subtitle }}</p>\n }\n </div>\n </div>\n <button\n type=\"button\"\n class=\"mj-drawer-close-btn\"\n (click)=\"closeDetailsDrawer()\"\n title=\"Close details\"\n >\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n\n <div class=\"mj-drawer-body\">\n <div class=\"mj-drawer-stats\">\n <div class=\"mj-stat-card\">\n <span class=\"mj-stat-num\">{{ SelectedNode.Depth }}</span>\n <span class=\"mj-stat-label\">Hierarchy Level</span>\n </div>\n <div class=\"mj-stat-card\">\n <span class=\"mj-stat-num\">{{ SelectedNode.DirectChildCount }}</span>\n <span class=\"mj-stat-label\">Direct Children</span>\n </div>\n <div class=\"mj-stat-card\">\n <span class=\"mj-stat-num\">{{ SelectedNode.TotalDescendantCount }}</span>\n <span class=\"mj-stat-label\">Total Subtree</span>\n </div>\n </div>\n\n @if (SelectedNode.Record && getRecordAttributeEntries(SelectedNode.Record).length > 0) {\n <div class=\"mj-drawer-section\">\n <h5 class=\"mj-section-heading\">Attributes</h5>\n <dl class=\"mj-attr-list\">\n @for (attr of getRecordAttributeEntries(SelectedNode.Record); track attr.key) {\n <div class=\"mj-attr-row\">\n <dt class=\"mj-attr-dt\">{{ attr.label }}</dt>\n <dd class=\"mj-attr-dd\" [title]=\"attr.value\">{{ attr.value }}</dd>\n </div>\n }\n </dl>\n </div>\n }\n\n <div class=\"mj-drawer-actions\">\n <button\n type=\"button\"\n class=\"mj-drawer-btn-primary\"\n (click)=\"navigateToRecord(SelectedNode)\"\n title=\"Open this record in Explorer\"\n >\n <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\n <span>Open Record</span>\n </button>\n\n <div class=\"mj-drawer-btn-row\">\n @if (SelectedNode.DirectChildCount > 0) {\n <button\n type=\"button\"\n class=\"mj-drawer-btn-secondary\"\n (click)=\"toggleNodeExpansion(SelectedNode)\"\n >\n <i class=\"fa-solid\" [class.fa-folder-open]=\"SelectedNode.IsExpanded\" [class.fa-folder]=\"!SelectedNode.IsExpanded\"></i>\n <span>{{ SelectedNode.IsExpanded ? 'Collapse' : 'Expand' }}</span>\n </button>\n }\n <button\n type=\"button\"\n class=\"mj-drawer-btn-secondary\"\n (click)=\"setFocusRoot(SelectedNode.ID)\"\n title=\"Focus hierarchy on this subtree\"\n >\n <i class=\"fa-solid fa-crosshairs\"></i>\n <span>Focus Tree</span>\n </button>\n </div>\n </div>\n </div>\n }\n </aside>\n </div>\n</div>\n", styles: [":host {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: 500px;\n position: relative;\n box-sizing: border-box;\n font-family: inherit;\n flex: 1;\n}\n\n.mj-hierarchy-wrapper {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: inherit;\n flex: 1;\n background: var(--mj-bg-surface-sunken, #080d1a);\n border: 1px solid var(--mj-border-default, #1e293b);\n border-radius: 12px;\n overflow: hidden;\n position: relative;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);\n}\n\n/* Toolbar */\n.mj-hierarchy-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n background: var(--mj-bg-surface, #0f172a);\n border-bottom: 1px solid var(--mj-border-default, #1e293b);\n gap: 12px;\n z-index: 10;\n flex-wrap: wrap;\n}\n\n/* Search Box */\n.mj-search-box {\n display: flex;\n align-items: center;\n gap: 8px;\n background: var(--mj-bg-surface-sunken, #080d1a);\n border: 1px solid var(--mj-border-default, #1e293b);\n border-radius: 8px;\n padding: 4px 10px;\n min-width: 240px;\n max-width: 340px;\n flex: 1;\n transition: border-color 0.18s ease;\n}\n\n.mj-search-box:focus-within {\n border-color: var(--mj-brand-primary, #38bdf8);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 18%, transparent);\n}\n\n.mj-search-icon {\n font-size: 12px;\n color: var(--mj-text-muted, #64748b);\n}\n\n.mj-search-input {\n background: transparent;\n border: none;\n outline: none;\n color: var(--mj-text-primary, #f8fafc);\n font-size: 12.5px;\n width: 100%;\n}\n\n.mj-search-input::placeholder {\n color: var(--mj-text-muted, #64748b);\n}\n\n.mj-clear-btn {\n background: transparent;\n border: none;\n color: var(--mj-text-muted, #64748b);\n cursor: pointer;\n font-size: 11px;\n padding: 2px 4px;\n border-radius: 4px;\n}\n\n.mj-clear-btn:hover {\n color: var(--mj-text-primary, #f8fafc);\n}\n\n.mj-match-badge {\n font-size: 11px;\n font-weight: 700;\n padding: 2px 7px;\n border-radius: 9999px;\n background: color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 15%, transparent);\n color: var(--mj-brand-primary, #38bdf8);\n white-space: nowrap;\n}\n\n/* Focus Subtree Indicator */\n.mj-focus-indicator {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n color: var(--mj-brand-primary, #38bdf8);\n background: color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 10%, transparent);\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 30%, transparent);\n padding: 4px 10px;\n border-radius: 6px;\n}\n\n.mj-reset-focus-btn {\n background: var(--mj-brand-primary, #38bdf8);\n color: var(--mj-text-inverse);\n border: none;\n border-radius: 4px;\n padding: 3px 8px;\n font-size: 11px;\n font-weight: 700;\n cursor: pointer;\n margin-left: 6px;\n transition: opacity 0.15s ease;\n}\n\n.mj-reset-focus-btn:hover {\n opacity: 0.9;\n}\n\n/* Toolbar Buttons */\n.mj-toolbar-actions {\n display: flex;\n align-items: center;\n gap: 4px;\n margin-left: auto;\n}\n\n.mj-tool-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n height: 30px;\n background: transparent;\n border: 1px solid var(--mj-border-subtle, #1e293b);\n border-radius: 6px;\n color: var(--mj-text-secondary, #94a3b8);\n font-size: 12px;\n cursor: pointer;\n transition: all 0.18s ease;\n}\n\n.mj-tool-btn:hover {\n background: var(--mj-bg-surface-elevated, #1e293b);\n color: var(--mj-text-primary, #f8fafc);\n border-color: var(--mj-border-default, #334155);\n}\n\n.mj-tool-divider {\n width: 1px;\n height: 18px;\n background: var(--mj-border-subtle, #1e293b);\n margin: 0 4px;\n}\n\n/* Canvas Area */\n.mj-canvas-container {\n flex: 1;\n position: relative;\n overflow: hidden;\n width: 100%;\n height: 100%;\n min-height: 450px;\n cursor: grab;\n}\n\n.mj-canvas-container:active {\n cursor: grabbing;\n}\n\n.mj-hierarchy-svg {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n display: block;\n}\n\n/* State Overlays */\n.mj-state-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n background: var(--mj-bg-overlay);\n backdrop-filter: blur(4px);\n z-index: 20;\n color: var(--mj-text-secondary, #94a3b8);\n font-size: 13px;\n}\n\n.mj-spinner {\n width: 32px;\n height: 32px;\n border: 3px solid color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 20%, transparent);\n border-top-color: var(--mj-brand-primary, #38bdf8);\n border-radius: 50%;\n animation: mj-spin 0.8s linear infinite;\n}\n\n@keyframes mj-spin {\n to { transform: rotate(360deg); }\n}\n\n.mj-error-icon, .mj-empty-icon {\n font-size: 28px;\n color: var(--mj-text-muted, #64748b);\n}\n\n.mj-retry-btn {\n background: var(--mj-brand-primary, #38bdf8);\n color: var(--mj-text-inverse);\n border: none;\n padding: 6px 14px;\n font-size: 12px;\n font-weight: 700;\n border-radius: 6px;\n cursor: pointer;\n}\n\n/* Node ForeignObject Container */\n.mj-node-foreign-object {\n overflow: visible;\n}\n\n/* Interactive Node Card */\n.mj-node-card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n background: var(--mj-bg-surface-card, #111a2e);\n border: 1px solid var(--mj-border-default, #223254);\n border-radius: 10px;\n padding: 8px 12px;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);\n position: relative;\n cursor: pointer;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n user-select: none;\n}\n\n.mj-node-card:hover {\n border-color: var(--mj-brand-primary, #38bdf8);\n transform: translateY(-2px);\n box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5), 0 0 12px color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 20%, transparent);\n}\n\n.mj-node-card.selected {\n border-color: var(--mj-brand-primary, #38bdf8);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 40%, transparent), 0 6px 20px rgba(0, 0, 0, 0.6);\n}\n\n.mj-node-card.highlighted {\n border-color: var(--mj-status-warning);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-status-warning) 40%, transparent), 0 6px 20px rgba(0, 0, 0, 0.6);\n}\n\n.mj-node-card.focus-root {\n border-color: var(--mj-status-success);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-status-success) 40%, transparent);\n}\n\n/* Accent Top Bar */\n.mj-card-accent {\n position: absolute;\n top: 0;\n left: 8px;\n right: 8px;\n height: 3px;\n border-radius: 3px 3px 0 0;\n opacity: 0.85;\n}\n\n/* Header */\n.mj-node-header {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-top: 4px;\n}\n\n.mj-node-icon {\n width: 28px;\n height: 28px;\n border-radius: 6px;\n background: rgba(255, 255, 255, 0.05);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 13px;\n flex-shrink: 0;\n}\n\n.mj-node-title-group {\n display: flex;\n flex-direction: column;\n min-width: 0;\n flex: 1;\n}\n\n.mj-node-name {\n font-size: 12.5px;\n font-weight: 700;\n color: var(--mj-text-primary, #f8fafc);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n line-height: 1.3;\n}\n\n.mj-node-subtitle {\n font-size: 11px;\n color: var(--mj-text-muted, #94a3b8);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n line-height: 1.3;\n}\n\n/* Footer */\n.mj-node-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-top: 6px;\n border-top: 1px solid rgba(255, 255, 255, 0.06);\n}\n\n/* Expansion Pill Badge */\n.mj-expand-badge {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n background: var(--mj-bg-surface-elevated, #1a2744);\n border: 1px solid var(--mj-border-default, #223254);\n border-radius: 9999px;\n padding: 2px 8px;\n color: var(--mj-text-secondary, #94a3b8);\n font-size: 11px;\n font-weight: 700;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.mj-expand-badge:hover {\n background: var(--mj-brand-primary, #38bdf8);\n color: var(--mj-text-inverse);\n border-color: var(--mj-brand-primary, #38bdf8);\n}\n\n.mj-expand-badge.collapsed {\n background: color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 15%, transparent);\n color: var(--mj-brand-primary, #38bdf8);\n border-color: color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 35%, transparent);\n}\n\n.mj-leaf-indicator {\n font-size: 10px;\n color: var(--mj-text-muted, #64748b);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n padding-left: 2px;\n}\n\n/* Actions */\n.mj-node-actions {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.mj-node-action-btn {\n width: 22px;\n height: 22px;\n border-radius: 4px;\n background: transparent;\n border: 1px solid transparent;\n color: var(--mj-text-muted, #64748b);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 10.5px;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.mj-node-action-btn:hover {\n background: var(--mj-bg-surface-elevated, #1a2744);\n border-color: var(--mj-border-default, #223254);\n color: var(--mj-brand-primary, #38bdf8);\n}\n\n/* ============================================================================\n * Slide-In Node Details Drawer\n * ============================================================================ */\n.mj-details-drawer {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 330px;\n max-width: 85%;\n background: var(--mj-bg-surface, #0f172a);\n border-left: 1px solid var(--mj-border-default, #1e293b);\n box-shadow: -6px 0 24px rgba(0, 0, 0, 0.45);\n z-index: 30;\n display: flex;\n flex-direction: column;\n transform: translateX(100%);\n transition: transform 0.26s cubic-bezier(0.16, 1, 0.3, 1);\n pointer-events: none;\n user-select: text;\n}\n\n.mj-details-drawer.open {\n transform: translateX(0);\n pointer-events: auto;\n}\n\n.mj-drawer-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n padding: 16px 18px;\n background: var(--mj-bg-surface-elevated, #151f36);\n border-bottom: 1px solid var(--mj-border-default, #1e293b);\n gap: 12px;\n}\n\n.mj-drawer-title-group {\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 0;\n flex: 1;\n}\n\n.mj-drawer-icon {\n width: 36px;\n height: 36px;\n border-radius: 8px;\n background: rgba(255, 255, 255, 0.05);\n border: 1px solid rgba(255, 255, 255, 0.08);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n flex-shrink: 0;\n}\n\n.mj-drawer-titles {\n min-width: 0;\n flex: 1;\n}\n\n.mj-drawer-name {\n margin: 0;\n font-size: 15px;\n font-weight: 700;\n color: var(--mj-text-primary, #f8fafc);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n line-height: 1.3;\n}\n\n.mj-drawer-subtitle {\n margin: 2px 0 0 0;\n font-size: 12px;\n color: var(--mj-text-muted, #94a3b8);\n line-height: 1.3;\n}\n\n.mj-drawer-close-btn {\n background: transparent;\n border: 1px solid transparent;\n border-radius: 6px;\n width: 28px;\n height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-muted, #94a3b8);\n cursor: pointer;\n font-size: 14px;\n transition: all 0.15s ease;\n flex-shrink: 0;\n}\n\n.mj-drawer-close-btn:hover {\n background: rgba(255, 255, 255, 0.08);\n color: var(--mj-text-primary, #f8fafc);\n border-color: var(--mj-border-default, #223254);\n}\n\n.mj-drawer-body {\n flex: 1;\n overflow-y: auto;\n padding: 18px;\n display: flex;\n flex-direction: column;\n gap: 18px;\n}\n\n.mj-drawer-stats {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 8px;\n}\n\n.mj-stat-card {\n background: var(--mj-bg-surface-sunken, #080d1a);\n border: 1px solid var(--mj-border-default, #1e293b);\n border-radius: 8px;\n padding: 8px 6px;\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n}\n\n.mj-stat-num {\n font-size: 16px;\n font-weight: 700;\n color: var(--mj-brand-primary, #38bdf8);\n line-height: 1.2;\n}\n\n.mj-stat-label {\n font-size: 10px;\n font-weight: 600;\n color: var(--mj-text-muted, #64748b);\n text-transform: uppercase;\n letter-spacing: 0.4px;\n margin-top: 2px;\n}\n\n.mj-drawer-section {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.mj-section-heading {\n margin: 0;\n font-size: 11px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.6px;\n color: var(--mj-text-muted, #64748b);\n}\n\n.mj-attr-list {\n margin: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n background: var(--mj-bg-surface-sunken, #080d1a);\n border: 1px solid var(--mj-border-default, #1e293b);\n border-radius: 8px;\n padding: 10px 12px;\n}\n\n.mj-attr-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n border-bottom: 1px solid rgba(255, 255, 255, 0.04);\n padding-bottom: 4px;\n}\n\n.mj-attr-row:last-child {\n border-bottom: none;\n padding-bottom: 0;\n}\n\n.mj-attr-dt {\n color: var(--mj-text-muted, #94a3b8);\n font-weight: 500;\n}\n\n.mj-attr-dd {\n margin: 0;\n color: var(--mj-text-primary, #f8fafc);\n font-weight: 600;\n text-align: right;\n max-width: 170px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.mj-drawer-actions {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: auto;\n padding-top: 12px;\n}\n\n.mj-drawer-btn-primary {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n background: var(--mj-brand-primary, #38bdf8);\n color: var(--mj-text-inverse);\n border: none;\n border-radius: 8px;\n padding: 9px 16px;\n font-size: 13px;\n font-weight: 700;\n cursor: pointer;\n transition: all 0.18s ease;\n box-shadow: 0 2px 8px color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 30%, transparent);\n}\n\n.mj-drawer-btn-primary:hover {\n background: var(--mj-brand-primary-hover);\n box-shadow: 0 4px 14px color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 45%, transparent);\n transform: translateY(-1px);\n}\n\n.mj-drawer-btn-row {\n display: flex;\n gap: 8px;\n}\n\n.mj-drawer-btn-secondary {\n flex: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n background: var(--mj-bg-surface-elevated, #1a2744);\n border: 1px solid var(--mj-border-default, #223254);\n border-radius: 8px;\n padding: 7px 12px;\n color: var(--mj-text-primary, #f8fafc);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.mj-drawer-btn-secondary:hover {\n background: var(--mj-border-default, #223254);\n border-color: var(--mj-brand-primary, #38bdf8);\n color: var(--mj-brand-primary, #38bdf8);\n}\n"] }]
1349
+ }], () => [{ type: i0.ChangeDetectorRef }], { svgContainerRef: [{
1350
+ type: ViewChild,
1351
+ args: ['svgContainer', { static: false }]
1352
+ }], svgRef: [{
1353
+ type: ViewChild,
1354
+ args: ['svgElement', { static: false }]
1355
+ }], Config: [{
1356
+ type: Input
1357
+ }], Data: [{
1358
+ type: Input
1359
+ }], ActiveRecordID: [{
1360
+ type: Input
1361
+ }], FocusRecordID: [{
1362
+ type: Input
1363
+ }], AllowNodeActions: [{
1364
+ type: Input
1365
+ }], NodeClick: [{
1366
+ type: Output
1367
+ }], NodeDoubleClick: [{
1368
+ type: Output
1369
+ }], NodeSelect: [{
1370
+ type: Output
1371
+ }], BeforeNodeExpand: [{
1372
+ type: Output
1373
+ }], AfterNodeExpand: [{
1374
+ type: Output
1375
+ }], BeforeNodeCollapse: [{
1376
+ type: Output
1377
+ }], AfterNodeCollapse: [{
1378
+ type: Output
1379
+ }], BeforeReparent: [{
1380
+ type: Output
1381
+ }], AfterReparent: [{
1382
+ type: Output
1383
+ }], NodeAction: [{
1384
+ type: Output
1385
+ }], ZoomLevel: [{
1386
+ type: Input
1387
+ }], ZoomChange: [{
1388
+ type: Output
1389
+ }], Navigate: [{
1390
+ type: Output
1391
+ }] }); })();
1392
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(HierarchyTreeComponent, { className: "HierarchyTreeComponent", filePath: "src/lib/components/hierarchy-tree.component.ts", lineNumber: 73 }); })();
1393
+ //# sourceMappingURL=hierarchy-tree.component.js.map