norma-scope 0.1.1 → 0.1.2
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 +1 -1
- package/dist/report.js +221 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# Norma
|
|
2
2
|
|
|
3
3
|
Norma compares your implementation screenshots against the original Figma designs and generates a visual diff report — automatically, every time you commit. No servers, no LLM, no blocking: just a report you can open and share.
|
|
4
4
|
|
package/dist/report.js
CHANGED
|
@@ -37,68 +37,243 @@ export async function generateReport(results, threshold, outputPath) {
|
|
|
37
37
|
const buildB64 = await toBase64(r.screenshotPath);
|
|
38
38
|
const figmaB64 = r.figmaPng.toString("base64");
|
|
39
39
|
const diffB64 = await toBase64(r.diffPath);
|
|
40
|
+
const buildSrc = `data:image/png;base64,${buildB64}`;
|
|
41
|
+
const figmaSrc = `data:image/png;base64,${figmaB64}`;
|
|
42
|
+
const diffSrc = `data:image/png;base64,${diffB64}`;
|
|
40
43
|
return `
|
|
41
|
-
<
|
|
44
|
+
<section class="component ${isFlagged ? "flagged" : "clean"}">
|
|
42
45
|
<div class="component-header">
|
|
43
|
-
<
|
|
44
|
-
|
|
46
|
+
<div class="component-title">
|
|
47
|
+
<span class="status-dot"></span>
|
|
48
|
+
<h2>${escapeHtml(r.label)}</h2>
|
|
49
|
+
</div>
|
|
50
|
+
<span class="component-status">${r.mismatchPercent.toFixed(1)}% diff</span>
|
|
45
51
|
</div>
|
|
46
52
|
<div class="component-images">
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
</
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
</
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
<
|
|
58
|
-
</
|
|
53
|
+
<figure class="image-col">
|
|
54
|
+
<img src="${buildSrc}" loading="lazy" onclick="openLightbox('${buildSrc}')" alt="${escapeHtml(r.label)} — your build" />
|
|
55
|
+
<figcaption>Your build</figcaption>
|
|
56
|
+
</figure>
|
|
57
|
+
<figure class="image-col">
|
|
58
|
+
<img src="${figmaSrc}" loading="lazy" onclick="openLightbox('${figmaSrc}')" alt="${escapeHtml(r.label)} — Figma design" />
|
|
59
|
+
<figcaption>Figma design</figcaption>
|
|
60
|
+
</figure>
|
|
61
|
+
<figure class="image-col">
|
|
62
|
+
<img src="${diffSrc}" loading="lazy" onclick="openLightbox('${diffSrc}')" alt="${escapeHtml(r.label)} — diff overlay" />
|
|
63
|
+
<figcaption>Diff overlay</figcaption>
|
|
64
|
+
</figure>
|
|
59
65
|
</div>
|
|
60
|
-
</
|
|
66
|
+
</section>`;
|
|
61
67
|
}));
|
|
62
68
|
const html = `<!DOCTYPE html>
|
|
63
69
|
<html lang="en">
|
|
64
70
|
<head>
|
|
65
71
|
<meta charset="UTF-8" />
|
|
72
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
66
73
|
<title>Norma Report</title>
|
|
67
74
|
<style>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
:root {
|
|
76
|
+
--clay: #A8736E;
|
|
77
|
+
--clay-dark: #7E5854;
|
|
78
|
+
--clay-tint: #F5EBE9;
|
|
79
|
+
--ink: #1C1B1A;
|
|
80
|
+
--ink-soft: #6B6664;
|
|
81
|
+
--line: #E7E1DF;
|
|
82
|
+
--bg: #FAF7F6;
|
|
83
|
+
--card: #FFFFFF;
|
|
84
|
+
--danger: #C2452F;
|
|
85
|
+
--danger-tint: #FBEAE6;
|
|
86
|
+
--success: #3E7D52;
|
|
87
|
+
--success-tint: #E8F3EB;
|
|
88
|
+
}
|
|
89
|
+
* { box-sizing: border-box; }
|
|
90
|
+
body {
|
|
91
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, sans-serif;
|
|
92
|
+
background: var(--bg);
|
|
93
|
+
color: var(--ink);
|
|
94
|
+
margin: 0;
|
|
95
|
+
padding: 0 0 64px;
|
|
96
|
+
-webkit-font-smoothing: antialiased;
|
|
97
|
+
}
|
|
98
|
+
.topbar {
|
|
99
|
+
background: var(--ink);
|
|
100
|
+
color: #fff;
|
|
101
|
+
padding: 28px 40px;
|
|
102
|
+
}
|
|
103
|
+
.topbar .wordmark {
|
|
104
|
+
font-size: 22px;
|
|
105
|
+
font-weight: 700;
|
|
106
|
+
color: var(--clay);
|
|
107
|
+
letter-spacing: -0.01em;
|
|
108
|
+
margin: 0 0 10px;
|
|
109
|
+
}
|
|
110
|
+
.meta-row {
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-wrap: wrap;
|
|
113
|
+
gap: 10px;
|
|
114
|
+
}
|
|
115
|
+
.meta-pill {
|
|
116
|
+
background: rgba(255,255,255,0.08);
|
|
117
|
+
border: 1px solid rgba(255,255,255,0.12);
|
|
118
|
+
color: rgba(255,255,255,0.85);
|
|
119
|
+
font-size: 12px;
|
|
120
|
+
padding: 5px 12px;
|
|
121
|
+
border-radius: 999px;
|
|
122
|
+
font-variant-numeric: tabular-nums;
|
|
123
|
+
}
|
|
124
|
+
main { max-width: 980px; margin: 0 auto; padding: 36px 40px 0; }
|
|
125
|
+
.summary {
|
|
126
|
+
display: grid;
|
|
127
|
+
grid-template-columns: repeat(3, 1fr);
|
|
128
|
+
gap: 16px;
|
|
129
|
+
margin: 0 0 36px;
|
|
130
|
+
}
|
|
131
|
+
.stat-card {
|
|
132
|
+
background: var(--card);
|
|
133
|
+
border: 1px solid var(--line);
|
|
134
|
+
border-radius: 14px;
|
|
135
|
+
padding: 20px 22px;
|
|
136
|
+
box-shadow: 0 1px 2px rgba(28,27,26,0.04), 0 8px 24px rgba(28,27,26,0.06);
|
|
137
|
+
}
|
|
138
|
+
.stat-card .stat-number { font-size: 30px; font-weight: 700; line-height: 1; }
|
|
139
|
+
.stat-card .stat-label { font-size: 13px; color: var(--ink-soft); margin-top: 6px; }
|
|
140
|
+
.stat-card.attention .stat-number { color: var(--danger); }
|
|
141
|
+
.stat-card.clean .stat-number { color: var(--success); }
|
|
142
|
+
.component {
|
|
143
|
+
background: var(--card);
|
|
144
|
+
border: 1px solid var(--line);
|
|
145
|
+
border-radius: 14px;
|
|
146
|
+
margin-bottom: 18px;
|
|
147
|
+
overflow: hidden;
|
|
148
|
+
box-shadow: 0 1px 2px rgba(28,27,26,0.04), 0 8px 24px rgba(28,27,26,0.05);
|
|
149
|
+
}
|
|
150
|
+
.component-header {
|
|
151
|
+
display: flex;
|
|
152
|
+
justify-content: space-between;
|
|
153
|
+
align-items: center;
|
|
154
|
+
padding: 16px 20px;
|
|
155
|
+
border-bottom: 1px solid var(--line);
|
|
156
|
+
}
|
|
157
|
+
.component-title { display: flex; align-items: center; gap: 10px; }
|
|
158
|
+
.component-title h2 { font-size: 15px; font-weight: 600; margin: 0; }
|
|
159
|
+
.status-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
|
|
160
|
+
.component.flagged .status-dot { background: var(--danger); }
|
|
161
|
+
.component.clean .status-dot { background: var(--success); }
|
|
162
|
+
.component-status {
|
|
163
|
+
font-size: 12px;
|
|
164
|
+
font-weight: 600;
|
|
165
|
+
padding: 5px 12px;
|
|
166
|
+
border-radius: 999px;
|
|
167
|
+
font-variant-numeric: tabular-nums;
|
|
168
|
+
}
|
|
169
|
+
.component.flagged .component-status { color: var(--danger); background: var(--danger-tint); }
|
|
170
|
+
.component.clean .component-status { color: var(--success); background: var(--success-tint); }
|
|
171
|
+
.component-images {
|
|
172
|
+
display: grid;
|
|
173
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
174
|
+
gap: 1px;
|
|
175
|
+
background: var(--line);
|
|
176
|
+
}
|
|
177
|
+
.image-col {
|
|
178
|
+
background: var(--card);
|
|
179
|
+
padding: 16px;
|
|
180
|
+
margin: 0;
|
|
181
|
+
text-align: center;
|
|
182
|
+
}
|
|
183
|
+
.image-col img {
|
|
184
|
+
width: 100%;
|
|
185
|
+
height: 280px;
|
|
186
|
+
object-fit: contain;
|
|
187
|
+
object-position: top center;
|
|
188
|
+
background: var(--bg);
|
|
189
|
+
border-radius: 8px;
|
|
190
|
+
border: 1px solid var(--line);
|
|
191
|
+
cursor: zoom-in;
|
|
192
|
+
transition: opacity 0.15s ease;
|
|
193
|
+
display: block;
|
|
194
|
+
}
|
|
195
|
+
.image-col img:hover { opacity: 0.85; }
|
|
196
|
+
.image-col figcaption {
|
|
197
|
+
margin-top: 10px;
|
|
198
|
+
font-size: 11px;
|
|
199
|
+
color: var(--ink-soft);
|
|
200
|
+
text-transform: uppercase;
|
|
201
|
+
letter-spacing: 0.06em;
|
|
202
|
+
font-weight: 600;
|
|
203
|
+
}
|
|
204
|
+
footer {
|
|
205
|
+
margin-top: 40px;
|
|
206
|
+
text-align: center;
|
|
207
|
+
color: var(--ink-soft);
|
|
208
|
+
font-size: 12px;
|
|
209
|
+
line-height: 1.7;
|
|
210
|
+
}
|
|
211
|
+
#lightbox {
|
|
212
|
+
display: none;
|
|
213
|
+
position: fixed;
|
|
214
|
+
inset: 0;
|
|
215
|
+
background: rgba(20,18,17,0.88);
|
|
216
|
+
align-items: center;
|
|
217
|
+
justify-content: center;
|
|
218
|
+
padding: 40px;
|
|
219
|
+
z-index: 10;
|
|
220
|
+
cursor: zoom-out;
|
|
221
|
+
}
|
|
222
|
+
#lightbox.open { display: flex; }
|
|
223
|
+
#lightbox img {
|
|
224
|
+
max-width: 100%;
|
|
225
|
+
max-height: 100%;
|
|
226
|
+
border-radius: 8px;
|
|
227
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
|
228
|
+
}
|
|
229
|
+
@media (max-width: 720px) {
|
|
230
|
+
.summary { grid-template-columns: 1fr; }
|
|
231
|
+
.component-images { grid-template-columns: 1fr; }
|
|
232
|
+
}
|
|
83
233
|
</style>
|
|
84
234
|
</head>
|
|
85
235
|
<body>
|
|
86
|
-
<
|
|
87
|
-
<
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
</
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
<div
|
|
236
|
+
<div class="topbar">
|
|
237
|
+
<p class="wordmark">norma</p>
|
|
238
|
+
<div class="meta-row">
|
|
239
|
+
<span class="meta-pill">Branch: ${escapeHtml(branch)}</span>
|
|
240
|
+
<span class="meta-pill">Commit: ${escapeHtml(commit)}</span>
|
|
241
|
+
<span class="meta-pill">${escapeHtml(generated)}</span>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
<main>
|
|
245
|
+
<div class="summary">
|
|
246
|
+
<div class="stat-card">
|
|
247
|
+
<div class="stat-number">${results.length}</div>
|
|
248
|
+
<div class="stat-label">Components checked</div>
|
|
249
|
+
</div>
|
|
250
|
+
<div class="stat-card attention">
|
|
251
|
+
<div class="stat-number">${needsAttention.length}</div>
|
|
252
|
+
<div class="stat-label">Need attention</div>
|
|
253
|
+
</div>
|
|
254
|
+
<div class="stat-card clean">
|
|
255
|
+
<div class="stat-number">${clean.length}</div>
|
|
256
|
+
<div class="stat-label">Clean</div>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
${rows.join("\n")}
|
|
260
|
+
<footer>
|
|
261
|
+
<p>Generated by Norma</p>
|
|
262
|
+
<p>To fix: update your implementation, re-screenshot, commit again.</p>
|
|
263
|
+
</footer>
|
|
264
|
+
</main>
|
|
265
|
+
<div id="lightbox" onclick="closeLightbox()">
|
|
266
|
+
<img id="lightbox-img" src="" alt="Full size preview" />
|
|
96
267
|
</div>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
268
|
+
<script>
|
|
269
|
+
function openLightbox(src) {
|
|
270
|
+
document.getElementById('lightbox-img').src = src;
|
|
271
|
+
document.getElementById('lightbox').classList.add('open');
|
|
272
|
+
}
|
|
273
|
+
function closeLightbox() {
|
|
274
|
+
document.getElementById('lightbox').classList.remove('open');
|
|
275
|
+
}
|
|
276
|
+
</script>
|
|
102
277
|
</body>
|
|
103
278
|
</html>`;
|
|
104
279
|
await writeFile(outputPath, html);
|