uidex 0.0.1 → 0.1.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 (39) hide show
  1. package/README.md +103 -152
  2. package/claude/audit-command.md +16 -0
  3. package/claude/rules.md +88 -0
  4. package/dist/core/index.cjs +3217 -0
  5. package/dist/core/index.cjs.map +1 -0
  6. package/dist/core/index.d.cts +331 -0
  7. package/dist/core/index.d.ts +331 -0
  8. package/dist/core/index.global.js +3199 -0
  9. package/dist/core/index.global.js.map +1 -0
  10. package/dist/core/index.js +3174 -0
  11. package/dist/core/index.js.map +1 -0
  12. package/dist/core/style.css +964 -0
  13. package/dist/index.cjs +3288 -0
  14. package/dist/index.cjs.map +1 -0
  15. package/dist/index.d.cts +74 -0
  16. package/dist/index.d.ts +74 -0
  17. package/dist/index.js +3251 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/playwright/index.cjs +72 -0
  20. package/dist/playwright/index.cjs.map +1 -0
  21. package/dist/playwright/index.d.cts +62 -0
  22. package/dist/playwright/index.d.ts +62 -0
  23. package/dist/playwright/index.js +39 -0
  24. package/dist/playwright/index.js.map +1 -0
  25. package/dist/playwright/reporter.cjs +88 -0
  26. package/dist/playwright/reporter.cjs.map +1 -0
  27. package/dist/playwright/reporter.d.cts +24 -0
  28. package/dist/playwright/reporter.d.ts +24 -0
  29. package/dist/playwright/reporter.js +57 -0
  30. package/dist/playwright/reporter.js.map +1 -0
  31. package/dist/react/index.cjs +3288 -0
  32. package/dist/react/index.cjs.map +1 -0
  33. package/dist/react/index.d.cts +74 -0
  34. package/dist/react/index.d.ts +74 -0
  35. package/dist/react/index.js +3251 -0
  36. package/dist/react/index.js.map +1 -0
  37. package/dist/scripts/cli.cjs +1167 -0
  38. package/package.json +106 -7
  39. package/uidex.schema.json +93 -0
