@ytspar/devbar 1.4.0 → 1.4.1

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 (55) hide show
  1. package/dist/constants.d.ts +8 -0
  2. package/dist/constants.d.ts.map +1 -1
  3. package/dist/constants.js +10 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/modules/rendering/buttons.d.ts +19 -0
  6. package/dist/modules/rendering/buttons.d.ts.map +1 -0
  7. package/dist/modules/rendering/buttons.js +369 -0
  8. package/dist/modules/rendering/buttons.js.map +1 -0
  9. package/dist/modules/rendering/collapsed.d.ts +6 -0
  10. package/dist/modules/rendering/collapsed.d.ts.map +1 -0
  11. package/dist/modules/rendering/collapsed.js +124 -0
  12. package/dist/modules/rendering/collapsed.js.map +1 -0
  13. package/dist/modules/rendering/common.d.ts +21 -0
  14. package/dist/modules/rendering/common.d.ts.map +1 -0
  15. package/dist/modules/rendering/common.js +60 -0
  16. package/dist/modules/rendering/common.js.map +1 -0
  17. package/dist/modules/rendering/compact.d.ts +6 -0
  18. package/dist/modules/rendering/compact.d.ts.map +1 -0
  19. package/dist/modules/rendering/compact.js +107 -0
  20. package/dist/modules/rendering/compact.js.map +1 -0
  21. package/dist/modules/rendering/console.d.ts +7 -0
  22. package/dist/modules/rendering/console.d.ts.map +1 -0
  23. package/dist/modules/rendering/console.js +78 -0
  24. package/dist/modules/rendering/console.js.map +1 -0
  25. package/dist/modules/rendering/expanded.d.ts +13 -0
  26. package/dist/modules/rendering/expanded.d.ts.map +1 -0
  27. package/dist/modules/rendering/expanded.js +439 -0
  28. package/dist/modules/rendering/expanded.js.map +1 -0
  29. package/dist/modules/rendering/index.d.ts +22 -0
  30. package/dist/modules/rendering/index.d.ts.map +1 -0
  31. package/dist/modules/rendering/index.js +109 -0
  32. package/dist/modules/rendering/index.js.map +1 -0
  33. package/dist/modules/rendering/modals.d.ts +9 -0
  34. package/dist/modules/rendering/modals.d.ts.map +1 -0
  35. package/dist/modules/rendering/modals.js +1068 -0
  36. package/dist/modules/rendering/modals.js.map +1 -0
  37. package/dist/modules/rendering/settings.d.ts +6 -0
  38. package/dist/modules/rendering/settings.d.ts.map +1 -0
  39. package/dist/modules/rendering/settings.js +605 -0
  40. package/dist/modules/rendering/settings.js.map +1 -0
  41. package/dist/modules/rendering.d.ts +15 -16
  42. package/dist/modules/rendering.d.ts.map +1 -1
  43. package/dist/modules/rendering.js +16 -2902
  44. package/dist/modules/rendering.js.map +1 -1
  45. package/dist/modules/tooltips.d.ts +6 -4
  46. package/dist/modules/tooltips.d.ts.map +1 -1
  47. package/dist/modules/tooltips.js +121 -145
  48. package/dist/modules/tooltips.js.map +1 -1
  49. package/dist/ui/buttons.js +9 -9
  50. package/dist/ui/buttons.js.map +1 -1
  51. package/dist/ui/cards.js +3 -3
  52. package/dist/ui/cards.js.map +1 -1
  53. package/dist/ui/modals.js +7 -7
  54. package/dist/ui/modals.js.map +1 -1
  55. package/package.json +2 -2
