@screenbook/ui 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,465 @@
1
+ ---
2
+ import Layout from "@/layouts/Layout.astro"
3
+ import { loadCoverage } from "@/utils/loadCoverage"
4
+ import { loadScreens } from "@/utils/loadScreens"
5
+
6
+ const coverage = loadCoverage()
7
+ const screens = loadScreens()
8
+
9
+ // Calculate color based on percentage
10
+ function getPercentageColor(percentage: number): string {
11
+ if (percentage >= 80) return "text-green-400"
12
+ if (percentage >= 50) return "text-yellow-400"
13
+ return "text-red-400"
14
+ }
15
+
16
+ function getPercentageBg(percentage: number): string {
17
+ if (percentage >= 80) return "bg-green-500/20 border-green-500/30"
18
+ if (percentage >= 50) return "bg-yellow-500/20 border-yellow-500/30"
19
+ return "bg-red-500/20 border-red-500/30"
20
+ }
21
+ ---
22
+
23
+ <Layout title="Coverage" currentPage="coverage">
24
+ <div class="container">
25
+ <div class="page-header">
26
+ <h1 class="page-title">Coverage Dashboard</h1>
27
+ <p class="page-description">
28
+ Track documentation coverage across your screens.
29
+ </p>
30
+ </div>
31
+
32
+ {
33
+ !coverage ? (
34
+ <div class="empty-state">
35
+ <svg
36
+ class="empty-state-icon"
37
+ aria-hidden="true"
38
+ fill="none"
39
+ viewBox="0 0 24 24"
40
+ stroke="currentColor"
41
+ stroke-width="1.5"
42
+ >
43
+ <path
44
+ stroke-linecap="round"
45
+ stroke-linejoin="round"
46
+ d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
47
+ />
48
+ </svg>
49
+ <h2 class="empty-state-title">No coverage data</h2>
50
+ <p class="empty-state-description">
51
+ Run the build command to generate coverage data.
52
+ </p>
53
+ <code class="empty-state-code">
54
+ <span class="prompt">$</span> screenbook build
55
+ </code>
56
+ </div>
57
+ ) : (
58
+ <>
59
+ {/* Main Stats */}
60
+ <div class="coverage-hero">
61
+ <div
62
+ class={`coverage-percentage ${getPercentageBg(coverage.percentage)}`}
63
+ >
64
+ <span
65
+ class={`percentage-value ${getPercentageColor(coverage.percentage)}`}
66
+ >
67
+ {coverage.percentage}%
68
+ </span>
69
+ <span class="percentage-label">Coverage</span>
70
+ </div>
71
+ <div class="coverage-stats">
72
+ <div class="stat-card">
73
+ <div class="stat-value">{coverage.covered}</div>
74
+ <div class="stat-label">Documented</div>
75
+ </div>
76
+ <div class="stat-card">
77
+ <div class="stat-value">{coverage.total}</div>
78
+ <div class="stat-label">Total Routes</div>
79
+ </div>
80
+ <div class="stat-card">
81
+ <div class="stat-value">{coverage.missing.length}</div>
82
+ <div class="stat-label">Missing</div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+
87
+ <div class="coverage-grid">
88
+ {/* By Owner */}
89
+ <div class="coverage-section">
90
+ <h2 class="section-title">
91
+ <svg
92
+ aria-hidden="true"
93
+ fill="none"
94
+ viewBox="0 0 24 24"
95
+ stroke="currentColor"
96
+ stroke-width="2"
97
+ >
98
+ <path
99
+ stroke-linecap="round"
100
+ stroke-linejoin="round"
101
+ d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"
102
+ />
103
+ </svg>
104
+ By Owner
105
+ </h2>
106
+ <div class="owner-list">
107
+ {Object.entries(coverage.byOwner).map(([owner, data]) => (
108
+ <div class="owner-item">
109
+ <div class="owner-info">
110
+ <span class="owner-name">{owner}</span>
111
+ <span class="owner-count">
112
+ {data.count} screen{data.count > 1 ? "s" : ""}
113
+ </span>
114
+ </div>
115
+ <div class="owner-bar">
116
+ <div
117
+ class="owner-bar-fill"
118
+ style={`width: ${Math.round((data.count / coverage.covered) * 100)}%`}
119
+ />
120
+ </div>
121
+ </div>
122
+ ))}
123
+ </div>
124
+ </div>
125
+
126
+ {/* By Tag */}
127
+ <div class="coverage-section">
128
+ <h2 class="section-title">
129
+ <svg
130
+ aria-hidden="true"
131
+ fill="none"
132
+ viewBox="0 0 24 24"
133
+ stroke="currentColor"
134
+ stroke-width="2"
135
+ >
136
+ <path
137
+ stroke-linecap="round"
138
+ stroke-linejoin="round"
139
+ d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z"
140
+ />
141
+ <path
142
+ stroke-linecap="round"
143
+ stroke-linejoin="round"
144
+ d="M6 6h.008v.008H6V6z"
145
+ />
146
+ </svg>
147
+ By Tag
148
+ </h2>
149
+ <div class="tags-grid">
150
+ {Object.entries(coverage.byTag)
151
+ .sort(([, a], [, b]) => b - a)
152
+ .map(([tag, count]) => (
153
+ <a
154
+ href={`/?tag=${encodeURIComponent(tag)}`}
155
+ class="tag-card"
156
+ >
157
+ <span class="tag-name">{tag}</span>
158
+ <span class="tag-count">{count}</span>
159
+ </a>
160
+ ))}
161
+ </div>
162
+ </div>
163
+ </div>
164
+
165
+ {/* Missing Routes */}
166
+ {coverage.missing.length > 0 && (
167
+ <div class="coverage-section missing-section">
168
+ <h2 class="section-title">
169
+ <svg
170
+ aria-hidden="true"
171
+ fill="none"
172
+ viewBox="0 0 24 24"
173
+ stroke="currentColor"
174
+ stroke-width="2"
175
+ >
176
+ <path
177
+ stroke-linecap="round"
178
+ stroke-linejoin="round"
179
+ d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
180
+ />
181
+ </svg>
182
+ Missing Documentation ({coverage.missing.length})
183
+ </h2>
184
+ <p class="section-description">
185
+ These routes don't have a <code>screen.meta.ts</code> file yet.
186
+ </p>
187
+ <div class="missing-list">
188
+ {coverage.missing.map((item) => (
189
+ <div class="missing-item">
190
+ <div class="missing-route">
191
+ <svg
192
+ aria-hidden="true"
193
+ fill="none"
194
+ viewBox="0 0 24 24"
195
+ stroke="currentColor"
196
+ stroke-width="2"
197
+ >
198
+ <path
199
+ stroke-linecap="round"
200
+ stroke-linejoin="round"
201
+ d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
202
+ />
203
+ </svg>
204
+ <span>{item.route}</span>
205
+ </div>
206
+ <div class="missing-action">
207
+ <code class="suggested-path">{item.suggestedPath}</code>
208
+ </div>
209
+ </div>
210
+ ))}
211
+ </div>
212
+ </div>
213
+ )}
214
+
215
+ <div class="coverage-footer">
216
+ <p class="timestamp">
217
+ Last updated: {new Date(coverage.timestamp).toLocaleString()}
218
+ </p>
219
+ </div>
220
+ </>
221
+ )
222
+ }
223
+ </div>
224
+ </Layout>
225
+
226
+ <style>
227
+ .coverage-hero {
228
+ display: flex;
229
+ gap: 32px;
230
+ align-items: center;
231
+ margin-bottom: 48px;
232
+ }
233
+
234
+ .coverage-percentage {
235
+ display: flex;
236
+ flex-direction: column;
237
+ align-items: center;
238
+ justify-content: center;
239
+ width: 180px;
240
+ height: 180px;
241
+ border-radius: 50%;
242
+ border: 3px solid;
243
+ flex-shrink: 0;
244
+ }
245
+
246
+ .percentage-value {
247
+ font-size: 3.5rem;
248
+ font-weight: 700;
249
+ line-height: 1;
250
+ }
251
+
252
+ .percentage-label {
253
+ font-size: var(--text-sm);
254
+ color: var(--color-text-secondary);
255
+ margin-top: 4px;
256
+ }
257
+
258
+ .coverage-stats {
259
+ display: flex;
260
+ gap: 24px;
261
+ flex-wrap: wrap;
262
+ }
263
+
264
+ .stat-card {
265
+ background: var(--color-surface);
266
+ border: 1px solid var(--color-border);
267
+ border-radius: var(--radius-lg);
268
+ padding: 24px 32px;
269
+ text-align: center;
270
+ min-width: 120px;
271
+ }
272
+
273
+ .stat-value {
274
+ font-size: var(--text-3xl);
275
+ font-weight: 700;
276
+ color: var(--color-text);
277
+ }
278
+
279
+ .stat-label {
280
+ font-size: var(--text-sm);
281
+ color: var(--color-text-muted);
282
+ margin-top: 4px;
283
+ }
284
+
285
+ .coverage-grid {
286
+ display: grid;
287
+ grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
288
+ gap: 32px;
289
+ margin-bottom: 48px;
290
+ }
291
+
292
+ .coverage-section {
293
+ background: var(--color-surface);
294
+ border: 1px solid var(--color-border);
295
+ border-radius: var(--radius-lg);
296
+ padding: 24px;
297
+ }
298
+
299
+ .section-title {
300
+ display: flex;
301
+ align-items: center;
302
+ gap: 10px;
303
+ font-size: var(--text-lg);
304
+ font-weight: 600;
305
+ margin-bottom: 20px;
306
+ color: var(--color-text);
307
+ }
308
+
309
+ .section-title svg {
310
+ width: 20px;
311
+ height: 20px;
312
+ color: var(--color-accent);
313
+ }
314
+
315
+ .section-description {
316
+ font-size: var(--text-sm);
317
+ color: var(--color-text-muted);
318
+ margin-bottom: 16px;
319
+ }
320
+
321
+ .owner-list {
322
+ display: flex;
323
+ flex-direction: column;
324
+ gap: 16px;
325
+ }
326
+
327
+ .owner-item {
328
+ display: flex;
329
+ flex-direction: column;
330
+ gap: 8px;
331
+ }
332
+
333
+ .owner-info {
334
+ display: flex;
335
+ justify-content: space-between;
336
+ align-items: center;
337
+ }
338
+
339
+ .owner-name {
340
+ font-size: var(--text-sm);
341
+ font-weight: 500;
342
+ color: var(--color-text);
343
+ }
344
+
345
+ .owner-count {
346
+ font-size: var(--text-xs);
347
+ color: var(--color-text-muted);
348
+ }
349
+
350
+ .owner-bar {
351
+ height: 8px;
352
+ background: var(--color-bg-muted);
353
+ border-radius: 4px;
354
+ overflow: hidden;
355
+ }
356
+
357
+ .owner-bar-fill {
358
+ height: 100%;
359
+ background: var(--color-accent);
360
+ border-radius: 4px;
361
+ transition: width 0.3s ease;
362
+ }
363
+
364
+ .tags-grid {
365
+ display: flex;
366
+ flex-wrap: wrap;
367
+ gap: 8px;
368
+ }
369
+
370
+ .tag-card {
371
+ display: flex;
372
+ align-items: center;
373
+ gap: 8px;
374
+ padding: 8px 12px;
375
+ background: var(--color-bg-muted);
376
+ border-radius: var(--radius-md);
377
+ text-decoration: none;
378
+ transition: background 0.15s ease;
379
+ }
380
+
381
+ .tag-card:hover {
382
+ background: var(--color-surface-hover);
383
+ }
384
+
385
+ .tag-name {
386
+ font-size: var(--text-sm);
387
+ color: var(--color-text);
388
+ }
389
+
390
+ .tag-count {
391
+ font-size: var(--text-xs);
392
+ color: var(--color-accent);
393
+ font-weight: 600;
394
+ background: var(--color-accent-bg);
395
+ padding: 2px 6px;
396
+ border-radius: 4px;
397
+ }
398
+
399
+ .missing-section {
400
+ grid-column: 1 / -1;
401
+ }
402
+
403
+ .missing-list {
404
+ display: flex;
405
+ flex-direction: column;
406
+ gap: 12px;
407
+ }
408
+
409
+ .missing-item {
410
+ display: flex;
411
+ justify-content: space-between;
412
+ align-items: center;
413
+ padding: 12px 16px;
414
+ background: var(--color-bg-muted);
415
+ border-radius: var(--radius-md);
416
+ border-left: 3px solid var(--color-warning);
417
+ }
418
+
419
+ .missing-route {
420
+ display: flex;
421
+ align-items: center;
422
+ gap: 10px;
423
+ font-size: var(--text-sm);
424
+ color: var(--color-text);
425
+ }
426
+
427
+ .missing-route svg {
428
+ width: 16px;
429
+ height: 16px;
430
+ color: var(--color-text-muted);
431
+ }
432
+
433
+ .suggested-path {
434
+ font-size: var(--text-xs);
435
+ color: var(--color-text-muted);
436
+ }
437
+
438
+ .coverage-footer {
439
+ text-align: center;
440
+ padding-top: 24px;
441
+ border-top: 1px solid var(--color-border);
442
+ }
443
+
444
+ .timestamp {
445
+ font-size: var(--text-xs);
446
+ color: var(--color-text-muted);
447
+ }
448
+
449
+ @media (max-width: 768px) {
450
+ .coverage-hero {
451
+ flex-direction: column;
452
+ text-align: center;
453
+ }
454
+
455
+ .coverage-stats {
456
+ justify-content: center;
457
+ }
458
+
459
+ .missing-item {
460
+ flex-direction: column;
461
+ align-items: flex-start;
462
+ gap: 8px;
463
+ }
464
+ }
465
+ </style>
@@ -0,0 +1,33 @@
1
+ ---
2
+ import Layout from "@/layouts/Layout.astro"
3
+ import { MockFormEditor } from "@/components/MockFormEditor"
4
+ import { loadScreens } from "@/utils/loadScreens"
5
+
6
+ const screens = loadScreens()
7
+ const url = Astro.url
8
+ const screenId = url.searchParams.get("screen")
9
+
10
+ const screen = screenId ? screens.find((s) => s.id === screenId) : null
11
+ ---
12
+
13
+ <Layout title="Mock Editor">
14
+ <div class="editor-container">
15
+ <MockFormEditor
16
+ client:only="react"
17
+ screenId={screen?.id}
18
+ screenTitle={screen?.title || "New Screen"}
19
+ initialMock={screen?.mock}
20
+ />
21
+ </div>
22
+ </Layout>
23
+
24
+ <style>
25
+ .editor-container {
26
+ position: fixed;
27
+ top: 60px;
28
+ left: 0;
29
+ right: 0;
30
+ bottom: 0;
31
+ background: #141822;
32
+ }
33
+ </style>