@plannotator/pi-extension 0.19.12 → 0.19.14
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/package.json +1 -1
- package/plannotator.html +2 -2
- package/review-editor.html +2 -2
- package/skills/plannotator-visual-explainer/SKILL.md +73 -86
- package/skills/plannotator-visual-explainer/references/design-system.md +560 -0
- package/skills/plannotator-visual-explainer/references/pr-components.md +717 -0
- package/skills/plannotator-visual-explainer/references/svg-patterns.md +368 -0
- package/skills/plannotator-visual-explainer/references/extended-patterns.md +0 -629
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
# PR Component Patterns
|
|
2
|
+
|
|
3
|
+
Component patterns specific to PR explainer documents. For the base design system (colors, typography, layout), see `../../plannotator-visual-plan/references/design-system.md`.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
1. [PR Header](#pr-header)
|
|
7
|
+
2. [TL;DR Box](#tldr-box)
|
|
8
|
+
3. [Diff Rendering](#diff-rendering)
|
|
9
|
+
4. [Review Comments](#review-comments)
|
|
10
|
+
5. [Risk Map](#risk-map)
|
|
11
|
+
6. [File Cards](#file-cards)
|
|
12
|
+
7. [Before / After](#before--after)
|
|
13
|
+
8. [Where to Focus](#where-to-focus)
|
|
14
|
+
9. [Test Plan](#test-plan)
|
|
15
|
+
10. [File Badges](#file-badges)
|
|
16
|
+
|
|
17
|
+
## PR header
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<header>
|
|
21
|
+
<span class="eyebrow">Pull request · repo-name</span>
|
|
22
|
+
<h1>Add real-time notification system</h1>
|
|
23
|
+
<div class="pr-meta">
|
|
24
|
+
<span>6 files</span>
|
|
25
|
+
<span class="additions">+142</span>
|
|
26
|
+
<span class="deletions">-38</span>
|
|
27
|
+
<span>feature/notifications → main</span>
|
|
28
|
+
</div>
|
|
29
|
+
</header>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```css
|
|
33
|
+
.pr-meta {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-wrap: wrap;
|
|
36
|
+
gap: 16px;
|
|
37
|
+
font-family: var(--font-mono);
|
|
38
|
+
font-size: 0.78rem;
|
|
39
|
+
color: var(--muted-foreground);
|
|
40
|
+
margin-top: 8px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.pr-meta .additions { color: var(--success); }
|
|
44
|
+
.pr-meta .deletions { color: var(--destructive); }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## TL;DR box
|
|
48
|
+
|
|
49
|
+
```html
|
|
50
|
+
<div class="tldr">
|
|
51
|
+
<h3>TL;DR</h3>
|
|
52
|
+
<p>Adds WebSocket-based notifications with per-user channels. Messages fan out
|
|
53
|
+
from a new NotificationService through Redis pub/sub. Existing REST endpoints
|
|
54
|
+
are unchanged.</p>
|
|
55
|
+
</div>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```css
|
|
59
|
+
.tldr {
|
|
60
|
+
background: var(--card);
|
|
61
|
+
border: 1.5px solid var(--border);
|
|
62
|
+
border-left: 4px solid var(--primary);
|
|
63
|
+
border-radius: var(--radius);
|
|
64
|
+
padding: 20px 24px;
|
|
65
|
+
max-width: 760px;
|
|
66
|
+
margin: 24px 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.tldr h3 {
|
|
70
|
+
font-family: var(--font-mono);
|
|
71
|
+
font-size: 0.72rem;
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
text-transform: uppercase;
|
|
74
|
+
letter-spacing: 0.06em;
|
|
75
|
+
color: var(--primary);
|
|
76
|
+
margin-bottom: 8px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.tldr p {
|
|
80
|
+
font-size: 0.95rem;
|
|
81
|
+
line-height: 1.6;
|
|
82
|
+
color: var(--muted-foreground);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Diff rendering
|
|
87
|
+
|
|
88
|
+
Use Pierre diffs via CDN for syntax-highlighted, theme-aware diff rendering. Renders into shadow DOM (no style conflicts) with Shiki syntax highlighting.
|
|
89
|
+
|
|
90
|
+
```html
|
|
91
|
+
<script type="module">
|
|
92
|
+
import { getSingularPatch, registerDiffsComponent } from 'https://cdn.jsdelivr.net/npm/@pierre/diffs@1.1.21/+esm';
|
|
93
|
+
registerDiffsComponent();
|
|
94
|
+
|
|
95
|
+
const patch = `--- a/src/handler.ts
|
|
96
|
+
+++ b/src/handler.ts
|
|
97
|
+
@@ -1,4 +1,6 @@
|
|
98
|
+
import { Router } from 'express';
|
|
99
|
+
+import { NotificationService } from './notifications';
|
|
100
|
+
-import { legacyPoll } from './polling';`;
|
|
101
|
+
|
|
102
|
+
const container = document.querySelector('diffs-container');
|
|
103
|
+
container.fileDiff = getSingularPatch(patch);
|
|
104
|
+
container.options = {
|
|
105
|
+
themeType: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
|
|
106
|
+
diffStyle: 'unified',
|
|
107
|
+
diffIndicators: 'bars',
|
|
108
|
+
lineDiffType: 'word-alt',
|
|
109
|
+
unsafeCSS: `
|
|
110
|
+
:host {
|
|
111
|
+
--diffs-bg: var(--background);
|
|
112
|
+
--diffs-fg: var(--foreground);
|
|
113
|
+
border-radius: var(--radius);
|
|
114
|
+
border: 1.5px solid var(--border);
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
}
|
|
117
|
+
`,
|
|
118
|
+
};
|
|
119
|
+
</script>
|
|
120
|
+
<diffs-container></diffs-container>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
For multiple diffs, create one `<diffs-container>` per file. Pierre handles syntax highlighting, line numbers, add/del coloring, and word-level diffs automatically.
|
|
124
|
+
|
|
125
|
+
## Review comments
|
|
126
|
+
|
|
127
|
+
Speech bubbles with severity-coded left borders, attached below a diff block.
|
|
128
|
+
|
|
129
|
+
```html
|
|
130
|
+
<div class="comments">
|
|
131
|
+
<div class="bubble blocking">
|
|
132
|
+
<span class="anchor">line 11</span>
|
|
133
|
+
<span class="severity">BLOCKING</span>
|
|
134
|
+
<p>This mutation isn't wrapped in a transaction. If the second write
|
|
135
|
+
fails, the first persists — leaving the user in a broken state.</p>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="bubble nit">
|
|
138
|
+
<span class="anchor">line 24</span>
|
|
139
|
+
<span class="severity">NIT</span>
|
|
140
|
+
<p>Prefer <code>const</code> here since <code>config</code> is never reassigned.</p>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```css
|
|
146
|
+
.comments {
|
|
147
|
+
padding: 18px 20px;
|
|
148
|
+
display: flex;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
gap: 14px;
|
|
151
|
+
background: var(--muted);
|
|
152
|
+
border-top: 1px solid var(--border);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.bubble {
|
|
156
|
+
position: relative;
|
|
157
|
+
background: var(--card);
|
|
158
|
+
border: 1.5px solid var(--border);
|
|
159
|
+
border-left-width: 4px;
|
|
160
|
+
border-radius: 8px;
|
|
161
|
+
padding: 12px 14px 12px 16px;
|
|
162
|
+
max-width: 680px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.bubble.blocking { border-left-color: var(--primary); }
|
|
166
|
+
.bubble.nit { border-left-color: var(--border); }
|
|
167
|
+
.bubble.suggestion { border-left-color: var(--success); }
|
|
168
|
+
|
|
169
|
+
.bubble::before {
|
|
170
|
+
content: "";
|
|
171
|
+
position: absolute;
|
|
172
|
+
left: -9px;
|
|
173
|
+
top: 16px;
|
|
174
|
+
width: 12px;
|
|
175
|
+
height: 12px;
|
|
176
|
+
background: var(--card);
|
|
177
|
+
border-left: 1.5px solid var(--border);
|
|
178
|
+
border-bottom: 1.5px solid var(--border);
|
|
179
|
+
transform: rotate(45deg);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.bubble.blocking::before {
|
|
183
|
+
border-left-color: var(--primary);
|
|
184
|
+
border-bottom-color: var(--primary);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.anchor {
|
|
188
|
+
font-family: var(--font-mono);
|
|
189
|
+
font-size: 0.68rem;
|
|
190
|
+
color: var(--muted-foreground);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.severity {
|
|
194
|
+
font-family: var(--font-mono);
|
|
195
|
+
font-size: 0.65rem;
|
|
196
|
+
font-weight: 700;
|
|
197
|
+
text-transform: uppercase;
|
|
198
|
+
letter-spacing: 0.06em;
|
|
199
|
+
margin-left: 8px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.bubble.blocking .severity { color: var(--primary); }
|
|
203
|
+
.bubble.nit .severity { color: var(--muted-foreground); }
|
|
204
|
+
.bubble.suggestion .severity { color: var(--success); }
|
|
205
|
+
|
|
206
|
+
.bubble p {
|
|
207
|
+
margin-top: 6px;
|
|
208
|
+
font-size: 0.88rem;
|
|
209
|
+
line-height: 1.55;
|
|
210
|
+
color: var(--foreground);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.bubble code {
|
|
214
|
+
font-family: var(--font-mono);
|
|
215
|
+
font-size: 0.82rem;
|
|
216
|
+
background: var(--muted);
|
|
217
|
+
padding: 1px 5px;
|
|
218
|
+
border-radius: 3px;
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Risk map
|
|
223
|
+
|
|
224
|
+
Chips that give a quick overview of file risk levels.
|
|
225
|
+
|
|
226
|
+
```html
|
|
227
|
+
<div class="risk-map">
|
|
228
|
+
<a href="#file-auth" class="chip attention">
|
|
229
|
+
<span class="dot"></span>
|
|
230
|
+
src/auth/middleware.ts
|
|
231
|
+
</a>
|
|
232
|
+
<a href="#file-db" class="chip medium">
|
|
233
|
+
<span class="dot"></span>
|
|
234
|
+
src/db/migrations/004.sql
|
|
235
|
+
</a>
|
|
236
|
+
<a href="#file-types" class="chip safe">
|
|
237
|
+
<span class="dot"></span>
|
|
238
|
+
src/types/index.ts
|
|
239
|
+
</a>
|
|
240
|
+
</div>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
```css
|
|
244
|
+
.risk-map {
|
|
245
|
+
display: flex;
|
|
246
|
+
flex-wrap: wrap;
|
|
247
|
+
gap: 8px;
|
|
248
|
+
margin: 24px 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.chip {
|
|
252
|
+
display: inline-flex;
|
|
253
|
+
align-items: center;
|
|
254
|
+
gap: 8px;
|
|
255
|
+
font-family: var(--font-mono);
|
|
256
|
+
font-size: 0.75rem;
|
|
257
|
+
padding: 6px 12px;
|
|
258
|
+
border: 1.5px solid var(--border);
|
|
259
|
+
border-radius: 20px;
|
|
260
|
+
text-decoration: none;
|
|
261
|
+
color: var(--foreground);
|
|
262
|
+
transition: box-shadow 0.15s;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.chip:hover {
|
|
266
|
+
box-shadow: 0 0 0 2px color-mix(in oklab, var(--primary) 25%, transparent);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.chip .dot {
|
|
270
|
+
width: 9px;
|
|
271
|
+
height: 9px;
|
|
272
|
+
border-radius: 50%;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.chip.attention {
|
|
276
|
+
background: color-mix(in oklab, var(--destructive) 8%, transparent);
|
|
277
|
+
border-color: color-mix(in oklab, var(--destructive) 40%, transparent);
|
|
278
|
+
}
|
|
279
|
+
.chip.attention .dot { background: var(--destructive); }
|
|
280
|
+
|
|
281
|
+
.chip.medium {
|
|
282
|
+
background: color-mix(in oklab, var(--warning) 10%, transparent);
|
|
283
|
+
border-color: color-mix(in oklab, var(--warning) 30%, transparent);
|
|
284
|
+
}
|
|
285
|
+
.chip.medium .dot { background: var(--warning); }
|
|
286
|
+
|
|
287
|
+
.chip.safe {
|
|
288
|
+
background: color-mix(in oklab, var(--success) 8%, transparent);
|
|
289
|
+
border-color: color-mix(in oklab, var(--success) 35%, transparent);
|
|
290
|
+
}
|
|
291
|
+
.chip.safe .dot { background: var(--success); }
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## File cards
|
|
295
|
+
|
|
296
|
+
Expandable cards grouping a file's diff and review commentary.
|
|
297
|
+
|
|
298
|
+
```html
|
|
299
|
+
<div class="file-card" id="file-auth">
|
|
300
|
+
<div class="file-head">
|
|
301
|
+
<div class="file-info">
|
|
302
|
+
<span class="file-path">src/auth/middleware.ts</span>
|
|
303
|
+
<span class="file-badge mod">MOD</span>
|
|
304
|
+
<span class="file-stats"><span class="additions">+28</span> <span class="deletions">-12</span></span>
|
|
305
|
+
</div>
|
|
306
|
+
<span class="risk-tag attention">ATTENTION</span>
|
|
307
|
+
</div>
|
|
308
|
+
<div class="file-why">
|
|
309
|
+
<p>Replaced session-cookie auth with JWT verification. The trust boundary
|
|
310
|
+
moves from the session store to the token signature check.</p>
|
|
311
|
+
</div>
|
|
312
|
+
<div class="diff"><!-- diff rows --></div>
|
|
313
|
+
<div class="comments"><!-- review bubbles --></div>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<!-- Safe files: collapsed -->
|
|
317
|
+
<details class="file-collapsed">
|
|
318
|
+
<summary>
|
|
319
|
+
<span class="file-path">src/types/index.ts</span>
|
|
320
|
+
<span class="file-badge mod">MOD</span>
|
|
321
|
+
<span class="file-stats"><span class="additions">+4</span> <span class="deletions">-0</span></span>
|
|
322
|
+
<span class="risk-tag safe">SAFE</span>
|
|
323
|
+
</summary>
|
|
324
|
+
<div class="file-why">
|
|
325
|
+
<p>Added NotificationPayload type export.</p>
|
|
326
|
+
</div>
|
|
327
|
+
</details>
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
```css
|
|
331
|
+
.file-card {
|
|
332
|
+
border: 1.5px solid var(--border);
|
|
333
|
+
border-radius: var(--radius);
|
|
334
|
+
background: var(--card);
|
|
335
|
+
overflow: hidden;
|
|
336
|
+
scroll-margin-top: 20px;
|
|
337
|
+
margin: 16px 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.file-head {
|
|
341
|
+
padding: 16px 20px;
|
|
342
|
+
border-bottom: 1.5px solid var(--border);
|
|
343
|
+
display: flex;
|
|
344
|
+
align-items: center;
|
|
345
|
+
justify-content: space-between;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.file-info {
|
|
349
|
+
display: flex;
|
|
350
|
+
align-items: center;
|
|
351
|
+
gap: 12px;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.file-path {
|
|
355
|
+
font-family: var(--font-mono);
|
|
356
|
+
font-size: 0.82rem;
|
|
357
|
+
font-weight: 600;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.file-stats {
|
|
361
|
+
font-family: var(--font-mono);
|
|
362
|
+
font-size: 0.72rem;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.file-stats .additions { color: var(--success); }
|
|
366
|
+
.file-stats .deletions { color: var(--destructive); }
|
|
367
|
+
|
|
368
|
+
.file-why {
|
|
369
|
+
padding: 12px 20px;
|
|
370
|
+
border-bottom: 1px solid var(--border);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.file-why p {
|
|
374
|
+
font-size: 0.9rem;
|
|
375
|
+
color: var(--muted-foreground);
|
|
376
|
+
line-height: 1.55;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* Collapsed (safe) files */
|
|
380
|
+
.file-collapsed {
|
|
381
|
+
border: 1.5px solid var(--border);
|
|
382
|
+
border-radius: var(--radius);
|
|
383
|
+
background: var(--card);
|
|
384
|
+
margin: 8px 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.file-collapsed summary {
|
|
388
|
+
list-style: none;
|
|
389
|
+
cursor: pointer;
|
|
390
|
+
padding: 14px 20px;
|
|
391
|
+
display: flex;
|
|
392
|
+
align-items: center;
|
|
393
|
+
gap: 12px;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.file-collapsed summary::after {
|
|
397
|
+
content: '+';
|
|
398
|
+
font-family: var(--font-mono);
|
|
399
|
+
font-size: 0.85rem;
|
|
400
|
+
color: var(--muted-foreground);
|
|
401
|
+
margin-left: auto;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.file-collapsed[open] summary::after {
|
|
405
|
+
content: '\2212';
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Before / after
|
|
410
|
+
|
|
411
|
+
Two-column comparison grid.
|
|
412
|
+
|
|
413
|
+
```html
|
|
414
|
+
<div class="before-after">
|
|
415
|
+
<div class="ba-panel before">
|
|
416
|
+
<h4>Before</h4>
|
|
417
|
+
<p>Auth checked via session cookie on every request.
|
|
418
|
+
Session store hit adds ~15ms latency.</p>
|
|
419
|
+
</div>
|
|
420
|
+
<div class="ba-panel after">
|
|
421
|
+
<h4>After</h4>
|
|
422
|
+
<p>JWT signature verified in-process. No external store hit.
|
|
423
|
+
Latency drops to ~1ms per request.</p>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
```css
|
|
429
|
+
.before-after {
|
|
430
|
+
display: grid;
|
|
431
|
+
grid-template-columns: 1fr 1fr;
|
|
432
|
+
gap: 16px;
|
|
433
|
+
margin: 16px 0;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
@media (max-width: 640px) {
|
|
437
|
+
.before-after { grid-template-columns: 1fr; }
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.ba-panel {
|
|
441
|
+
background: var(--card);
|
|
442
|
+
border: 1.5px solid var(--border);
|
|
443
|
+
border-radius: var(--radius);
|
|
444
|
+
padding: 18px 20px;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.ba-panel.after {
|
|
448
|
+
border-color: var(--success);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.ba-panel h4 {
|
|
452
|
+
font-family: var(--font-mono);
|
|
453
|
+
font-size: 0.72rem;
|
|
454
|
+
font-weight: 600;
|
|
455
|
+
text-transform: uppercase;
|
|
456
|
+
letter-spacing: 0.06em;
|
|
457
|
+
margin-bottom: 8px;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.ba-panel.before h4 { color: var(--muted-foreground); }
|
|
461
|
+
.ba-panel.after h4 { color: var(--success); }
|
|
462
|
+
|
|
463
|
+
.ba-panel p {
|
|
464
|
+
font-size: 0.9rem;
|
|
465
|
+
line-height: 1.55;
|
|
466
|
+
color: var(--foreground);
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## Where to focus
|
|
471
|
+
|
|
472
|
+
Numbered callout cards directing reviewers.
|
|
473
|
+
|
|
474
|
+
```html
|
|
475
|
+
<div class="focus-list">
|
|
476
|
+
<div class="focus-item">
|
|
477
|
+
<span class="focus-number">1</span>
|
|
478
|
+
<div>
|
|
479
|
+
<strong>src/auth/middleware.ts:verifyToken()</strong>
|
|
480
|
+
<p>New trust boundary. Verify the JWT validation covers all edge cases:
|
|
481
|
+
expired tokens, malformed signatures, missing claims.</p>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
```css
|
|
488
|
+
.focus-list {
|
|
489
|
+
display: flex;
|
|
490
|
+
flex-direction: column;
|
|
491
|
+
gap: 12px;
|
|
492
|
+
margin: 16px 0;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.focus-item {
|
|
496
|
+
background: var(--card);
|
|
497
|
+
border: 1.5px solid var(--border);
|
|
498
|
+
border-radius: var(--radius);
|
|
499
|
+
padding: 16px 20px;
|
|
500
|
+
display: flex;
|
|
501
|
+
gap: 16px;
|
|
502
|
+
align-items: flex-start;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.focus-number {
|
|
506
|
+
font-family: var(--font-mono);
|
|
507
|
+
font-size: 0.78rem;
|
|
508
|
+
font-weight: 600;
|
|
509
|
+
color: var(--primary-foreground);
|
|
510
|
+
background: var(--primary);
|
|
511
|
+
width: 26px;
|
|
512
|
+
height: 26px;
|
|
513
|
+
border-radius: 50%;
|
|
514
|
+
display: flex;
|
|
515
|
+
align-items: center;
|
|
516
|
+
justify-content: center;
|
|
517
|
+
flex-shrink: 0;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.focus-item strong {
|
|
521
|
+
font-family: var(--font-mono);
|
|
522
|
+
font-size: 0.82rem;
|
|
523
|
+
display: block;
|
|
524
|
+
margin-bottom: 4px;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.focus-item p {
|
|
528
|
+
font-size: 0.88rem;
|
|
529
|
+
color: var(--muted-foreground);
|
|
530
|
+
line-height: 1.5;
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
## Test plan
|
|
535
|
+
|
|
536
|
+
Checkbox-style verification checklist.
|
|
537
|
+
|
|
538
|
+
```html
|
|
539
|
+
<div class="test-list">
|
|
540
|
+
<div class="test-item done">
|
|
541
|
+
<span class="check"></span>
|
|
542
|
+
<span>Expired JWT returns 401 with proper error body</span>
|
|
543
|
+
</div>
|
|
544
|
+
<div class="test-item">
|
|
545
|
+
<span class="check"></span>
|
|
546
|
+
<span>Concurrent WebSocket connections scale to 1000 without memory leak</span>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
```css
|
|
552
|
+
.test-list {
|
|
553
|
+
display: flex;
|
|
554
|
+
flex-direction: column;
|
|
555
|
+
gap: 8px;
|
|
556
|
+
margin: 16px 0;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.test-item {
|
|
560
|
+
display: flex;
|
|
561
|
+
align-items: flex-start;
|
|
562
|
+
gap: 12px;
|
|
563
|
+
background: var(--card);
|
|
564
|
+
border: 1.5px solid var(--border);
|
|
565
|
+
border-radius: var(--radius);
|
|
566
|
+
padding: 13px 18px;
|
|
567
|
+
font-size: 0.88rem;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.check {
|
|
571
|
+
width: 18px;
|
|
572
|
+
height: 18px;
|
|
573
|
+
border-radius: 5px;
|
|
574
|
+
border: 1.5px solid var(--border);
|
|
575
|
+
flex-shrink: 0;
|
|
576
|
+
position: relative;
|
|
577
|
+
margin-top: 2px;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.test-item.done .check {
|
|
581
|
+
background: var(--success);
|
|
582
|
+
border-color: var(--success);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.test-item.done .check::after {
|
|
586
|
+
content: '';
|
|
587
|
+
position: absolute;
|
|
588
|
+
left: 5px;
|
|
589
|
+
top: 2px;
|
|
590
|
+
width: 5px;
|
|
591
|
+
height: 9px;
|
|
592
|
+
border-right: 2px solid var(--card);
|
|
593
|
+
border-bottom: 2px solid var(--card);
|
|
594
|
+
transform: rotate(40deg);
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
## File badges
|
|
599
|
+
|
|
600
|
+
```css
|
|
601
|
+
.file-badge {
|
|
602
|
+
font-family: var(--font-mono);
|
|
603
|
+
font-size: 0.62rem;
|
|
604
|
+
font-weight: 700;
|
|
605
|
+
text-transform: uppercase;
|
|
606
|
+
letter-spacing: 0.04em;
|
|
607
|
+
padding: 2px 6px;
|
|
608
|
+
border-radius: calc(var(--radius) - 4px);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.file-badge.new {
|
|
612
|
+
background: color-mix(in oklab, var(--success) 15%, transparent);
|
|
613
|
+
color: var(--success);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.file-badge.mod {
|
|
617
|
+
background: color-mix(in oklab, var(--warning) 15%, transparent);
|
|
618
|
+
color: var(--warning);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.file-badge.del {
|
|
622
|
+
background: color-mix(in oklab, var(--destructive) 15%, transparent);
|
|
623
|
+
color: var(--destructive);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.risk-tag {
|
|
627
|
+
font-family: var(--font-mono);
|
|
628
|
+
font-size: 0.65rem;
|
|
629
|
+
font-weight: 600;
|
|
630
|
+
text-transform: uppercase;
|
|
631
|
+
letter-spacing: 0.06em;
|
|
632
|
+
padding: 3px 8px;
|
|
633
|
+
border-radius: calc(var(--radius) - 4px);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.risk-tag.attention {
|
|
637
|
+
background: color-mix(in oklab, var(--destructive) 12%, transparent);
|
|
638
|
+
color: var(--destructive);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.risk-tag.medium {
|
|
642
|
+
background: color-mix(in oklab, var(--warning) 12%, transparent);
|
|
643
|
+
color: var(--warning);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.risk-tag.safe {
|
|
647
|
+
background: color-mix(in oklab, var(--success) 12%, transparent);
|
|
648
|
+
color: var(--success);
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
## Rollout plan
|
|
653
|
+
|
|
654
|
+
Phased deployment strip showing ramp percentages.
|
|
655
|
+
|
|
656
|
+
```html
|
|
657
|
+
<div class="rollout">
|
|
658
|
+
<div class="rollout-step">
|
|
659
|
+
<div class="rollout-when">Day 0</div>
|
|
660
|
+
<div class="rollout-pct">internal</div>
|
|
661
|
+
<div class="rollout-desc">Team only. Watch error rates.</div>
|
|
662
|
+
</div>
|
|
663
|
+
<div class="rollout-step">
|
|
664
|
+
<div class="rollout-when">Day 2</div>
|
|
665
|
+
<div class="rollout-pct">10%</div>
|
|
666
|
+
<div class="rollout-desc">Random sample. Alert on anomalies.</div>
|
|
667
|
+
</div>
|
|
668
|
+
<div class="rollout-step">
|
|
669
|
+
<div class="rollout-when">Day 4</div>
|
|
670
|
+
<div class="rollout-pct">100%</div>
|
|
671
|
+
<div class="rollout-desc">Full ramp.</div>
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
```css
|
|
677
|
+
.rollout { display: flex; gap: 0; }
|
|
678
|
+
|
|
679
|
+
.rollout-step {
|
|
680
|
+
flex: 1;
|
|
681
|
+
background: var(--card);
|
|
682
|
+
border: 1.5px solid var(--border);
|
|
683
|
+
padding: 16px 18px;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.rollout-step:first-child { border-radius: var(--radius) 0 0 var(--radius); }
|
|
687
|
+
.rollout-step:last-child { border-radius: 0 var(--radius) var(--radius) 0; }
|
|
688
|
+
.rollout-step + .rollout-step { border-left: none; }
|
|
689
|
+
|
|
690
|
+
.rollout-when {
|
|
691
|
+
font-family: var(--font-mono);
|
|
692
|
+
font-size: 0.68rem;
|
|
693
|
+
text-transform: uppercase;
|
|
694
|
+
letter-spacing: 0.05em;
|
|
695
|
+
color: var(--muted-foreground);
|
|
696
|
+
margin-bottom: 8px;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.rollout-pct {
|
|
700
|
+
font-family: var(--font-mono);
|
|
701
|
+
font-size: 1.35rem;
|
|
702
|
+
font-weight: 600;
|
|
703
|
+
color: var(--primary);
|
|
704
|
+
margin-bottom: 6px;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.rollout-desc {
|
|
708
|
+
font-size: 0.82rem;
|
|
709
|
+
color: var(--muted-foreground);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
@media (max-width: 720px) {
|
|
713
|
+
.rollout { flex-direction: column; }
|
|
714
|
+
.rollout-step { border-radius: var(--radius); }
|
|
715
|
+
.rollout-step + .rollout-step { border-left: 1.5px solid var(--border); margin-top: 10px; }
|
|
716
|
+
}
|
|
717
|
+
```
|