@ytspar/devbar 1.3.1 → 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.
- package/README.md +43 -0
- package/dist/accessibility.d.ts +4 -0
- package/dist/accessibility.d.ts.map +1 -1
- package/dist/accessibility.js +57 -0
- package/dist/accessibility.js.map +1 -1
- package/dist/constants.d.ts +8 -23
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +10 -3
- package/dist/constants.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/index.d.ts +1 -1
- package/dist/modules/index.d.ts.map +1 -1
- package/dist/modules/index.js +1 -0
- package/dist/modules/index.js.map +1 -1
- package/dist/modules/keyboard.d.ts +1 -1
- package/dist/modules/keyboard.d.ts.map +1 -1
- package/dist/modules/keyboard.js +4 -11
- package/dist/modules/keyboard.js.map +1 -1
- package/dist/modules/rendering/buttons.d.ts +19 -0
- package/dist/modules/rendering/buttons.d.ts.map +1 -0
- package/dist/modules/rendering/buttons.js +369 -0
- package/dist/modules/rendering/buttons.js.map +1 -0
- package/dist/modules/rendering/collapsed.d.ts +6 -0
- package/dist/modules/rendering/collapsed.d.ts.map +1 -0
- package/dist/modules/rendering/collapsed.js +124 -0
- package/dist/modules/rendering/collapsed.js.map +1 -0
- package/dist/modules/rendering/common.d.ts +21 -0
- package/dist/modules/rendering/common.d.ts.map +1 -0
- package/dist/modules/rendering/common.js +60 -0
- package/dist/modules/rendering/common.js.map +1 -0
- package/dist/modules/rendering/compact.d.ts +6 -0
- package/dist/modules/rendering/compact.d.ts.map +1 -0
- package/dist/modules/rendering/compact.js +107 -0
- package/dist/modules/rendering/compact.js.map +1 -0
- package/dist/modules/rendering/console.d.ts +7 -0
- package/dist/modules/rendering/console.d.ts.map +1 -0
- package/dist/modules/rendering/console.js +78 -0
- package/dist/modules/rendering/console.js.map +1 -0
- package/dist/modules/rendering/expanded.d.ts +13 -0
- package/dist/modules/rendering/expanded.d.ts.map +1 -0
- package/dist/modules/rendering/expanded.js +439 -0
- package/dist/modules/rendering/expanded.js.map +1 -0
- package/dist/modules/rendering/index.d.ts +22 -0
- package/dist/modules/rendering/index.d.ts.map +1 -0
- package/dist/modules/rendering/index.js +109 -0
- package/dist/modules/rendering/index.js.map +1 -0
- package/dist/modules/rendering/modals.d.ts +9 -0
- package/dist/modules/rendering/modals.d.ts.map +1 -0
- package/dist/modules/rendering/modals.js +1068 -0
- package/dist/modules/rendering/modals.js.map +1 -0
- package/dist/modules/rendering/settings.d.ts +6 -0
- package/dist/modules/rendering/settings.d.ts.map +1 -0
- package/dist/modules/rendering/settings.js +605 -0
- package/dist/modules/rendering/settings.js.map +1 -0
- package/dist/modules/rendering.d.ts +15 -16
- package/dist/modules/rendering.d.ts.map +1 -1
- package/dist/modules/rendering.js +15 -2919
- package/dist/modules/rendering.js.map +1 -1
- package/dist/modules/screenshot.d.ts +11 -2
- package/dist/modules/screenshot.d.ts.map +1 -1
- package/dist/modules/screenshot.js +32 -29
- package/dist/modules/screenshot.js.map +1 -1
- package/dist/modules/tooltips.d.ts +7 -5
- package/dist/modules/tooltips.d.ts.map +1 -1
- package/dist/modules/tooltips.js +133 -157
- package/dist/modules/tooltips.js.map +1 -1
- package/dist/modules/types.d.ts +7 -0
- package/dist/modules/types.d.ts.map +1 -1
- package/dist/modules/types.js +14 -1
- package/dist/modules/types.js.map +1 -1
- package/dist/modules/websocket.d.ts.map +1 -1
- package/dist/modules/websocket.js +334 -264
- package/dist/modules/websocket.js.map +1 -1
- package/dist/ui/buttons.d.ts.map +1 -1
- package/dist/ui/buttons.js +11 -9
- package/dist/ui/buttons.js.map +1 -1
- package/dist/ui/cards.js +3 -3
- package/dist/ui/cards.js.map +1 -1
- package/dist/ui/icons.d.ts +13 -0
- package/dist/ui/icons.d.ts.map +1 -1
- package/dist/ui/icons.js +24 -3
- package/dist/ui/icons.js.map +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/modals.d.ts +3 -2
- package/dist/ui/modals.d.ts.map +1 -1
- package/dist/ui/modals.js +28 -26
- package/dist/ui/modals.js.map +1 -1
- package/package.json +3 -4
|
@@ -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
|