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