@bilalba/fig-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +112 -0
  2. package/dist/compare-get-vector.d.ts +2 -0
  3. package/dist/compare-get-vector.d.ts.map +1 -0
  4. package/dist/compare-get-vector.js +124 -0
  5. package/dist/compare-get-vector.js.map +1 -0
  6. package/dist/compare-mcp-vs-direct.d.ts +2 -0
  7. package/dist/compare-mcp-vs-direct.d.ts.map +1 -0
  8. package/dist/compare-mcp-vs-direct.js +173 -0
  9. package/dist/compare-mcp-vs-direct.js.map +1 -0
  10. package/dist/compare-renderers.d.ts +2 -0
  11. package/dist/compare-renderers.d.ts.map +1 -0
  12. package/dist/compare-renderers.js +110 -0
  13. package/dist/compare-renderers.js.map +1 -0
  14. package/dist/debug/debug-stroke-geom.d.ts +2 -0
  15. package/dist/debug/debug-stroke-geom.d.ts.map +1 -0
  16. package/dist/debug/debug-stroke-geom.js +67 -0
  17. package/dist/debug/debug-stroke-geom.js.map +1 -0
  18. package/dist/debug/debug-transforms.d.ts +2 -0
  19. package/dist/debug/debug-transforms.d.ts.map +1 -0
  20. package/dist/debug/debug-transforms.js +97 -0
  21. package/dist/debug/debug-transforms.js.map +1 -0
  22. package/dist/debug/debug-vertex.d.ts +2 -0
  23. package/dist/debug/debug-vertex.d.ts.map +1 -0
  24. package/dist/debug/debug-vertex.js +72 -0
  25. package/dist/debug/debug-vertex.js.map +1 -0
  26. package/dist/debug-group.d.ts +5 -0
  27. package/dist/debug-group.d.ts.map +1 -0
  28. package/dist/debug-group.js +44 -0
  29. package/dist/debug-group.js.map +1 -0
  30. package/dist/debug-stroke-geom.d.ts +2 -0
  31. package/dist/debug-stroke-geom.d.ts.map +1 -0
  32. package/dist/debug-stroke-geom.js +67 -0
  33. package/dist/debug-stroke-geom.js.map +1 -0
  34. package/dist/debug-transforms.d.ts +2 -0
  35. package/dist/debug-transforms.d.ts.map +1 -0
  36. package/dist/debug-transforms.js +97 -0
  37. package/dist/debug-transforms.js.map +1 -0
  38. package/dist/debug-vertex.d.ts +2 -0
  39. package/dist/debug-vertex.d.ts.map +1 -0
  40. package/dist/debug-vertex.js +72 -0
  41. package/dist/debug-vertex.js.map +1 -0
  42. package/dist/decode-vector-network.d.ts +5 -0
  43. package/dist/decode-vector-network.d.ts.map +1 -0
  44. package/dist/decode-vector-network.js +160 -0
  45. package/dist/decode-vector-network.js.map +1 -0
  46. package/dist/experimental/paint-utils.d.ts +35 -0
  47. package/dist/experimental/paint-utils.d.ts.map +1 -0
  48. package/dist/experimental/paint-utils.js +105 -0
  49. package/dist/experimental/paint-utils.js.map +1 -0
  50. package/dist/experimental/render-screen-v2.d.ts +32 -0
  51. package/dist/experimental/render-screen-v2.d.ts.map +1 -0
  52. package/dist/experimental/render-screen-v2.js +366 -0
  53. package/dist/experimental/render-screen-v2.js.map +1 -0
  54. package/dist/experimental/render-screen.d.ts +26 -0
  55. package/dist/experimental/render-screen.d.ts.map +1 -0
  56. package/dist/experimental/render-screen.js +547 -0
  57. package/dist/experimental/render-screen.js.map +1 -0
  58. package/dist/experimental/render-types.d.ts +43 -0
  59. package/dist/experimental/render-types.d.ts.map +1 -0
  60. package/dist/experimental/render-types.js +22 -0
  61. package/dist/experimental/render-types.js.map +1 -0
  62. package/dist/experimental/render-utils.d.ts +38 -0
  63. package/dist/experimental/render-utils.d.ts.map +1 -0
  64. package/dist/experimental/render-utils.js +126 -0
  65. package/dist/experimental/render-utils.js.map +1 -0
  66. package/dist/experimental/screenshot.d.ts +11 -0
  67. package/dist/experimental/screenshot.d.ts.map +1 -0
  68. package/dist/experimental/screenshot.js +26 -0
  69. package/dist/experimental/screenshot.js.map +1 -0
  70. package/dist/experimental/vector-renderer.d.ts +31 -0
  71. package/dist/experimental/vector-renderer.d.ts.map +1 -0
  72. package/dist/experimental/vector-renderer.js +427 -0
  73. package/dist/experimental/vector-renderer.js.map +1 -0
  74. package/dist/explore-images.d.ts +9 -0
  75. package/dist/explore-images.d.ts.map +1 -0
  76. package/dist/explore-images.js +307 -0
  77. package/dist/explore-images.js.map +1 -0
  78. package/dist/http-server.d.ts +8 -0
  79. package/dist/http-server.d.ts.map +1 -0
  80. package/dist/http-server.js +95 -0
  81. package/dist/http-server.js.map +1 -0
  82. package/dist/index.d.ts +9 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +34 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/inspect-fig.d.ts +16 -0
  87. package/dist/inspect-fig.d.ts.map +1 -0
  88. package/dist/inspect-fig.js +134 -0
  89. package/dist/inspect-fig.js.map +1 -0
  90. package/dist/inspect-frame.d.ts +2 -0
  91. package/dist/inspect-frame.d.ts.map +1 -0
  92. package/dist/inspect-frame.js +90 -0
  93. package/dist/inspect-frame.js.map +1 -0
  94. package/dist/inspect-nodes.d.ts +5 -0
  95. package/dist/inspect-nodes.d.ts.map +1 -0
  96. package/dist/inspect-nodes.js +193 -0
  97. package/dist/inspect-nodes.js.map +1 -0
  98. package/dist/mcp/server.d.ts +38 -0
  99. package/dist/mcp/server.d.ts.map +1 -0
  100. package/dist/mcp/server.js +1524 -0
  101. package/dist/mcp/server.js.map +1 -0
  102. package/dist/parser/fig-reader.d.ts +29 -0
  103. package/dist/parser/fig-reader.d.ts.map +1 -0
  104. package/dist/parser/fig-reader.js +182 -0
  105. package/dist/parser/fig-reader.js.map +1 -0
  106. package/dist/parser/index.d.ts +48 -0
  107. package/dist/parser/index.d.ts.map +1 -0
  108. package/dist/parser/index.js +106 -0
  109. package/dist/parser/index.js.map +1 -0
  110. package/dist/parser/kiwi-parser.d.ts +66 -0
  111. package/dist/parser/kiwi-parser.d.ts.map +1 -0
  112. package/dist/parser/kiwi-parser.js +491 -0
  113. package/dist/parser/kiwi-parser.js.map +1 -0
  114. package/dist/parser/layout-inference.d.ts +63 -0
  115. package/dist/parser/layout-inference.d.ts.map +1 -0
  116. package/dist/parser/layout-inference.js +446 -0
  117. package/dist/parser/layout-inference.js.map +1 -0
  118. package/dist/parser/types.d.ts +286 -0
  119. package/dist/parser/types.d.ts.map +1 -0
  120. package/dist/parser/types.js +6 -0
  121. package/dist/parser/types.js.map +1 -0
  122. package/dist/render-single.d.ts +2 -0
  123. package/dist/render-single.d.ts.map +1 -0
  124. package/dist/render-single.js +53 -0
  125. package/dist/render-single.js.map +1 -0
  126. package/dist/renderer/index.d.ts +16 -0
  127. package/dist/renderer/index.d.ts.map +1 -0
  128. package/dist/renderer/index.js +18 -0
  129. package/dist/renderer/index.js.map +1 -0
  130. package/dist/renderer/paint-utils.d.ts +35 -0
  131. package/dist/renderer/paint-utils.d.ts.map +1 -0
  132. package/dist/renderer/paint-utils.js +105 -0
  133. package/dist/renderer/paint-utils.js.map +1 -0
  134. package/dist/renderer/render-screen.d.ts +26 -0
  135. package/dist/renderer/render-screen.d.ts.map +1 -0
  136. package/dist/renderer/render-screen.js +547 -0
  137. package/dist/renderer/render-screen.js.map +1 -0
  138. package/dist/renderer/render-types.d.ts +43 -0
  139. package/dist/renderer/render-types.d.ts.map +1 -0
  140. package/dist/renderer/render-types.js +22 -0
  141. package/dist/renderer/render-types.js.map +1 -0
  142. package/dist/renderer/render-utils.d.ts +38 -0
  143. package/dist/renderer/render-utils.d.ts.map +1 -0
  144. package/dist/renderer/render-utils.js +126 -0
  145. package/dist/renderer/render-utils.js.map +1 -0
  146. package/dist/renderer/screenshot.d.ts +11 -0
  147. package/dist/renderer/screenshot.d.ts.map +1 -0
  148. package/dist/renderer/screenshot.js +26 -0
  149. package/dist/renderer/screenshot.js.map +1 -0
  150. package/dist/renderer/vector-renderer.d.ts +31 -0
  151. package/dist/renderer/vector-renderer.d.ts.map +1 -0
  152. package/dist/renderer/vector-renderer.js +427 -0
  153. package/dist/renderer/vector-renderer.js.map +1 -0
  154. package/dist/shared-config.d.ts +9 -0
  155. package/dist/shared-config.d.ts.map +1 -0
  156. package/dist/shared-config.js +9 -0
  157. package/dist/shared-config.js.map +1 -0
  158. package/dist/test-parser.d.ts +3 -0
  159. package/dist/test-parser.d.ts.map +1 -0
  160. package/dist/test-parser.js +74 -0
  161. package/dist/test-parser.js.map +1 -0
  162. package/dist/test-render-v2.d.ts +5 -0
  163. package/dist/test-render-v2.d.ts.map +1 -0
  164. package/dist/test-render-v2.js +76 -0
  165. package/dist/test-render-v2.js.map +1 -0
  166. package/dist/test-render.d.ts +5 -0
  167. package/dist/test-render.d.ts.map +1 -0
  168. package/dist/test-render.js +76 -0
  169. package/dist/test-render.js.map +1 -0
  170. package/dist/vector-export.d.ts +52 -0
  171. package/dist/vector-export.d.ts.map +1 -0
  172. package/dist/vector-export.js +628 -0
  173. package/dist/vector-export.js.map +1 -0
  174. package/dist/web-viewer/build-client.d.ts +6 -0
  175. package/dist/web-viewer/build-client.d.ts.map +1 -0
  176. package/dist/web-viewer/build-client.js +36 -0
  177. package/dist/web-viewer/build-client.js.map +1 -0
  178. package/dist/web-viewer/client/viewer.d.ts +7 -0
  179. package/dist/web-viewer/client/viewer.d.ts.map +1 -0
  180. package/dist/web-viewer/client/viewer.js +873 -0
  181. package/dist/web-viewer/client/viewer.js.map +1 -0
  182. package/dist/web-viewer/server.d.ts +16 -0
  183. package/dist/web-viewer/server.d.ts.map +1 -0
  184. package/dist/web-viewer/server.js +420 -0
  185. package/dist/web-viewer/server.js.map +1 -0
  186. package/package.json +66 -0