@@ -0,0 +1,1068 @@
1
+ /**
2
+ * Modal rendering for the DevBar: outline, schema, a11y, and design review modals.
3
+ */
4
+ import { BUTTON_COLORS, CATEGORY_COLORS, CSS_COLORS, FONT_MONO, withAlpha } from '../../constants.js';
5
+ import { extractDocumentOutline, outlineToMarkdown } from '../../outline.js';
6
+ import { checkMissingTags, extractFavicons, extractPageSchema, isImageKey, schemaToMarkdown } from '../../schema.js';
7
+ import { createEmptyMessage, createInfoBox, createModalBox, createModalContent, createModalHeader, createModalOverlay, createStyledButton, } from '../../ui/index.js';
8
+ import { a11yToMarkdown, runA11yAudit, groupViolationsByImpact, getImpactColor, getViolationCounts, } from '../../accessibility.js';
9
+ import { calculateCostEstimate, closeDesignReviewConfirm, handleSaveA11yAudit, handleSaveOutline, handleSaveSchema, proceedWithDesignReview, } from '../screenshot.js';
10
+ import { clearChildren } from './common.js';
11
+ // ============================================================================
12
+ // Outline Modal
13
+ // ============================================================================
14
+ export function renderOutlineModal(state) {
15
+ const outline = extractDocumentOutline();
16
+ const color = BUTTON_COLORS.outline;
17
+ const closeModal = () => {
18
+ state.showOutlineModal = false;
19
+ state.render();
20
+ };
21
+ const overlay = createModalOverlay(closeModal);
22
+ const modal = createModalBox(color);
23
+ const header = createModalHeader({
24
+ color,
25
+ title: 'Document Outline',
26
+ onClose: closeModal,
27
+ onCopyMd: async () => {
28
+ const markdown = outlineToMarkdown(outline);
29
+ await navigator.clipboard.writeText(markdown);
30
+ },
31
+ onSave: () => handleSaveOutline(state),
32
+ sweetlinkConnected: state.sweetlinkConnected,
33
+ saveLocation: state.options.saveLocation,
34
+ isSaving: state.savingOutline,
35
+ savedPath: state.lastOutline,
36
+ });
37
+ modal.appendChild(header);
38
+ const content = createModalContent();
39
+ if (outline.length === 0) {
40
+ content.appendChild(createEmptyMessage('No semantic elements found in this document'));
41
+ }
42
+ else {
43
+ renderOutlineNodes(outline, content, 0, { lastHeadingLevel: 0 });
44
+ }
45
+ modal.appendChild(content);
46
+ overlay.appendChild(modal);
47
+ state.overlayElement = overlay;
48
+ document.body.appendChild(overlay);
49
+ }
50
+ function renderOutlineNodes(nodes, parentEl, depth, headingTracker) {
51
+ for (const node of nodes) {
52
+ const isHeading = node.category === 'heading' && node.level > 0;
53
+ const skippedLevel = isHeading && node.level > headingTracker.lastHeadingLevel + 1;
54
+ if (isHeading) {
55
+ headingTracker.lastHeadingLevel = node.level;
56
+ }
57
+ const nodeEl = document.createElement('div');
58
+ Object.assign(nodeEl.style, {
59
+ padding: `4px 0 4px ${depth * 16}px`,
60
+ });
61
+ // Warning icon for heading hierarchy breaks
62
+ if (skippedLevel) {
63
+ const warn = document.createElement('span');
64
+ Object.assign(warn.style, {
65
+ color: CSS_COLORS.error,
66
+ fontSize: '0.625rem',
67
+ marginRight: '4px',
68
+ });
69
+ warn.textContent = '\u26A0';
70
+ warn.title = `Heading level skipped (expected h${node.level - 1} or higher before h${node.level})`;
71
+ nodeEl.appendChild(warn);
72
+ }
73
+ const tagSpan = document.createElement('span');
74
+ const categoryColor = CATEGORY_COLORS[node.category || 'other'] || CATEGORY_COLORS.other;
75
+ Object.assign(tagSpan.style, {
76
+ color: skippedLevel ? CSS_COLORS.error : categoryColor,
77
+ fontSize: '0.6875rem',
78
+ fontWeight: '500',
79
+ });
80
+ tagSpan.textContent = `<${node.tagName}>`;
81
+ nodeEl.appendChild(tagSpan);
82
+ if (node.category) {
83
+ const categorySpan = document.createElement('span');
84
+ Object.assign(categorySpan.style, {
85
+ color: CSS_COLORS.textMuted,
86
+ fontSize: '0.625rem',
87
+ marginLeft: '6px',
88
+ });
89
+ categorySpan.textContent = `[${node.category}]`;
90
+ nodeEl.appendChild(categorySpan);
91
+ }
92
+ const textSpan = document.createElement('span');
93
+ Object.assign(textSpan.style, {
94
+ color: '#d1d5db',
95
+ fontSize: '0.6875rem',
96
+ marginLeft: '8px',
97
+ });
98
+ const truncatedText = node.text.length > 60 ? `${node.text.slice(0, 60)}...` : node.text;
99
+ textSpan.textContent = truncatedText;
100
+ nodeEl.appendChild(textSpan);
101
+ if (node.id) {
102
+ const idSpan = document.createElement('span');
103
+ Object.assign(idSpan.style, {
104
+ color: CSS_COLORS.textSecondary,
105
+ fontSize: '0.625rem',
106
+ marginLeft: '6px',
107
+ });
108
+ idSpan.textContent = `#${node.id}`;
109
+ nodeEl.appendChild(idSpan);
110
+ }
111
+ parentEl.appendChild(nodeEl);
112
+ if (node.children.length > 0) {
113
+ renderOutlineNodes(node.children, parentEl, depth + 1, headingTracker);
114
+ }
115
+ }
116
+ }
117
+ // ============================================================================
118
+ // Schema Modal
119
+ // ============================================================================
120
+ export function renderSchemaModal(state) {
121
+ const schema = extractPageSchema();
122
+ const color = BUTTON_COLORS.schema;
123
+ const closeModal = () => {
124
+ state.showSchemaModal = false;
125
+ state.render();
126
+ };
127
+ const overlay = createModalOverlay(closeModal);
128
+ const modal = createModalBox(color);
129
+ const missingTags = checkMissingTags(schema);
130
+ const favicons = extractFavicons();
131
+ const header = createModalHeader({
132
+ color,
133
+ title: 'Page Schema',
134
+ onClose: closeModal,
135
+ onCopyMd: async () => {
136
+ const markdown = schemaToMarkdown(schema, { missingTags, favicons });
137
+ await navigator.clipboard.writeText(markdown);
138
+ },
139
+ onSave: () => handleSaveSchema(state),
140
+ sweetlinkConnected: state.sweetlinkConnected,
141
+ saveLocation: state.options.saveLocation,
142
+ isSaving: state.savingSchema,
143
+ savedPath: state.lastSchema,
144
+ });
145
+ modal.appendChild(header);
146
+ const content = createModalContent();
147
+ const hasContent = schema.jsonLd.length > 0 ||
148
+ Object.keys(schema.openGraph).length > 0 ||
149
+ Object.keys(schema.twitter).length > 0 ||
150
+ Object.keys(schema.metaTags).length > 0 ||
151
+ favicons.length > 0 ||
152
+ missingTags.length > 0;
153
+ if (!hasContent) {
154
+ content.appendChild(createEmptyMessage('No structured data found on this page'));
155
+ }
156
+ else {
157
+ if (missingTags.length > 0)
158
+ renderMissingTagsSection(content, missingTags);
159
+ renderSchemaSection(content, 'Open Graph', schema.openGraph, CSS_COLORS.info);
160
+ renderSchemaSection(content, 'Twitter Cards', schema.twitter, CSS_COLORS.cyan);
161
+ if (favicons.length > 0)
162
+ renderFaviconsSection(content, favicons);
163
+ renderSchemaSection(content, 'JSON-LD', schema.jsonLd, color);
164
+ renderSchemaSection(content, 'Meta Tags', schema.metaTags, CSS_COLORS.textMuted);
165
+ }
166
+ modal.appendChild(content);
167
+ overlay.appendChild(modal);
168
+ state.overlayElement = overlay;
169
+ document.body.appendChild(overlay);
170
+ }
171
+ function renderSchemaSectionHeader(section, title, color, count) {
172
+ const header = document.createElement('div');
173
+ Object.assign(header.style, {
174
+ display: 'flex',
175
+ alignItems: 'center',
176
+ gap: '8px',
177
+ marginBottom: '10px',
178
+ paddingBottom: '6px',
179
+ borderBottom: `1px solid ${withAlpha(color, 19)}`,
180
+ });
181
+ const titleEl = document.createElement('h3');
182
+ Object.assign(titleEl.style, {
183
+ color,
184
+ fontSize: '0.8125rem',
185
+ fontWeight: '600',
186
+ margin: '0',
187
+ });
188
+ titleEl.textContent = title;
189
+ header.appendChild(titleEl);
190
+ const badge = document.createElement('span');
191
+ Object.assign(badge.style, {
192
+ color: withAlpha(color, 80),
193
+ fontSize: '0.5625rem',
194
+ backgroundColor: withAlpha(color, 9),
195
+ padding: '1px 6px',
196
+ borderRadius: '8px',
197
+ letterSpacing: '0.03em',
198
+ });
199
+ badge.textContent = String(count);
200
+ header.appendChild(badge);
201
+ section.appendChild(header);
202
+ }
203
+ function renderSchemaSection(container, title, items, color) {
204
+ const count = Array.isArray(items) ? items.length : Object.keys(items).length;
205
+ if (count === 0)
206
+ return;
207
+ const section = document.createElement('div');
208
+ section.style.marginBottom = '20px';
209
+ renderSchemaSectionHeader(section, title, color, count);
210
+ if (Array.isArray(items)) {
211
+ renderJsonLdItems(section, items, color);
212
+ }
213
+ else {
214
+ renderKeyValueItems(section, items);
215
+ }
216
+ container.appendChild(section);
217
+ }
218
+ function renderJsonLdItems(container, items, color) {
219
+ items.forEach((item, i) => {
220
+ const itemEl = document.createElement('div');
221
+ itemEl.style.marginBottom = '10px';
222
+ // Extract @type for a meaningful label
223
+ const typed = item;
224
+ const schemaType = typeof typed?.['@type'] === 'string' ? typed['@type'] : null;
225
+ const itemHeader = document.createElement('div');
226
+ Object.assign(itemHeader.style, {
227
+ display: 'flex',
228
+ alignItems: 'center',
229
+ gap: '6px',
230
+ marginBottom: '4px',
231
+ });
232
+ const itemTitle = document.createElement('span');
233
+ Object.assign(itemTitle.style, {
234
+ color: CSS_COLORS.textSecondary,
235
+ fontSize: '0.6875rem',
236
+ });
237
+ itemTitle.textContent = `Schema ${i + 1}`;
238
+ itemHeader.appendChild(itemTitle);
239
+ if (schemaType) {
240
+ const typeTag = document.createElement('span');
241
+ Object.assign(typeTag.style, {
242
+ color: withAlpha(color, 80),
243
+ fontSize: '0.5625rem',
244
+ backgroundColor: withAlpha(color, 8),
245
+ border: `1px solid ${withAlpha(color, 15)}`,
246
+ padding: '0 5px',
247
+ borderRadius: '3px',
248
+ });
249
+ typeTag.textContent = schemaType;
250
+ itemHeader.appendChild(typeTag);
251
+ }
252
+ itemEl.appendChild(itemHeader);
253
+ const codeEl = document.createElement('pre');
254
+ Object.assign(codeEl.style, {
255
+ backgroundColor: 'rgba(0, 0, 0, 0.25)',
256
+ borderRadius: '4px',
257
+ borderLeft: `2px solid ${withAlpha(color, 31)}`,
258
+ padding: '10px 10px 10px 12px',
259
+ fontSize: '0.625rem',
260
+ margin: '0',
261
+ whiteSpace: 'pre-wrap',
262
+ wordBreak: 'break-word',
263
+ });
264
+ appendHighlightedJson(codeEl, JSON.stringify(item, null, 2));
265
+ itemEl.appendChild(codeEl);
266
+ container.appendChild(itemEl);
267
+ });
268
+ }
269
+ function appendHighlightedJson(container, json) {
270
+ // Color map for different token types
271
+ const colors = {
272
+ key: CSS_COLORS.primary, // green
273
+ string: CSS_COLORS.warning, // amber/yellow
274
+ number: CSS_COLORS.purple, // purple
275
+ boolean: CSS_COLORS.info, // blue
276
+ nullVal: CSS_COLORS.error, // red
277
+ punct: CSS_COLORS.textMuted, // gray
278
+ };
279
+ // Simple tokenizer for JSON using matchAll for safety
280
+ const tokenPattern = /("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|(\btrue\b|\bfalse\b)|(\bnull\b)|(-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)|([{}[\],])|(\s+)/g;
281
+ for (const match of json.matchAll(tokenPattern)) {
282
+ const [, str, colon, bool, nullToken, num, punct, whitespace] = match;
283
+ if (whitespace) {
284
+ container.appendChild(document.createTextNode(whitespace));
285
+ }
286
+ else if (str !== undefined) {
287
+ const span = document.createElement('span');
288
+ span.style.color = colon ? colors.key : colors.string;
289
+ span.textContent = str;
290
+ container.appendChild(span);
291
+ if (colon) {
292
+ const colonSpan = document.createElement('span');
293
+ colonSpan.style.color = colors.punct;
294
+ colonSpan.textContent = ':';
295
+ container.appendChild(colonSpan);
296
+ }
297
+ }
298
+ else if (bool) {
299
+ const span = document.createElement('span');
300
+ span.style.color = colors.boolean;
301
+ span.textContent = bool;
302
+ container.appendChild(span);
303
+ }
304
+ else if (nullToken) {
305
+ const span = document.createElement('span');
306
+ span.style.color = colors.nullVal;
307
+ span.textContent = nullToken;
308
+ container.appendChild(span);
309
+ }
310
+ else if (num) {
311
+ const span = document.createElement('span');
312
+ span.style.color = colors.number;
313
+ span.textContent = num;
314
+ container.appendChild(span);
315
+ }
316
+ else if (punct) {
317
+ const span = document.createElement('span');
318
+ span.style.color = colors.punct;
319
+ span.textContent = punct;
320
+ container.appendChild(span);
321
+ }
322
+ }
323
+ }
324
+ function renderKeyValueItems(container, items) {
325
+ const entries = Object.entries(items);
326
+ entries.forEach(([key, value], i) => {
327
+ const isImage = isImageKey(key);
328
+ const row = document.createElement('div');
329
+ Object.assign(row.style, {
330
+ display: 'flex',
331
+ padding: isImage ? '6px 8px' : '3px 8px',
332
+ alignItems: 'flex-start',
333
+ borderRadius: '3px',
334
+ backgroundColor: i % 2 === 0 ? 'rgba(255, 255, 255, 0.02)' : 'transparent',
335
+ });
336
+ const keyEl = document.createElement('span');
337
+ Object.assign(keyEl.style, {
338
+ color: CSS_COLORS.textSecondary,
339
+ fontSize: '0.6875rem',
340
+ width: '120px',
341
+ minWidth: '120px',
342
+ maxWidth: '120px',
343
+ flexShrink: '0',
344
+ overflow: 'hidden',
345
+ textOverflow: 'ellipsis',
346
+ whiteSpace: 'nowrap',
347
+ paddingTop: isImage ? '2px' : '0',
348
+ });
349
+ keyEl.textContent = key;
350
+ if (key.length > 18)
351
+ keyEl.title = key;
352
+ row.appendChild(keyEl);
353
+ if (isImage && value) {
354
+ const valueCol = document.createElement('div');
355
+ Object.assign(valueCol.style, { flex: '1', minWidth: '0' });
356
+ // Image frame with subtle border -- fixed height to prevent layout jitter
357
+ const frame = document.createElement('div');
358
+ Object.assign(frame.style, {
359
+ display: 'inline-block',
360
+ padding: '4px',
361
+ backgroundColor: 'rgba(0, 0, 0, 0.2)',
362
+ border: '1px solid rgba(255, 255, 255, 0.06)',
363
+ borderRadius: '4px',
364
+ marginBottom: '4px',
365
+ minHeight: '60px',
366
+ minWidth: '80px',
367
+ });
368
+ const thumb = document.createElement('img');
369
+ Object.assign(thumb.style, {
370
+ width: '200px',
371
+ height: '120px',
372
+ objectFit: 'contain',
373
+ borderRadius: '2px',
374
+ display: 'block',
375
+ });
376
+ thumb.src = value;
377
+ thumb.alt = key;
378
+ thumb.onerror = () => { frame.style.display = 'none'; };
379
+ thumb.onload = () => {
380
+ if (thumb.naturalWidth) {
381
+ dimEl.textContent = `${thumb.naturalWidth}\u00d7${thumb.naturalHeight}`;
382
+ }
383
+ };
384
+ frame.appendChild(thumb);
385
+ valueCol.appendChild(frame);
386
+ // Reserve space for dimension text to avoid reflow
387
+ const dimEl = document.createElement('div');
388
+ Object.assign(dimEl.style, {
389
+ color: CSS_COLORS.textMuted,
390
+ fontSize: '0.5625rem',
391
+ minHeight: '0.75rem',
392
+ letterSpacing: '0.02em',
393
+ });
394
+ valueCol.appendChild(dimEl);
395
+ const urlEl = document.createElement('div');
396
+ Object.assign(urlEl.style, {
397
+ color: CSS_COLORS.textMuted,
398
+ fontSize: '0.5625rem',
399
+ wordBreak: 'break-all',
400
+ opacity: '0.7',
401
+ });
402
+ urlEl.textContent = value;
403
+ valueCol.appendChild(urlEl);
404
+ row.appendChild(valueCol);
405
+ }
406
+ else {
407
+ const valueEl = document.createElement('span');
408
+ Object.assign(valueEl.style, {
409
+ color: CSS_COLORS.text,
410
+ fontSize: '0.6875rem',
411
+ flex: '1',
412
+ wordBreak: 'break-word',
413
+ whiteSpace: 'pre-wrap',
414
+ opacity: '0.85',
415
+ });
416
+ valueEl.textContent = String(value);
417
+ row.appendChild(valueEl);
418
+ }
419
+ container.appendChild(row);
420
+ });
421
+ }
422
+ /** Derive intended device/purpose from favicon label and declared size */
423
+ function faviconDevice(label, size) {
424
+ const s = parseInt(size || '', 10);
425
+ if (label.includes('apple'))
426
+ return { text: 'Apple home screen', color: CSS_COLORS.info };
427
+ if (size === 'any' || label.includes('svg'))
428
+ return { text: 'Scalable (any)', color: CSS_COLORS.cyan };
429
+ if (s >= 192)
430
+ return { text: 'Android / PWA', color: CSS_COLORS.primary };
431
+ if (s >= 48)
432
+ return { text: 'Taskbar / shortcut', color: CSS_COLORS.purple };
433
+ if (s > 0)
434
+ return { text: 'Browser tab', color: CSS_COLORS.textSecondary };
435
+ return { text: 'General', color: CSS_COLORS.textMuted };
436
+ }
437
+ function renderFaviconsSection(container, icons) {
438
+ const color = CSS_COLORS.purple;
439
+ const section = document.createElement('div');
440
+ section.style.marginBottom = '20px';
441
+ renderSchemaSectionHeader(section, 'Favicons', color, icons.length);
442
+ icons.forEach((icon, i) => {
443
+ const device = faviconDevice(icon.label, icon.size);
444
+ const row = document.createElement('div');
445
+ Object.assign(row.style, {
446
+ display: 'flex',
447
+ alignItems: 'center',
448
+ padding: '6px 8px',
449
+ gap: '10px',
450
+ borderRadius: '3px',
451
+ backgroundColor: i % 2 === 0 ? 'rgba(255, 255, 255, 0.02)' : 'transparent',
452
+ });
453
+ // Thumbnail frame
454
+ const frame = document.createElement('div');
455
+ Object.assign(frame.style, {
456
+ width: '32px',
457
+ height: '32px',
458
+ display: 'flex',
459
+ alignItems: 'center',
460
+ justifyContent: 'center',
461
+ backgroundColor: 'rgba(0, 0, 0, 0.25)',
462
+ border: '1px solid rgba(255, 255, 255, 0.06)',
463
+ borderRadius: '4px',
464
+ flexShrink: '0',
465
+ });
466
+ const thumb = document.createElement('img');
467
+ Object.assign(thumb.style, {
468
+ width: '22px',
469
+ height: '22px',
470
+ objectFit: 'contain',
471
+ });
472
+ thumb.src = icon.url;
473
+ thumb.alt = icon.label;
474
+ thumb.onerror = () => { frame.style.opacity = '0.3'; };
475
+ frame.appendChild(thumb);
476
+ row.appendChild(frame);
477
+ // Info column: label, device, dimensions + URL
478
+ const infoCol = document.createElement('div');
479
+ Object.assign(infoCol.style, {
480
+ flex: '1',
481
+ minWidth: '0',
482
+ display: 'flex',
483
+ flexDirection: 'column',
484
+ gap: '2px',
485
+ });
486
+ // Top row: label + device pill
487
+ const topRow = document.createElement('div');
488
+ Object.assign(topRow.style, {
489
+ display: 'flex',
490
+ alignItems: 'center',
491
+ gap: '6px',
492
+ });
493
+ const labelEl = document.createElement('span');
494
+ Object.assign(labelEl.style, {
495
+ color: CSS_COLORS.text,
496
+ fontSize: '0.6875rem',
497
+ fontWeight: '500',
498
+ overflow: 'hidden',
499
+ textOverflow: 'ellipsis',
500
+ whiteSpace: 'nowrap',
501
+ });
502
+ labelEl.textContent = icon.label;
503
+ if (icon.label.length > 24)
504
+ labelEl.title = icon.label;
505
+ topRow.appendChild(labelEl);
506
+ const devicePill = document.createElement('span');
507
+ Object.assign(devicePill.style, {
508
+ color: device.color,
509
+ fontSize: '0.5rem',
510
+ backgroundColor: withAlpha(device.color, 7),
511
+ padding: '1px 6px',
512
+ borderRadius: '6px',
513
+ letterSpacing: '0.03em',
514
+ whiteSpace: 'nowrap',
515
+ flexShrink: '0',
516
+ });
517
+ devicePill.textContent = device.text;
518
+ topRow.appendChild(devicePill);
519
+ infoCol.appendChild(topRow);
520
+ // Bottom row: declared size + actual dimensions + URL
521
+ const bottomRow = document.createElement('div');
522
+ Object.assign(bottomRow.style, {
523
+ display: 'flex',
524
+ alignItems: 'center',
525
+ gap: '6px',
526
+ fontSize: '0.5625rem',
527
+ color: CSS_COLORS.textMuted,
528
+ });
529
+ if (icon.size) {
530
+ const declaredEl = document.createElement('span');
531
+ declaredEl.textContent = icon.size;
532
+ declaredEl.style.opacity = '0.8';
533
+ bottomRow.appendChild(declaredEl);
534
+ }
535
+ // Actual dimensions (populated on load)
536
+ const dimEl = document.createElement('span');
537
+ dimEl.style.letterSpacing = '0.02em';
538
+ bottomRow.appendChild(dimEl);
539
+ thumb.onload = () => {
540
+ if (thumb.naturalWidth) {
541
+ const actual = `${thumb.naturalWidth}\u00d7${thumb.naturalHeight}`;
542
+ if (icon.size) {
543
+ dimEl.textContent = `\u2192 ${actual}`;
544
+ }
545
+ else {
546
+ dimEl.textContent = actual;
547
+ }
548
+ }
549
+ };
550
+ const sep = document.createElement('span');
551
+ sep.textContent = '\u00b7';
552
+ sep.style.opacity = '0.4';
553
+ bottomRow.appendChild(sep);
554
+ const urlEl = document.createElement('span');
555
+ Object.assign(urlEl.style, {
556
+ overflow: 'hidden',
557
+ textOverflow: 'ellipsis',
558
+ whiteSpace: 'nowrap',
559
+ opacity: '0.6',
560
+ });
561
+ urlEl.textContent = icon.url;
562
+ urlEl.title = icon.url;
563
+ bottomRow.appendChild(urlEl);
564
+ infoCol.appendChild(bottomRow);
565
+ row.appendChild(infoCol);
566
+ section.appendChild(row);
567
+ });
568
+ container.appendChild(section);
569
+ }
570
+ function renderMissingTagsSection(container, tags) {
571
+ const section = document.createElement('div');
572
+ section.style.marginBottom = '20px';
573
+ const errorCount = tags.filter((t) => t.severity === 'error').length;
574
+ const warnCount = tags.length - errorCount;
575
+ const hasErrors = errorCount > 0;
576
+ const sectionColor = hasErrors ? CSS_COLORS.error : CSS_COLORS.warning;
577
+ renderSchemaSectionHeader(section, 'Missing Tags', sectionColor, tags.length);
578
+ // Summary pill row
579
+ if (errorCount > 0 || warnCount > 0) {
580
+ const summary = document.createElement('div');
581
+ Object.assign(summary.style, {
582
+ display: 'flex',
583
+ gap: '8px',
584
+ marginBottom: '8px',
585
+ });
586
+ if (errorCount > 0) {
587
+ const errPill = document.createElement('span');
588
+ Object.assign(errPill.style, {
589
+ color: CSS_COLORS.error,
590
+ fontSize: '0.5625rem',
591
+ backgroundColor: withAlpha(CSS_COLORS.error, 8),
592
+ padding: '2px 8px',
593
+ borderRadius: '8px',
594
+ letterSpacing: '0.03em',
595
+ });
596
+ errPill.textContent = `${errorCount} error${errorCount > 1 ? 's' : ''}`;
597
+ summary.appendChild(errPill);
598
+ }
599
+ if (warnCount > 0) {
600
+ const warnPill = document.createElement('span');
601
+ Object.assign(warnPill.style, {
602
+ color: CSS_COLORS.warning,
603
+ fontSize: '0.5625rem',
604
+ backgroundColor: withAlpha(CSS_COLORS.warning, 8),
605
+ padding: '2px 8px',
606
+ borderRadius: '8px',
607
+ letterSpacing: '0.03em',
608
+ });
609
+ warnPill.textContent = `${warnCount} warning${warnCount > 1 ? 's' : ''}`;
610
+ summary.appendChild(warnPill);
611
+ }
612
+ section.appendChild(summary);
613
+ }
614
+ tags.forEach((tag, i) => {
615
+ const isError = tag.severity === 'error';
616
+ const tagColor = isError ? CSS_COLORS.error : CSS_COLORS.warning;
617
+ const row = document.createElement('div');
618
+ Object.assign(row.style, {
619
+ display: 'flex',
620
+ alignItems: 'center',
621
+ padding: '4px 8px',
622
+ gap: '8px',
623
+ borderRadius: '3px',
624
+ backgroundColor: i % 2 === 0 ? 'rgba(255, 255, 255, 0.02)' : 'transparent',
625
+ borderLeft: `2px solid ${withAlpha(tagColor, 25)}`,
626
+ });
627
+ const icon = document.createElement('span');
628
+ Object.assign(icon.style, {
629
+ fontSize: '0.625rem',
630
+ flexShrink: '0',
631
+ width: '14px',
632
+ textAlign: 'center',
633
+ color: tagColor,
634
+ });
635
+ icon.textContent = isError ? '\u2718' : '\u26a0';
636
+ row.appendChild(icon);
637
+ const tagName = document.createElement('span');
638
+ Object.assign(tagName.style, {
639
+ color: CSS_COLORS.text,
640
+ fontSize: '0.6875rem',
641
+ width: '120px',
642
+ minWidth: '120px',
643
+ flexShrink: '0',
644
+ fontWeight: '500',
645
+ });
646
+ tagName.textContent = tag.tag;
647
+ row.appendChild(tagName);
648
+ const hint = document.createElement('span');
649
+ Object.assign(hint.style, {
650
+ color: CSS_COLORS.textMuted,
651
+ fontSize: '0.6875rem',
652
+ flex: '1',
653
+ opacity: '0.85',
654
+ });
655
+ hint.textContent = tag.hint;
656
+ row.appendChild(hint);
657
+ section.appendChild(row);
658
+ });
659
+ container.appendChild(section);
660
+ }
661
+ // ============================================================================
662
+ // Accessibility Audit Modal
663
+ // ============================================================================
664
+ export function renderA11yModal(state) {
665
+ const color = BUTTON_COLORS.a11y;
666
+ const closeModal = () => {
667
+ state.showA11yModal = false;
668
+ state.render();
669
+ };
670
+ const overlay = createModalOverlay(closeModal);
671
+ const modal = createModalBox(color);
672
+ // Show loading state initially
673
+ const loadingContent = createModalContent();
674
+ const loadingMsg = document.createElement('div');
675
+ Object.assign(loadingMsg.style, {
676
+ textAlign: 'center',
677
+ padding: '40px',
678
+ color: CSS_COLORS.textSecondary,
679
+ fontSize: '0.875rem',
680
+ });
681
+ loadingMsg.textContent = 'Running accessibility audit...';
682
+ loadingMsg.style.animation = 'pulse 1.5s ease-in-out infinite';
683
+ loadingContent.appendChild(loadingMsg);
684
+ // Temporary header without save/copy (shown during loading)
685
+ const loadingHeader = createModalHeader({
686
+ color,
687
+ title: 'Accessibility Audit',
688
+ onClose: closeModal,
689
+ onCopyMd: async () => { },
690
+ sweetlinkConnected: state.sweetlinkConnected,
691
+ saveLocation: state.options.saveLocation,
692
+ });
693
+ modal.appendChild(loadingHeader);
694
+ modal.appendChild(loadingContent);
695
+ overlay.appendChild(modal);
696
+ state.overlayElement = overlay;
697
+ document.body.appendChild(overlay);
698
+ // Run the audit async and replace content when done
699
+ runA11yAudit().then((result) => {
700
+ // Check modal is still open
701
+ if (!state.showA11yModal)
702
+ return;
703
+ const markdown = a11yToMarkdown(result);
704
+ // Replace modal content
705
+ clearChildren(modal);
706
+ const violationCount = result.violations.length;
707
+ const titleText = violationCount === 0
708
+ ? 'Accessibility Audit \u2014 No Issues'
709
+ : `Accessibility Audit \u2014 ${violationCount} Violation${violationCount === 1 ? '' : 's'}`;
710
+ const header = createModalHeader({
711
+ color,
712
+ title: titleText,
713
+ onClose: closeModal,
714
+ onCopyMd: async () => {
715
+ await navigator.clipboard.writeText(markdown);
716
+ },
717
+ onSave: () => handleSaveA11yAudit(state, result),
718
+ sweetlinkConnected: state.sweetlinkConnected,
719
+ saveLocation: state.options.saveLocation,
720
+ isSaving: state.savingA11yAudit,
721
+ savedPath: state.lastA11yAudit,
722
+ });
723
+ modal.appendChild(header);
724
+ const content = createModalContent();
725
+ if (result.violations.length === 0) {
726
+ const successMsg = document.createElement('div');
727
+ Object.assign(successMsg.style, {
728
+ textAlign: 'center',
729
+ padding: '40px',
730
+ color: CSS_COLORS.primary,
731
+ fontSize: '0.875rem',
732
+ });
733
+ successMsg.textContent = 'No accessibility violations found!';
734
+ content.appendChild(successMsg);
735
+ // Show pass count
736
+ if (result.passes.length > 0) {
737
+ const passInfo = document.createElement('div');
738
+ Object.assign(passInfo.style, {
739
+ textAlign: 'center',
740
+ color: CSS_COLORS.textMuted,
741
+ fontSize: '0.75rem',
742
+ marginTop: '8px',
743
+ });
744
+ passInfo.textContent = `${result.passes.length} rules passed`;
745
+ content.appendChild(passInfo);
746
+ }
747
+ }
748
+ else {
749
+ // Summary bar
750
+ const counts = getViolationCounts(result.violations);
751
+ const summaryBar = document.createElement('div');
752
+ Object.assign(summaryBar.style, {
753
+ display: 'flex',
754
+ gap: '12px',
755
+ marginBottom: '16px',
756
+ padding: '10px 12px',
757
+ backgroundColor: withAlpha(color, 6),
758
+ border: `1px solid ${withAlpha(color, 19)}`,
759
+ borderRadius: '6px',
760
+ flexWrap: 'wrap',
761
+ });
762
+ for (const impact of ['critical', 'serious', 'moderate', 'minor']) {
763
+ if (counts[impact] === 0)
764
+ continue;
765
+ const badge = document.createElement('span');
766
+ const impactColor = getImpactColor(impact);
767
+ Object.assign(badge.style, {
768
+ display: 'inline-flex',
769
+ alignItems: 'center',
770
+ gap: '4px',
771
+ fontSize: '0.6875rem',
772
+ fontWeight: '600',
773
+ color: impactColor,
774
+ });
775
+ const dot = document.createElement('span');
776
+ Object.assign(dot.style, {
777
+ width: '6px',
778
+ height: '6px',
779
+ borderRadius: '50%',
780
+ backgroundColor: impactColor,
781
+ });
782
+ badge.appendChild(dot);
783
+ badge.appendChild(document.createTextNode(`${counts[impact]} ${impact}`));
784
+ summaryBar.appendChild(badge);
785
+ }
786
+ content.appendChild(summaryBar);
787
+ // Grouped violations
788
+ const grouped = groupViolationsByImpact(result.violations);
789
+ for (const [impact, violations] of grouped) {
790
+ if (violations.length === 0)
791
+ continue;
792
+ renderA11yViolationGroup(content, impact, violations);
793
+ }
794
+ }
795
+ modal.appendChild(content);
796
+ }).catch((err) => {
797
+ if (!state.showA11yModal)
798
+ return;
799
+ clearChildren(modal);
800
+ const header = createModalHeader({
801
+ color: CSS_COLORS.error,
802
+ title: 'Accessibility Audit \u2014 Error',
803
+ onClose: closeModal,
804
+ onCopyMd: async () => { },
805
+ sweetlinkConnected: state.sweetlinkConnected,
806
+ saveLocation: state.options.saveLocation,
807
+ });
808
+ modal.appendChild(header);
809
+ const content = createModalContent();
810
+ content.appendChild(createInfoBox(CSS_COLORS.error, 'Audit Failed', `${err instanceof Error ? err.message : 'Unknown error'}`));
811
+ modal.appendChild(content);
812
+ });
813
+ }
814
+ function renderA11yViolationGroup(container, impact, violations) {
815
+ const impactColor = getImpactColor(impact);
816
+ const section = document.createElement('div');
817
+ section.style.marginBottom = '20px';
818
+ // Section header
819
+ const sectionTitle = document.createElement('h3');
820
+ Object.assign(sectionTitle.style, {
821
+ color: impactColor,
822
+ fontSize: '0.8125rem',
823
+ fontWeight: '600',
824
+ marginBottom: '10px',
825
+ borderBottom: `1px solid ${withAlpha(impactColor, 25)}`,
826
+ paddingBottom: '6px',
827
+ textTransform: 'capitalize',
828
+ });
829
+ sectionTitle.textContent = `${impact} (${violations.length})`;
830
+ section.appendChild(sectionTitle);
831
+ for (const violation of violations) {
832
+ const violationEl = document.createElement('div');
833
+ Object.assign(violationEl.style, {
834
+ marginBottom: '12px',
835
+ padding: '10px 12px',
836
+ backgroundColor: withAlpha(impactColor, 3),
837
+ border: `1px solid ${withAlpha(impactColor, 13)}`,
838
+ borderRadius: '6px',
839
+ });
840
+ // Rule ID
841
+ const ruleId = document.createElement('div');
842
+ Object.assign(ruleId.style, {
843
+ color: impactColor,
844
+ fontSize: '0.6875rem',
845
+ fontWeight: '600',
846
+ marginBottom: '4px',
847
+ });
848
+ ruleId.textContent = violation.id;
849
+ violationEl.appendChild(ruleId);
850
+ // Help text
851
+ const helpText = document.createElement('div');
852
+ Object.assign(helpText.style, {
853
+ color: CSS_COLORS.text,
854
+ fontSize: '0.75rem',
855
+ marginBottom: '4px',
856
+ });
857
+ helpText.textContent = violation.help;
858
+ violationEl.appendChild(helpText);
859
+ // Description
860
+ const desc = document.createElement('div');
861
+ Object.assign(desc.style, {
862
+ color: CSS_COLORS.textSecondary,
863
+ fontSize: '0.6875rem',
864
+ marginBottom: '6px',
865
+ });
866
+ desc.textContent = violation.description;
867
+ violationEl.appendChild(desc);
868
+ // Node count
869
+ const nodeCount = document.createElement('div');
870
+ Object.assign(nodeCount.style, {
871
+ color: CSS_COLORS.textMuted,
872
+ fontSize: '0.625rem',
873
+ marginBottom: '4px',
874
+ });
875
+ nodeCount.textContent = `${violation.nodes.length} element${violation.nodes.length === 1 ? '' : 's'} affected`;
876
+ violationEl.appendChild(nodeCount);
877
+ // Affected nodes (collapsed by default, show first 3)
878
+ const nodesPreview = document.createElement('div');
879
+ Object.assign(nodesPreview.style, {
880
+ marginTop: '6px',
881
+ });
882
+ const visibleNodes = violation.nodes.slice(0, 3);
883
+ for (const node of visibleNodes) {
884
+ const nodeEl = document.createElement('div');
885
+ Object.assign(nodeEl.style, {
886
+ padding: '3px 6px',
887
+ marginBottom: '2px',
888
+ backgroundColor: 'rgba(0,0,0,0.2)',
889
+ borderRadius: '3px',
890
+ fontSize: '0.625rem',
891
+ color: CSS_COLORS.textSecondary,
892
+ fontFamily: 'monospace',
893
+ whiteSpace: 'nowrap',
894
+ overflow: 'hidden',
895
+ textOverflow: 'ellipsis',
896
+ });
897
+ nodeEl.textContent = node.html.length > 100 ? `${node.html.slice(0, 100)}...` : node.html;
898
+ nodeEl.title = node.html;
899
+ nodesPreview.appendChild(nodeEl);
900
+ }
901
+ if (violation.nodes.length > 3) {
902
+ const moreBtn = document.createElement('button');
903
+ Object.assign(moreBtn.style, {
904
+ background: 'none',
905
+ border: 'none',
906
+ color: impactColor,
907
+ fontSize: '0.625rem',
908
+ cursor: 'pointer',
909
+ padding: '2px 0',
910
+ fontFamily: FONT_MONO,
911
+ });
912
+ moreBtn.textContent = `+ ${violation.nodes.length - 3} more`;
913
+ moreBtn.onclick = () => {
914
+ // Show remaining nodes
915
+ moreBtn.remove();
916
+ for (const node of violation.nodes.slice(3)) {
917
+ const nodeEl = document.createElement('div');
918
+ Object.assign(nodeEl.style, {
919
+ padding: '3px 6px',
920
+ marginBottom: '2px',
921
+ backgroundColor: 'rgba(0,0,0,0.2)',
922
+ borderRadius: '3px',
923
+ fontSize: '0.625rem',
924
+ color: CSS_COLORS.textSecondary,
925
+ fontFamily: 'monospace',
926
+ whiteSpace: 'nowrap',
927
+ overflow: 'hidden',
928
+ textOverflow: 'ellipsis',
929
+ });
930
+ nodeEl.textContent = node.html.length > 100 ? `${node.html.slice(0, 100)}...` : node.html;
931
+ nodeEl.title = node.html;
932
+ nodesPreview.appendChild(nodeEl);
933
+ }
934
+ };
935
+ nodesPreview.appendChild(moreBtn);
936
+ }
937
+ violationEl.appendChild(nodesPreview);
938
+ section.appendChild(violationEl);
939
+ }
940
+ container.appendChild(section);
941
+ }
942
+ // ============================================================================
943
+ // Design Review Confirmation Modal
944
+ // ============================================================================
945
+ export function renderDesignReviewConfirmModal(state) {
946
+ const color = BUTTON_COLORS.review;
947
+ const closeModal = () => closeDesignReviewConfirm(state);
948
+ const overlay = createModalOverlay(closeModal);
949
+ const modal = createModalBox(color);
950
+ modal.style.maxWidth = '450px';
951
+ // Minimal header (title + close only, no Copy MD / Save)
952
+ modal.appendChild(createModalHeader({ color, title: 'AI Design Review', onClose: closeModal }));
953
+ // Content
954
+ const content = createModalContent();
955
+ Object.assign(content.style, {
956
+ color: CSS_COLORS.text,
957
+ fontSize: '0.8125rem',
958
+ lineHeight: '1.6',
959
+ });
960
+ if (state.apiKeyStatus === null) {
961
+ content.appendChild(createEmptyMessage('Checking API key configuration...'));
962
+ }
963
+ else if (!state.apiKeyStatus.configured) {
964
+ content.appendChild(renderApiKeyNotConfiguredContent());
965
+ }
966
+ else {
967
+ content.appendChild(renderApiKeyConfiguredContent(state));
968
+ }
969
+ modal.appendChild(content);
970
+ // Footer with action button
971
+ if (state.apiKeyStatus?.configured) {
972
+ const footer = document.createElement('div');
973
+ Object.assign(footer.style, {
974
+ display: 'flex',
975
+ justifyContent: 'flex-end',
976
+ gap: '10px',
977
+ padding: '14px 18px',
978
+ borderTop: `1px solid ${CSS_COLORS.border}`,
979
+ });
980
+ const proceedBtn = createStyledButton({ color, text: 'Run Review', padding: '8px 16px' });
981
+ proceedBtn.style.backgroundColor = withAlpha(color, 13);
982
+ proceedBtn.onclick = () => proceedWithDesignReview(state);
983
+ footer.appendChild(proceedBtn);
984
+ modal.appendChild(footer);
985
+ }
986
+ overlay.appendChild(modal);
987
+ state.overlayElement = overlay;
988
+ document.body.appendChild(overlay);
989
+ }
990
+ function renderApiKeyNotConfiguredContent() {
991
+ const wrapper = document.createElement('div');
992
+ wrapper.appendChild(createInfoBox(CSS_COLORS.error, 'API Key Not Configured', 'The ANTHROPIC_API_KEY environment variable is not set.'));
993
+ // Instructions
994
+ const instructions = document.createElement('div');
995
+ Object.assign(instructions.style, { marginBottom: '12px' });
996
+ const instructTitle = document.createElement('div');
997
+ Object.assign(instructTitle.style, {
998
+ color: CSS_COLORS.textSecondary,
999
+ fontWeight: '600',
1000
+ marginBottom: '8px',
1001
+ });
1002
+ instructTitle.textContent = 'To configure:';
1003
+ instructions.appendChild(instructTitle);
1004
+ const steps = [
1005
+ { text: '1. Get an API key from console.anthropic.com', highlight: false },
1006
+ { text: '2. Add to your .env file:', highlight: false },
1007
+ { text: ' ANTHROPIC_API_KEY=sk-ant-...', highlight: true },
1008
+ { text: '3. Restart your dev server', highlight: false },
1009
+ ];
1010
+ steps.forEach(({ text, highlight }) => {
1011
+ const stepDiv = document.createElement('div');
1012
+ Object.assign(stepDiv.style, {
1013
+ color: highlight ? CSS_COLORS.primary : CSS_COLORS.textMuted,
1014
+ fontSize: '0.75rem',
1015
+ marginBottom: '4px',
1016
+ fontFamily: FONT_MONO,
1017
+ });
1018
+ stepDiv.textContent = text;
1019
+ instructions.appendChild(stepDiv);
1020
+ });
1021
+ wrapper.appendChild(instructions);
1022
+ return wrapper;
1023
+ }
1024
+ function renderApiKeyConfiguredContent(state) {
1025
+ const wrapper = document.createElement('div');
1026
+ Object.assign(wrapper.style, { marginBottom: '16px' });
1027
+ const desc = document.createElement('p');
1028
+ Object.assign(desc.style, { color: CSS_COLORS.textSecondary, marginBottom: '12px' });
1029
+ desc.textContent = 'This will capture a screenshot and send it to Claude for design analysis.';
1030
+ wrapper.appendChild(desc);
1031
+ // Cost estimate
1032
+ const estimate = calculateCostEstimate(state);
1033
+ if (estimate) {
1034
+ const costBox = createInfoBox(CSS_COLORS.primary, 'Estimated Cost', []);
1035
+ // Remove default margin and adjust padding
1036
+ costBox.style.marginBottom = '0';
1037
+ costBox.style.padding = '12px';
1038
+ const costDetails = document.createElement('div');
1039
+ Object.assign(costDetails.style, {
1040
+ display: 'flex',
1041
+ justifyContent: 'space-between',
1042
+ color: CSS_COLORS.textSecondary,
1043
+ fontSize: '0.75rem',
1044
+ });
1045
+ const tokensSpan = document.createElement('span');
1046
+ tokensSpan.textContent = `~${estimate.tokens.toLocaleString()} tokens`;
1047
+ costDetails.appendChild(tokensSpan);
1048
+ const priceSpan = document.createElement('span');
1049
+ Object.assign(priceSpan.style, { color: CSS_COLORS.warning, fontWeight: '600' });
1050
+ priceSpan.textContent = estimate.cost;
1051
+ costDetails.appendChild(priceSpan);
1052
+ costBox.appendChild(costDetails);
1053
+ wrapper.appendChild(costBox);
1054
+ }
1055
+ // Model info
1056
+ if (state.apiKeyStatus?.model) {
1057
+ const modelDiv = document.createElement('div');
1058
+ Object.assign(modelDiv.style, {
1059
+ color: CSS_COLORS.textMuted,
1060
+ fontSize: '0.6875rem',
1061
+ marginTop: '12px',
1062
+ });
1063
+ modelDiv.textContent = `Model: ${state.apiKeyStatus.model}`;
1064
+ wrapper.appendChild(modelDiv);
1065
+ }
1066
+ return wrapper;
1067
+ }
1068
+ //# sourceMappingURL=modals.js.map