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