@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,227 @@
1
+ ---
2
+ import Layout from "@/layouts/Layout.astro"
3
+ import { loadScreens } from "@/utils/loadScreens"
4
+
5
+ const screens = loadScreens()
6
+ const allTags = [...new Set(screens.flatMap((s) => s.tags ?? []))]
7
+ ---
8
+
9
+ <Layout title="Screens" currentPage="screens">
10
+ <div class="container">
11
+ <div class="page-header">
12
+ <h1 class="page-title">Screens</h1>
13
+ <p class="page-description">
14
+ Browse all screens in your application. Click on a screen to see details
15
+ and navigation flows.
16
+ </p>
17
+ </div>
18
+
19
+ {
20
+ screens.length === 0 ? (
21
+ <div class="empty-state">
22
+ <svg
23
+ class="empty-state-icon"
24
+ aria-hidden="true"
25
+ fill="none"
26
+ viewBox="0 0 24 24"
27
+ stroke="currentColor"
28
+ stroke-width="1.5"
29
+ >
30
+ <path
31
+ stroke-linecap="round"
32
+ stroke-linejoin="round"
33
+ 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"
34
+ />
35
+ </svg>
36
+ <h2 class="empty-state-title">No screens found</h2>
37
+ <p class="empty-state-description">
38
+ Get started by generating screen metadata from your routes.
39
+ </p>
40
+
41
+ <div class="empty-state-steps">
42
+ <div class="empty-state-step">
43
+ <span class="step-number">1</span>
44
+ <div class="step-content">
45
+ <div class="step-title">Generate screen.meta.ts files</div>
46
+ <code class="step-code">screenbook generate</code>
47
+ </div>
48
+ </div>
49
+ <div class="empty-state-step">
50
+ <span class="step-number">2</span>
51
+ <div class="step-content">
52
+ <div class="step-title">Build the screen catalog</div>
53
+ <code class="step-code">screenbook build</code>
54
+ </div>
55
+ </div>
56
+ <div class="empty-state-step">
57
+ <span class="step-number">3</span>
58
+ <div class="step-content">
59
+ <div class="step-title">Restart the dev server</div>
60
+ <code class="step-code">screenbook dev</code>
61
+ </div>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="empty-state-features">
66
+ <p class="features-title">What you'll get:</p>
67
+ <ul class="features-list">
68
+ <li>Screen catalog with search & filter</li>
69
+ <li>Navigation graph visualization</li>
70
+ <li>Impact analysis for API changes</li>
71
+ <li>CI lint for documentation coverage</li>
72
+ </ul>
73
+ </div>
74
+ </div>
75
+ ) : (
76
+ <>
77
+ <div class="search-wrapper">
78
+ <svg
79
+ class="search-icon"
80
+ aria-hidden="true"
81
+ fill="none"
82
+ viewBox="0 0 24 24"
83
+ stroke="currentColor"
84
+ stroke-width="2"
85
+ >
86
+ <path
87
+ stroke-linecap="round"
88
+ stroke-linejoin="round"
89
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
90
+ />
91
+ </svg>
92
+ <label for="search" class="sr-only">
93
+ Search screens
94
+ </label>
95
+ <input
96
+ type="text"
97
+ id="search"
98
+ placeholder="Search screens..."
99
+ class="search-input"
100
+ />
101
+ </div>
102
+
103
+ {allTags.length > 0 && (
104
+ <div class="tags" role="group" aria-label="Filter by tag">
105
+ {allTags.map((tag) => (
106
+ <button class="tag" data-tag={tag} aria-pressed="false">
107
+ {tag}
108
+ </button>
109
+ ))}
110
+ </div>
111
+ )}
112
+
113
+ <h2 class="sr-only">All Screens</h2>
114
+ <div class="screen-grid" id="screen-list" aria-live="polite">
115
+ {screens.map((screen) => (
116
+ <a
117
+ href={`/screen/${screen.id}`}
118
+ class="card-link"
119
+ data-tags={screen.tags?.join(",") ?? ""}
120
+ data-title={screen.title.toLowerCase()}
121
+ data-id={screen.id.toLowerCase()}
122
+ >
123
+ <div class="card">
124
+ <h3 class="card-title">{screen.title}</h3>
125
+ <div class="card-route">{screen.route}</div>
126
+ <div class="card-meta">
127
+ <span class="card-meta-item">
128
+ <svg
129
+ aria-hidden="true"
130
+ fill="none"
131
+ viewBox="0 0 24 24"
132
+ stroke="currentColor"
133
+ stroke-width="2"
134
+ >
135
+ <path
136
+ stroke-linecap="round"
137
+ stroke-linejoin="round"
138
+ d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5l-3.9 19.5m-2.1-19.5l-3.9 19.5"
139
+ />
140
+ </svg>
141
+ {screen.id}
142
+ </span>
143
+ {screen.owner && screen.owner.length > 0 && (
144
+ <span class="card-meta-item">
145
+ <svg
146
+ aria-hidden="true"
147
+ fill="none"
148
+ viewBox="0 0 24 24"
149
+ stroke="currentColor"
150
+ stroke-width="2"
151
+ >
152
+ <path
153
+ stroke-linecap="round"
154
+ stroke-linejoin="round"
155
+ 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"
156
+ />
157
+ </svg>
158
+ {screen.owner.join(", ")}
159
+ </span>
160
+ )}
161
+ </div>
162
+ {screen.tags && screen.tags.length > 0 && (
163
+ <div class="card-tags">
164
+ {screen.tags.map((tag) => (
165
+ <span class="card-tag">{tag}</span>
166
+ ))}
167
+ </div>
168
+ )}
169
+ </div>
170
+ </a>
171
+ ))}
172
+ </div>
173
+ </>
174
+ )
175
+ }
176
+ </div>
177
+
178
+ <script>
179
+ const searchInput = document.getElementById("search") as HTMLInputElement
180
+ const screenList = document.getElementById("screen-list")
181
+ const tagButtons = document.querySelectorAll(".tag")
182
+
183
+ let activeTag: string | null = null
184
+
185
+ function filterScreens() {
186
+ const query = searchInput?.value.toLowerCase() ?? ""
187
+ const cards = screenList?.querySelectorAll(
188
+ ".card-link",
189
+ ) as NodeListOf<HTMLElement>
190
+
191
+ cards?.forEach((card) => {
192
+ const title = card.dataset.title ?? ""
193
+ const id = card.dataset.id ?? ""
194
+ const tags = card.dataset.tags ?? ""
195
+
196
+ const matchesSearch = title.includes(query) || id.includes(query)
197
+ const matchesTag = !activeTag || tags.includes(activeTag)
198
+
199
+ card.style.display = matchesSearch && matchesTag ? "block" : "none"
200
+ })
201
+ }
202
+
203
+ searchInput?.addEventListener("input", filterScreens)
204
+
205
+ tagButtons.forEach((btn) => {
206
+ btn.addEventListener("click", () => {
207
+ const tag = (btn as HTMLElement).dataset.tag
208
+ if (activeTag === tag) {
209
+ activeTag = null
210
+ tagButtons.forEach((b) => {
211
+ b.classList.remove("active")
212
+ b.setAttribute("aria-pressed", "false")
213
+ })
214
+ } else {
215
+ tagButtons.forEach((b) => {
216
+ b.classList.remove("active")
217
+ b.setAttribute("aria-pressed", "false")
218
+ })
219
+ activeTag = tag ?? null
220
+ btn.classList.add("active")
221
+ btn.setAttribute("aria-pressed", "true")
222
+ }
223
+ filterScreens()
224
+ })
225
+ })
226
+ </script>
227
+ </Layout>
@@ -0,0 +1,299 @@
1
+ ---
2
+ import Layout from "@/layouts/Layout.astro"
3
+ import LinkIcon from "@/components/LinkIcon.astro"
4
+ import MockPreview from "@/components/MockPreview.astro"
5
+ import { loadScreens } from "@/utils/loadScreens"
6
+ import { getApiDependencyCount } from "@/utils/impactAnalysis"
7
+
8
+ const screens = loadScreens()
9
+ const apiCounts = getApiDependencyCount(screens)
10
+
11
+ export function getStaticPaths() {
12
+ const screens = loadScreens()
13
+ return screens.map((screen) => ({
14
+ params: { id: screen.id },
15
+ }))
16
+ }
17
+
18
+ const { id } = Astro.params
19
+ const screen = screens.find((s) => s.id === id)
20
+
21
+ if (!screen) {
22
+ return Astro.redirect("/")
23
+ }
24
+
25
+ // Find related screens
26
+ const entryScreens = screens.filter((s) => screen.entryPoints?.includes(s.id))
27
+ const nextScreens = screens.filter((s) => screen.next?.includes(s.id))
28
+ ---
29
+
30
+ <Layout title={screen.title}>
31
+ <div class="container">
32
+ <a href="/" class="back-link">
33
+ <svg
34
+ aria-hidden="true"
35
+ fill="none"
36
+ viewBox="0 0 24 24"
37
+ stroke="currentColor"
38
+ stroke-width="2"
39
+ >
40
+ <path
41
+ stroke-linecap="round"
42
+ stroke-linejoin="round"
43
+ d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path>
44
+ </svg>
45
+ Back to screens
46
+ </a>
47
+
48
+ <div class="detail-header">
49
+ <h1 class="detail-title">{screen.title}</h1>
50
+ <div class="detail-route">
51
+ <svg
52
+ aria-hidden="true"
53
+ fill="none"
54
+ viewBox="0 0 24 24"
55
+ stroke="currentColor"
56
+ stroke-width="2"
57
+ >
58
+ <path
59
+ stroke-linecap="round"
60
+ stroke-linejoin="round"
61
+ d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244"
62
+ ></path>
63
+ </svg>
64
+ {screen.route}
65
+ </div>
66
+ </div>
67
+
68
+ <div class="detail-content">
69
+ <div>
70
+ <!-- Information -->
71
+ <div class="section">
72
+ <h2 class="section-title">Information</h2>
73
+ <div class="info-list">
74
+ <div class="info-item">
75
+ <span class="info-label">ID</span>
76
+ <span class="info-value"><code>{screen.id}</code></span>
77
+ </div>
78
+ {
79
+ screen.owner && screen.owner.length > 0 && (
80
+ <div class="info-item">
81
+ <span class="info-label">Owner</span>
82
+ <span class="info-value">{screen.owner.join(", ")}</span>
83
+ </div>
84
+ )
85
+ }
86
+ {
87
+ screen.tags && screen.tags.length > 0 && (
88
+ <div class="info-item">
89
+ <span class="info-label">Tags</span>
90
+ <span class="info-value">
91
+ <div class="card-tags">
92
+ {screen.tags.map((tag) => (
93
+ <span class="card-tag">{tag}</span>
94
+ ))}
95
+ </div>
96
+ </span>
97
+ </div>
98
+ )
99
+ }
100
+ {
101
+ screen.description && (
102
+ <div class="info-item">
103
+ <span class="info-label">Description</span>
104
+ <span class="info-value">{screen.description}</span>
105
+ </div>
106
+ )
107
+ }
108
+ </div>
109
+ </div>
110
+
111
+ <!-- Dependencies -->
112
+ {
113
+ screen.dependsOn && screen.dependsOn.length > 0 && (
114
+ <div class="section">
115
+ <h2 class="section-title">Dependencies</h2>
116
+ <div class="dep-list">
117
+ {screen.dependsOn.map((dep) => {
118
+ const count = apiCounts.get(dep) || 1
119
+ const otherCount = count - 1
120
+ return (
121
+ <a
122
+ href={`/impact?api=${encodeURIComponent(dep)}`}
123
+ class="dep-item dep-item-link"
124
+ >
125
+ <div class="dep-item-main">
126
+ <svg
127
+ aria-hidden="true"
128
+ fill="none"
129
+ viewBox="0 0 24 24"
130
+ stroke="currentColor"
131
+ stroke-width="2"
132
+ >
133
+ <path
134
+ stroke-linecap="round"
135
+ stroke-linejoin="round"
136
+ d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"
137
+ />
138
+ </svg>
139
+ <span>{dep}</span>
140
+ </div>
141
+ <div class="dep-item-meta">
142
+ {otherCount > 0 && (
143
+ <span class="dep-count">
144
+ +{otherCount} other{otherCount > 1 ? "s" : ""}
145
+ </span>
146
+ )}
147
+ <svg
148
+ class="dep-arrow"
149
+ fill="none"
150
+ viewBox="0 0 24 24"
151
+ stroke="currentColor"
152
+ stroke-width="2"
153
+ >
154
+ <path
155
+ stroke-linecap="round"
156
+ stroke-linejoin="round"
157
+ d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"
158
+ />
159
+ </svg>
160
+ </div>
161
+ </a>
162
+ )
163
+ })}
164
+ </div>
165
+ </div>
166
+ )
167
+ }
168
+
169
+ <!-- External Links -->
170
+ {
171
+ screen.links && screen.links.length > 0 && (
172
+ <div class="section">
173
+ <h2 class="section-title">Links</h2>
174
+ <div class="dep-list">
175
+ {screen.links.map((link) => (
176
+ <a
177
+ href={link.url}
178
+ target="_blank"
179
+ rel="noopener noreferrer"
180
+ class="dep-item"
181
+ >
182
+ <LinkIcon type={link.type} url={link.url} />
183
+ {link.label}
184
+ </a>
185
+ ))}
186
+ </div>
187
+ </div>
188
+ )
189
+ }
190
+
191
+ <!-- Mock Preview -->
192
+ {
193
+ screen.mock && (
194
+ <div class="section">
195
+ <h2 class="section-title">UI Wireframe</h2>
196
+ <MockPreview mock={screen.mock} screenId={screen.id} />
197
+ </div>
198
+ )
199
+ }
200
+ </div>
201
+
202
+ <div>
203
+ <!-- Entry Points -->
204
+ {
205
+ entryScreens.length > 0 && (
206
+ <div class="sidebar-card">
207
+ <h3 class="sidebar-card-title">
208
+ <svg
209
+ aria-hidden="true"
210
+ fill="none"
211
+ viewBox="0 0 24 24"
212
+ stroke="currentColor"
213
+ stroke-width="2"
214
+ >
215
+ <path
216
+ stroke-linecap="round"
217
+ stroke-linejoin="round"
218
+ d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3"
219
+ />
220
+ </svg>
221
+ Entry Points
222
+ </h3>
223
+ <div class="screen-link-list">
224
+ {entryScreens.map((s) => (
225
+ <a href={`/screen/${s.id}`} class="screen-link">
226
+ <div class="screen-link-info">
227
+ <span class="screen-link-title">{s.title}</span>
228
+ <span class="screen-link-id">{s.id}</span>
229
+ </div>
230
+ <svg
231
+ class="screen-link-arrow"
232
+ fill="none"
233
+ viewBox="0 0 24 24"
234
+ stroke="currentColor"
235
+ stroke-width="2"
236
+ >
237
+ <path
238
+ stroke-linecap="round"
239
+ stroke-linejoin="round"
240
+ d="M8.25 4.5l7.5 7.5-7.5 7.5"
241
+ />
242
+ </svg>
243
+ </a>
244
+ ))}
245
+ </div>
246
+ </div>
247
+ )
248
+ }
249
+
250
+ <!-- Next Screens -->
251
+ {
252
+ nextScreens.length > 0 && (
253
+ <div class="sidebar-card">
254
+ <h3 class="sidebar-card-title">
255
+ <svg
256
+ aria-hidden="true"
257
+ fill="none"
258
+ viewBox="0 0 24 24"
259
+ stroke="currentColor"
260
+ stroke-width="2"
261
+ >
262
+ <path
263
+ stroke-linecap="round"
264
+ stroke-linejoin="round"
265
+ d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
266
+ />
267
+ </svg>
268
+ Next Screens
269
+ </h3>
270
+ <div class="screen-link-list">
271
+ {nextScreens.map((s) => (
272
+ <a href={`/screen/${s.id}`} class="screen-link">
273
+ <div class="screen-link-info">
274
+ <span class="screen-link-title">{s.title}</span>
275
+ <span class="screen-link-id">{s.id}</span>
276
+ </div>
277
+ <svg
278
+ class="screen-link-arrow"
279
+ fill="none"
280
+ viewBox="0 0 24 24"
281
+ stroke="currentColor"
282
+ stroke-width="2"
283
+ >
284
+ <path
285
+ stroke-linecap="round"
286
+ stroke-linejoin="round"
287
+ d="M8.25 4.5l7.5 7.5-7.5 7.5"
288
+ />
289
+ </svg>
290
+ </a>
291
+ ))}
292
+ </div>
293
+ </div>
294
+ )
295
+ }
296
+ </div>
297
+ </div>
298
+ </div>
299
+ </Layout>