package/dist/index.js ADDED
@@ -0,0 +1,3251 @@
1
+ "use client";
2
+
3
+ // src/react/UidexDevtools.tsx
4
+ import { useEffect, useMemo, useRef } from "react";
5
+
6
+ // src/core/menu.ts
7
+ var POSITION_CLASSES = {
8
+ "bottom-right": "uidex-bottom-right",
9
+ "bottom-left": "uidex-bottom-left",
10
+ "top-right": "uidex-top-right",
11
+ "top-left": "uidex-top-left"
12
+ };
13
+ var NS = "http://www.w3.org/2000/svg";
14
+ function createIconSVG(children) {
15
+ const svg = document.createElementNS(NS, "svg");
16
+ svg.setAttribute("class", "uidex-trigger-icon");
17
+ svg.setAttribute("viewBox", "0 0 24 24");
18
+ svg.setAttribute("fill", "none");
19
+ svg.setAttribute("stroke", "currentColor");
20
+ svg.setAttribute("stroke-width", "2");
21
+ svg.setAttribute("stroke-linecap", "round");
22
+ svg.setAttribute("stroke-linejoin", "round");
23
+ for (const child of children) svg.appendChild(child);
24
+ return svg;
25
+ }
26
+ function createListSVG() {
27
+ const lines = [6, 12, 18].map((y) => {
28
+ const line = document.createElementNS(NS, "line");
29
+ line.setAttribute("x1", "4");
30
+ line.setAttribute("y1", String(y));
31
+ line.setAttribute("x2", "20");
32
+ line.setAttribute("y2", String(y));
33
+ return line;
34
+ });
35
+ return createIconSVG(lines);
36
+ }
37
+ function createSelectSVG() {
38
+ const path1 = document.createElementNS(NS, "path");
39
+ path1.setAttribute("d", "M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z");
40
+ const path2 = document.createElementNS(NS, "path");
41
+ path2.setAttribute("d", "M13 13l6 6");
42
+ return createIconSVG([path1, path2]);
43
+ }
44
+ function createCopySVG() {
45
+ const rect = document.createElementNS(NS, "rect");
46
+ rect.setAttribute("x", "9");
47
+ rect.setAttribute("y", "9");
48
+ rect.setAttribute("width", "13");
49
+ rect.setAttribute("height", "13");
50
+ rect.setAttribute("rx", "2");
51
+ const path = document.createElementNS(NS, "path");
52
+ path.setAttribute("d", "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1");
53
+ return createIconSVG([rect, path]);
54
+ }
55
+ function createFeedbackSVG() {
56
+ const path = document.createElementNS(NS, "path");
57
+ path.setAttribute("d", "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z");
58
+ return createIconSVG([path]);
59
+ }
60
+ var Menu = class {
61
+ container = null;
62
+ group = null;
63
+ expandBtn = null;
64
+ inspectBtn = null;
65
+ copyBtn = null;
66
+ feedbackBtn = null;
67
+ options;
68
+ // Drag state
69
+ isDragging = false;
70
+ dragStartX = 0;
71
+ dragStartY = 0;
72
+ dragStartLeft = 0;
73
+ dragStartTop = 0;
74
+ hasDragged = false;
75
+ boundPointerDown;
76
+ boundPointerMove;
77
+ boundPointerUp;
78
+ constructor(options = {}) {
79
+ this.options = options;
80
+ this.boundPointerDown = this.handlePointerDown.bind(this);
81
+ this.boundPointerMove = this.handlePointerMove.bind(this);
82
+ this.boundPointerUp = this.handlePointerUp.bind(this);
83
+ }
84
+ getContainer() {
85
+ return this.container;
86
+ }
87
+ /** Update a button's active state. */
88
+ setButtonActive(btn, active) {
89
+ if (!btn) return;
90
+ btn.classList.toggle("uidex-trigger-btn--active", active);
91
+ btn.setAttribute("aria-pressed", String(active));
92
+ }
93
+ setInspectActive(active) {
94
+ this.setButtonActive(this.inspectBtn, active);
95
+ }
96
+ setCopyActive(active) {
97
+ this.setButtonActive(this.copyBtn, active);
98
+ }
99
+ setFeedbackActive(active) {
100
+ this.setButtonActive(this.feedbackBtn, active);
101
+ }
102
+ createButton(modifier, ariaLabel, title, icon, onClick, toggle = false) {
103
+ const btn = document.createElement("button");
104
+ btn.className = `uidex-trigger-btn uidex-trigger-btn--${modifier}`;
105
+ btn.setAttribute("aria-label", ariaLabel);
106
+ if (toggle) btn.setAttribute("aria-pressed", "false");
107
+ btn.title = title;
108
+ btn.appendChild(icon);
109
+ btn.addEventListener("click", () => {
110
+ if (this.hasDragged) return;
111
+ onClick();
112
+ });
113
+ return btn;
114
+ }
115
+ create() {
116
+ this.container = document.createElement("div");
117
+ this.container.className = "uidex-container";
118
+ this.group = document.createElement("div");
119
+ this.group.className = "uidex-trigger-group";
120
+ this.inspectBtn = this.createButton("inspect", "Toggle select mode", "Select mode", createSelectSVG(), () => this.options.onInspectToggle?.(), true);
121
+ this.copyBtn = this.createButton("copy", "Toggle copy mode", "Copy mode", createCopySVG(), () => this.options.onCopyToggle?.(), true);
122
+ this.feedbackBtn = this.createButton("feedback", "Toggle feedback mode", "Feedback mode", createFeedbackSVG(), () => this.options.onFeedbackToggle?.(), true);
123
+ this.expandBtn = this.createButton("expand", "Open panel", "Open panel", createListSVG(), () => this.options.onExpandClick?.());
124
+ const buttons = [this.inspectBtn, this.copyBtn, this.feedbackBtn, this.expandBtn];
125
+ for (let i = 0; i < buttons.length; i++) {
126
+ if (i > 0) {
127
+ const divider = document.createElement("div");
128
+ divider.className = "uidex-trigger-divider";
129
+ this.group.appendChild(divider);
130
+ }
131
+ this.group.appendChild(buttons[i]);
132
+ }
133
+ this.group.addEventListener("pointerdown", this.boundPointerDown);
134
+ this.container.appendChild(this.group);
135
+ return this.container;
136
+ }
137
+ destroy() {
138
+ document.removeEventListener("pointermove", this.boundPointerMove);
139
+ document.removeEventListener("pointerup", this.boundPointerUp);
140
+ if (this.container && this.container.parentNode) {
141
+ this.container.parentNode.removeChild(this.container);
142
+ }
143
+ this.container = null;
144
+ this.group = null;
145
+ this.expandBtn = null;
146
+ this.inspectBtn = null;
147
+ this.copyBtn = null;
148
+ this.feedbackBtn = null;
149
+ }
150
+ // ——— Drag handling ———
151
+ handlePointerDown(e) {
152
+ if (!this.container) return;
153
+ this.hasDragged = false;
154
+ this.isDragging = true;
155
+ this.dragStartX = e.clientX;
156
+ this.dragStartY = e.clientY;
157
+ const rect = this.container.getBoundingClientRect();
158
+ this.dragStartLeft = rect.left;
159
+ this.dragStartTop = rect.top;
160
+ this.container.classList.add("uidex-container--dragging");
161
+ document.addEventListener("pointermove", this.boundPointerMove);
162
+ document.addEventListener("pointerup", this.boundPointerUp);
163
+ }
164
+ handlePointerMove(e) {
165
+ if (!this.isDragging || !this.container) return;
166
+ const dx = e.clientX - this.dragStartX;
167
+ const dy = e.clientY - this.dragStartY;
168
+ if (!this.hasDragged && Math.abs(dx) < 4 && Math.abs(dy) < 4) return;
169
+ this.hasDragged = true;
170
+ const newLeft = this.dragStartLeft + dx;
171
+ const newTop = this.dragStartTop + dy;
172
+ this.container.classList.remove(
173
+ "uidex-bottom-right",
174
+ "uidex-bottom-left",
175
+ "uidex-top-right",
176
+ "uidex-top-left"
177
+ );
178
+ this.container.style.left = `${newLeft}px`;
179
+ this.container.style.top = `${newTop}px`;
180
+ this.container.style.right = "auto";
181
+ this.container.style.bottom = "auto";
182
+ }
183
+ handlePointerUp() {
184
+ if (!this.isDragging || !this.container) return;
185
+ this.isDragging = false;
186
+ this.container.classList.remove("uidex-container--dragging");
187
+ document.removeEventListener("pointermove", this.boundPointerMove);
188
+ document.removeEventListener("pointerup", this.boundPointerUp);
189
+ if (!this.hasDragged) return;
190
+ this.snapToCorner();
191
+ }
192
+ snapToCorner() {
193
+ if (!this.container) return;
194
+ const rect = this.container.getBoundingClientRect();
195
+ const centerX = rect.left + rect.width / 2;
196
+ const centerY = rect.top + rect.height / 2;
197
+ const vpW = window.innerWidth;
198
+ const vpH = window.innerHeight;
199
+ const isRight = centerX > vpW / 2;
200
+ const isBottom = centerY > vpH / 2;
201
+ let corner;
202
+ if (isBottom && isRight) corner = "bottom-right";
203
+ else if (isBottom && !isRight) corner = "bottom-left";
204
+ else if (!isBottom && isRight) corner = "top-right";
205
+ else corner = "top-left";
206
+ this.container.style.left = "";
207
+ this.container.style.top = "";
208
+ this.container.style.right = "";
209
+ this.container.style.bottom = "";
210
+ this.container.classList.add(POSITION_CLASSES[corner]);
211
+ }
212
+ };
213
+
214
+ // src/core/utils.ts
215
+ function classNames(...classes) {
216
+ return classes.filter(Boolean).join(" ");
217
+ }
218
+ function getContrastColor(hexColor) {
219
+ const hex = hexColor.replace("#", "");
220
+ const r = parseInt(hex.substring(0, 2), 16);
221
+ const g = parseInt(hex.substring(2, 4), 16);
222
+ const b = parseInt(hex.substring(4, 6), 16);
223
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
224
+ return luminance > 0.5 ? "#000000" : "#ffffff";
225
+ }
226
+ function hexToRgba(hex, alpha) {
227
+ const hexValue = hex.replace("#", "");
228
+ const r = parseInt(hexValue.substring(0, 2), 16);
229
+ const g = parseInt(hexValue.substring(2, 4), 16);
230
+ const b = parseInt(hexValue.substring(4, 6), 16);
231
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
232
+ }
233
+ function resolveColor(color, colorMap) {
234
+ if (!color) return void 0;
235
+ if (color.startsWith("#")) return color;
236
+ return colorMap?.[color] ?? color;
237
+ }
238
+
239
+ // src/core/overlay.ts
240
+ var DEFAULT_COLOR = "#e5e5e5";
241
+ var DEFAULT_BORDER_STYLE = "solid";
242
+ var DEFAULT_BORDER_WIDTH = 2;
243
+ var DEFAULT_LABEL_POSITION = "top-left";
244
+ var LABEL_POSITION_CLASSES = {
245
+ "top-left": "uidex-label-top-left",
246
+ "top-right": "uidex-label-top-right",
247
+ "bottom-left": "uidex-label-bottom-left",
248
+ "bottom-right": "uidex-label-bottom-right"
249
+ };
250
+ var Overlay = class {
251
+ element = null;
252
+ labelElement = null;
253
+ target = null;
254
+ options;
255
+ boundUpdatePosition;
256
+ rafId = null;
257
+ constructor(options = {}) {
258
+ this.options = options;
259
+ this.boundUpdatePosition = () => {
260
+ if (this.rafId !== null) return;
261
+ this.rafId = requestAnimationFrame(() => {
262
+ this.rafId = null;
263
+ this.updatePosition();
264
+ });
265
+ };
266
+ }
267
+ show(target, label) {
268
+ this.target = target;
269
+ if (!this.element) {
270
+ this.createElement();
271
+ }
272
+ if (label !== void 0) {
273
+ this.options.label = label;
274
+ }
275
+ this.updateLabel();
276
+ this.updatePosition();
277
+ this.addListeners();
278
+ if (this.element && !this.element.parentNode) {
279
+ document.body.appendChild(this.element);
280
+ }
281
+ }
282
+ hide() {
283
+ this.removeListeners();
284
+ if (this.element && this.element.parentNode) {
285
+ this.element.parentNode.removeChild(this.element);
286
+ }
287
+ this.target = null;
288
+ }
289
+ destroy() {
290
+ this.hide();
291
+ this.element = null;
292
+ this.labelElement = null;
293
+ }
294
+ updateOptions(options) {
295
+ this.options = { ...this.options, ...options };
296
+ this.updateStyles();
297
+ this.updateLabel();
298
+ }
299
+ createElement() {
300
+ this.element = document.createElement("div");
301
+ this.element.className = "uidex-overlay";
302
+ this.element.setAttribute("data-testid", "annotation-overlay");
303
+ this.element.style.position = "fixed";
304
+ this.element.style.pointerEvents = "none";
305
+ this.element.style.zIndex = "2147483645";
306
+ this.labelElement = document.createElement("span");
307
+ this.element.appendChild(this.labelElement);
308
+ this.updateStyles();
309
+ }
310
+ updateStyles() {
311
+ if (!this.element) return;
312
+ const {
313
+ color,
314
+ borderStyle = DEFAULT_BORDER_STYLE,
315
+ borderWidth = DEFAULT_BORDER_WIDTH,
316
+ colors
317
+ } = this.options;
318
+ const resolvedColor = resolveColor(color, colors) ?? DEFAULT_COLOR;
319
+ this.element.style.borderColor = resolvedColor;
320
+ this.element.style.borderWidth = `${borderWidth}px`;
321
+ this.element.style.borderStyle = borderStyle;
322
+ }
323
+ updateLabel() {
324
+ if (!this.labelElement) return;
325
+ const {
326
+ label,
327
+ showLabel = true,
328
+ labelPosition = DEFAULT_LABEL_POSITION,
329
+ color,
330
+ colors
331
+ } = this.options;
332
+ if (!showLabel || !label) {
333
+ this.labelElement.style.display = "none";
334
+ return;
335
+ }
336
+ const resolvedColor = resolveColor(color, colors) ?? DEFAULT_COLOR;
337
+ this.labelElement.style.display = "";
338
+ this.labelElement.className = classNames(
339
+ "uidex-label",
340
+ LABEL_POSITION_CLASSES[labelPosition]
341
+ );
342
+ this.labelElement.style.position = "absolute";
343
+ this.labelElement.style.fontFamily = 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace';
344
+ this.labelElement.style.fontSize = "11px";
345
+ this.labelElement.style.fontWeight = "500";
346
+ this.labelElement.style.padding = "2px 6px";
347
+ this.labelElement.style.borderRadius = "4px";
348
+ this.labelElement.style.whiteSpace = "nowrap";
349
+ this.labelElement.style.backgroundColor = resolvedColor;
350
+ this.labelElement.style.color = getContrastColor(resolvedColor);
351
+ this.labelElement.textContent = label;
352
+ this.applyLabelPosition(labelPosition);
353
+ }
354
+ applyLabelPosition(position) {
355
+ if (!this.labelElement) return;
356
+ this.labelElement.style.top = "";
357
+ this.labelElement.style.bottom = "";
358
+ this.labelElement.style.left = "";
359
+ this.labelElement.style.right = "";
360
+ this.labelElement.style.transform = "";
361
+ switch (position) {
362
+ case "top-left":
363
+ this.labelElement.style.top = "-8px";
364
+ this.labelElement.style.left = "-2px";
365
+ this.labelElement.style.transform = "translateY(-100%)";
366
+ break;
367
+ case "top-right":
368
+ this.labelElement.style.top = "-8px";
369
+ this.labelElement.style.right = "-2px";
370
+ this.labelElement.style.transform = "translateY(-100%)";
371
+ break;
372
+ case "bottom-left":
373
+ this.labelElement.style.bottom = "-8px";
374
+ this.labelElement.style.left = "-2px";
375
+ this.labelElement.style.transform = "translateY(100%)";
376
+ break;
377
+ case "bottom-right":
378
+ this.labelElement.style.bottom = "-8px";
379
+ this.labelElement.style.right = "-2px";
380
+ this.labelElement.style.transform = "translateY(100%)";
381
+ break;
382
+ }
383
+ }
384
+ updatePosition() {
385
+ if (!this.element || !this.target) return;
386
+ const rect = this.target.getBoundingClientRect();
387
+ this.element.style.top = `${rect.top}px`;
388
+ this.element.style.left = `${rect.left}px`;
389
+ this.element.style.width = `${rect.width}px`;
390
+ this.element.style.height = `${rect.height}px`;
391
+ }
392
+ addListeners() {
393
+ window.addEventListener("resize", this.boundUpdatePosition);
394
+ window.addEventListener("scroll", this.boundUpdatePosition, { capture: true, passive: true });
395
+ }
396
+ removeListeners() {
397
+ window.removeEventListener("resize", this.boundUpdatePosition);
398
+ window.removeEventListener("scroll", this.boundUpdatePosition, { capture: true });
399
+ if (this.rafId !== null) {
400
+ cancelAnimationFrame(this.rafId);
401
+ this.rafId = null;
402
+ }
403
+ }
404
+ };
405
+
406
+ // src/core/inspector.ts
407
+ var DEFAULT_SHORTCUT = {
408
+ key: "u",
409
+ shiftKey: true,
410
+ metaKey: true
411
+ };
412
+ var INSPECTING_CLASS = "uidex-inspecting";
413
+ var INSPECTING_STYLE_ID = "uidex-inspecting-style";
414
+ var INSPECTING_CSS = `body.${INSPECTING_CLASS},body.${INSPECTING_CLASS} *{cursor:crosshair!important}`;
415
+ function isEditableTarget(target) {
416
+ if (!(target instanceof HTMLElement)) return false;
417
+ return target.isContentEditable || target.getAttribute("contenteditable") === "true" || target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
418
+ }
419
+ function matchesShortcut(e, shortcut) {
420
+ return e.key.toLowerCase() === shortcut.key.toLowerCase() && !!e.ctrlKey === !!shortcut.ctrlKey && !!e.shiftKey === !!shortcut.shiftKey && !!e.altKey === !!shortcut.altKey && !!e.metaKey === !!shortcut.metaKey;
421
+ }
422
+ function findAnnotatedAncestor(target) {
423
+ if (!(target instanceof Element)) return null;
424
+ if (target.closest(".uidex-container")) return null;
425
+ const annotated = target.closest("[data-uidex]");
426
+ if (!(annotated instanceof HTMLElement)) return null;
427
+ const id = annotated.getAttribute("data-uidex");
428
+ if (!id) return null;
429
+ return { element: annotated, id };
430
+ }
431
+ var Inspector = class {
432
+ active = false;
433
+ shortcut;
434
+ currentTarget = null;
435
+ onHighlight;
436
+ onSelect;
437
+ onActivate;
438
+ onDeactivate;
439
+ boundKeyDown;
440
+ boundMouseMove;
441
+ boundClick;
442
+ constructor(options = {}) {
443
+ this.shortcut = options.shortcut ?? DEFAULT_SHORTCUT;
444
+ this.onHighlight = options.onHighlight;
445
+ this.onSelect = options.onSelect;
446
+ this.onActivate = options.onActivate;
447
+ this.onDeactivate = options.onDeactivate;
448
+ this.boundKeyDown = this.handleKeyDown.bind(this);
449
+ this.boundMouseMove = this.handleMouseMove.bind(this);
450
+ this.boundClick = this.handleClick.bind(this);
451
+ }
452
+ mount() {
453
+ document.addEventListener("keydown", this.boundKeyDown);
454
+ if (!document.getElementById(INSPECTING_STYLE_ID)) {
455
+ const style = document.createElement("style");
456
+ style.id = INSPECTING_STYLE_ID;
457
+ style.textContent = INSPECTING_CSS;
458
+ document.head.appendChild(style);
459
+ }
460
+ }
461
+ destroy() {
462
+ if (this.active) {
463
+ this.deactivate();
464
+ }
465
+ document.removeEventListener("keydown", this.boundKeyDown);
466
+ document.getElementById(INSPECTING_STYLE_ID)?.remove();
467
+ }
468
+ activate() {
469
+ if (this.active) return;
470
+ this.active = true;
471
+ this.currentTarget = null;
472
+ document.addEventListener("mousemove", this.boundMouseMove);
473
+ document.addEventListener("click", this.boundClick, true);
474
+ document.body.classList.add(INSPECTING_CLASS);
475
+ this.onActivate?.();
476
+ }
477
+ deactivate() {
478
+ if (!this.active) return;
479
+ this.active = false;
480
+ this.currentTarget = null;
481
+ document.removeEventListener("mousemove", this.boundMouseMove);
482
+ document.removeEventListener("click", this.boundClick, true);
483
+ document.body.classList.remove(INSPECTING_CLASS);
484
+ this.onHighlight?.(null, null);
485
+ this.onDeactivate?.();
486
+ }
487
+ isActive() {
488
+ return this.active;
489
+ }
490
+ handleKeyDown(e) {
491
+ if (this.active && e.key === "Escape") {
492
+ e.preventDefault();
493
+ this.deactivate();
494
+ return;
495
+ }
496
+ if (isEditableTarget(e.target)) return;
497
+ if (matchesShortcut(e, this.shortcut)) {
498
+ e.preventDefault();
499
+ if (this.active) {
500
+ this.deactivate();
501
+ } else {
502
+ this.activate();
503
+ }
504
+ }
505
+ }
506
+ handleMouseMove(e) {
507
+ const result = findAnnotatedAncestor(e.target);
508
+ if (result) {
509
+ if (result.element !== this.currentTarget) {
510
+ this.currentTarget = result.element;
511
+ this.onHighlight?.(result.element, result.id);
512
+ }
513
+ } else if (this.currentTarget) {
514
+ this.currentTarget = null;
515
+ this.onHighlight?.(null, null);
516
+ }
517
+ }
518
+ handleClick(e) {
519
+ if (!(e.target instanceof Element)) return;
520
+ if (e.target.closest(".uidex-container")) return;
521
+ e.preventDefault();
522
+ e.stopPropagation();
523
+ const result = findAnnotatedAncestor(e.target);
524
+ if (result) {
525
+ this.onSelect?.(result.element, result.id);
526
+ }
527
+ this.deactivate();
528
+ }
529
+ };
530
+
531
+ // src/core/modal.ts
532
+ var Modal = class _Modal {
533
+ backdrop = null;
534
+ sidebar = null;
535
+ mainContent = null;
536
+ renderTarget = document.body;
537
+ boundKeyDown;
538
+ data = {
539
+ pages: [],
540
+ features: [],
541
+ components: {},
542
+ presentIds: /* @__PURE__ */ new Set()
543
+ };
544
+ currentView = null;
545
+ modalType = null;
546
+ sidebarLevel = "root";
547
+ expandedGroups = /* @__PURE__ */ new Set();
548
+ searchQuery = "";
549
+ searchInput = null;
550
+ options;
551
+ static VIEW_TYPE_TO_LEVEL = {
552
+ page: "pages",
553
+ feature: "features",
554
+ component: "components",
555
+ graph: "root"
556
+ };
557
+ /** Strip the scanner root dir (e.g. "app") so pages display as routes: app → /, app/foo → /foo */
558
+ static formatPageDir(dir) {
559
+ const parts = dir.split("/");
560
+ const route = parts.slice(1).join("/");
561
+ return "/" + route;
562
+ }
563
+ constructor(options = {}) {
564
+ this.options = options;
565
+ this.boundKeyDown = this.handleKeyDown.bind(this);
566
+ }
567
+ /** Set the shadow root to render modals into. */
568
+ setShadowRoot(root) {
569
+ this.renderTarget = root;
570
+ }
571
+ /** Update the data model. Refreshes sidebar and content if modal is open. */
572
+ setData(data) {
573
+ this.data = data;
574
+ if (this.backdrop) {
575
+ this.renderSidebar();
576
+ if (this.currentView) {
577
+ this.renderMainContent(this.currentView);
578
+ }
579
+ }
580
+ }
581
+ /** Open the modal. If already open, navigates to the given view. */
582
+ open(view) {
583
+ if (this.backdrop && this.modalType === "panel") {
584
+ if (view) this.navigateTo(view);
585
+ return;
586
+ }
587
+ this.createShell();
588
+ const targetView = view ?? this.getDefaultView();
589
+ if (targetView) {
590
+ this.navigateTo(targetView);
591
+ }
592
+ }
593
+ /** Navigate to a view within the open modal. */
594
+ navigateTo(view) {
595
+ this.currentView = view;
596
+ if (this.searchQuery) {
597
+ this.searchQuery = "";
598
+ if (this.searchInput) this.searchInput.value = "";
599
+ }
600
+ const newLevel = _Modal.VIEW_TYPE_TO_LEVEL[view.type];
601
+ const levelChanged = this.sidebarLevel !== newLevel;
602
+ if (levelChanged) {
603
+ this.sidebarLevel = newLevel;
604
+ }
605
+ if (view.type === "component") {
606
+ this.expandedGroups.add(this.getComponentPrefix(view.id));
607
+ }
608
+ if (levelChanged || view.type === "component") {
609
+ this.renderSidebar();
610
+ } else {
611
+ this.updateSidebarActive();
612
+ }
613
+ this.renderMainContent(view);
614
+ if (view.type === "component") {
615
+ this.options.onComponentNavigate?.(view.id);
616
+ }
617
+ }
618
+ hide() {
619
+ if (this.backdrop && this.backdrop.parentNode) {
620
+ this.backdrop.parentNode.removeChild(this.backdrop);
621
+ }
622
+ this.backdrop = null;
623
+ this.sidebar = null;
624
+ this.mainContent = null;
625
+ this.currentView = null;
626
+ this.modalType = null;
627
+ this.sidebarLevel = "root";
628
+ this.expandedGroups = /* @__PURE__ */ new Set();
629
+ this.searchQuery = "";
630
+ this.searchInput = null;
631
+ document.removeEventListener("keydown", this.boundKeyDown);
632
+ }
633
+ destroy() {
634
+ this.hide();
635
+ }
636
+ isOpen() {
637
+ return this.backdrop !== null;
638
+ }
639
+ // ——— Default view selection ———
640
+ getDefaultView() {
641
+ const visiblePage = this.data.pages.find(
642
+ (p) => p.componentIds.some((id) => this.data.presentIds.has(id))
643
+ );
644
+ if (visiblePage) return { type: "page", dir: visiblePage.dir };
645
+ if (this.data.pages.length > 0)
646
+ return { type: "page", dir: this.data.pages[0].dir };
647
+ if (this.data.features.length > 0)
648
+ return { type: "feature", dir: this.data.features[0].dir };
649
+ const firstPresent = [...this.data.presentIds][0];
650
+ if (firstPresent) return { type: "component", id: firstPresent };
651
+ const firstId = Object.keys(this.data.components)[0];
652
+ if (firstId) return { type: "component", id: firstId };
653
+ return null;
654
+ }
655
+ // ——— Shell creation ———
656
+ /**
657
+ * Create the shared modal frame: backdrop, dialog, header (title + close), and mount.
658
+ * Returns the dialog and header elements so callers can append extra header/body content.
659
+ */
660
+ createModalFrame(opts) {
661
+ this.hide();
662
+ this.modalType = opts.type;
663
+ this.backdrop = document.createElement("div");
664
+ this.backdrop.className = "uidex-modal-backdrop";
665
+ this.backdrop.addEventListener("click", (e) => {
666
+ if (e.target === this.backdrop) this.hide();
667
+ });
668
+ const dialog = document.createElement("div");
669
+ dialog.className = `uidex-modal uidex-modal--${opts.type}`;
670
+ dialog.setAttribute("role", "dialog");
671
+ dialog.setAttribute("aria-label", opts.ariaLabel);
672
+ const header = document.createElement("div");
673
+ header.className = "uidex-modal-header";
674
+ const title = document.createElement("h2");
675
+ title.className = "uidex-modal-title";
676
+ title.textContent = opts.title;
677
+ header.appendChild(title);
678
+ return { dialog, header };
679
+ }
680
+ /** Finalize modal frame: append close button to header, header to dialog, mount to DOM. */
681
+ mountModalFrame(dialog, header) {
682
+ const closeBtn = document.createElement("button");
683
+ closeBtn.className = "uidex-modal-close";
684
+ closeBtn.setAttribute("aria-label", "Close");
685
+ closeBtn.textContent = "\xD7";
686
+ closeBtn.addEventListener("click", () => this.hide());
687
+ header.appendChild(closeBtn);
688
+ dialog.appendChild(header);
689
+ this.backdrop.appendChild(dialog);
690
+ this.renderTarget.appendChild(this.backdrop);
691
+ document.addEventListener("keydown", this.boundKeyDown);
692
+ }
693
+ createShell() {
694
+ const { dialog, header } = this.createModalFrame({
695
+ type: "panel",
696
+ title: "uidex",
697
+ ariaLabel: "uidex"
698
+ });
699
+ this.searchInput = document.createElement("input");
700
+ this.searchInput.type = "text";
701
+ this.searchInput.className = "uidex-search-input";
702
+ this.searchInput.placeholder = "Search\u2026";
703
+ this.searchInput.addEventListener("input", () => {
704
+ this.searchQuery = this.searchInput.value;
705
+ this.renderSidebar();
706
+ });
707
+ this.searchInput.addEventListener("keydown", (e) => {
708
+ if (e.key !== "Escape" || this.searchQuery) {
709
+ e.stopPropagation();
710
+ }
711
+ if (e.key === "Enter") {
712
+ const results = this.getSearchResults();
713
+ if (results.pages.length > 0) {
714
+ this.navigateTo({ type: "page", dir: results.pages[0].dir });
715
+ } else if (results.features.length > 0) {
716
+ this.navigateTo({ type: "feature", dir: results.features[0].dir });
717
+ } else if (results.components.length > 0) {
718
+ this.navigateTo({ type: "component", id: results.components[0] });
719
+ }
720
+ e.preventDefault();
721
+ }
722
+ if (e.key === "Escape" && this.searchQuery) {
723
+ this.searchQuery = "";
724
+ this.searchInput.value = "";
725
+ this.renderSidebar();
726
+ }
727
+ });
728
+ header.appendChild(this.searchInput);
729
+ this.mountModalFrame(dialog, header);
730
+ const body = document.createElement("div");
731
+ body.className = "uidex-panel-body";
732
+ this.sidebar = document.createElement("div");
733
+ this.sidebar.className = "uidex-panel-sidebar";
734
+ this.renderSidebar();
735
+ this.mainContent = document.createElement("div");
736
+ this.mainContent.className = "uidex-panel-main";
737
+ body.appendChild(this.sidebar);
738
+ body.appendChild(this.mainContent);
739
+ dialog.appendChild(body);
740
+ }
741
+ // ——— Sidebar ———
742
+ renderSidebar() {
743
+ if (!this.sidebar) return;
744
+ this.sidebar.innerHTML = "";
745
+ if (this.searchQuery) {
746
+ this.renderSearchResults();
747
+ } else if (this.sidebarLevel === "root") {
748
+ this.renderSidebarRoot();
749
+ } else {
750
+ this.renderSidebarSublevel(this.sidebarLevel);
751
+ }
752
+ this.updateSidebarActive();
753
+ }
754
+ renderSidebarRoot() {
755
+ if (!this.sidebar) return;
756
+ const categories = [];
757
+ if (this.data.pages.length > 0) {
758
+ categories.push({ label: "Pages", level: "pages", count: this.data.pages.length });
759
+ }
760
+ if (this.data.features.length > 0) {
761
+ categories.push({ label: "Features", level: "features", count: this.data.features.length });
762
+ }
763
+ const componentCount = Object.keys(this.data.components).length;
764
+ if (componentCount > 0) {
765
+ categories.push({ label: "Components", level: "components", count: componentCount });
766
+ }
767
+ for (const cat of categories) {
768
+ const item = document.createElement("div");
769
+ item.className = `uidex-sidebar-root-item uidex-sidebar-root-item--${cat.level}`;
770
+ item.addEventListener("click", () => {
771
+ this.sidebarLevel = cat.level;
772
+ this.renderSidebar();
773
+ if (!this.currentView || !this.currentViewMatchesLevel(cat.level)) {
774
+ const firstView = this.getFirstViewForLevel(cat.level);
775
+ if (firstView) this.navigateTo(firstView);
776
+ }
777
+ });
778
+ const label = document.createElement("span");
779
+ label.className = "uidex-sidebar-root-label";
780
+ label.textContent = cat.label;
781
+ item.appendChild(label);
782
+ const count = document.createElement("span");
783
+ count.className = "uidex-sidebar-count";
784
+ count.textContent = `(${cat.count})`;
785
+ item.appendChild(count);
786
+ const chevron = document.createElement("span");
787
+ chevron.className = "uidex-sidebar-chevron";
788
+ chevron.textContent = "\u203A";
789
+ item.appendChild(chevron);
790
+ this.sidebar.appendChild(item);
791
+ }
792
+ const hasRelationships = this.data.pages.length > 0 || this.data.features.length > 0;
793
+ if (hasRelationships) {
794
+ const divider = document.createElement("div");
795
+ divider.className = "uidex-sidebar-divider";
796
+ this.sidebar.appendChild(divider);
797
+ const mapItem = document.createElement("div");
798
+ mapItem.className = "uidex-sidebar-root-item";
799
+ mapItem.dataset.viewType = "graph";
800
+ mapItem.addEventListener("click", () => this.navigateTo({ type: "graph" }));
801
+ const mapLabel = document.createElement("span");
802
+ mapLabel.className = "uidex-sidebar-root-label";
803
+ mapLabel.textContent = "Map";
804
+ mapItem.appendChild(mapLabel);
805
+ const mapChevron = document.createElement("span");
806
+ mapChevron.className = "uidex-sidebar-chevron";
807
+ mapChevron.textContent = "\u203A";
808
+ mapItem.appendChild(mapChevron);
809
+ this.sidebar.appendChild(mapItem);
810
+ }
811
+ }
812
+ renderSidebarSublevel(level) {
813
+ if (!this.sidebar) return;
814
+ const labelMap = { pages: "Pages", features: "Features", components: "Components" };
815
+ const back = document.createElement("div");
816
+ back.className = "uidex-sidebar-back";
817
+ back.addEventListener("click", () => {
818
+ this.sidebarLevel = "root";
819
+ this.renderSidebar();
820
+ });
821
+ const arrow = document.createElement("span");
822
+ arrow.className = "uidex-sidebar-back-arrow";
823
+ arrow.textContent = "\u2039";
824
+ back.appendChild(arrow);
825
+ const backLabel = document.createElement("span");
826
+ backLabel.textContent = labelMap[level];
827
+ back.appendChild(backLabel);
828
+ this.sidebar.appendChild(back);
829
+ if (level === "pages") {
830
+ for (const page of this.data.pages) {
831
+ const hasVisible = page.componentIds.some((id) => this.data.presentIds.has(id));
832
+ const item = this.createSidebarItem(_Modal.formatPageDir(page.dir), !hasVisible);
833
+ item.classList.add("uidex-sidebar-item--mono", "uidex-sidebar-item--pages");
834
+ item.dataset.viewType = "page";
835
+ item.dataset.viewId = page.dir;
836
+ item.addEventListener("click", () => this.navigateTo({ type: "page", dir: page.dir }));
837
+ this.sidebar.appendChild(item);
838
+ }
839
+ } else if (level === "features") {
840
+ for (const feature of this.data.features) {
841
+ const hasVisible = feature.componentIds.some((id) => this.data.presentIds.has(id));
842
+ const item = this.createSidebarItem(feature.title, !hasVisible);
843
+ item.classList.add("uidex-sidebar-item--features");
844
+ item.dataset.viewType = "feature";
845
+ item.dataset.viewId = feature.dir;
846
+ item.addEventListener("click", () => this.navigateTo({ type: "feature", dir: feature.dir }));
847
+ this.sidebar.appendChild(item);
848
+ }
849
+ } else {
850
+ const groups = this.groupComponentsByPrefix(Object.keys(this.data.components));
851
+ if (groups.size === 1) {
852
+ const onlyKey = groups.keys().next().value;
853
+ this.expandedGroups.add(onlyKey);
854
+ }
855
+ for (const [prefix, ids] of groups) {
856
+ const isExpanded = this.expandedGroups.has(prefix);
857
+ const header = document.createElement("div");
858
+ header.className = "uidex-sidebar-group-header";
859
+ if (isExpanded) header.classList.add("uidex-sidebar-group-header--expanded");
860
+ header.addEventListener("click", () => {
861
+ if (this.expandedGroups.has(prefix)) {
862
+ this.expandedGroups.delete(prefix);
863
+ } else {
864
+ this.expandedGroups.add(prefix);
865
+ }
866
+ this.renderSidebar();
867
+ });
868
+ const chevron = document.createElement("span");
869
+ chevron.className = "uidex-sidebar-group-chevron";
870
+ chevron.textContent = "\u203A";
871
+ header.appendChild(chevron);
872
+ const label = document.createElement("span");
873
+ label.className = "uidex-sidebar-group-label";
874
+ label.textContent = prefix;
875
+ header.appendChild(label);
876
+ const count = document.createElement("span");
877
+ count.className = "uidex-sidebar-group-count";
878
+ count.textContent = `${ids.length}`;
879
+ header.appendChild(count);
880
+ this.sidebar.appendChild(header);
881
+ if (isExpanded) {
882
+ for (const id of ids) {
883
+ const isPresent = this.data.presentIds.has(id);
884
+ const item = this.createSidebarItem(id, !isPresent);
885
+ item.classList.add("uidex-sidebar-item--mono", "uidex-sidebar-item--grouped", "uidex-sidebar-item--components");
886
+ item.dataset.viewType = "component";
887
+ item.dataset.viewId = id;
888
+ item.addEventListener("click", () => this.navigateTo({ type: "component", id }));
889
+ this.sidebar.appendChild(item);
890
+ }
891
+ }
892
+ }
893
+ }
894
+ }
895
+ currentViewMatchesLevel(level) {
896
+ if (!this.currentView) return false;
897
+ return _Modal.VIEW_TYPE_TO_LEVEL[this.currentView.type] === level;
898
+ }
899
+ getFirstViewForLevel(level) {
900
+ if (level === "pages" && this.data.pages.length > 0) {
901
+ return { type: "page", dir: this.data.pages[0].dir };
902
+ }
903
+ if (level === "features" && this.data.features.length > 0) {
904
+ return { type: "feature", dir: this.data.features[0].dir };
905
+ }
906
+ if (level === "components") {
907
+ const firstId = Object.keys(this.data.components)[0];
908
+ if (firstId) return { type: "component", id: firstId };
909
+ }
910
+ return null;
911
+ }
912
+ getComponentPrefix(id) {
913
+ const dashIndex = id.indexOf("-");
914
+ return dashIndex === -1 ? id : id.substring(0, dashIndex);
915
+ }
916
+ groupComponentsByPrefix(ids) {
917
+ const groups = /* @__PURE__ */ new Map();
918
+ for (const id of ids) {
919
+ const prefix = this.getComponentPrefix(id);
920
+ let group = groups.get(prefix);
921
+ if (!group) {
922
+ group = [];
923
+ groups.set(prefix, group);
924
+ }
925
+ group.push(id);
926
+ }
927
+ return groups;
928
+ }
929
+ createSidebarItem(text, dimmed) {
930
+ const item = document.createElement("div");
931
+ item.className = "uidex-sidebar-item";
932
+ if (dimmed) item.classList.add("uidex-sidebar-item--dimmed");
933
+ item.textContent = text;
934
+ return item;
935
+ }
936
+ updateSidebarActive() {
937
+ if (!this.sidebar || !this.currentView) return;
938
+ const mapItem = this.sidebar.querySelector('[data-view-type="graph"]');
939
+ if (mapItem) {
940
+ mapItem.classList.toggle(
941
+ "uidex-sidebar-root-item--active",
942
+ this.currentView.type === "graph"
943
+ );
944
+ }
945
+ if (this.currentView.type === "graph") return;
946
+ const activeId = this.currentView.type === "component" ? this.currentView.id : this.currentView.dir;
947
+ for (const el of this.sidebar.querySelectorAll(".uidex-sidebar-item")) {
948
+ const item = el;
949
+ const matches = item.dataset.viewType === this.currentView.type && item.dataset.viewId === activeId;
950
+ item.classList.toggle("uidex-sidebar-item--active", matches);
951
+ }
952
+ }
953
+ // ——— Search ———
954
+ getSearchResults() {
955
+ const q = this.searchQuery.toLowerCase();
956
+ return {
957
+ pages: this.data.pages.filter(
958
+ (p) => p.title.toLowerCase().includes(q) || _Modal.formatPageDir(p.dir).toLowerCase().includes(q)
959
+ ),
960
+ features: this.data.features.filter(
961
+ (f) => f.title.toLowerCase().includes(q) || f.dir.toLowerCase().includes(q)
962
+ ),
963
+ components: Object.keys(this.data.components).filter((id) => {
964
+ if (id.toLowerCase().includes(q)) return true;
965
+ const doc = this.data.components[id]?.find((l) => l.doc)?.doc;
966
+ return doc ? doc.toLowerCase().includes(q) : false;
967
+ })
968
+ };
969
+ }
970
+ renderSearchResults() {
971
+ if (!this.sidebar) return;
972
+ const results = this.getSearchResults();
973
+ const hasResults = results.pages.length > 0 || results.features.length > 0 || results.components.length > 0;
974
+ if (!hasResults) {
975
+ const empty = document.createElement("div");
976
+ empty.className = "uidex-search-empty";
977
+ empty.textContent = "No results";
978
+ this.sidebar.appendChild(empty);
979
+ return;
980
+ }
981
+ if (results.pages.length > 0) {
982
+ const label = document.createElement("div");
983
+ label.className = "uidex-search-group-label uidex-search-group-label--pages";
984
+ label.textContent = "Pages";
985
+ this.sidebar.appendChild(label);
986
+ for (const page of results.pages) {
987
+ const item = this.createSidebarItem(_Modal.formatPageDir(page.dir), false);
988
+ item.classList.add("uidex-sidebar-item--mono", "uidex-sidebar-item--pages");
989
+ item.addEventListener(
990
+ "click",
991
+ () => this.navigateTo({ type: "page", dir: page.dir })
992
+ );
993
+ this.sidebar.appendChild(item);
994
+ }
995
+ }
996
+ if (results.features.length > 0) {
997
+ const label = document.createElement("div");
998
+ label.className = "uidex-search-group-label uidex-search-group-label--features";
999
+ label.textContent = "Features";
1000
+ this.sidebar.appendChild(label);
1001
+ for (const feature of results.features) {
1002
+ const item = this.createSidebarItem(feature.title, false);
1003
+ item.classList.add("uidex-sidebar-item--features");
1004
+ item.addEventListener(
1005
+ "click",
1006
+ () => this.navigateTo({ type: "feature", dir: feature.dir })
1007
+ );
1008
+ this.sidebar.appendChild(item);
1009
+ }
1010
+ }
1011
+ if (results.components.length > 0) {
1012
+ const label = document.createElement("div");
1013
+ label.className = "uidex-search-group-label uidex-search-group-label--components";
1014
+ label.textContent = "Components";
1015
+ this.sidebar.appendChild(label);
1016
+ for (const id of results.components) {
1017
+ const item = this.createSidebarItem(id, false);
1018
+ item.classList.add("uidex-sidebar-item--mono", "uidex-sidebar-item--components");
1019
+ item.addEventListener(
1020
+ "click",
1021
+ () => this.navigateTo({ type: "component", id })
1022
+ );
1023
+ this.sidebar.appendChild(item);
1024
+ }
1025
+ }
1026
+ }
1027
+ // ——— Main content rendering ———
1028
+ renderMainContent(view) {
1029
+ if (!this.mainContent) return;
1030
+ this.mainContent.innerHTML = "";
1031
+ this.mainContent.scrollTop = 0;
1032
+ switch (view.type) {
1033
+ case "page":
1034
+ this.renderPageView(view.dir);
1035
+ break;
1036
+ case "feature":
1037
+ this.renderFeatureView(view.dir);
1038
+ break;
1039
+ case "component":
1040
+ this.renderComponentView(view.id);
1041
+ break;
1042
+ case "graph":
1043
+ this.renderGraphView();
1044
+ break;
1045
+ }
1046
+ }
1047
+ renderPageView(dir) {
1048
+ const page = this.data.pages.find((p) => p.dir === dir);
1049
+ if (!page || !this.mainContent) return;
1050
+ const title = document.createElement("h3");
1051
+ title.className = "uidex-main-title";
1052
+ title.textContent = page.title;
1053
+ this.mainContent.appendChild(title);
1054
+ const dirCode = document.createElement("code");
1055
+ dirCode.className = "uidex-modal-code";
1056
+ dirCode.textContent = _Modal.formatPageDir(page.dir);
1057
+ this.mainContent.appendChild(dirCode);
1058
+ if (page.content.trim()) {
1059
+ const section = document.createElement("div");
1060
+ section.className = "uidex-modal-section uidex-modal-section--spaced";
1061
+ section.appendChild(this.renderMarkdown(page.content));
1062
+ this.mainContent.appendChild(section);
1063
+ }
1064
+ const pageComponentSet = new Set(page.componentIds);
1065
+ const relatedFeatures = this.data.features.filter(
1066
+ (f) => f.componentIds.some((cid) => pageComponentSet.has(cid))
1067
+ );
1068
+ if (relatedFeatures.length > 0) {
1069
+ this.mainContent.appendChild(
1070
+ this.createFeatureChipsSection(relatedFeatures)
1071
+ );
1072
+ }
1073
+ if (page.componentIds.length > 0) {
1074
+ this.mainContent.appendChild(
1075
+ this.createComponentChipsSection(
1076
+ page.componentIds,
1077
+ (id) => this.navigateTo({ type: "component", id })
1078
+ )
1079
+ );
1080
+ }
1081
+ }
1082
+ renderFeatureView(dir) {
1083
+ const feature = this.data.features.find((f) => f.dir === dir);
1084
+ if (!feature || !this.mainContent) return;
1085
+ const badge = document.createElement("div");
1086
+ badge.className = "uidex-badge uidex-badge--features";
1087
+ badge.textContent = "Feature";
1088
+ this.mainContent.appendChild(badge);
1089
+ const title = document.createElement("h3");
1090
+ title.className = "uidex-main-title";
1091
+ title.textContent = feature.title;
1092
+ this.mainContent.appendChild(title);
1093
+ const dirCode = document.createElement("code");
1094
+ dirCode.className = "uidex-modal-code";
1095
+ dirCode.textContent = feature.dir;
1096
+ this.mainContent.appendChild(dirCode);
1097
+ if (feature.content.trim()) {
1098
+ const section = document.createElement("div");
1099
+ section.className = "uidex-modal-section uidex-modal-section--spaced";
1100
+ section.appendChild(this.renderMarkdown(feature.content));
1101
+ this.mainContent.appendChild(section);
1102
+ }
1103
+ if (feature.componentIds.length > 0) {
1104
+ const absentIds = new Set(
1105
+ feature.componentIds.filter((id) => !this.data.presentIds.has(id))
1106
+ );
1107
+ this.mainContent.appendChild(
1108
+ this.createComponentChipsSection(
1109
+ feature.componentIds,
1110
+ (id) => this.navigateTo({ type: "component", id }),
1111
+ absentIds
1112
+ )
1113
+ );
1114
+ }
1115
+ }
1116
+ renderComponentView(id) {
1117
+ if (!this.mainContent) return;
1118
+ const locations = this.data.components[id] ?? [];
1119
+ const element = this.options.elementGetter?.(id) ?? null;
1120
+ const title = document.createElement("h3");
1121
+ title.className = "uidex-main-title uidex-main-title--mono";
1122
+ title.textContent = id;
1123
+ this.mainContent.appendChild(title);
1124
+ if (!this.data.presentIds.has(id)) {
1125
+ const notice = document.createElement("div");
1126
+ notice.className = "uidex-absent-notice";
1127
+ notice.textContent = "Not on current page";
1128
+ this.mainContent.appendChild(notice);
1129
+ }
1130
+ const parentFeatures = this.data.features.filter(
1131
+ (f) => f.componentIds.includes(id)
1132
+ );
1133
+ if (parentFeatures.length > 0) {
1134
+ this.mainContent.appendChild(
1135
+ this.createFeatureChipsSection(parentFeatures)
1136
+ );
1137
+ }
1138
+ const doc = locations.find((l) => l.doc)?.doc;
1139
+ if (doc) {
1140
+ const docSection = document.createElement("div");
1141
+ docSection.className = "uidex-modal-section";
1142
+ const docLabel = document.createElement("div");
1143
+ docLabel.className = "uidex-modal-label";
1144
+ docLabel.textContent = "Description";
1145
+ const docText = document.createElement("p");
1146
+ docText.className = "uidex-modal-doc";
1147
+ docText.textContent = doc;
1148
+ docSection.appendChild(docLabel);
1149
+ docSection.appendChild(docText);
1150
+ this.mainContent.appendChild(docSection);
1151
+ }
1152
+ if (element) {
1153
+ const elSection = document.createElement("div");
1154
+ elSection.className = "uidex-modal-section";
1155
+ const elLabel = document.createElement("div");
1156
+ elLabel.className = "uidex-modal-label";
1157
+ elLabel.textContent = "Element";
1158
+ const elCode = document.createElement("code");
1159
+ elCode.className = "uidex-modal-code";
1160
+ elCode.textContent = this.describeElement(element);
1161
+ elSection.appendChild(elLabel);
1162
+ elSection.appendChild(elCode);
1163
+ this.mainContent.appendChild(elSection);
1164
+ }
1165
+ if (locations.length > 0) {
1166
+ const locSection = document.createElement("div");
1167
+ locSection.className = "uidex-modal-section";
1168
+ const locLabel = document.createElement("div");
1169
+ locLabel.className = "uidex-modal-label";
1170
+ locLabel.textContent = locations.length === 1 ? "Source" : "Sources";
1171
+ locSection.appendChild(locLabel);
1172
+ for (const loc of locations) {
1173
+ const row = document.createElement("div");
1174
+ row.className = "uidex-modal-location";
1175
+ const pathText = document.createElement("code");
1176
+ pathText.className = "uidex-modal-code";
1177
+ pathText.textContent = `${loc.filePath}:${loc.line}`;
1178
+ const copyBtn = document.createElement("button");
1179
+ copyBtn.className = "uidex-modal-copy";
1180
+ copyBtn.textContent = "Copy";
1181
+ copyBtn.addEventListener("click", () => {
1182
+ navigator.clipboard.writeText(`${loc.filePath}:${loc.line}`).then(() => {
1183
+ copyBtn.textContent = "Copied!";
1184
+ setTimeout(() => {
1185
+ copyBtn.textContent = "Copy";
1186
+ }, 1500);
1187
+ }).catch(() => {
1188
+ });
1189
+ });
1190
+ row.appendChild(pathText);
1191
+ row.appendChild(copyBtn);
1192
+ locSection.appendChild(row);
1193
+ }
1194
+ this.mainContent.appendChild(locSection);
1195
+ }
1196
+ const env = this.collectEnv();
1197
+ const envSection = document.createElement("div");
1198
+ envSection.className = "uidex-modal-section";
1199
+ const envLabel = document.createElement("div");
1200
+ envLabel.className = "uidex-modal-label";
1201
+ envLabel.textContent = "Environment";
1202
+ envSection.appendChild(envLabel);
1203
+ const envItems = [
1204
+ `${env.viewport.width} \xD7 ${env.viewport.height} viewport`,
1205
+ env.browserName,
1206
+ window.location.hostname + window.location.pathname
1207
+ ];
1208
+ for (const item of envItems) {
1209
+ const envLine = document.createElement("div");
1210
+ envLine.className = "uidex-modal-env-item";
1211
+ envLine.textContent = item;
1212
+ envSection.appendChild(envLine);
1213
+ }
1214
+ this.mainContent.appendChild(envSection);
1215
+ }
1216
+ // ——— Graph view ———
1217
+ renderGraphView() {
1218
+ if (!this.mainContent) return;
1219
+ const pages = this.data.pages;
1220
+ const features = this.data.features;
1221
+ const allComponentIds = Object.keys(this.data.components);
1222
+ const connectedIds = /* @__PURE__ */ new Set();
1223
+ for (const p of pages) {
1224
+ for (const cid of p.componentIds) {
1225
+ if (this.data.components[cid]) connectedIds.add(cid);
1226
+ }
1227
+ }
1228
+ for (const f of features) {
1229
+ for (const cid of f.componentIds) {
1230
+ if (this.data.components[cid]) connectedIds.add(cid);
1231
+ }
1232
+ }
1233
+ const shownComponentIds = allComponentIds.filter((id) => connectedIds.has(id));
1234
+ const unconnectedCount = allComponentIds.length - shownComponentIds.length;
1235
+ if (shownComponentIds.length === 0 && pages.length === 0 && features.length === 0) {
1236
+ const empty = document.createElement("div");
1237
+ empty.className = "uidex-empty-message";
1238
+ empty.textContent = "No relationships to display";
1239
+ this.mainContent.appendChild(empty);
1240
+ return;
1241
+ }
1242
+ const SVG_W = 440;
1243
+ const NODE_W = 110;
1244
+ const NODE_H = 24;
1245
+ const NODE_GAP = 6;
1246
+ const COL_HEADER_Y = 14;
1247
+ const START_Y = 28;
1248
+ const columns = [];
1249
+ if (pages.length > 0) columns.push({ type: "pages", x: 0 });
1250
+ if (features.length > 0) columns.push({ type: "features", x: 0 });
1251
+ if (shownComponentIds.length > 0) columns.push({ type: "components", x: 0 });
1252
+ const colSpacing = SVG_W / columns.length;
1253
+ for (let i = 0; i < columns.length; i++) {
1254
+ columns[i].x = colSpacing * i + colSpacing / 2;
1255
+ }
1256
+ const colX = {};
1257
+ for (const c of columns) colX[c.type] = c.x;
1258
+ const nodes = [];
1259
+ const posOf = {};
1260
+ const addNodes = (type, items, cx) => {
1261
+ items.forEach((item, i) => {
1262
+ const y = START_Y + i * (NODE_H + NODE_GAP);
1263
+ nodes.push({ type, key: item.key, label: item.label, x: cx, y });
1264
+ posOf[item.key] = { x: cx, y };
1265
+ });
1266
+ };
1267
+ if (colX["pages"] !== void 0) {
1268
+ addNodes(
1269
+ "page",
1270
+ pages.map((p) => ({ key: `page:${p.dir}`, label: _Modal.formatPageDir(p.dir) })),
1271
+ colX["pages"]
1272
+ );
1273
+ }
1274
+ if (colX["features"] !== void 0) {
1275
+ addNodes(
1276
+ "feature",
1277
+ features.map((f) => ({ key: `feature:${f.dir}`, label: f.title })),
1278
+ colX["features"]
1279
+ );
1280
+ }
1281
+ if (colX["components"] !== void 0) {
1282
+ addNodes(
1283
+ "component",
1284
+ shownComponentIds.map((id) => ({ key: `component:${id}`, label: id })),
1285
+ colX["components"]
1286
+ );
1287
+ }
1288
+ const edges = [];
1289
+ for (const p of pages) {
1290
+ for (const cid of p.componentIds) {
1291
+ if (connectedIds.has(cid)) {
1292
+ edges.push({ fromKey: `page:${p.dir}`, toKey: `component:${cid}`, type: "page" });
1293
+ }
1294
+ }
1295
+ }
1296
+ for (const f of features) {
1297
+ for (const cid of f.componentIds) {
1298
+ if (connectedIds.has(cid)) {
1299
+ edges.push({ fromKey: `feature:${f.dir}`, toKey: `component:${cid}`, type: "feature" });
1300
+ }
1301
+ }
1302
+ }
1303
+ const maxItems = Math.max(pages.length, features.length, shownComponentIds.length);
1304
+ const svgH = START_Y + maxItems * (NODE_H + NODE_GAP) + 12;
1305
+ const NS2 = "http://www.w3.org/2000/svg";
1306
+ const svg = document.createElementNS(NS2, "svg");
1307
+ svg.setAttribute("viewBox", `0 0 ${SVG_W} ${svgH}`);
1308
+ svg.style.width = "100%";
1309
+ svg.style.height = "auto";
1310
+ svg.classList.add("uidex-graph-svg");
1311
+ const headerLabels = {
1312
+ pages: "PAGES",
1313
+ features: "FEATURES",
1314
+ components: "COMPONENTS"
1315
+ };
1316
+ for (const col of columns) {
1317
+ const text = document.createElementNS(NS2, "text");
1318
+ text.setAttribute("x", String(col.x));
1319
+ text.setAttribute("y", String(COL_HEADER_Y));
1320
+ text.setAttribute("text-anchor", "middle");
1321
+ text.classList.add("uidex-graph-header", `uidex-graph-header--${col.type}`);
1322
+ text.textContent = headerLabels[col.type];
1323
+ svg.appendChild(text);
1324
+ }
1325
+ const edgeGroup = document.createElementNS(NS2, "g");
1326
+ for (const edge of edges) {
1327
+ const from = posOf[edge.fromKey];
1328
+ const to = posOf[edge.toKey];
1329
+ if (!from || !to) continue;
1330
+ const x1 = from.x + NODE_W / 2;
1331
+ const y1 = from.y + NODE_H / 2;
1332
+ const x2 = to.x - NODE_W / 2;
1333
+ const y2 = to.y + NODE_H / 2;
1334
+ const midX = (x1 + x2) / 2;
1335
+ const path = document.createElementNS(NS2, "path");
1336
+ path.setAttribute(
1337
+ "d",
1338
+ `M${x1},${y1} C${midX},${y1} ${midX},${y2} ${x2},${y2}`
1339
+ );
1340
+ path.classList.add("uidex-graph-edge", `uidex-graph-edge--${edge.type}`);
1341
+ path.dataset.from = edge.fromKey;
1342
+ path.dataset.to = edge.toKey;
1343
+ edgeGroup.appendChild(path);
1344
+ }
1345
+ svg.appendChild(edgeGroup);
1346
+ const nodeGroup = document.createElementNS(NS2, "g");
1347
+ const truncate = (s, max) => s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
1348
+ for (const node of nodes) {
1349
+ const g = document.createElementNS(NS2, "g");
1350
+ g.classList.add("uidex-graph-node", `uidex-graph-node--${node.type}`);
1351
+ g.dataset.key = node.key;
1352
+ const rect = document.createElementNS(NS2, "rect");
1353
+ rect.setAttribute("x", String(node.x - NODE_W / 2));
1354
+ rect.setAttribute("y", String(node.y));
1355
+ rect.setAttribute("width", String(NODE_W));
1356
+ rect.setAttribute("height", String(NODE_H));
1357
+ g.appendChild(rect);
1358
+ const text = document.createElementNS(NS2, "text");
1359
+ text.setAttribute("x", String(node.x));
1360
+ text.setAttribute("y", String(node.y + NODE_H / 2 + 3.5));
1361
+ text.setAttribute("text-anchor", "middle");
1362
+ text.classList.add("uidex-graph-label");
1363
+ text.textContent = truncate(node.label, 14);
1364
+ g.appendChild(text);
1365
+ g.addEventListener("click", () => {
1366
+ if (node.type === "page") {
1367
+ this.navigateTo({ type: "page", dir: node.key.slice(5) });
1368
+ } else if (node.type === "feature") {
1369
+ this.navigateTo({ type: "feature", dir: node.key.slice(8) });
1370
+ } else {
1371
+ this.navigateTo({ type: "component", id: node.key.slice(10) });
1372
+ }
1373
+ });
1374
+ g.addEventListener("mouseenter", () => {
1375
+ svg.classList.add("uidex-graph--hover");
1376
+ g.classList.add("uidex-graph-node--highlighted");
1377
+ for (const edge of edgeGroup.children) {
1378
+ const el = edge;
1379
+ if (el.dataset.from === node.key || el.dataset.to === node.key) {
1380
+ el.classList.add("uidex-graph-edge--highlighted");
1381
+ const otherKey = el.dataset.from === node.key ? el.dataset.to : el.dataset.from;
1382
+ const otherNode = nodeGroup.querySelector(`[data-key="${otherKey}"]`);
1383
+ if (otherNode) otherNode.classList.add("uidex-graph-node--highlighted");
1384
+ }
1385
+ }
1386
+ });
1387
+ g.addEventListener("mouseleave", () => {
1388
+ svg.classList.remove("uidex-graph--hover");
1389
+ for (const el of svg.querySelectorAll(".uidex-graph-node--highlighted")) {
1390
+ el.classList.remove("uidex-graph-node--highlighted");
1391
+ }
1392
+ for (const el of svg.querySelectorAll(".uidex-graph-edge--highlighted")) {
1393
+ el.classList.remove("uidex-graph-edge--highlighted");
1394
+ }
1395
+ });
1396
+ nodeGroup.appendChild(g);
1397
+ }
1398
+ svg.appendChild(nodeGroup);
1399
+ const title = document.createElement("h3");
1400
+ title.className = "uidex-main-title";
1401
+ title.textContent = "Map";
1402
+ this.mainContent.appendChild(title);
1403
+ const container = document.createElement("div");
1404
+ container.className = "uidex-graph";
1405
+ container.appendChild(svg);
1406
+ this.mainContent.appendChild(container);
1407
+ if (unconnectedCount > 0) {
1408
+ const note = document.createElement("div");
1409
+ note.className = "uidex-graph-note";
1410
+ note.textContent = `${unconnectedCount} component${unconnectedCount === 1 ? "" : "s"} without page or feature connections`;
1411
+ this.mainContent.appendChild(note);
1412
+ }
1413
+ }
1414
+ // ——— Feedback ———
1415
+ /** Open a dedicated feedback modal for a specific component. */
1416
+ openFeedback(id) {
1417
+ this.createFeedbackShell(id);
1418
+ }
1419
+ createFeedbackShell(id) {
1420
+ const locations = this.data.components[id] ?? [];
1421
+ const element = this.options.elementGetter?.(id) ?? null;
1422
+ const { dialog, header } = this.createModalFrame({
1423
+ type: "feedback",
1424
+ title: "Feedback",
1425
+ ariaLabel: "Feedback"
1426
+ });
1427
+ const componentLabel = document.createElement("code");
1428
+ componentLabel.className = "uidex-modal-code uidex-modal-code--inline";
1429
+ componentLabel.textContent = id;
1430
+ header.appendChild(componentLabel);
1431
+ const spacer = document.createElement("div");
1432
+ spacer.className = "uidex-header-spacer";
1433
+ header.appendChild(spacer);
1434
+ this.mountModalFrame(dialog, header);
1435
+ this.mainContent = document.createElement("div");
1436
+ this.mainContent.className = "uidex-modal-body";
1437
+ this.renderFeedbackContext(id, locations);
1438
+ this.renderFeedbackFormContent(id, locations, element);
1439
+ dialog.appendChild(this.mainContent);
1440
+ }
1441
+ /** Render component context (features, pages, doc) above the feedback form. */
1442
+ renderFeedbackContext(id, locations) {
1443
+ if (!this.mainContent) return;
1444
+ let hasContext = false;
1445
+ const parentFeatures = this.data.features.filter(
1446
+ (f) => f.componentIds.includes(id)
1447
+ );
1448
+ if (parentFeatures.length > 0) {
1449
+ const container = this.createChipContainer();
1450
+ for (const feature of parentFeatures) {
1451
+ const chip = document.createElement("span");
1452
+ chip.className = "uidex-chip uidex-chip--features";
1453
+ chip.textContent = feature.title;
1454
+ container.appendChild(chip);
1455
+ }
1456
+ this.mainContent.appendChild(container);
1457
+ hasContext = true;
1458
+ }
1459
+ const parentPage = this.data.pages.find(
1460
+ (p) => p.componentIds.includes(id)
1461
+ );
1462
+ if (parentPage) {
1463
+ const container = this.createChipContainer();
1464
+ const chip = document.createElement("span");
1465
+ chip.className = "uidex-chip uidex-chip--pages";
1466
+ chip.textContent = _Modal.formatPageDir(parentPage.dir);
1467
+ container.appendChild(chip);
1468
+ this.mainContent.appendChild(container);
1469
+ hasContext = true;
1470
+ }
1471
+ const doc = locations.find((l) => l.doc)?.doc;
1472
+ if (doc) {
1473
+ const docText = document.createElement("p");
1474
+ docText.className = "uidex-modal-doc uidex-modal-doc--spaced";
1475
+ docText.textContent = doc;
1476
+ this.mainContent.appendChild(docText);
1477
+ hasContext = true;
1478
+ }
1479
+ if (hasContext) {
1480
+ const divider = document.createElement("div");
1481
+ divider.className = "uidex-main-divider";
1482
+ this.mainContent.appendChild(divider);
1483
+ }
1484
+ }
1485
+ createChipContainer() {
1486
+ const container = document.createElement("div");
1487
+ container.className = "uidex-chips uidex-chips--spaced";
1488
+ return container;
1489
+ }
1490
+ renderFeedbackFormContent(id, locations, element) {
1491
+ if (!this.mainContent) return;
1492
+ const form = document.createElement("form");
1493
+ form.className = "uidex-form";
1494
+ form.addEventListener("submit", (e) => e.preventDefault());
1495
+ const typeSelect = this.createSelect("Type", [
1496
+ { value: "bug", label: "Bug Report" },
1497
+ { value: "feature", label: "Feature Request" },
1498
+ { value: "improvement", label: "Improvement" },
1499
+ { value: "question", label: "Question" }
1500
+ ]);
1501
+ form.appendChild(typeSelect.group);
1502
+ const severitySelect = this.createSelect(
1503
+ "Severity",
1504
+ [
1505
+ { value: "low", label: "Low" },
1506
+ { value: "medium", label: "Medium" },
1507
+ { value: "high", label: "High" },
1508
+ { value: "critical", label: "Critical" }
1509
+ ],
1510
+ "medium"
1511
+ );
1512
+ form.appendChild(severitySelect.group);
1513
+ const descGroup = this.createFormGroup("Description");
1514
+ const textarea = document.createElement("textarea");
1515
+ textarea.className = "uidex-form-textarea";
1516
+ textarea.placeholder = "Describe the issue or idea...";
1517
+ textarea.rows = 4;
1518
+ descGroup.appendChild(textarea);
1519
+ form.appendChild(descGroup);
1520
+ const screenshotGroup = document.createElement("div");
1521
+ screenshotGroup.className = "uidex-form-group";
1522
+ const screenshotLabel = document.createElement("label");
1523
+ screenshotLabel.className = "uidex-form-toggle";
1524
+ const screenshotCheckbox = document.createElement("input");
1525
+ screenshotCheckbox.type = "checkbox";
1526
+ screenshotCheckbox.className = "uidex-form-checkbox";
1527
+ screenshotCheckbox.checked = true;
1528
+ screenshotLabel.appendChild(screenshotCheckbox);
1529
+ const screenshotText = document.createElement("span");
1530
+ screenshotText.className = "uidex-form-toggle-text";
1531
+ screenshotText.textContent = "Include screenshot";
1532
+ screenshotLabel.appendChild(screenshotText);
1533
+ screenshotGroup.appendChild(screenshotLabel);
1534
+ form.appendChild(screenshotGroup);
1535
+ const submitBtn = document.createElement("button");
1536
+ submitBtn.type = "button";
1537
+ submitBtn.className = "uidex-form-submit";
1538
+ submitBtn.textContent = "Submit";
1539
+ submitBtn.addEventListener("click", async () => {
1540
+ if (!textarea.value.trim()) {
1541
+ textarea.focus();
1542
+ return;
1543
+ }
1544
+ submitBtn.disabled = true;
1545
+ let screenshot;
1546
+ if (screenshotCheckbox.checked) {
1547
+ const freshElement = this.options.elementGetter?.(id) ?? element;
1548
+ if (freshElement) {
1549
+ submitBtn.textContent = "Capturing\u2026";
1550
+ screenshot = await this.captureElementScreenshot(freshElement) ?? void 0;
1551
+ }
1552
+ }
1553
+ const env = this.collectEnv();
1554
+ const page = this.data.pages.find((p) => p.componentIds.includes(id));
1555
+ const report = {
1556
+ type: typeSelect.select.value,
1557
+ severity: severitySelect.select.value,
1558
+ description: textarea.value.trim(),
1559
+ componentId: id,
1560
+ element: element ? this.describeElement(element) : null,
1561
+ sources: locations.map((l) => ({ filePath: l.filePath, line: l.line })),
1562
+ url: env.url,
1563
+ path: window.location.pathname,
1564
+ route: page?.dir ?? null,
1565
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1566
+ viewport: env.viewport,
1567
+ screen: env.screen,
1568
+ userAgent: env.userAgent,
1569
+ screenshot
1570
+ };
1571
+ console.log("[uidex] Feedback submitted:", report);
1572
+ submitBtn.textContent = "Submitted!";
1573
+ setTimeout(() => {
1574
+ submitBtn.textContent = "Submit";
1575
+ submitBtn.disabled = false;
1576
+ }, 1500);
1577
+ });
1578
+ form.appendChild(submitBtn);
1579
+ this.mainContent.appendChild(form);
1580
+ }
1581
+ // ——— Component chips ———
1582
+ createComponentChip(id, onComponentClick, absent) {
1583
+ const chip = document.createElement("span");
1584
+ chip.className = "uidex-chip uidex-chip--components";
1585
+ if (absent) chip.classList.add("uidex-chip--absent");
1586
+ chip.textContent = id;
1587
+ if (onComponentClick) {
1588
+ chip.style.cursor = "pointer";
1589
+ chip.addEventListener("click", () => onComponentClick(id));
1590
+ }
1591
+ return chip;
1592
+ }
1593
+ createFeatureChipsSection(features) {
1594
+ const section = document.createElement("div");
1595
+ section.className = "uidex-modal-section";
1596
+ const label = document.createElement("div");
1597
+ label.className = "uidex-modal-label";
1598
+ label.textContent = features.length === 1 ? "Feature" : "Features";
1599
+ section.appendChild(label);
1600
+ const container = document.createElement("div");
1601
+ container.className = "uidex-chips";
1602
+ for (const feature of features) {
1603
+ const chip = document.createElement("span");
1604
+ chip.className = "uidex-chip uidex-chip--features";
1605
+ chip.textContent = feature.title;
1606
+ chip.style.cursor = "pointer";
1607
+ chip.addEventListener(
1608
+ "click",
1609
+ () => this.navigateTo({ type: "feature", dir: feature.dir })
1610
+ );
1611
+ container.appendChild(chip);
1612
+ }
1613
+ section.appendChild(container);
1614
+ return section;
1615
+ }
1616
+ createComponentChipsSection(componentIds, onComponentClick, absentIds) {
1617
+ const section = document.createElement("div");
1618
+ section.className = "uidex-modal-section";
1619
+ const label = document.createElement("div");
1620
+ label.className = "uidex-modal-label";
1621
+ label.textContent = `Components (${componentIds.length})`;
1622
+ section.appendChild(label);
1623
+ const container = document.createElement("div");
1624
+ container.className = "uidex-chips";
1625
+ for (const id of componentIds) {
1626
+ container.appendChild(
1627
+ this.createComponentChip(id, onComponentClick, absentIds?.has(id))
1628
+ );
1629
+ }
1630
+ section.appendChild(container);
1631
+ return section;
1632
+ }
1633
+ // ——— Form helpers ———
1634
+ createFormGroup(labelText) {
1635
+ const group = document.createElement("div");
1636
+ group.className = "uidex-form-group";
1637
+ const label = document.createElement("label");
1638
+ label.className = "uidex-form-label";
1639
+ label.textContent = labelText;
1640
+ group.appendChild(label);
1641
+ return group;
1642
+ }
1643
+ createSelect(labelText, options, defaultValue) {
1644
+ const group = this.createFormGroup(labelText);
1645
+ const select = document.createElement("select");
1646
+ select.className = "uidex-form-select";
1647
+ for (const opt of options) {
1648
+ const option = document.createElement("option");
1649
+ option.value = opt.value;
1650
+ option.textContent = opt.label;
1651
+ if (opt.value === defaultValue) option.selected = true;
1652
+ select.appendChild(option);
1653
+ }
1654
+ group.appendChild(select);
1655
+ return { group, select };
1656
+ }
1657
+ // ——— Markdown rendering ———
1658
+ createChecklistItem(text, checked = false) {
1659
+ const li = document.createElement("li");
1660
+ li.className = "uidex-checklist-item";
1661
+ const label = document.createElement("label");
1662
+ label.className = "uidex-checklist-label";
1663
+ const checkbox = document.createElement("input");
1664
+ checkbox.type = "checkbox";
1665
+ checkbox.className = "uidex-checklist-checkbox";
1666
+ checkbox.checked = checked;
1667
+ label.appendChild(checkbox);
1668
+ const span = document.createElement("span");
1669
+ span.textContent = text;
1670
+ label.appendChild(span);
1671
+ li.appendChild(label);
1672
+ return li;
1673
+ }
1674
+ renderMarkdown(content) {
1675
+ const container = document.createElement("div");
1676
+ container.className = "uidex-page-content";
1677
+ const lines = content.split("\n");
1678
+ let currentList = null;
1679
+ let skippedFirstH1 = false;
1680
+ let inAcceptanceSection = false;
1681
+ for (const line of lines) {
1682
+ const trimmed = line.trim();
1683
+ if (!trimmed) {
1684
+ currentList = null;
1685
+ continue;
1686
+ }
1687
+ const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)/);
1688
+ if (headingMatch) {
1689
+ if (!skippedFirstH1 && headingMatch[1].length === 1) {
1690
+ skippedFirstH1 = true;
1691
+ continue;
1692
+ }
1693
+ currentList = null;
1694
+ inAcceptanceSection = /^acceptance$/i.test(headingMatch[2].trim());
1695
+ const level = headingMatch[1].length;
1696
+ const tag = ["h1", "h2", "h3", "h4", "h5", "h6"][level - 1];
1697
+ const heading = document.createElement(tag);
1698
+ heading.textContent = headingMatch[2];
1699
+ container.appendChild(heading);
1700
+ continue;
1701
+ }
1702
+ const checkboxMatch = trimmed.match(/^- \[([ xX])\]\s+(.*)/);
1703
+ if (checkboxMatch) {
1704
+ if (!currentList) {
1705
+ currentList = document.createElement("ul");
1706
+ currentList.className = "uidex-checklist";
1707
+ container.appendChild(currentList);
1708
+ }
1709
+ currentList.appendChild(
1710
+ this.createChecklistItem(checkboxMatch[2], checkboxMatch[1] !== " ")
1711
+ );
1712
+ continue;
1713
+ }
1714
+ if (trimmed.startsWith("- ")) {
1715
+ if (!currentList) {
1716
+ currentList = document.createElement("ul");
1717
+ currentList.className = "uidex-checklist";
1718
+ container.appendChild(currentList);
1719
+ }
1720
+ const text = trimmed.slice(2);
1721
+ if (inAcceptanceSection) {
1722
+ currentList.appendChild(this.createChecklistItem(text));
1723
+ } else {
1724
+ const li = document.createElement("li");
1725
+ li.className = "uidex-checklist-item";
1726
+ li.textContent = text;
1727
+ currentList.appendChild(li);
1728
+ }
1729
+ continue;
1730
+ }
1731
+ currentList = null;
1732
+ const p = document.createElement("p");
1733
+ p.textContent = trimmed;
1734
+ container.appendChild(p);
1735
+ }
1736
+ return container;
1737
+ }
1738
+ // ——— Screenshot capture ———
1739
+ /**
1740
+ * Capture an element by cloning it with inlined computed styles into an
1741
+ * SVG foreignObject, rendering to canvas via an <img> data-URI, and
1742
+ * returning a PNG data-URL.
1743
+ */
1744
+ async captureElementScreenshot(element) {
1745
+ try {
1746
+ const rect = element.getBoundingClientRect();
1747
+ const width = Math.ceil(rect.width);
1748
+ const height = Math.ceil(rect.height);
1749
+ if (width <= 0 || height <= 0 || width > 4096 || height > 4096)
1750
+ return null;
1751
+ const clone = element.cloneNode(true);
1752
+ this.inlineComputedStyles(element, clone);
1753
+ const wrapper = document.createElement("div");
1754
+ wrapper.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
1755
+ wrapper.style.width = `${width}px`;
1756
+ wrapper.style.height = `${height}px`;
1757
+ wrapper.style.overflow = "hidden";
1758
+ wrapper.appendChild(clone);
1759
+ const serialized = new XMLSerializer().serializeToString(wrapper);
1760
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><foreignObject width="100%" height="100%">` + serialized + `</foreignObject></svg>`;
1761
+ const img = new Image();
1762
+ img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
1763
+ return await Promise.race([
1764
+ new Promise((resolve) => {
1765
+ img.onload = () => {
1766
+ try {
1767
+ const canvas = document.createElement("canvas");
1768
+ canvas.width = width;
1769
+ canvas.height = height;
1770
+ const ctx = canvas.getContext("2d");
1771
+ if (!ctx) return resolve(null);
1772
+ ctx.drawImage(img, 0, 0);
1773
+ resolve(canvas.toDataURL("image/png"));
1774
+ } catch {
1775
+ resolve(null);
1776
+ }
1777
+ };
1778
+ img.onerror = () => resolve(null);
1779
+ }),
1780
+ new Promise((resolve) => setTimeout(() => resolve(null), 5e3))
1781
+ ]);
1782
+ } catch {
1783
+ return null;
1784
+ }
1785
+ }
1786
+ /** Visual CSS properties worth inlining for a screenshot clone. */
1787
+ static VISUAL_PROPS = [
1788
+ "display",
1789
+ "position",
1790
+ "box-sizing",
1791
+ "width",
1792
+ "min-width",
1793
+ "max-width",
1794
+ "height",
1795
+ "min-height",
1796
+ "max-height",
1797
+ "margin",
1798
+ "padding",
1799
+ "border",
1800
+ "border-radius",
1801
+ "overflow",
1802
+ "flex-direction",
1803
+ "flex-wrap",
1804
+ "flex-grow",
1805
+ "flex-shrink",
1806
+ "flex-basis",
1807
+ "align-items",
1808
+ "justify-content",
1809
+ "gap",
1810
+ "grid-template-columns",
1811
+ "grid-template-rows",
1812
+ "grid-column",
1813
+ "grid-row",
1814
+ "color",
1815
+ "background",
1816
+ "background-color",
1817
+ "background-image",
1818
+ "font-family",
1819
+ "font-size",
1820
+ "font-weight",
1821
+ "font-style",
1822
+ "line-height",
1823
+ "letter-spacing",
1824
+ "text-align",
1825
+ "text-decoration",
1826
+ "text-transform",
1827
+ "white-space",
1828
+ "word-break",
1829
+ "opacity",
1830
+ "visibility",
1831
+ "transform",
1832
+ "box-shadow",
1833
+ "text-shadow",
1834
+ "vertical-align",
1835
+ "list-style",
1836
+ "text-overflow"
1837
+ ];
1838
+ /** Recursively copy visual computed styles from source onto a cloned target. */
1839
+ inlineComputedStyles(source, target) {
1840
+ if (source instanceof HTMLElement && target instanceof HTMLElement) {
1841
+ const computed = window.getComputedStyle(source);
1842
+ for (const prop of _Modal.VISUAL_PROPS) {
1843
+ const value = computed.getPropertyValue(prop);
1844
+ if (value) {
1845
+ target.style.setProperty(prop, value);
1846
+ }
1847
+ }
1848
+ }
1849
+ const srcChildren = source.children;
1850
+ const tgtChildren = target.children;
1851
+ for (let i = 0; i < srcChildren.length; i++) {
1852
+ if (tgtChildren[i]) {
1853
+ this.inlineComputedStyles(srcChildren[i], tgtChildren[i]);
1854
+ }
1855
+ }
1856
+ }
1857
+ // ——— Utilities ———
1858
+ collectEnv() {
1859
+ return {
1860
+ viewport: { width: window.innerWidth, height: window.innerHeight },
1861
+ screen: { width: screen.width, height: screen.height },
1862
+ userAgent: navigator.userAgent,
1863
+ browserName: this.getBrowserName(),
1864
+ url: window.location.href
1865
+ };
1866
+ }
1867
+ getBrowserName() {
1868
+ const ua = navigator.userAgent;
1869
+ if (ua.includes("Firefox/")) return "Firefox";
1870
+ if (ua.includes("Edg/")) return "Edge";
1871
+ if (ua.includes("Chrome/")) return "Chrome";
1872
+ if (ua.includes("Safari/") && !ua.includes("Chrome")) return "Safari";
1873
+ return "Unknown browser";
1874
+ }
1875
+ describeElement(element) {
1876
+ const tag = element.tagName.toLowerCase();
1877
+ const id = element.id ? `#${element.id}` : "";
1878
+ const classes = element.className ? `.${String(element.className).split(/\s+/).join(".")}` : "";
1879
+ return `<${tag}${id}${classes}>`;
1880
+ }
1881
+ handleKeyDown(e) {
1882
+ if (e.key === "Escape") {
1883
+ this.hide();
1884
+ }
1885
+ }
1886
+ };
1887
+
1888
+ // src/core/registry.ts
1889
+ var registeredComponents = null;
1890
+ var registeredPages = null;
1891
+ var registeredFeatures = null;
1892
+ function registerComponents(components) {
1893
+ registeredComponents = components;
1894
+ }
1895
+ function getComponents() {
1896
+ return registeredComponents;
1897
+ }
1898
+ function registerPages(pages) {
1899
+ registeredPages = pages;
1900
+ }
1901
+ function getPages() {
1902
+ return registeredPages;
1903
+ }
1904
+ function registerFeatures(features) {
1905
+ registeredFeatures = features;
1906
+ }
1907
+ function getFeatures() {
1908
+ return registeredFeatures;
1909
+ }
1910
+
1911
+ // src/core/css-text.ts
1912
+ var cssText = `/* Design tokens \u2014 :host targets the shadow root, :where(:root) for standalone use */
1913
+ /* Theme: shadcn lyra \u2014 monochromatic, sharp, monospace */
1914
+ :where(:host, :root) {
1915
+ /* Colors \u2014 dark theme (lyra neutral) */
1916
+ --uidex-color-primary: #e5e5e5;
1917
+ --uidex-color-primary-hover: #ffffff;
1918
+ --uidex-color-bg: #1c1c1c;
1919
+ --uidex-color-bg-elevated: #2e2e2e;
1920
+ --uidex-color-text: #f9f9f9;
1921
+ --uidex-color-text-strong: #ffffff;
1922
+ --uidex-color-text-muted: #ababab; /* chrome: labels, counts, placeholders */
1923
+ --uidex-color-text-secondary: #ababab; /* content: doc text, descriptions */
1924
+ --uidex-color-surface-hover: rgba(255, 255, 255, 0.08);
1925
+ --uidex-color-surface-active: rgba(255, 255, 255, 0.12);
1926
+ --uidex-color-border: rgba(255, 255, 255, 0.1);
1927
+ --uidex-color-border-light: rgba(255, 255, 255, 0.06);
1928
+ --uidex-color-backdrop: rgba(0, 0, 0, 0.6);
1929
+
1930
+ /* Category colors */
1931
+ --uidex-color-pages: #60a5fa;
1932
+ --uidex-color-features: #a78bfa;
1933
+ --uidex-color-components: #34d399;
1934
+
1935
+ /* Shadows \u2014 minimal, border-driven definition */
1936
+ --uidex-shadow-sm: none;
1937
+ --uidex-shadow-md: 0 0 0 1px var(--uidex-color-border);
1938
+ --uidex-shadow-lg: 0 0 0 1px var(--uidex-color-border);
1939
+
1940
+ /* Border radius \u2014 lyra: none */
1941
+ --uidex-radius-sm: 0;
1942
+ --uidex-radius-md: 0;
1943
+ --uidex-radius-lg: 0;
1944
+ --uidex-radius-full: 0;
1945
+
1946
+ /* Z-index \u2014 high enough to stay above app UI */
1947
+ --uidex-z-container: 2147483644;
1948
+ --uidex-z-overlay: 2147483645;
1949
+ --uidex-z-modal: 2147483646;
1950
+
1951
+ /* Typography \u2014 mono-first (lyra). Both stacks are mono by default;
1952
+ consumers can override --uidex-font-sans to restore a proportional body font. */
1953
+ --uidex-font-sans: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
1954
+ --uidex-font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
1955
+ --uidex-font-size-xs: 11px;
1956
+ --uidex-font-size-sm: 12px;
1957
+ --uidex-font-size-md: 13px;
1958
+ --uidex-font-size-lg: 14px;
1959
+
1960
+ /* Transitions */
1961
+ --uidex-transition-fast: 0.1s ease;
1962
+ --uidex-transition-normal: 0.15s ease;
1963
+ --uidex-transition-smooth: 0.2s ease;
1964
+ }
1965
+
1966
+ /* \u2014\u2014\u2014 Container \u2014\u2014\u2014 */
1967
+ .uidex-container {
1968
+ position: fixed;
1969
+ z-index: var(--uidex-z-container);
1970
+ pointer-events: auto;
1971
+ font-family: var(--uidex-font-sans);
1972
+ -webkit-font-smoothing: antialiased;
1973
+ -moz-osx-font-smoothing: grayscale;
1974
+ }
1975
+
1976
+ .uidex-bottom-right { bottom: 20px; right: 20px; }
1977
+ .uidex-bottom-left { bottom: 20px; left: 20px; }
1978
+ .uidex-top-right { top: 20px; right: 20px; }
1979
+ .uidex-top-left { top: 20px; left: 20px; }
1980
+
1981
+ /* \u2014\u2014\u2014 Trigger button group \u2014\u2014\u2014 */
1982
+ .uidex-trigger-group {
1983
+ display: flex;
1984
+ align-items: center;
1985
+ border-radius: 0;
1986
+ background: var(--uidex-color-bg);
1987
+ border: 1px solid var(--uidex-color-border);
1988
+ overflow: hidden;
1989
+ user-select: none;
1990
+ height: 32px;
1991
+ }
1992
+
1993
+ .uidex-trigger-group:hover {
1994
+ border-color: rgba(255, 255, 255, 0.16);
1995
+ }
1996
+
1997
+ .uidex-trigger-btn {
1998
+ display: flex;
1999
+ align-items: center;
2000
+ justify-content: center;
2001
+ gap: 6px;
2002
+ height: 100%;
2003
+ padding: 0 10px;
2004
+ border: none;
2005
+ background: transparent;
2006
+ color: var(--uidex-color-text-muted);
2007
+ font-size: var(--uidex-font-size-sm);
2008
+ font-weight: 500;
2009
+ font-family: var(--uidex-font-mono);
2010
+ cursor: pointer;
2011
+ transition: background-color var(--uidex-transition-fast), color var(--uidex-transition-fast);
2012
+ }
2013
+
2014
+ .uidex-trigger-btn:hover {
2015
+ background: var(--uidex-color-surface-hover);
2016
+ color: var(--uidex-color-text-strong);
2017
+ }
2018
+
2019
+ .uidex-trigger-btn:active {
2020
+ background: var(--uidex-color-surface-active);
2021
+ transform: translateY(1px);
2022
+ }
2023
+
2024
+ .uidex-trigger-btn--active {
2025
+ color: var(--uidex-color-text-strong);
2026
+ background: var(--uidex-color-surface-active);
2027
+ }
2028
+
2029
+ .uidex-trigger-btn--active:hover {
2030
+ background: rgba(255, 255, 255, 0.16);
2031
+ }
2032
+
2033
+ .uidex-trigger-divider {
2034
+ width: 1px;
2035
+ height: 16px;
2036
+ background: var(--uidex-color-border);
2037
+ flex-shrink: 0;
2038
+ }
2039
+
2040
+ .uidex-trigger-icon {
2041
+ width: 14px;
2042
+ height: 14px;
2043
+ flex-shrink: 0;
2044
+ }
2045
+
2046
+ /* \u2014\u2014\u2014 Draggable \u2014\u2014\u2014 */
2047
+ .uidex-container--dragging {
2048
+ transition: none !important;
2049
+ }
2050
+
2051
+ .uidex-container--dragging .uidex-trigger-btn {
2052
+ cursor: grabbing !important;
2053
+ }
2054
+
2055
+ /* \u2014\u2014\u2014 Inspect mode cursor \u2014\u2014\u2014 */
2056
+ body.uidex-inspecting,
2057
+ body.uidex-inspecting * {
2058
+ cursor: crosshair !important;
2059
+ }
2060
+
2061
+ /* \u2014\u2014\u2014 Modal \u2014\u2014\u2014 */
2062
+ .uidex-modal-backdrop {
2063
+ position: fixed;
2064
+ inset: 0;
2065
+ z-index: var(--uidex-z-modal);
2066
+ pointer-events: auto;
2067
+ display: flex;
2068
+ align-items: center;
2069
+ justify-content: center;
2070
+ background: var(--uidex-color-backdrop);
2071
+ }
2072
+
2073
+ .uidex-modal {
2074
+ background: var(--uidex-color-bg);
2075
+ border: 1px solid var(--uidex-color-border);
2076
+ border-radius: 0;
2077
+ max-width: calc(100vw - 32px);
2078
+ max-height: calc(100vh - 64px);
2079
+ font-family: var(--uidex-font-sans);
2080
+ }
2081
+
2082
+ .uidex-modal-header {
2083
+ display: flex;
2084
+ align-items: center;
2085
+ gap: 12px;
2086
+ padding: 12px 16px;
2087
+ border-bottom: 1px solid var(--uidex-color-border);
2088
+ }
2089
+
2090
+ .uidex-modal-title {
2091
+ margin: 0;
2092
+ font-size: var(--uidex-font-size-sm);
2093
+ font-weight: 600;
2094
+ color: var(--uidex-color-text-strong);
2095
+ font-family: var(--uidex-font-mono);
2096
+ text-transform: uppercase;
2097
+ letter-spacing: 0.05em;
2098
+ }
2099
+
2100
+ .uidex-modal-close {
2101
+ display: flex;
2102
+ align-items: center;
2103
+ justify-content: center;
2104
+ width: 28px;
2105
+ height: 28px;
2106
+ border: 1px solid transparent;
2107
+ border-radius: 0;
2108
+ background: transparent;
2109
+ color: var(--uidex-color-text-muted);
2110
+ font-size: 18px;
2111
+ cursor: pointer;
2112
+ transition: background-color var(--uidex-transition-fast), color var(--uidex-transition-fast), border-color var(--uidex-transition-fast);
2113
+ }
2114
+
2115
+ .uidex-modal-close:hover {
2116
+ background: var(--uidex-color-surface-hover);
2117
+ border-color: var(--uidex-color-border);
2118
+ color: var(--uidex-color-text-strong);
2119
+ }
2120
+
2121
+ .uidex-modal-close:active {
2122
+ transform: translateY(1px);
2123
+ }
2124
+
2125
+ .uidex-modal-body {
2126
+ padding: 12px 16px;
2127
+ }
2128
+
2129
+ .uidex-modal-section {
2130
+ margin-bottom: 12px;
2131
+ }
2132
+
2133
+ .uidex-modal-section:last-child {
2134
+ margin-bottom: 0;
2135
+ }
2136
+
2137
+ .uidex-modal-label {
2138
+ font-size: var(--uidex-font-size-xs);
2139
+ font-weight: 600;
2140
+ text-transform: uppercase;
2141
+ letter-spacing: 0.06em;
2142
+ color: var(--uidex-color-text-muted);
2143
+ margin-bottom: 6px;
2144
+ }
2145
+
2146
+ .uidex-modal-section--spaced {
2147
+ margin-top: 12px;
2148
+ }
2149
+
2150
+ .uidex-modal-doc {
2151
+ margin: 0;
2152
+ font-size: var(--uidex-font-size-sm);
2153
+ line-height: 1.5;
2154
+ color: var(--uidex-color-text-secondary);
2155
+ }
2156
+
2157
+ .uidex-modal-code {
2158
+ display: block;
2159
+ font-family: var(--uidex-font-mono);
2160
+ font-size: var(--uidex-font-size-sm);
2161
+ color: var(--uidex-color-text);
2162
+ background: var(--uidex-color-bg-elevated);
2163
+ padding: 6px 10px;
2164
+ border-radius: 0;
2165
+ border: 1px solid var(--uidex-color-border);
2166
+ word-break: break-all;
2167
+ }
2168
+
2169
+ .uidex-modal-location {
2170
+ display: flex;
2171
+ align-items: center;
2172
+ gap: 8px;
2173
+ margin-bottom: 6px;
2174
+ }
2175
+
2176
+ .uidex-modal-location:last-child {
2177
+ margin-bottom: 0;
2178
+ }
2179
+
2180
+ .uidex-modal-location .uidex-modal-code {
2181
+ flex: 1;
2182
+ min-width: 0;
2183
+ }
2184
+
2185
+ .uidex-modal-copy {
2186
+ flex-shrink: 0;
2187
+ padding: 4px 10px;
2188
+ border: 1px solid var(--uidex-color-border);
2189
+ border-radius: 0;
2190
+ background: var(--uidex-color-bg-elevated);
2191
+ color: var(--uidex-color-text-secondary);
2192
+ font-size: var(--uidex-font-size-sm);
2193
+ font-weight: 500;
2194
+ font-family: var(--uidex-font-mono);
2195
+ cursor: pointer;
2196
+ transition: background-color var(--uidex-transition-fast), color var(--uidex-transition-fast);
2197
+ white-space: nowrap;
2198
+ }
2199
+
2200
+ .uidex-modal-copy:hover {
2201
+ background: var(--uidex-color-surface-hover);
2202
+ color: var(--uidex-color-text-strong);
2203
+ }
2204
+
2205
+ .uidex-modal-copy:active {
2206
+ transform: translateY(1px);
2207
+ }
2208
+
2209
+ .uidex-modal-env-item {
2210
+ font-size: var(--uidex-font-size-sm);
2211
+ color: var(--uidex-color-text-secondary);
2212
+ line-height: 1.6;
2213
+ }
2214
+
2215
+ .uidex-empty-message {
2216
+ padding: 16px 12px;
2217
+ color: var(--uidex-color-text-muted);
2218
+ font-size: var(--uidex-font-size-sm);
2219
+ text-align: center;
2220
+ }
2221
+
2222
+ /* \u2014\u2014\u2014 Panel layout (sidebar modal) \u2014\u2014\u2014 */
2223
+ .uidex-modal--panel {
2224
+ width: calc(100vw - 32px);
2225
+ max-width: 720px;
2226
+ height: calc(100vh - 64px);
2227
+ max-height: 600px;
2228
+ display: flex;
2229
+ flex-direction: column;
2230
+ overflow: hidden;
2231
+ }
2232
+
2233
+ .uidex-panel-body {
2234
+ display: flex;
2235
+ flex: 1;
2236
+ min-height: 0;
2237
+ }
2238
+
2239
+ .uidex-panel-sidebar {
2240
+ width: 200px;
2241
+ flex-shrink: 0;
2242
+ border-right: 1px solid var(--uidex-color-border);
2243
+ overflow-y: auto;
2244
+ padding: 0;
2245
+ scrollbar-width: thin;
2246
+ scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
2247
+ }
2248
+
2249
+ .uidex-panel-sidebar::-webkit-scrollbar {
2250
+ width: 6px;
2251
+ }
2252
+
2253
+ .uidex-panel-sidebar::-webkit-scrollbar-track {
2254
+ background: transparent;
2255
+ }
2256
+
2257
+ .uidex-panel-sidebar::-webkit-scrollbar-thumb {
2258
+ background: rgba(255, 255, 255, 0.1);
2259
+ border-radius: 0;
2260
+ }
2261
+
2262
+ .uidex-panel-main {
2263
+ flex: 1;
2264
+ min-width: 0;
2265
+ overflow-y: auto;
2266
+ padding: 16px 20px;
2267
+ scrollbar-width: thin;
2268
+ scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
2269
+ }
2270
+
2271
+ .uidex-panel-main::-webkit-scrollbar {
2272
+ width: 6px;
2273
+ }
2274
+
2275
+ .uidex-panel-main::-webkit-scrollbar-track {
2276
+ background: transparent;
2277
+ }
2278
+
2279
+ .uidex-panel-main::-webkit-scrollbar-thumb {
2280
+ background: rgba(255, 255, 255, 0.1);
2281
+ border-radius: 0;
2282
+ }
2283
+
2284
+ /* \u2014\u2014\u2014 Sidebar navigation \u2014\u2014\u2014 */
2285
+ .uidex-sidebar-back {
2286
+ display: flex;
2287
+ align-items: center;
2288
+ gap: 6px;
2289
+ padding: 6px 8px;
2290
+ margin: 0;
2291
+ border-bottom: 1px solid var(--uidex-color-border);
2292
+ font-size: var(--uidex-font-size-sm);
2293
+ font-weight: 600;
2294
+ color: var(--uidex-color-text-secondary);
2295
+ cursor: pointer;
2296
+ transition: color var(--uidex-transition-fast);
2297
+ position: sticky;
2298
+ top: 0;
2299
+ background: var(--uidex-color-bg);
2300
+ z-index: 1;
2301
+ }
2302
+
2303
+ .uidex-sidebar-back:hover {
2304
+ color: var(--uidex-color-text-strong);
2305
+ }
2306
+
2307
+ .uidex-sidebar-back-arrow {
2308
+ font-size: 16px;
2309
+ line-height: 1;
2310
+ }
2311
+
2312
+ .uidex-sidebar-root-item {
2313
+ display: flex;
2314
+ align-items: center;
2315
+ gap: 6px;
2316
+ padding: 6px 10px;
2317
+ border-radius: 0;
2318
+ font-size: var(--uidex-font-size-sm);
2319
+ font-weight: 500;
2320
+ color: var(--uidex-color-text);
2321
+ cursor: pointer;
2322
+ transition: background-color var(--uidex-transition-fast);
2323
+ }
2324
+
2325
+ .uidex-sidebar-root-item:hover {
2326
+ background: var(--uidex-color-surface-hover);
2327
+ }
2328
+
2329
+ .uidex-sidebar-root-label {
2330
+ flex: 0 0 auto;
2331
+ }
2332
+
2333
+ .uidex-sidebar-count {
2334
+ font-size: var(--uidex-font-size-xs);
2335
+ color: var(--uidex-color-text-muted);
2336
+ font-weight: 400;
2337
+ }
2338
+
2339
+ .uidex-sidebar-chevron {
2340
+ margin-left: auto;
2341
+ font-size: 16px;
2342
+ line-height: 1;
2343
+ color: var(--uidex-color-text-muted);
2344
+ }
2345
+
2346
+ .uidex-sidebar-item {
2347
+ padding: 4px 10px;
2348
+ border-radius: 0;
2349
+ font-size: var(--uidex-font-size-sm);
2350
+ color: var(--uidex-color-text);
2351
+ cursor: pointer;
2352
+ transition: background-color var(--uidex-transition-fast);
2353
+ white-space: nowrap;
2354
+ overflow: hidden;
2355
+ text-overflow: ellipsis;
2356
+ }
2357
+
2358
+ .uidex-sidebar-item:hover {
2359
+ background: var(--uidex-color-surface-hover);
2360
+ }
2361
+
2362
+ .uidex-sidebar-item--active {
2363
+ background: var(--uidex-color-surface-active);
2364
+ color: var(--uidex-color-text-strong);
2365
+ }
2366
+
2367
+ .uidex-sidebar-item--active:hover {
2368
+ background: var(--uidex-color-surface-active);
2369
+ }
2370
+
2371
+ .uidex-sidebar-item--dimmed {
2372
+ opacity: 0.5;
2373
+ }
2374
+
2375
+ .uidex-sidebar-item--mono {
2376
+ font-family: var(--uidex-font-mono);
2377
+ }
2378
+
2379
+ .uidex-sidebar-item--grouped {
2380
+ padding-left: 24px;
2381
+ }
2382
+
2383
+ /* Sidebar category colors */
2384
+ .uidex-sidebar-item--pages.uidex-sidebar-item--active {
2385
+ color: var(--uidex-color-pages);
2386
+ }
2387
+
2388
+ .uidex-sidebar-item--features.uidex-sidebar-item--active {
2389
+ color: var(--uidex-color-features);
2390
+ }
2391
+
2392
+ .uidex-sidebar-item--components.uidex-sidebar-item--active {
2393
+ color: var(--uidex-color-components);
2394
+ }
2395
+
2396
+ .uidex-sidebar-root-item--pages .uidex-sidebar-root-label {
2397
+ color: var(--uidex-color-pages);
2398
+ }
2399
+
2400
+ .uidex-sidebar-root-item--features .uidex-sidebar-root-label {
2401
+ color: var(--uidex-color-features);
2402
+ }
2403
+
2404
+ .uidex-sidebar-root-item--components .uidex-sidebar-root-label {
2405
+ color: var(--uidex-color-components);
2406
+ }
2407
+
2408
+ /* \u2014\u2014\u2014 Sidebar component groups \u2014\u2014\u2014 */
2409
+ .uidex-sidebar-group-header {
2410
+ display: flex;
2411
+ align-items: center;
2412
+ gap: 4px;
2413
+ padding: 4px 10px;
2414
+ border-radius: 0;
2415
+ font-size: var(--uidex-font-size-sm);
2416
+ font-weight: 600;
2417
+ color: var(--uidex-color-text);
2418
+ cursor: pointer;
2419
+ transition: background-color var(--uidex-transition-fast);
2420
+ user-select: none;
2421
+ }
2422
+
2423
+ .uidex-sidebar-group-header:hover {
2424
+ background: var(--uidex-color-surface-hover);
2425
+ }
2426
+
2427
+ .uidex-sidebar-group-chevron {
2428
+ font-size: 14px;
2429
+ line-height: 1;
2430
+ color: var(--uidex-color-text-muted);
2431
+ width: 14px;
2432
+ text-align: center;
2433
+ flex-shrink: 0;
2434
+ transition: transform var(--uidex-transition-normal);
2435
+ transform: rotate(0deg);
2436
+ }
2437
+
2438
+ .uidex-sidebar-group-header--expanded .uidex-sidebar-group-chevron {
2439
+ transform: rotate(90deg);
2440
+ }
2441
+
2442
+ .uidex-sidebar-group-count {
2443
+ margin-left: auto;
2444
+ font-size: var(--uidex-font-size-xs);
2445
+ font-weight: 400;
2446
+ color: var(--uidex-color-text-muted);
2447
+ }
2448
+
2449
+ /* \u2014\u2014\u2014 Search \u2014\u2014\u2014 */
2450
+ .uidex-search-input {
2451
+ flex: 1;
2452
+ min-width: 0;
2453
+ padding: 4px 8px;
2454
+ border: 1px solid var(--uidex-color-border);
2455
+ border-radius: 0;
2456
+ background: transparent;
2457
+ color: var(--uidex-color-text);
2458
+ font-size: var(--uidex-font-size-sm);
2459
+ font-family: var(--uidex-font-mono);
2460
+ outline: none;
2461
+ box-sizing: border-box;
2462
+ }
2463
+
2464
+ .uidex-search-input::placeholder {
2465
+ color: var(--uidex-color-text-muted);
2466
+ }
2467
+
2468
+ .uidex-search-input:focus {
2469
+ border-color: rgba(255, 255, 255, 0.3);
2470
+ }
2471
+
2472
+ .uidex-search-group-label {
2473
+ padding: 8px 10px 2px;
2474
+ font-size: var(--uidex-font-size-xs);
2475
+ font-weight: 600;
2476
+ text-transform: uppercase;
2477
+ letter-spacing: 0.06em;
2478
+ color: var(--uidex-color-text-muted);
2479
+ }
2480
+
2481
+ .uidex-search-group-label--pages { color: var(--uidex-color-pages); }
2482
+ .uidex-search-group-label--features { color: var(--uidex-color-features); }
2483
+ .uidex-search-group-label--components { color: var(--uidex-color-components); }
2484
+
2485
+ .uidex-search-empty {
2486
+ padding: 20px 10px;
2487
+ font-size: var(--uidex-font-size-sm);
2488
+ color: var(--uidex-color-text-muted);
2489
+ text-align: center;
2490
+ }
2491
+
2492
+ .uidex-sidebar-divider {
2493
+ height: 1px;
2494
+ background: var(--uidex-color-border);
2495
+ margin: 4px 10px;
2496
+ }
2497
+
2498
+ .uidex-sidebar-root-item--active {
2499
+ background: var(--uidex-color-surface-active);
2500
+ }
2501
+
2502
+ /* \u2014\u2014\u2014 Main content \u2014\u2014\u2014 */
2503
+ .uidex-main-title {
2504
+ margin: 0 0 8px;
2505
+ font-size: var(--uidex-font-size-lg);
2506
+ font-weight: 600;
2507
+ color: var(--uidex-color-text-strong);
2508
+ }
2509
+
2510
+ .uidex-main-title--mono {
2511
+ font-family: var(--uidex-font-mono);
2512
+ color: var(--uidex-color-text-strong);
2513
+ }
2514
+
2515
+ .uidex-main-divider {
2516
+ height: 1px;
2517
+ background: var(--uidex-color-border);
2518
+ margin: 16px 0;
2519
+ }
2520
+
2521
+ .uidex-absent-notice {
2522
+ font-size: var(--uidex-font-size-sm);
2523
+ color: var(--uidex-color-text-muted);
2524
+ font-style: italic;
2525
+ margin-bottom: 12px;
2526
+ }
2527
+
2528
+ /* \u2014\u2014\u2014 Page content in modal \u2014\u2014\u2014 */
2529
+ .uidex-page-content {
2530
+ font-size: var(--uidex-font-size-sm);
2531
+ line-height: 1.6;
2532
+ color: var(--uidex-color-text);
2533
+ }
2534
+
2535
+ .uidex-page-content h2,
2536
+ .uidex-page-content h3,
2537
+ .uidex-page-content h4,
2538
+ .uidex-page-content h5,
2539
+ .uidex-page-content h6 {
2540
+ margin: 12px 0 6px;
2541
+ font-size: var(--uidex-font-size-sm);
2542
+ font-weight: 600;
2543
+ color: var(--uidex-color-text-strong);
2544
+ text-transform: uppercase;
2545
+ letter-spacing: 0.04em;
2546
+ }
2547
+
2548
+ .uidex-page-content h2 {
2549
+ font-size: var(--uidex-font-size-md);
2550
+ }
2551
+
2552
+ .uidex-page-content p {
2553
+ margin: 0 0 8px;
2554
+ }
2555
+
2556
+ .uidex-checklist {
2557
+ list-style: none;
2558
+ margin: 0 0 8px;
2559
+ padding-left: 0;
2560
+ }
2561
+
2562
+ .uidex-checklist-item {
2563
+ margin-bottom: 4px;
2564
+ }
2565
+
2566
+ .uidex-checklist-label {
2567
+ display: flex;
2568
+ align-items: baseline;
2569
+ gap: 8px;
2570
+ cursor: pointer;
2571
+ }
2572
+
2573
+ .uidex-checklist-checkbox {
2574
+ flex-shrink: 0;
2575
+ margin: 0;
2576
+ accent-color: var(--uidex-color-text-strong);
2577
+ }
2578
+
2579
+ /* \u2014\u2014\u2014 Chips \u2014\u2014\u2014 */
2580
+ .uidex-chips {
2581
+ display: flex;
2582
+ flex-wrap: wrap;
2583
+ gap: 4px;
2584
+ }
2585
+
2586
+ .uidex-chip {
2587
+ display: inline-block;
2588
+ padding: 2px 8px;
2589
+ border-radius: 0;
2590
+ background: transparent;
2591
+ border: 1px solid var(--uidex-color-border);
2592
+ font-family: var(--uidex-font-mono);
2593
+ font-size: var(--uidex-font-size-xs);
2594
+ color: var(--uidex-color-text);
2595
+ transition: background-color var(--uidex-transition-fast);
2596
+ }
2597
+
2598
+ .uidex-chip:hover {
2599
+ background: var(--uidex-color-surface-hover);
2600
+ }
2601
+
2602
+ .uidex-chip--absent {
2603
+ opacity: 0.4;
2604
+ }
2605
+
2606
+ .uidex-chip--pages {
2607
+ color: var(--uidex-color-pages);
2608
+ border-color: color-mix(in srgb, var(--uidex-color-pages) 30%, transparent);
2609
+ }
2610
+
2611
+ .uidex-chip--features {
2612
+ color: var(--uidex-color-features);
2613
+ border-color: color-mix(in srgb, var(--uidex-color-features) 30%, transparent);
2614
+ }
2615
+
2616
+ .uidex-chip--components {
2617
+ color: var(--uidex-color-components);
2618
+ border-color: color-mix(in srgb, var(--uidex-color-components) 30%, transparent);
2619
+ }
2620
+
2621
+ /* \u2014\u2014\u2014 Category badges \u2014\u2014\u2014 */
2622
+ .uidex-badge {
2623
+ display: inline-block;
2624
+ padding: 2px 8px;
2625
+ border-radius: 0;
2626
+ border: 1px solid var(--uidex-color-border);
2627
+ background: transparent;
2628
+ color: var(--uidex-color-text-muted);
2629
+ font-size: var(--uidex-font-size-xs);
2630
+ font-weight: 600;
2631
+ text-transform: uppercase;
2632
+ letter-spacing: 0.06em;
2633
+ margin-bottom: 12px;
2634
+ }
2635
+
2636
+ .uidex-badge--pages {
2637
+ color: var(--uidex-color-pages);
2638
+ border-color: color-mix(in srgb, var(--uidex-color-pages) 30%, transparent);
2639
+ }
2640
+
2641
+ .uidex-badge--features {
2642
+ color: var(--uidex-color-features);
2643
+ border-color: color-mix(in srgb, var(--uidex-color-features) 30%, transparent);
2644
+ }
2645
+
2646
+ .uidex-badge--components {
2647
+ color: var(--uidex-color-components);
2648
+ border-color: color-mix(in srgb, var(--uidex-color-components) 30%, transparent);
2649
+ }
2650
+
2651
+ /* \u2014\u2014\u2014 Feedback modal \u2014\u2014\u2014 */
2652
+ .uidex-modal--feedback {
2653
+ width: calc(100vw - 32px);
2654
+ max-width: 400px;
2655
+ }
2656
+
2657
+ .uidex-modal-code--inline {
2658
+ border: none;
2659
+ background: none;
2660
+ padding: 0;
2661
+ }
2662
+
2663
+ .uidex-header-spacer {
2664
+ flex: 1;
2665
+ }
2666
+
2667
+ .uidex-chips--spaced {
2668
+ margin-bottom: 8px;
2669
+ }
2670
+
2671
+ .uidex-modal-doc--spaced {
2672
+ margin-bottom: 12px;
2673
+ }
2674
+
2675
+ /* \u2014\u2014\u2014 Feedback form \u2014\u2014\u2014 */
2676
+ .uidex-form {
2677
+ display: flex;
2678
+ flex-direction: column;
2679
+ gap: 12px;
2680
+ }
2681
+
2682
+ .uidex-form-group {
2683
+ display: flex;
2684
+ flex-direction: column;
2685
+ gap: 4px;
2686
+ }
2687
+
2688
+ .uidex-form-label {
2689
+ font-size: var(--uidex-font-size-xs);
2690
+ font-weight: 600;
2691
+ text-transform: uppercase;
2692
+ letter-spacing: 0.06em;
2693
+ color: var(--uidex-color-text-muted);
2694
+ }
2695
+
2696
+ .uidex-form-select {
2697
+ appearance: none;
2698
+ padding: 6px 10px;
2699
+ border: 1px solid var(--uidex-color-border);
2700
+ border-radius: 0;
2701
+ background: transparent;
2702
+ color: var(--uidex-color-text);
2703
+ font-size: var(--uidex-font-size-sm);
2704
+ font-family: var(--uidex-font-mono);
2705
+ cursor: pointer;
2706
+ outline: none;
2707
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23ababab' d='M3 5l3 3 3-3'/%3E%3C/svg%3E");
2708
+ background-repeat: no-repeat;
2709
+ background-position: right 8px center;
2710
+ padding-right: 26px;
2711
+ }
2712
+
2713
+ .uidex-form-select:focus {
2714
+ border-color: rgba(255, 255, 255, 0.3);
2715
+ }
2716
+
2717
+ .uidex-form-textarea {
2718
+ padding: 8px 10px;
2719
+ border: 1px solid var(--uidex-color-border);
2720
+ border-radius: 0;
2721
+ background: transparent;
2722
+ color: var(--uidex-color-text);
2723
+ font-size: var(--uidex-font-size-sm);
2724
+ font-family: var(--uidex-font-mono);
2725
+ line-height: 1.5;
2726
+ resize: vertical;
2727
+ outline: none;
2728
+ min-height: 80px;
2729
+ }
2730
+
2731
+ .uidex-form-textarea::placeholder {
2732
+ color: var(--uidex-color-text-muted);
2733
+ }
2734
+
2735
+ .uidex-form-textarea:focus {
2736
+ border-color: rgba(255, 255, 255, 0.3);
2737
+ }
2738
+
2739
+ .uidex-form-submit {
2740
+ padding: 6px 16px;
2741
+ border: 1px solid var(--uidex-color-border);
2742
+ border-radius: 0;
2743
+ background: var(--uidex-color-primary);
2744
+ color: var(--uidex-color-bg);
2745
+ font-size: var(--uidex-font-size-sm);
2746
+ font-weight: 600;
2747
+ font-family: var(--uidex-font-mono);
2748
+ cursor: pointer;
2749
+ transition: background-color var(--uidex-transition-fast);
2750
+ align-self: flex-end;
2751
+ }
2752
+
2753
+ .uidex-form-submit:hover {
2754
+ background: var(--uidex-color-primary-hover);
2755
+ }
2756
+
2757
+ .uidex-form-submit:active {
2758
+ transform: translateY(1px);
2759
+ }
2760
+
2761
+ .uidex-form-submit:disabled {
2762
+ opacity: 0.5;
2763
+ cursor: default;
2764
+ }
2765
+
2766
+ /* \u2014\u2014\u2014 Form toggle \u2014\u2014\u2014 */
2767
+ .uidex-form-toggle {
2768
+ display: flex;
2769
+ align-items: center;
2770
+ gap: 8px;
2771
+ cursor: pointer;
2772
+ }
2773
+
2774
+ .uidex-form-checkbox {
2775
+ flex-shrink: 0;
2776
+ margin: 0;
2777
+ accent-color: var(--uidex-color-text-strong);
2778
+ }
2779
+
2780
+ .uidex-form-toggle-text {
2781
+ font-size: var(--uidex-font-size-sm);
2782
+ color: var(--uidex-color-text-secondary);
2783
+ }
2784
+
2785
+ /* \u2014\u2014\u2014 Responsive \u2014\u2014\u2014 */
2786
+ @media (max-width: 600px) {
2787
+ .uidex-modal--panel {
2788
+ width: auto;
2789
+ }
2790
+
2791
+ .uidex-panel-body {
2792
+ flex-direction: column;
2793
+ }
2794
+
2795
+ .uidex-panel-sidebar {
2796
+ width: auto;
2797
+ max-height: 200px;
2798
+ border-right: none;
2799
+ border-bottom: 1px solid var(--uidex-color-border);
2800
+ }
2801
+ }
2802
+
2803
+ /* \u2014\u2014\u2014 Graph \u2014\u2014\u2014 */
2804
+ .uidex-graph {
2805
+ margin-top: 8px;
2806
+ }
2807
+
2808
+ .uidex-graph-svg {
2809
+ display: block;
2810
+ }
2811
+
2812
+ .uidex-graph-header {
2813
+ font-size: 10px;
2814
+ font-weight: 600;
2815
+ letter-spacing: 0.08em;
2816
+ }
2817
+
2818
+ .uidex-graph-header--pages { fill: var(--uidex-color-pages); }
2819
+ .uidex-graph-header--features { fill: var(--uidex-color-features); }
2820
+ .uidex-graph-header--components { fill: var(--uidex-color-components); }
2821
+
2822
+ .uidex-graph-node {
2823
+ cursor: pointer;
2824
+ }
2825
+
2826
+ .uidex-graph-node rect {
2827
+ fill: var(--uidex-color-bg-elevated);
2828
+ stroke: var(--uidex-color-border);
2829
+ stroke-width: 1;
2830
+ }
2831
+
2832
+ .uidex-graph-node:hover rect {
2833
+ fill: var(--uidex-color-surface-hover);
2834
+ }
2835
+
2836
+ .uidex-graph-node--page rect { stroke: color-mix(in srgb, var(--uidex-color-pages) 50%, transparent); }
2837
+ .uidex-graph-node--feature rect { stroke: color-mix(in srgb, var(--uidex-color-features) 50%, transparent); }
2838
+ .uidex-graph-node--component rect { stroke: color-mix(in srgb, var(--uidex-color-components) 50%, transparent); }
2839
+
2840
+ .uidex-graph-label {
2841
+ font-size: 10px;
2842
+ font-family: var(--uidex-font-mono);
2843
+ fill: var(--uidex-color-text);
2844
+ }
2845
+
2846
+ .uidex-graph-edge {
2847
+ fill: none;
2848
+ stroke-width: 1;
2849
+ opacity: 0.3;
2850
+ }
2851
+
2852
+ .uidex-graph-edge--page { stroke: var(--uidex-color-pages); }
2853
+ .uidex-graph-edge--feature { stroke: var(--uidex-color-features); }
2854
+
2855
+ .uidex-graph-svg.uidex-graph--hover .uidex-graph-edge { opacity: 0.08; }
2856
+ .uidex-graph-svg.uidex-graph--hover .uidex-graph-node { opacity: 0.3; }
2857
+ .uidex-graph-svg.uidex-graph--hover .uidex-graph-edge--highlighted { opacity: 0.6; stroke-width: 1.5; }
2858
+ .uidex-graph-svg.uidex-graph--hover .uidex-graph-node--highlighted { opacity: 1; }
2859
+
2860
+ .uidex-graph-note {
2861
+ font-size: var(--uidex-font-size-sm);
2862
+ color: var(--uidex-color-text-muted);
2863
+ margin-top: 12px;
2864
+ font-style: italic;
2865
+ }
2866
+
2867
+ /* \u2014\u2014\u2014 Overlay \u2014\u2014\u2014
2868
+ NOTE: The overlay renders in document.body (outside the Shadow DOM)
2869
+ so all its styles are set inline in overlay.ts. These CSS classes
2870
+ are retained only for consumers who import style.css standalone. */
2871
+ .uidex-overlay {
2872
+ position: fixed;
2873
+ pointer-events: none;
2874
+ z-index: 2147483645;
2875
+ }
2876
+ `;
2877
+ var css_text_default = cssText;
2878
+
2879
+ // src/core/shadow.ts
2880
+ function injectStyles(shadowRoot) {
2881
+ const style = document.createElement("style");
2882
+ style.textContent = css_text_default;
2883
+ shadowRoot.appendChild(style);
2884
+ }
2885
+
2886
+ // src/core/uidex-ui.ts
2887
+ var UidexUI = class {
2888
+ menu;
2889
+ overlay;
2890
+ modal;
2891
+ inspector;
2892
+ options;
2893
+ mounted = false;
2894
+ shadowHost = null;
2895
+ shadowRoot = null;
2896
+ observer = null;
2897
+ refreshTimer = null;
2898
+ copyTimer = null;
2899
+ currentPresentIds = [];
2900
+ activeMode = null;
2901
+ constructor(options = {}) {
2902
+ this.options = options;
2903
+ this.overlay = new Overlay({
2904
+ color: options.config?.defaults?.color,
2905
+ borderStyle: options.config?.defaults?.borderStyle,
2906
+ borderWidth: options.config?.defaults?.borderWidth,
2907
+ showLabel: options.config?.defaults?.showLabel,
2908
+ labelPosition: options.config?.defaults?.labelPosition,
2909
+ colors: options.config?.colors
2910
+ });
2911
+ this.modal = new Modal({
2912
+ onComponentNavigate: (id) => {
2913
+ const element = this.findElement(id);
2914
+ if (element && typeof element.scrollIntoView === "function") {
2915
+ element.scrollIntoView({ behavior: "smooth", block: "center" });
2916
+ }
2917
+ this.options.onSelect?.(id);
2918
+ },
2919
+ elementGetter: (id) => this.findElement(id)
2920
+ });
2921
+ this.menu = new Menu({
2922
+ onInspectToggle: () => this.toggleMode("inspect"),
2923
+ onCopyToggle: () => this.toggleMode("copy"),
2924
+ onFeedbackToggle: () => this.toggleMode("feedback"),
2925
+ onExpandClick: () => this.handleExpandClick()
2926
+ });
2927
+ if (options.inspectShortcut === false) {
2928
+ this.inspector = null;
2929
+ } else {
2930
+ this.inspector = new Inspector({
2931
+ shortcut: options.inspectShortcut,
2932
+ onHighlight: (_element, id) => this.handleHighlight(id),
2933
+ onSelect: (_element, id) => this.handleSelect(id),
2934
+ onActivate: () => {
2935
+ this.updateMenuActiveStates();
2936
+ },
2937
+ onDeactivate: () => {
2938
+ this.overlay.hide();
2939
+ this.activeMode = null;
2940
+ this.updateMenuActiveStates();
2941
+ }
2942
+ });
2943
+ }
2944
+ }
2945
+ mount(container) {
2946
+ if (this.mounted) return;
2947
+ const components = this.getComponentsMap();
2948
+ const componentIds = Object.keys(components);
2949
+ if (componentIds.length === 0) return;
2950
+ this.shadowHost = document.createElement("div");
2951
+ this.shadowHost.id = "uidex-root";
2952
+ this.shadowHost.style.position = "fixed";
2953
+ this.shadowHost.style.top = "0";
2954
+ this.shadowHost.style.left = "0";
2955
+ this.shadowHost.style.width = "0";
2956
+ this.shadowHost.style.height = "0";
2957
+ this.shadowHost.style.pointerEvents = "none";
2958
+ this.shadowRoot = this.shadowHost.attachShadow({ mode: "open" });
2959
+ injectStyles(this.shadowRoot);
2960
+ const presentIds = this.scanPresentIds(componentIds);
2961
+ const menuContainer = this.menu.create();
2962
+ const position = this.options.buttonPosition ?? "bottom-right";
2963
+ menuContainer.classList.add(POSITION_CLASSES[position]);
2964
+ const mountTarget = container ?? document.body;
2965
+ mountTarget.appendChild(this.shadowHost);
2966
+ this.shadowRoot.appendChild(menuContainer);
2967
+ this.modal.setShadowRoot(this.shadowRoot);
2968
+ this.updateModalData(presentIds);
2969
+ this.inspector?.mount();
2970
+ this.startObserving();
2971
+ this.mounted = true;
2972
+ }
2973
+ destroy() {
2974
+ if (!this.mounted) return;
2975
+ this.stopObserving();
2976
+ if (this.copyTimer !== null) {
2977
+ clearTimeout(this.copyTimer);
2978
+ this.copyTimer = null;
2979
+ }
2980
+ this.inspector?.destroy();
2981
+ this.menu.destroy();
2982
+ this.overlay.destroy();
2983
+ this.modal.destroy();
2984
+ if (this.shadowHost && this.shadowHost.parentNode) {
2985
+ this.shadowHost.parentNode.removeChild(this.shadowHost);
2986
+ }
2987
+ this.shadowHost = null;
2988
+ this.shadowRoot = null;
2989
+ this.mounted = false;
2990
+ }
2991
+ startInspect() {
2992
+ this.inspector?.activate();
2993
+ }
2994
+ stopInspect() {
2995
+ this.inspector?.deactivate();
2996
+ }
2997
+ isInspecting() {
2998
+ return this.inspector?.isActive() ?? false;
2999
+ }
3000
+ /** Open the modal panel, optionally to a specific view. */
3001
+ openPanel(view) {
3002
+ this.modal.open(view);
3003
+ }
3004
+ toggleMode(mode) {
3005
+ if (this.inspector?.isActive()) {
3006
+ if (this.activeMode === mode) {
3007
+ this.inspector.deactivate();
3008
+ } else {
3009
+ this.activeMode = mode;
3010
+ this.updateMenuActiveStates();
3011
+ }
3012
+ } else {
3013
+ this.activeMode = mode;
3014
+ this.inspector?.activate();
3015
+ }
3016
+ }
3017
+ updateMenuActiveStates() {
3018
+ this.menu.setInspectActive(this.activeMode === "inspect");
3019
+ this.menu.setCopyActive(this.activeMode === "copy");
3020
+ this.menu.setFeedbackActive(this.activeMode === "feedback");
3021
+ }
3022
+ getComponentsMap() {
3023
+ return this.options.components ?? getComponents() ?? {};
3024
+ }
3025
+ getPagesArray() {
3026
+ return this.options.pages ?? getPages() ?? [];
3027
+ }
3028
+ getFeaturesArray() {
3029
+ return this.options.features ?? getFeatures() ?? [];
3030
+ }
3031
+ extractPageTitle(content) {
3032
+ const match = content.match(/^#\s+(.+)/m);
3033
+ return match ? match[1].trim() : "Untitled";
3034
+ }
3035
+ /** Hamburger button handler — open modal to default (current page) view. */
3036
+ handleExpandClick() {
3037
+ this.inspector?.deactivate();
3038
+ this.overlay.hide();
3039
+ this.modal.open();
3040
+ }
3041
+ /** Selection handler — routes by active mode. */
3042
+ handleSelect(id) {
3043
+ switch (this.activeMode) {
3044
+ case "inspect":
3045
+ this.overlay.hide();
3046
+ this.modal.open({ type: "component", id });
3047
+ break;
3048
+ case "copy":
3049
+ this.handleCopySelect(id);
3050
+ break;
3051
+ case "feedback":
3052
+ this.overlay.hide();
3053
+ this.modal.openFeedback(id);
3054
+ break;
3055
+ }
3056
+ }
3057
+ handleCopySelect(id) {
3058
+ const components = this.getComponentsMap();
3059
+ const locations = components[id];
3060
+ if (!locations || locations.length === 0) return;
3061
+ const loc = locations[0];
3062
+ const text = `${loc.filePath}:${loc.line}`;
3063
+ navigator.clipboard.writeText(text).catch(() => {
3064
+ });
3065
+ const element = this.findElement(id);
3066
+ if (element instanceof HTMLElement) {
3067
+ this.overlay.show(element, "Copied!");
3068
+ this.copyTimer = setTimeout(() => {
3069
+ this.copyTimer = null;
3070
+ this.overlay.hide();
3071
+ }, 600);
3072
+ }
3073
+ }
3074
+ findElement(id) {
3075
+ return document.querySelector(`[data-uidex="${id}"]`);
3076
+ }
3077
+ handleHighlight(id) {
3078
+ if (id) {
3079
+ const element = this.findElement(id);
3080
+ if (element instanceof HTMLElement) {
3081
+ this.overlay.show(element, id);
3082
+ }
3083
+ } else {
3084
+ this.overlay.hide();
3085
+ }
3086
+ }
3087
+ /** Push current pages/features/components/presentIds to the modal. */
3088
+ updateModalData(presentIds) {
3089
+ const pages = this.getPagesArray();
3090
+ const features = this.getFeaturesArray();
3091
+ const components = this.getComponentsMap();
3092
+ this.modal.setData({
3093
+ pages: pages.map((p) => ({
3094
+ title: this.extractPageTitle(p.content),
3095
+ dir: p.dir,
3096
+ content: p.content,
3097
+ componentIds: p.componentIds
3098
+ })),
3099
+ features: features.map((f) => ({
3100
+ title: this.extractPageTitle(f.content),
3101
+ dir: f.dir,
3102
+ content: f.content,
3103
+ componentIds: f.componentIds
3104
+ })),
3105
+ components,
3106
+ presentIds: new Set(presentIds)
3107
+ });
3108
+ }
3109
+ // ——— DOM observation for SPA navigation ———
3110
+ /** Scan which registered component IDs are currently in the DOM. */
3111
+ scanPresentIds(allIds) {
3112
+ const componentIds = allIds ?? Object.keys(this.getComponentsMap());
3113
+ const found = /* @__PURE__ */ new Set();
3114
+ for (const el of document.querySelectorAll("[data-uidex]")) {
3115
+ const id = el.getAttribute("data-uidex");
3116
+ if (id) found.add(id);
3117
+ }
3118
+ const presentIds = componentIds.filter((id) => found.has(id));
3119
+ this.currentPresentIds = presentIds;
3120
+ return presentIds;
3121
+ }
3122
+ /** Schedule a debounced refresh of the modal data after DOM mutations. */
3123
+ scheduleRefresh() {
3124
+ if (this.refreshTimer !== null) return;
3125
+ this.refreshTimer = setTimeout(() => {
3126
+ this.refreshTimer = null;
3127
+ this.refresh();
3128
+ }, 100);
3129
+ }
3130
+ /** Re-scan the DOM and update modal data if the set of present IDs changed. */
3131
+ refresh() {
3132
+ const oldIds = this.currentPresentIds;
3133
+ const newIds = this.scanPresentIds();
3134
+ if (newIds.length === oldIds.length && newIds.every((id, i) => id === oldIds[i])) {
3135
+ return;
3136
+ }
3137
+ this.updateModalData(newIds);
3138
+ }
3139
+ startObserving() {
3140
+ this.observer = new MutationObserver(() => this.scheduleRefresh());
3141
+ this.observer.observe(document.body, {
3142
+ childList: true,
3143
+ subtree: true,
3144
+ attributes: true,
3145
+ attributeFilter: ["data-uidex"]
3146
+ });
3147
+ }
3148
+ stopObserving() {
3149
+ if (this.refreshTimer !== null) {
3150
+ clearTimeout(this.refreshTimer);
3151
+ this.refreshTimer = null;
3152
+ }
3153
+ this.observer?.disconnect();
3154
+ this.observer = null;
3155
+ }
3156
+ };
3157
+
3158
+ // src/react/UidexDevtools.tsx
3159
+ function UidexDevtools({
3160
+ components,
3161
+ config,
3162
+ buttonPosition = "bottom-right",
3163
+ disabled = false,
3164
+ onSelect,
3165
+ inspectShortcut
3166
+ }) {
3167
+ const uiRef = useRef(null);
3168
+ const stableShortcut = useMemo(
3169
+ () => inspectShortcut,
3170
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3171
+ [
3172
+ inspectShortcut === false ? false : `${inspectShortcut?.key}:${inspectShortcut?.ctrlKey}:${inspectShortcut?.shiftKey}:${inspectShortcut?.altKey}:${inspectShortcut?.metaKey}`
3173
+ ]
3174
+ );
3175
+ useEffect(() => {
3176
+ if (disabled) {
3177
+ return;
3178
+ }
3179
+ const ui = new UidexUI({
3180
+ components,
3181
+ config,
3182
+ buttonPosition,
3183
+ onSelect,
3184
+ inspectShortcut: stableShortcut
3185
+ });
3186
+ ui.mount();
3187
+ uiRef.current = ui;
3188
+ return () => {
3189
+ ui.destroy();
3190
+ uiRef.current = null;
3191
+ };
3192
+ }, [components, config, buttonPosition, disabled, onSelect, stableShortcut]);
3193
+ return null;
3194
+ }
3195
+
3196
+ // src/react/UidexOverlay.tsx
3197
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
3198
+ function UidexOverlay({
3199
+ target,
3200
+ color,
3201
+ borderStyle,
3202
+ borderWidth,
3203
+ label,
3204
+ showLabel,
3205
+ labelPosition,
3206
+ colors
3207
+ }) {
3208
+ const overlayRef = useRef2(null);
3209
+ useEffect2(() => {
3210
+ const overlay = new Overlay();
3211
+ overlayRef.current = overlay;
3212
+ return () => {
3213
+ overlay.destroy();
3214
+ overlayRef.current = null;
3215
+ };
3216
+ }, []);
3217
+ useEffect2(() => {
3218
+ overlayRef.current?.updateOptions({
3219
+ color,
3220
+ borderStyle,
3221
+ borderWidth,
3222
+ showLabel,
3223
+ labelPosition,
3224
+ colors
3225
+ });
3226
+ }, [color, borderStyle, borderWidth, showLabel, labelPosition, colors]);
3227
+ useEffect2(() => {
3228
+ if (!overlayRef.current) return;
3229
+ if (target) {
3230
+ overlayRef.current.show(target, label);
3231
+ } else {
3232
+ overlayRef.current.hide();
3233
+ }
3234
+ }, [target, label]);
3235
+ return null;
3236
+ }
3237
+ export {
3238
+ UidexDevtools,
3239
+ UidexOverlay,
3240
+ classNames,
3241
+ getComponents,
3242
+ getContrastColor,
3243
+ getFeatures,
3244
+ getPages,
3245
+ hexToRgba,
3246
+ registerComponents,
3247
+ registerFeatures,
3248
+ registerPages,
3249
+ resolveColor
3250
+ };
3251
+ //# sourceMappingURL=index.js.map