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