uidex 0.0.1 → 0.1.0

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