@@ -0,0 +1,873 @@
1
+ /**
2
+ * Fig Viewer - Client Side
3
+ *
4
+ * Handles tree navigation, node selection, and SVG preview rendering.
5
+ */
6
+ // State
7
+ let currentTree = null;
8
+ let pages = [];
9
+ let selectedPageId = null;
10
+ let selectedNodeId = null;
11
+ let zoomLevel = 1;
12
+ let searchQuery = "";
13
+ // Hover/selection state for canvas interaction
14
+ let currentRenderNodeId = null; // The node being rendered in the canvas
15
+ let flatNodes = []; // Cached flat nodes for the current rendered node
16
+ let hoveredNodeId = null; // Node under cursor
17
+ let renderBounds = null;
18
+ // DOM elements
19
+ const $ = (id) => document.getElementById(id);
20
+ const elements = {
21
+ fileName: $("file-name"),
22
+ openBtn: $("open-btn"),
23
+ search: $("search"),
24
+ pagesList: $("pages-list"),
25
+ tree: $("tree"),
26
+ canvas: $("canvas"),
27
+ canvasPlaceholder: $("canvas-placeholder"),
28
+ zoomIn: $("zoom-in"),
29
+ zoomOut: $("zoom-out"),
30
+ zoomLevel: $("zoom-level"),
31
+ zoomFit: $("zoom-fit"),
32
+ noSelection: $("no-selection"),
33
+ nodeDetails: $("node-details"),
34
+ nodeId: $("node-id"),
35
+ copyId: $("copy-id"),
36
+ nodeType: $("node-type"),
37
+ nodeName: $("node-name"),
38
+ nodeX: $("node-x"),
39
+ nodeY: $("node-y"),
40
+ nodeWidth: $("node-width"),
41
+ nodeHeight: $("node-height"),
42
+ textSection: $("text-section"),
43
+ nodeText: $("node-text"),
44
+ nodeJson: $("node-json"),
45
+ fileDialog: $("file-dialog"),
46
+ filePathInput: $("file-path-input"),
47
+ cancelOpen: $("cancel-open"),
48
+ confirmOpen: $("confirm-open"),
49
+ };
50
+ // API helpers
51
+ async function api(path, options) {
52
+ const res = await fetch(path, options);
53
+ if (!res.ok) {
54
+ const err = await res.json().catch(() => ({ error: res.statusText }));
55
+ throw new Error(err.error || "Request failed");
56
+ }
57
+ return res.json();
58
+ }
59
+ async function fetchTree() {
60
+ return api("/api/tree");
61
+ }
62
+ async function fetchNodeDetails(nodeId) {
63
+ return api(`/api/node/${encodeURIComponent(nodeId)}`);
64
+ }
65
+ async function fetchNodeRaw(nodeId) {
66
+ return api(`/api/node-raw/${encodeURIComponent(nodeId)}`);
67
+ }
68
+ async function fetchRenderSvg(nodeId) {
69
+ const res = await fetch(`/api/render/${encodeURIComponent(nodeId)}`);
70
+ if (!res.ok) {
71
+ throw new Error("Failed to render");
72
+ }
73
+ return res.text();
74
+ }
75
+ async function fetchFlatNodes(nodeId) {
76
+ const res = await api(`/api/flat-nodes/${encodeURIComponent(nodeId)}`);
77
+ return res.nodes;
78
+ }
79
+ async function openFile(filePath) {
80
+ await api("/api/open", {
81
+ method: "POST",
82
+ headers: { "Content-Type": "application/json" },
83
+ body: JSON.stringify({ filePath }),
84
+ });
85
+ }
86
+ // Tree rendering
87
+ function getTypeIcon(type) {
88
+ const icons = {
89
+ DOCUMENT: "D",
90
+ CANVAS: "P",
91
+ FRAME: "F",
92
+ GROUP: "G",
93
+ TEXT: "T",
94
+ RECTANGLE: "R",
95
+ ELLIPSE: "O",
96
+ VECTOR: "V",
97
+ LINE: "L",
98
+ STAR: "*",
99
+ REGULAR_POLYGON: "P",
100
+ COMPONENT: "C",
101
+ COMPONENT_SET: "S",
102
+ INSTANCE: "I",
103
+ BOOLEAN_OPERATION: "B",
104
+ SLICE: "S",
105
+ STICKY: "N",
106
+ SHAPE_WITH_TEXT: "ST",
107
+ CONNECTOR: "CN",
108
+ SECTION: "SE",
109
+ };
110
+ return icons[type] || "?";
111
+ }
112
+ // Pages rendering
113
+ function renderPages() {
114
+ elements.pagesList.innerHTML = "";
115
+ for (const page of pages) {
116
+ const item = document.createElement("div");
117
+ item.className = "page-item";
118
+ if (page.id === selectedPageId) {
119
+ item.classList.add("selected");
120
+ }
121
+ item.dataset.pageId = page.id;
122
+ const icon = document.createElement("span");
123
+ icon.className = "page-icon";
124
+ icon.textContent = "P";
125
+ item.appendChild(icon);
126
+ const name = document.createElement("span");
127
+ name.className = "page-name";
128
+ name.textContent = page.name || "(unnamed)";
129
+ item.appendChild(name);
130
+ item.addEventListener("click", () => selectPage(page.id));
131
+ elements.pagesList.appendChild(item);
132
+ }
133
+ }
134
+ function selectPage(pageId) {
135
+ selectedPageId = pageId;
136
+ selectedNodeId = null;
137
+ // Update pages list selection
138
+ elements.pagesList.querySelectorAll(".page-item").forEach((el) => {
139
+ el.classList.toggle("selected", el.getAttribute("data-page-id") === pageId);
140
+ });
141
+ // Re-render tree for selected page
142
+ renderTree();
143
+ // Clear canvas preview
144
+ elements.canvas.innerHTML = `<div id="canvas-placeholder">Select a node to preview</div>`;
145
+ // Hide node details
146
+ elements.noSelection.classList.remove("hidden");
147
+ elements.nodeDetails.classList.add("hidden");
148
+ }
149
+ function matchesSearch(node, query) {
150
+ if (!query)
151
+ return true;
152
+ const q = query.toLowerCase();
153
+ return (node.name.toLowerCase().includes(q) ||
154
+ node.type.toLowerCase().includes(q) ||
155
+ node.id.toLowerCase().includes(q));
156
+ }
157
+ function hasMatchingDescendant(node, query) {
158
+ if (matchesSearch(node, query))
159
+ return true;
160
+ if (node.children) {
161
+ return node.children.some((child) => hasMatchingDescendant(child, query));
162
+ }
163
+ return false;
164
+ }
165
+ function renderTreeNode(node, depth = 0) {
166
+ const container = document.createElement("div");
167
+ container.className = "tree-node";
168
+ container.dataset.nodeId = node.id;
169
+ const hasChildren = node.children && node.children.length > 0;
170
+ const isExpanded = depth < 2; // Auto-expand first 2 levels
171
+ if (isExpanded && hasChildren) {
172
+ container.classList.add("expanded");
173
+ }
174
+ // Node row
175
+ const row = document.createElement("div");
176
+ row.className = "tree-node-row";
177
+ row.style.paddingLeft = `${depth * 16 + 8}px`;
178
+ if (node.id === selectedNodeId) {
179
+ row.classList.add("selected");
180
+ }
181
+ // Toggle
182
+ const toggle = document.createElement("span");
183
+ toggle.className = `tree-toggle ${hasChildren ? (isExpanded ? "expanded" : "collapsed") : ""}`;
184
+ if (hasChildren) {
185
+ toggle.addEventListener("click", (e) => {
186
+ e.stopPropagation();
187
+ container.classList.toggle("expanded");
188
+ toggle.classList.toggle("collapsed");
189
+ toggle.classList.toggle("expanded");
190
+ });
191
+ }
192
+ row.appendChild(toggle);
193
+ // Icon
194
+ const icon = document.createElement("span");
195
+ icon.className = `tree-icon type-${node.type}`;
196
+ icon.textContent = getTypeIcon(node.type);
197
+ row.appendChild(icon);
198
+ // Name
199
+ const name = document.createElement("span");
200
+ name.className = "tree-name";
201
+ name.textContent = node.name || "(unnamed)";
202
+ row.appendChild(name);
203
+ // Type badge
204
+ const typeBadge = document.createElement("span");
205
+ typeBadge.className = "tree-type";
206
+ typeBadge.textContent = node.type;
207
+ row.appendChild(typeBadge);
208
+ // Click handler
209
+ row.addEventListener("click", () => selectNode(node.id));
210
+ container.appendChild(row);
211
+ // Children container
212
+ if (hasChildren) {
213
+ const childrenContainer = document.createElement("div");
214
+ childrenContainer.className = "tree-children";
215
+ for (const child of node.children) {
216
+ if (searchQuery && !hasMatchingDescendant(child, searchQuery)) {
217
+ continue;
218
+ }
219
+ childrenContainer.appendChild(renderTreeNode(child, depth + 1));
220
+ }
221
+ container.appendChild(childrenContainer);
222
+ }
223
+ return container;
224
+ }
225
+ function renderTree() {
226
+ elements.tree.innerHTML = "";
227
+ // Find the selected page
228
+ const selectedPage = pages.find((p) => p.id === selectedPageId);
229
+ if (!selectedPage) {
230
+ elements.tree.innerHTML = `<div style="padding: 20px; color: #999;">Select a page to view its contents</div>`;
231
+ return;
232
+ }
233
+ // If searching, expand all matching paths
234
+ if (searchQuery) {
235
+ const expandMatching = (el) => {
236
+ el.classList.add("expanded");
237
+ const toggle = el.querySelector(".tree-toggle");
238
+ if (toggle) {
239
+ toggle.classList.remove("collapsed");
240
+ toggle.classList.add("expanded");
241
+ }
242
+ };
243
+ const fragment = document.createDocumentFragment();
244
+ fragment.appendChild(renderTreeNode(selectedPage));
245
+ elements.tree.appendChild(fragment);
246
+ // Expand all nodes when searching
247
+ elements.tree.querySelectorAll(".tree-node").forEach((el) => {
248
+ expandMatching(el);
249
+ });
250
+ }
251
+ else {
252
+ elements.tree.appendChild(renderTreeNode(selectedPage));
253
+ }
254
+ }
255
+ // Node selection
256
+ async function selectNode(nodeId, options) {
257
+ selectedNodeId = nodeId;
258
+ // Update tree selection
259
+ elements.tree.querySelectorAll(".tree-node-row.selected").forEach((el) => {
260
+ el.classList.remove("selected");
261
+ });
262
+ const nodeEl = elements.tree.querySelector(`[data-node-id="${nodeId}"] > .tree-node-row`);
263
+ if (nodeEl) {
264
+ nodeEl.classList.add("selected");
265
+ }
266
+ // Show details panel
267
+ elements.noSelection.classList.add("hidden");
268
+ elements.nodeDetails.classList.remove("hidden");
269
+ elements.nodeId.textContent = nodeId;
270
+ try {
271
+ // Fetch full node details
272
+ const [detailsResult, rawResult] = await Promise.allSettled([
273
+ fetchNodeDetails(nodeId),
274
+ fetchNodeRaw(nodeId),
275
+ ]);
276
+ if (detailsResult.status !== "fulfilled") {
277
+ throw detailsResult.reason;
278
+ }
279
+ const { node } = detailsResult.value;
280
+ const rawNode = rawResult.status === "fulfilled"
281
+ ? rawResult.value.node
282
+ : node;
283
+ elements.nodeType.textContent = node.type;
284
+ elements.nodeName.textContent = node.name || "(unnamed)";
285
+ elements.nodeX.textContent = node.x?.toFixed(1) ?? "-";
286
+ elements.nodeY.textContent = node.y?.toFixed(1) ?? "-";
287
+ elements.nodeWidth.textContent = node.width?.toFixed(1) ?? "-";
288
+ elements.nodeHeight.textContent = node.height?.toFixed(1) ?? "-";
289
+ // Text content
290
+ if (node.characters) {
291
+ elements.textSection.classList.remove("hidden");
292
+ elements.nodeText.textContent = node.characters;
293
+ }
294
+ else {
295
+ elements.textSection.classList.add("hidden");
296
+ }
297
+ // JSON dump
298
+ elements.nodeJson.textContent = JSON.stringify(rawNode, null, 2);
299
+ // Render preview (unless skipRender is true)
300
+ if (!options?.skipRender) {
301
+ await renderPreview(nodeId);
302
+ }
303
+ else {
304
+ // Just update the selection highlight on the canvas
305
+ updateHoverOverlay(nodeId);
306
+ }
307
+ }
308
+ catch (err) {
309
+ console.error("Failed to load node details:", err);
310
+ }
311
+ }
312
+ async function renderPreview(nodeId) {
313
+ // Track if this is a new render target (different node being rendered)
314
+ const isNewRenderTarget = nodeId !== currentRenderNodeId;
315
+ try {
316
+ // Fetch SVG and flat nodes in parallel
317
+ const [svg, nodes] = await Promise.all([
318
+ fetchRenderSvg(nodeId),
319
+ fetchFlatNodes(nodeId),
320
+ ]);
321
+ elements.canvas.innerHTML = svg;
322
+ elements.canvasPlaceholder?.remove();
323
+ // Store flat nodes for hit testing
324
+ currentRenderNodeId = nodeId;
325
+ flatNodes = nodes;
326
+ // Calculate render bounds from flat nodes
327
+ if (nodes.length > 0) {
328
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
329
+ for (const node of nodes) {
330
+ minX = Math.min(minX, node.absX);
331
+ minY = Math.min(minY, node.absY);
332
+ maxX = Math.max(maxX, node.absX + node.width);
333
+ maxY = Math.max(maxY, node.absY + node.height);
334
+ }
335
+ renderBounds = { minX, minY, width: maxX - minX, height: maxY - minY };
336
+ }
337
+ else {
338
+ renderBounds = null;
339
+ }
340
+ // Create hover overlay
341
+ createHoverOverlay();
342
+ // Only reset zoom and fit for new render targets
343
+ if (isNewRenderTarget) {
344
+ // Reset original dimensions for new SVG
345
+ originalSvgWidth = 0;
346
+ originalSvgHeight = 0;
347
+ zoomLevel = 1;
348
+ const fitsInViewport = applyZoom();
349
+ const container = elements.canvas.parentElement;
350
+ // Auto-fit if content is larger than viewport
351
+ if (!fitsInViewport) {
352
+ zoomFit();
353
+ }
354
+ else {
355
+ // Content fits - flexbox centers it, just reset scroll
356
+ container.scrollLeft = 0;
357
+ container.scrollTop = 0;
358
+ }
359
+ }
360
+ }
361
+ catch (err) {
362
+ console.error("Failed to render preview:", err);
363
+ elements.canvas.innerHTML = `<div id="canvas-placeholder">Failed to render: ${err}</div>`;
364
+ elements.canvas.style.width = "";
365
+ elements.canvas.style.height = "";
366
+ flatNodes = [];
367
+ renderBounds = null;
368
+ }
369
+ }
370
+ // Zoom controls
371
+ // Store original SVG dimensions
372
+ let originalSvgWidth = 0;
373
+ let originalSvgHeight = 0;
374
+ // Apply zoom level by modifying SVG dimensions directly (no CSS transform)
375
+ // Returns true if content fits in viewport
376
+ function applyZoom() {
377
+ const container = elements.canvas.parentElement;
378
+ const containerRect = container.getBoundingClientRect();
379
+ elements.zoomLevel.textContent = `${Math.round(zoomLevel * 100)}%`;
380
+ // Remove CSS transform - we're zooming by changing SVG size
381
+ elements.canvas.style.transform = "";
382
+ const svg = elements.canvas.querySelector("svg:not(#hover-overlay)");
383
+ if (svg) {
384
+ // Store original dimensions on first call
385
+ if (originalSvgWidth === 0) {
386
+ originalSvgWidth = svg.width.baseVal.value || parseFloat(svg.getAttribute("width") || "100");
387
+ originalSvgHeight = svg.height.baseVal.value || parseFloat(svg.getAttribute("height") || "100");
388
+ }
389
+ // Scale SVG dimensions directly
390
+ const scaledWidth = originalSvgWidth * zoomLevel;
391
+ const scaledHeight = originalSvgHeight * zoomLevel;
392
+ svg.setAttribute("width", String(scaledWidth));
393
+ svg.setAttribute("height", String(scaledHeight));
394
+ // Also update hover overlay if it exists
395
+ const overlay = elements.canvas.querySelector("#hover-overlay");
396
+ if (overlay) {
397
+ overlay.setAttribute("width", String(scaledWidth));
398
+ overlay.setAttribute("height", String(scaledHeight));
399
+ }
400
+ const padding = 80;
401
+ const contentWidth = scaledWidth + padding;
402
+ const contentHeight = scaledHeight + padding;
403
+ // Content fits if the scaled size fits in container
404
+ const fitsInViewport = contentWidth <= containerRect.width &&
405
+ contentHeight <= containerRect.height;
406
+ if (fitsInViewport) {
407
+ // Content fits - clear dimensions, let flexbox center it
408
+ elements.canvas.style.width = "";
409
+ elements.canvas.style.height = "";
410
+ }
411
+ else {
412
+ // Content larger - set explicit dimensions for proper scrolling
413
+ elements.canvas.style.width = `${contentWidth}px`;
414
+ elements.canvas.style.height = `${contentHeight}px`;
415
+ }
416
+ return fitsInViewport;
417
+ }
418
+ return true;
419
+ }
420
+ // Zoom toward a focal point, keeping that point stationary on screen
421
+ function zoomTo(newZoom, focalPoint) {
422
+ const container = elements.canvas.parentElement;
423
+ const containerRect = container.getBoundingClientRect();
424
+ const oldZoom = zoomLevel;
425
+ // Use provided focal point (relative to container), or center of viewport
426
+ const focal = focalPoint || {
427
+ x: containerRect.width / 2,
428
+ y: containerRect.height / 2,
429
+ };
430
+ // Calculate the content position under the focal point before zoom
431
+ // Account for scroll and padding (40px on each side)
432
+ const padding = 40;
433
+ const contentX = (container.scrollLeft + focal.x - padding) / oldZoom;
434
+ const contentY = (container.scrollTop + focal.y - padding) / oldZoom;
435
+ // Apply new zoom level
436
+ zoomLevel = newZoom;
437
+ const fitsInViewport = applyZoom();
438
+ if (fitsInViewport) {
439
+ // Content fits - flexbox handles centering, reset scroll
440
+ container.scrollLeft = 0;
441
+ container.scrollTop = 0;
442
+ }
443
+ else {
444
+ // Calculate new scroll to keep the same content point under focal point
445
+ const newScrollLeft = contentX * newZoom + padding - focal.x;
446
+ const newScrollTop = contentY * newZoom + padding - focal.y;
447
+ // Clamp scroll to valid range
448
+ const maxScrollX = Math.max(0, container.scrollWidth - container.clientWidth);
449
+ const maxScrollY = Math.max(0, container.scrollHeight - container.clientHeight);
450
+ container.scrollLeft = Math.max(0, Math.min(maxScrollX, newScrollLeft));
451
+ container.scrollTop = Math.max(0, Math.min(maxScrollY, newScrollTop));
452
+ }
453
+ }
454
+ // Legacy updateZoom for cases that don't need focal point (initial render, etc.)
455
+ function updateZoom() {
456
+ applyZoom();
457
+ }
458
+ function zoomIn() {
459
+ const newZoom = Math.min(zoomLevel * 1.15, 10);
460
+ zoomTo(newZoom);
461
+ }
462
+ function zoomOut() {
463
+ const newZoom = Math.max(zoomLevel / 1.15, 0.1);
464
+ zoomTo(newZoom);
465
+ }
466
+ // Smooth zoom with smaller increments for wheel/pinch
467
+ function zoomByDelta(delta, focalPoint) {
468
+ // Use very small increments for smooth zooming
469
+ // Clamp delta to avoid extreme jumps from trackpad acceleration
470
+ const clampedDelta = Math.max(-100, Math.min(100, delta));
471
+ const factor = 1 + Math.abs(clampedDelta) * 0.003;
472
+ let newZoom;
473
+ if (clampedDelta < 0) {
474
+ newZoom = Math.min(zoomLevel * factor, 10);
475
+ }
476
+ else {
477
+ newZoom = Math.max(zoomLevel / factor, 0.1);
478
+ }
479
+ zoomTo(newZoom, focalPoint);
480
+ }
481
+ function zoomFit() {
482
+ const svg = elements.canvas.querySelector("svg:not(#hover-overlay)");
483
+ if (!svg)
484
+ return;
485
+ const container = elements.canvas.parentElement;
486
+ const containerRect = container.getBoundingClientRect();
487
+ const svgWidth = svg.width.baseVal.value || parseFloat(svg.getAttribute("width") || "100");
488
+ const svgHeight = svg.height.baseVal.value || parseFloat(svg.getAttribute("height") || "100");
489
+ const padding = 80; // Account for padding on both sides
490
+ const scaleX = (containerRect.width - padding) / svgWidth;
491
+ const scaleY = (containerRect.height - padding) / svgHeight;
492
+ zoomLevel = Math.min(scaleX, scaleY, 1);
493
+ const fitsInViewport = applyZoom();
494
+ // Reset scroll - flexbox handles centering when content fits
495
+ container.scrollLeft = 0;
496
+ container.scrollTop = 0;
497
+ }
498
+ // Center the scroll position to show the content in the middle of the viewport
499
+ function centerScrollOnContent() {
500
+ const container = elements.canvas.parentElement;
501
+ const canvas = elements.canvas;
502
+ // Get the actual scrollable dimensions
503
+ const scrollWidth = container.scrollWidth;
504
+ const scrollHeight = container.scrollHeight;
505
+ const viewportWidth = container.clientWidth;
506
+ const viewportHeight = container.clientHeight;
507
+ // Center the scroll position
508
+ if (scrollWidth > viewportWidth) {
509
+ container.scrollLeft = (scrollWidth - viewportWidth) / 2;
510
+ }
511
+ else {
512
+ container.scrollLeft = 0;
513
+ }
514
+ if (scrollHeight > viewportHeight) {
515
+ container.scrollTop = (scrollHeight - viewportHeight) / 2;
516
+ }
517
+ else {
518
+ container.scrollTop = 0;
519
+ }
520
+ }
521
+ // ============================================================================
522
+ // Hover Overlay & Hit Testing
523
+ // ============================================================================
524
+ let hoverOverlay = null;
525
+ let svgWrapper = null;
526
+ function createHoverOverlay() {
527
+ // Remove existing wrapper if any
528
+ if (svgWrapper) {
529
+ svgWrapper.remove();
530
+ }
531
+ // Find the content SVG
532
+ const contentSvg = elements.canvas.querySelector("svg");
533
+ if (!contentSvg)
534
+ return;
535
+ // Create a wrapper div that will contain both SVGs
536
+ svgWrapper = document.createElement("div");
537
+ svgWrapper.id = "svg-wrapper";
538
+ svgWrapper.style.cssText = `
539
+ position: relative;
540
+ display: inline-block;
541
+ `;
542
+ // Move the content SVG into the wrapper
543
+ contentSvg.parentNode?.insertBefore(svgWrapper, contentSvg);
544
+ svgWrapper.appendChild(contentSvg);
545
+ // Create the overlay SVG
546
+ hoverOverlay = document.createElementNS("http://www.w3.org/2000/svg", "svg");
547
+ hoverOverlay.id = "hover-overlay";
548
+ hoverOverlay.style.cssText = `
549
+ position: absolute;
550
+ top: 0;
551
+ left: 0;
552
+ pointer-events: none;
553
+ overflow: visible;
554
+ `;
555
+ svgWrapper.appendChild(hoverOverlay);
556
+ }
557
+ function getContentSvg() {
558
+ // Try to find the content SVG (could be in wrapper or directly in canvas)
559
+ if (svgWrapper) {
560
+ return svgWrapper.querySelector("svg:not(#hover-overlay)");
561
+ }
562
+ return elements.canvas.querySelector("svg:not(#hover-overlay)");
563
+ }
564
+ function updateHoverOverlay(nodeId) {
565
+ if (!hoverOverlay || !renderBounds)
566
+ return;
567
+ // Clear existing content
568
+ hoverOverlay.innerHTML = "";
569
+ if (!nodeId)
570
+ return;
571
+ // Find the node in flatNodes
572
+ const node = flatNodes.find((n) => n.id === nodeId);
573
+ if (!node)
574
+ return;
575
+ // Get SVG dimensions
576
+ const svg = getContentSvg();
577
+ if (!svg)
578
+ return;
579
+ const svgWidth = svg.width.baseVal.value || parseFloat(svg.getAttribute("width") || "0");
580
+ const svgHeight = svg.height.baseVal.value || parseFloat(svg.getAttribute("height") || "0");
581
+ // Set viewBox to match SVG
582
+ hoverOverlay.setAttribute("viewBox", `0 0 ${svgWidth} ${svgHeight}`);
583
+ hoverOverlay.setAttribute("width", String(svgWidth));
584
+ hoverOverlay.setAttribute("height", String(svgHeight));
585
+ // Calculate position relative to render bounds
586
+ const x = node.absX - renderBounds.minX;
587
+ const y = node.absY - renderBounds.minY;
588
+ // Create hover rectangle
589
+ const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
590
+ rect.setAttribute("x", String(x));
591
+ rect.setAttribute("y", String(y));
592
+ rect.setAttribute("width", String(node.width));
593
+ rect.setAttribute("height", String(node.height));
594
+ rect.setAttribute("fill", "rgba(0, 120, 212, 0.1)");
595
+ rect.setAttribute("stroke", "#0078d4");
596
+ rect.setAttribute("stroke-width", "2");
597
+ hoverOverlay.appendChild(rect);
598
+ }
599
+ function screenToDesignCoords(clientX, clientY) {
600
+ const svg = getContentSvg();
601
+ if (!svg || !renderBounds)
602
+ return null;
603
+ const svgRect = svg.getBoundingClientRect();
604
+ // Get the viewBox dimensions
605
+ const svgWidth = svg.width.baseVal.value || parseFloat(svg.getAttribute("width") || "0");
606
+ const svgHeight = svg.height.baseVal.value || parseFloat(svg.getAttribute("height") || "0");
607
+ // Calculate position within the SVG element (accounting for zoom via CSS transform)
608
+ const relX = (clientX - svgRect.left) / zoomLevel;
609
+ const relY = (clientY - svgRect.top) / zoomLevel;
610
+ // Convert to design coordinates
611
+ const designX = relX + renderBounds.minX;
612
+ const designY = relY + renderBounds.minY;
613
+ return { x: designX, y: designY };
614
+ }
615
+ function findNodesAtPoint(x, y) {
616
+ // Find all nodes that contain the point, sorted by depth (deepest first)
617
+ return flatNodes
618
+ .filter((node) => {
619
+ return (node.visible &&
620
+ x >= node.absX &&
621
+ x <= node.absX + node.width &&
622
+ y >= node.absY &&
623
+ y <= node.absY + node.height);
624
+ })
625
+ .sort((a, b) => b.depth - a.depth); // Deepest first
626
+ }
627
+ function findOutermostFrame(nodes) {
628
+ // Find the outermost FRAME (not CANVAS) - shallowest depth that isn't CANVAS
629
+ const frames = nodes.filter((n) => n.type === "FRAME" || n.type === "COMPONENT" || n.type === "INSTANCE");
630
+ if (frames.length === 0)
631
+ return null;
632
+ // Sort by depth ascending (shallowest first)
633
+ frames.sort((a, b) => a.depth - b.depth);
634
+ return frames[0];
635
+ }
636
+ function findInnermostSelectable(nodes) {
637
+ // Find the innermost (deepest) node - first in the already sorted array
638
+ if (nodes.length === 0)
639
+ return null;
640
+ return nodes[0];
641
+ }
642
+ function expandToNode(nodeId) {
643
+ // Find and expand all parent nodes in the tree to reveal the target
644
+ const nodeEl = elements.tree.querySelector(`[data-node-id="${nodeId}"]`);
645
+ if (!nodeEl)
646
+ return;
647
+ // Walk up the DOM tree and expand all parent tree-nodes
648
+ let current = nodeEl.parentElement;
649
+ while (current) {
650
+ if (current.classList?.contains("tree-node")) {
651
+ current.classList.add("expanded");
652
+ const toggle = current.querySelector(":scope > .tree-node-row > .tree-toggle");
653
+ if (toggle) {
654
+ toggle.classList.remove("collapsed");
655
+ toggle.classList.add("expanded");
656
+ }
657
+ }
658
+ if (current.id === "tree")
659
+ break;
660
+ current = current.parentElement;
661
+ }
662
+ // Scroll the node into view
663
+ const row = nodeEl.querySelector(":scope > .tree-node-row");
664
+ if (row) {
665
+ row.scrollIntoView({ behavior: "smooth", block: "center" });
666
+ }
667
+ }
668
+ function handleCanvasMouseMove(e) {
669
+ if (flatNodes.length === 0)
670
+ return;
671
+ const coords = screenToDesignCoords(e.clientX, e.clientY);
672
+ if (!coords) {
673
+ hoveredNodeId = null;
674
+ updateHoverOverlay(null);
675
+ return;
676
+ }
677
+ const nodesAtPoint = findNodesAtPoint(coords.x, coords.y);
678
+ const innermost = findInnermostSelectable(nodesAtPoint);
679
+ if (innermost?.id !== hoveredNodeId) {
680
+ hoveredNodeId = innermost?.id ?? null;
681
+ updateHoverOverlay(hoveredNodeId);
682
+ }
683
+ }
684
+ function handleCanvasClick(e) {
685
+ if (flatNodes.length === 0)
686
+ return;
687
+ const coords = screenToDesignCoords(e.clientX, e.clientY);
688
+ if (!coords)
689
+ return;
690
+ const nodesAtPoint = findNodesAtPoint(coords.x, coords.y);
691
+ const innermost = findInnermostSelectable(nodesAtPoint);
692
+ if (innermost) {
693
+ expandToNode(innermost.id);
694
+ // If the node is in the current flat nodes list, we don't need to re-render
695
+ const isInCurrentView = flatNodes.some((n) => n.id === innermost.id);
696
+ selectNode(innermost.id, { skipRender: isInCurrentView });
697
+ }
698
+ }
699
+ function handleCanvasDblClick(e) {
700
+ if (flatNodes.length === 0)
701
+ return;
702
+ const coords = screenToDesignCoords(e.clientX, e.clientY);
703
+ if (!coords)
704
+ return;
705
+ const nodesAtPoint = findNodesAtPoint(coords.x, coords.y);
706
+ const innermost = findInnermostSelectable(nodesAtPoint);
707
+ if (innermost) {
708
+ expandToNode(innermost.id);
709
+ selectNode(innermost.id);
710
+ }
711
+ }
712
+ function handleCanvasMouseLeave() {
713
+ hoveredNodeId = null;
714
+ updateHoverOverlay(null);
715
+ }
716
+ // File dialog
717
+ function showFileDialog() {
718
+ elements.fileDialog.classList.remove("hidden");
719
+ elements.filePathInput.focus();
720
+ }
721
+ function hideFileDialog() {
722
+ elements.fileDialog.classList.add("hidden");
723
+ }
724
+ async function handleOpenFile() {
725
+ const filePath = elements.filePathInput.value.trim();
726
+ if (!filePath)
727
+ return;
728
+ try {
729
+ elements.confirmOpen.disabled = true;
730
+ elements.confirmOpen.textContent = "Loading...";
731
+ await openFile(filePath);
732
+ hideFileDialog();
733
+ // Reset page selection for new file
734
+ selectedPageId = null;
735
+ selectedNodeId = null;
736
+ // Reload tree
737
+ await loadTree();
738
+ // Update file name display
739
+ elements.fileName.textContent = filePath.split("/").pop() || filePath;
740
+ }
741
+ catch (err) {
742
+ alert(`Failed to open file: ${err}`);
743
+ }
744
+ finally {
745
+ elements.confirmOpen.disabled = false;
746
+ elements.confirmOpen.textContent = "Open";
747
+ }
748
+ }
749
+ // Initial load
750
+ async function loadTree() {
751
+ try {
752
+ const { tree, meta } = await fetchTree();
753
+ currentTree = tree;
754
+ // Extract pages (CANVAS nodes) from the document
755
+ pages = [];
756
+ if (tree.children) {
757
+ pages = tree.children.filter((child) => child.type === "CANVAS");
758
+ }
759
+ // Auto-select first page if none selected
760
+ if (pages.length > 0 && !selectedPageId) {
761
+ selectedPageId = pages[0].id;
762
+ }
763
+ // Render pages list and tree
764
+ renderPages();
765
+ renderTree();
766
+ // Update file name if available
767
+ if (meta?.name) {
768
+ elements.fileName.textContent = meta.name;
769
+ }
770
+ }
771
+ catch (err) {
772
+ console.error("Failed to load tree:", err);
773
+ pages = [];
774
+ elements.pagesList.innerHTML = "";
775
+ elements.tree.innerHTML = `<div style="padding: 20px; color: #999;">No file loaded. Click "Open File" to start.</div>`;
776
+ }
777
+ }
778
+ // Copy to clipboard
779
+ async function copyNodeId() {
780
+ if (!selectedNodeId)
781
+ return;
782
+ try {
783
+ await navigator.clipboard.writeText(selectedNodeId);
784
+ const originalText = elements.copyId.textContent;
785
+ elements.copyId.textContent = "Copied!";
786
+ setTimeout(() => {
787
+ elements.copyId.textContent = originalText;
788
+ }, 1000);
789
+ }
790
+ catch {
791
+ // Fallback
792
+ const textarea = document.createElement("textarea");
793
+ textarea.value = selectedNodeId;
794
+ document.body.appendChild(textarea);
795
+ textarea.select();
796
+ document.execCommand("copy");
797
+ document.body.removeChild(textarea);
798
+ }
799
+ }
800
+ // Event listeners
801
+ function init() {
802
+ // Zoom controls
803
+ elements.zoomIn.addEventListener("click", zoomIn);
804
+ elements.zoomOut.addEventListener("click", zoomOut);
805
+ elements.zoomFit.addEventListener("click", zoomFit);
806
+ // Keyboard zoom
807
+ document.addEventListener("keydown", (e) => {
808
+ if (e.target instanceof HTMLInputElement)
809
+ return;
810
+ if (e.key === "=" || e.key === "+") {
811
+ e.preventDefault();
812
+ zoomIn();
813
+ }
814
+ else if (e.key === "-") {
815
+ e.preventDefault();
816
+ zoomOut();
817
+ }
818
+ else if (e.key === "0") {
819
+ e.preventDefault();
820
+ zoomFit();
821
+ }
822
+ });
823
+ // Mouse wheel zoom (with smooth micro-increments and focal point)
824
+ elements.canvas.parentElement?.addEventListener("wheel", (e) => {
825
+ if (e.ctrlKey || e.metaKey) {
826
+ e.preventDefault();
827
+ // Get mouse position relative to container for focal point
828
+ const container = elements.canvas.parentElement;
829
+ const containerRect = container.getBoundingClientRect();
830
+ const focalPoint = {
831
+ x: e.clientX - containerRect.left,
832
+ y: e.clientY - containerRect.top,
833
+ };
834
+ // Use deltaY directly for smooth, proportional zooming
835
+ zoomByDelta(e.deltaY, focalPoint);
836
+ }
837
+ }, { passive: false });
838
+ // Canvas hover and selection handlers
839
+ elements.canvas.addEventListener("mousemove", handleCanvasMouseMove);
840
+ elements.canvas.addEventListener("click", handleCanvasClick);
841
+ elements.canvas.addEventListener("dblclick", handleCanvasDblClick);
842
+ elements.canvas.addEventListener("mouseleave", handleCanvasMouseLeave);
843
+ // Search
844
+ elements.search.addEventListener("input", (e) => {
845
+ searchQuery = e.target.value;
846
+ renderTree();
847
+ });
848
+ // File dialog
849
+ elements.openBtn.addEventListener("click", showFileDialog);
850
+ elements.cancelOpen.addEventListener("click", hideFileDialog);
851
+ elements.confirmOpen.addEventListener("click", handleOpenFile);
852
+ elements.filePathInput.addEventListener("keydown", (e) => {
853
+ if (e.key === "Enter") {
854
+ handleOpenFile();
855
+ }
856
+ else if (e.key === "Escape") {
857
+ hideFileDialog();
858
+ }
859
+ });
860
+ elements.fileDialog.addEventListener("click", (e) => {
861
+ if (e.target === elements.fileDialog) {
862
+ hideFileDialog();
863
+ }
864
+ });
865
+ // Copy ID
866
+ elements.copyId.addEventListener("click", copyNodeId);
867
+ // Initial load
868
+ loadTree();
869
+ }
870
+ // Start
871
+ document.addEventListener("DOMContentLoaded", init);
872
+ export {};
873
+ //# sourceMappingURL=viewer.js.map