@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,555 @@
1
+ ---
2
+ import Layout from "@/layouts/Layout.astro"
3
+ import { loadScreens } from "@/utils/loadScreens"
4
+ import {
5
+ analyzeImpact,
6
+ getAllApis,
7
+ generateImpactMermaid,
8
+ formatImpactMarkdown,
9
+ } from "@/utils/impactAnalysis"
10
+
11
+ const screens = loadScreens()
12
+ const allApis = getAllApis(screens)
13
+
14
+ const url = Astro.url
15
+ const apiQuery = url.searchParams.get("api") || ""
16
+
17
+ let result = null
18
+ let mermaidGraph = ""
19
+ let markdown = ""
20
+
21
+ if (apiQuery && screens.length > 0) {
22
+ result = analyzeImpact(screens, apiQuery, 3)
23
+ mermaidGraph = generateImpactMermaid(screens, result)
24
+ markdown = formatImpactMarkdown(result)
25
+ }
26
+ ---
27
+
28
+ <Layout title="Impact Analysis" currentPage="impact">
29
+ <div class="container">
30
+ <div class="page-header">
31
+ <h1 class="page-title">Impact Analysis</h1>
32
+ <p class="page-description">
33
+ Analyze which screens are affected when an API changes.
34
+ </p>
35
+ </div>
36
+
37
+ {
38
+ screens.length === 0 ? (
39
+ <div class="empty-state">
40
+ <svg
41
+ class="empty-state-icon"
42
+ aria-hidden="true"
43
+ fill="none"
44
+ viewBox="0 0 24 24"
45
+ stroke="currentColor"
46
+ stroke-width="1.5"
47
+ >
48
+ <path
49
+ stroke-linecap="round"
50
+ stroke-linejoin="round"
51
+ d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"
52
+ />
53
+ </svg>
54
+ <h2 class="empty-state-title">No screen data</h2>
55
+ <p class="empty-state-description">
56
+ Run the build command to generate screen metadata.
57
+ </p>
58
+ <code class="empty-state-code">
59
+ <span class="prompt">$</span> screenbook build
60
+ </code>
61
+ </div>
62
+ ) : (
63
+ <>
64
+ <div class="impact-search">
65
+ <form method="get" class="search-form" role="search">
66
+ <div class="search-wrapper impact-search-wrapper">
67
+ <svg
68
+ class="search-icon"
69
+ aria-hidden="true"
70
+ fill="none"
71
+ viewBox="0 0 24 24"
72
+ stroke="currentColor"
73
+ stroke-width="2"
74
+ >
75
+ <path
76
+ stroke-linecap="round"
77
+ stroke-linejoin="round"
78
+ d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"
79
+ />
80
+ </svg>
81
+ <label for="api-input" class="sr-only">
82
+ API name to analyze
83
+ </label>
84
+ <input
85
+ type="text"
86
+ id="api-input"
87
+ name="api"
88
+ class="search-input"
89
+ placeholder="Enter API name (e.g., InvoiceAPI.getDetail)"
90
+ value={apiQuery}
91
+ list="api-suggestions"
92
+ />
93
+ <datalist id="api-suggestions">
94
+ {allApis.map((api) => (
95
+ <option value={api} />
96
+ ))}
97
+ </datalist>
98
+ <button type="submit" class="search-button">
99
+ Analyze
100
+ </button>
101
+ </div>
102
+ </form>
103
+
104
+ {allApis.length > 0 && !apiQuery && (
105
+ <div class="api-suggestions">
106
+ <p class="suggestions-label">Available APIs:</p>
107
+ <div class="tags">
108
+ {allApis.slice(0, 10).map((api) => (
109
+ <a
110
+ href={`/impact?api=${encodeURIComponent(api)}`}
111
+ class="tag"
112
+ >
113
+ {api}
114
+ </a>
115
+ ))}
116
+ {allApis.length > 10 && (
117
+ <span class="tag tag-more">
118
+ +{allApis.length - 10} more
119
+ </span>
120
+ )}
121
+ </div>
122
+ </div>
123
+ )}
124
+ </div>
125
+
126
+ {result && (
127
+ <div class="impact-results">
128
+ <div class="impact-summary">
129
+ <div class="stats">
130
+ <div class="stat">
131
+ <div class="stat-value stat-total">{result.totalCount}</div>
132
+ <div class="stat-label">Total Affected</div>
133
+ </div>
134
+ <div class="stat">
135
+ <div class="stat-value stat-direct">
136
+ {result.direct.length}
137
+ </div>
138
+ <div class="stat-label">Direct</div>
139
+ </div>
140
+ <div class="stat">
141
+ <div class="stat-value stat-transitive">
142
+ {result.transitive.length}
143
+ </div>
144
+ <div class="stat-label">Transitive</div>
145
+ </div>
146
+ </div>
147
+
148
+ <button
149
+ id="copy-markdown"
150
+ class="copy-button"
151
+ data-markdown={markdown}
152
+ aria-label="Copy impact analysis as Markdown"
153
+ >
154
+ <svg
155
+ aria-hidden="true"
156
+ fill="none"
157
+ viewBox="0 0 24 24"
158
+ stroke="currentColor"
159
+ stroke-width="2"
160
+ >
161
+ <path
162
+ stroke-linecap="round"
163
+ stroke-linejoin="round"
164
+ d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
165
+ />
166
+ </svg>
167
+ Copy as Markdown
168
+ </button>
169
+ </div>
170
+
171
+ {result.totalCount === 0 ? (
172
+ <div class="no-impact">
173
+ <svg
174
+ aria-hidden="true"
175
+ fill="none"
176
+ viewBox="0 0 24 24"
177
+ stroke="currentColor"
178
+ stroke-width="1.5"
179
+ >
180
+ <path
181
+ stroke-linecap="round"
182
+ stroke-linejoin="round"
183
+ d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
184
+ />
185
+ </svg>
186
+ <p>
187
+ No screens depend on <code>{apiQuery}</code>
188
+ </p>
189
+ </div>
190
+ ) : (
191
+ <>
192
+ <div
193
+ class="impact-graph"
194
+ role="img"
195
+ aria-label={`Impact analysis graph showing ${result.direct.length} direct and ${result.transitive.length} transitive dependencies for ${apiQuery}`}
196
+ >
197
+ <div class="graph-container">
198
+ <pre class="mermaid" aria-hidden="true">
199
+ {mermaidGraph}
200
+ </pre>
201
+ </div>
202
+ <div class="graph-legend">
203
+ <div class="graph-legend-item">
204
+ <div class="legend-node legend-direct" />
205
+ <span>Direct dependency</span>
206
+ </div>
207
+ <div class="graph-legend-item">
208
+ <div class="legend-node legend-transitive" />
209
+ <span>Transitive</span>
210
+ </div>
211
+ <div class="graph-legend-item">
212
+ <div class="legend-node" />
213
+ <span>Not affected</span>
214
+ </div>
215
+ </div>
216
+ </div>
217
+
218
+ <div class="impact-details">
219
+ {result.direct.length > 0 && (
220
+ <div class="section">
221
+ <h3 class="section-title">
222
+ <svg
223
+ aria-hidden="true"
224
+ fill="none"
225
+ viewBox="0 0 24 24"
226
+ stroke="currentColor"
227
+ stroke-width="2"
228
+ >
229
+ <path
230
+ stroke-linecap="round"
231
+ stroke-linejoin="round"
232
+ 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"
233
+ />
234
+ </svg>
235
+ Direct Dependencies ({result.direct.length})
236
+ </h3>
237
+ <div class="screen-link-list">
238
+ {result.direct.map((screen) => (
239
+ <a
240
+ href={`/screen/${screen.id}`}
241
+ class="screen-link impact-screen direct"
242
+ >
243
+ <div class="screen-link-info">
244
+ <div class="screen-link-title">
245
+ {screen.title}
246
+ </div>
247
+ <div class="screen-link-id">{screen.route}</div>
248
+ </div>
249
+ {screen.owner && screen.owner.length > 0 && (
250
+ <div class="screen-link-owner">
251
+ {screen.owner.join(", ")}
252
+ </div>
253
+ )}
254
+ </a>
255
+ ))}
256
+ </div>
257
+ </div>
258
+ )}
259
+
260
+ {result.transitive.length > 0 && (
261
+ <div class="section">
262
+ <h3 class="section-title">
263
+ <svg
264
+ aria-hidden="true"
265
+ fill="none"
266
+ viewBox="0 0 24 24"
267
+ stroke="currentColor"
268
+ stroke-width="2"
269
+ >
270
+ <path
271
+ stroke-linecap="round"
272
+ stroke-linejoin="round"
273
+ d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"
274
+ />
275
+ </svg>
276
+ Transitive Dependencies ({result.transitive.length})
277
+ </h3>
278
+ <div class="screen-link-list">
279
+ {result.transitive.map(({ screen, path }) => (
280
+ <a
281
+ href={`/screen/${screen.id}`}
282
+ class="screen-link impact-screen transitive"
283
+ >
284
+ <div class="screen-link-info">
285
+ <div class="screen-link-title">
286
+ {screen.title}
287
+ </div>
288
+ <div class="screen-link-path">
289
+ {path.join(" → ")}
290
+ </div>
291
+ </div>
292
+ </a>
293
+ ))}
294
+ </div>
295
+ </div>
296
+ )}
297
+ </div>
298
+ </>
299
+ )}
300
+ </div>
301
+ )}
302
+ </>
303
+ )
304
+ }
305
+ </div>
306
+
307
+ <script>
308
+ import mermaid from "mermaid"
309
+
310
+ mermaid.initialize({
311
+ startOnLoad: true,
312
+ theme: "base",
313
+ themeVariables: {
314
+ darkMode: true,
315
+ background: "transparent",
316
+ fontFamily: "system-ui, sans-serif",
317
+ primaryColor: "#1e293b",
318
+ primaryTextColor: "#e2e8f0",
319
+ primaryBorderColor: "#64748b",
320
+ lineColor: "#64748b",
321
+ secondaryColor: "#1e293b",
322
+ tertiaryColor: "#1e293b",
323
+ nodeTextColor: "#ffffff",
324
+ },
325
+ flowchart: {
326
+ useMaxWidth: true,
327
+ htmlLabels: true,
328
+ curve: "basis",
329
+ padding: 20,
330
+ nodeSpacing: 50,
331
+ rankSpacing: 60,
332
+ },
333
+ })
334
+
335
+ // Copy markdown functionality
336
+ const copyButton = document.getElementById("copy-markdown")
337
+ if (copyButton) {
338
+ copyButton.addEventListener("click", async () => {
339
+ const markdown = copyButton.dataset.markdown
340
+ if (markdown) {
341
+ await navigator.clipboard.writeText(markdown)
342
+ const originalText = copyButton.innerHTML
343
+ copyButton.innerHTML = `
344
+ <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
345
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
346
+ </svg>
347
+ Copied!
348
+ `
349
+ setTimeout(() => {
350
+ copyButton.innerHTML = originalText
351
+ }, 2000)
352
+ }
353
+ })
354
+ }
355
+ </script>
356
+ </Layout>
357
+
358
+ <style>
359
+ .impact-search {
360
+ margin-bottom: 32px;
361
+ }
362
+
363
+ .search-form {
364
+ margin-bottom: 16px;
365
+ }
366
+
367
+ .impact-search-wrapper {
368
+ display: flex;
369
+ gap: 12px;
370
+ max-width: 600px;
371
+ }
372
+
373
+ .impact-search-wrapper .search-input {
374
+ flex: 1;
375
+ }
376
+
377
+ .search-button {
378
+ padding: 10px 20px;
379
+ font-size: var(--text-sm);
380
+ font-weight: 500;
381
+ color: var(--color-bg);
382
+ background: var(--color-accent);
383
+ border: none;
384
+ border-radius: var(--radius-md);
385
+ cursor: pointer;
386
+ transition: background 0.15s ease;
387
+ }
388
+
389
+ .search-button:hover {
390
+ background: var(--color-accent-hover);
391
+ }
392
+
393
+ .api-suggestions {
394
+ margin-top: 16px;
395
+ }
396
+
397
+ .suggestions-label {
398
+ font-size: var(--text-sm);
399
+ color: var(--color-text-muted);
400
+ margin-bottom: 8px;
401
+ }
402
+
403
+ .tag-more {
404
+ background: transparent;
405
+ border-style: dashed;
406
+ cursor: default;
407
+ }
408
+
409
+ .impact-summary {
410
+ display: flex;
411
+ justify-content: space-between;
412
+ align-items: center;
413
+ padding: 24px;
414
+ background: var(--color-surface);
415
+ border: 1px solid var(--color-border);
416
+ border-radius: var(--radius-lg);
417
+ margin-bottom: 24px;
418
+ }
419
+
420
+ .stat-direct {
421
+ color: #f87171;
422
+ }
423
+
424
+ .stat-transitive {
425
+ color: #fb923c;
426
+ }
427
+
428
+ .copy-button {
429
+ display: flex;
430
+ align-items: center;
431
+ gap: 8px;
432
+ padding: 10px 16px;
433
+ font-size: var(--text-sm);
434
+ font-weight: 500;
435
+ color: var(--color-text-secondary);
436
+ background: var(--color-bg-muted);
437
+ border: 1px solid var(--color-border);
438
+ border-radius: var(--radius-md);
439
+ cursor: pointer;
440
+ transition: all 0.15s ease;
441
+ }
442
+
443
+ .copy-button:hover {
444
+ border-color: var(--color-border-hover);
445
+ color: var(--color-text);
446
+ }
447
+
448
+ .copy-button svg {
449
+ width: 16px;
450
+ height: 16px;
451
+ }
452
+
453
+ .no-impact {
454
+ display: flex;
455
+ flex-direction: column;
456
+ align-items: center;
457
+ gap: 12px;
458
+ padding: 48px 24px;
459
+ background: var(--color-surface);
460
+ border: 1px solid var(--color-border);
461
+ border-radius: var(--radius-lg);
462
+ text-align: center;
463
+ }
464
+
465
+ .no-impact svg {
466
+ width: 48px;
467
+ height: 48px;
468
+ color: var(--color-success);
469
+ }
470
+
471
+ .no-impact p {
472
+ font-size: var(--text-lg);
473
+ color: var(--color-text-secondary);
474
+ }
475
+
476
+ .impact-graph {
477
+ margin-bottom: 32px;
478
+ }
479
+
480
+ /* Override Mermaid text colors for better contrast */
481
+ .impact-graph :global(.mermaid .node.direct .nodeLabel),
482
+ .impact-graph :global(.mermaid .node.direct .nodeLabel p),
483
+ .impact-graph :global(.mermaid .node.direct .nodeLabel span),
484
+ .impact-graph :global(.mermaid .node.transitive .nodeLabel),
485
+ .impact-graph :global(.mermaid .node.transitive .nodeLabel p),
486
+ .impact-graph :global(.mermaid .node.transitive .nodeLabel span) {
487
+ fill: #ffffff !important;
488
+ color: #ffffff !important;
489
+ font-weight: 600 !important;
490
+ }
491
+
492
+ .impact-graph :global(.mermaid .node.normal .nodeLabel),
493
+ .impact-graph :global(.mermaid .node.normal .nodeLabel p),
494
+ .impact-graph :global(.mermaid .node.normal .nodeLabel span) {
495
+ fill: #e2e8f0 !important;
496
+ color: #e2e8f0 !important;
497
+ }
498
+
499
+ .impact-graph :global(.mermaid .nodeLabel) {
500
+ font-weight: 500;
501
+ }
502
+
503
+ .impact-graph :global(.mermaid .nodeLabel p) {
504
+ margin: 0;
505
+ }
506
+
507
+ .legend-direct {
508
+ background: #dc2626 !important;
509
+ border-color: #fef2f2 !important;
510
+ }
511
+
512
+ .legend-transitive {
513
+ background: #ea580c !important;
514
+ border-color: #fff7ed !important;
515
+ }
516
+
517
+ .impact-details {
518
+ display: grid;
519
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
520
+ gap: 24px;
521
+ }
522
+
523
+ .section-title {
524
+ display: flex;
525
+ align-items: center;
526
+ gap: 8px;
527
+ }
528
+
529
+ .section-title svg {
530
+ width: 18px;
531
+ height: 18px;
532
+ }
533
+
534
+ .impact-screen.direct {
535
+ border-left: 3px solid #dc2626;
536
+ }
537
+
538
+ .impact-screen.transitive {
539
+ border-left: 3px solid #ea580c;
540
+ }
541
+
542
+ .screen-link-path {
543
+ font-size: var(--text-xs);
544
+ font-family: ui-monospace, monospace;
545
+ color: var(--color-text-muted);
546
+ }
547
+
548
+ .screen-link-owner {
549
+ font-size: var(--text-xs);
550
+ color: var(--color-text-muted);
551
+ padding: 4px 8px;
552
+ background: var(--color-bg);
553
+ border-radius: var(--radius-sm);
554
+ }
555
+ </style>