@screenbook/ui 1.1.0 → 1.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.
Files changed (37) hide show
  1. package/README.md +30 -0
  2. package/dist/client/_astro/coverage.BzPU-EGZ.css +1 -0
  3. package/dist/server/entry.mjs +1 -1
  4. package/dist/server/{manifest_smcahUO6.mjs → manifest_BGl49hHW.mjs} +1 -1
  5. package/dist/server/pages/coverage.astro.mjs +1 -1
  6. package/dist/server/pages/editor.astro.mjs +1 -1
  7. package/dist/server/pages/graph.astro.mjs +1 -1
  8. package/dist/server/pages/impact.astro.mjs +1 -1
  9. package/dist/server/pages/index.astro.mjs +1 -1
  10. package/dist/server/pages/screen/_id_.astro.mjs +1 -1
  11. package/package.json +5 -1
  12. package/.astro/content-assets.mjs +0 -1
  13. package/.astro/content-modules.mjs +0 -1
  14. package/.astro/content.d.ts +0 -199
  15. package/.astro/types.d.ts +0 -2
  16. package/.prettierrc +0 -15
  17. package/CHANGELOG.md +0 -77
  18. package/astro.config.mjs +0 -20
  19. package/dist/client/_astro/coverage.DLKSOM4m.css +0 -1
  20. package/public/logo.svg +0 -5
  21. package/src/components/MockFormEditor.tsx +0 -1280
  22. package/src/components/MockPreview.astro +0 -811
  23. package/src/layouts/Layout.astro +0 -77
  24. package/src/pages/api/save-mock.ts +0 -182
  25. package/src/pages/coverage.astro +0 -399
  26. package/src/pages/editor.astro +0 -33
  27. package/src/pages/graph.astro +0 -368
  28. package/src/pages/impact.astro +0 -462
  29. package/src/pages/index.astro +0 -176
  30. package/src/pages/screen/[id].astro +0 -195
  31. package/src/styles/global.css +0 -904
  32. package/src/styles/mock-editor.css +0 -1351
  33. package/src/utils/impactAnalysis.ts +0 -304
  34. package/src/utils/loadCoverage.ts +0 -30
  35. package/src/utils/loadScreens.ts +0 -18
  36. package/tsconfig.json +0 -10
  37. /package/dist/server/chunks/{loadScreens_CkCqdbH2.mjs → loadScreens_B8bVK3q5.mjs} +0 -0
@@ -1,77 +0,0 @@
1
- ---
2
- import "@/styles/global.css"
3
-
4
- interface Props {
5
- title: string
6
- currentPage?: "screens" | "graph" | "impact" | "coverage"
7
- }
8
-
9
- const { title, currentPage } = Astro.props
10
- ---
11
-
12
- <!doctype html>
13
- <html lang="en">
14
- <head>
15
- <meta charset="UTF-8" />
16
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
17
- <title>{title} | Screenbook</title>
18
- </head>
19
- <body>
20
- <a href="#main-content" class="skip-link">Skip to main content</a>
21
- <header class="header">
22
- <div class="container">
23
- <div class="header-content">
24
- <a href="/" class="logo">
25
- <img src="/logo.svg" alt="Screenbook home" class="logo-icon" width="24" height="24" />
26
- Screenbook
27
- </a>
28
- <nav class="nav" aria-label="Main navigation">
29
- <a
30
- href="/"
31
- class:list={["nav-link", { active: currentPage === "screens" }]}
32
- aria-current={currentPage === "screens" ? "page" : undefined}
33
- >
34
- <svg aria-hidden="true" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
35
- <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
36
- </svg>
37
- Screens
38
- </a>
39
- <a
40
- href="/graph"
41
- class:list={["nav-link", { active: currentPage === "graph" }]}
42
- aria-current={currentPage === "graph" ? "page" : undefined}
43
- >
44
- <svg aria-hidden="true" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
45
- <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
46
- </svg>
47
- Graph
48
- </a>
49
- <a
50
- href="/impact"
51
- class:list={["nav-link", { active: currentPage === "impact" }]}
52
- aria-current={currentPage === "impact" ? "page" : undefined}
53
- >
54
- <svg aria-hidden="true" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
55
- <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
56
- </svg>
57
- Impact
58
- </a>
59
- <a
60
- href="/coverage"
61
- class:list={["nav-link", { active: currentPage === "coverage" }]}
62
- aria-current={currentPage === "coverage" ? "page" : undefined}
63
- >
64
- <svg aria-hidden="true" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
65
- <path stroke-linecap="round" stroke-linejoin="round" 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" />
66
- </svg>
67
- Coverage
68
- </a>
69
- </nav>
70
- </div>
71
- </div>
72
- </header>
73
- <main id="main-content">
74
- <slot />
75
- </main>
76
- </body>
77
- </html>
@@ -1,182 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs"
2
- import { join } from "node:path"
3
- import type { APIRoute } from "astro"
4
- import { Project, SyntaxKind } from "ts-morph"
5
-
6
- interface ScreenWithFilePath {
7
- id: string
8
- filePath: string
9
- [key: string]: unknown
10
- }
11
-
12
- interface SaveMockRequest {
13
- screenId: string
14
- mock: {
15
- sections: Array<{
16
- title?: string
17
- layout?: string
18
- elements: Array<Record<string, unknown>>
19
- }>
20
- }
21
- }
22
-
23
- export const POST: APIRoute = async ({ request }) => {
24
- try {
25
- const body = (await request.json()) as SaveMockRequest
26
- const { screenId, mock } = body
27
-
28
- if (!screenId || !mock) {
29
- return new Response(
30
- JSON.stringify({ error: "screenId and mock are required" }),
31
- { status: 400, headers: { "Content-Type": "application/json" } },
32
- )
33
- }
34
-
35
- // Load screens.json to get filePath
36
- const screensPath = join(process.cwd(), ".screenbook", "screens.json")
37
- if (!existsSync(screensPath)) {
38
- return new Response(
39
- JSON.stringify({
40
- error: "screens.json not found. Run screenbook build first.",
41
- }),
42
- { status: 404, headers: { "Content-Type": "application/json" } },
43
- )
44
- }
45
-
46
- const screens = JSON.parse(
47
- readFileSync(screensPath, "utf-8"),
48
- ) as ScreenWithFilePath[]
49
- const screen = screens.find((s) => s.id === screenId)
50
-
51
- if (!screen) {
52
- return new Response(
53
- JSON.stringify({ error: `Screen '${screenId}' not found` }),
54
- { status: 404, headers: { "Content-Type": "application/json" } },
55
- )
56
- }
57
-
58
- if (!screen.filePath) {
59
- return new Response(
60
- JSON.stringify({
61
- error: `Screen '${screenId}' does not have filePath. Rebuild with latest CLI.`,
62
- }),
63
- { status: 400, headers: { "Content-Type": "application/json" } },
64
- )
65
- }
66
-
67
- // Use ts-morph to update the file
68
- const result = await updateMockInFile(screen.filePath, mock)
69
-
70
- if (!result.success) {
71
- return new Response(JSON.stringify({ error: result.error }), {
72
- status: 500,
73
- headers: { "Content-Type": "application/json" },
74
- })
75
- }
76
-
77
- return new Response(
78
- JSON.stringify({ success: true, filePath: screen.filePath }),
79
- { status: 200, headers: { "Content-Type": "application/json" } },
80
- )
81
- } catch (error) {
82
- const message = error instanceof Error ? error.message : "Unknown error"
83
- return new Response(JSON.stringify({ error: message }), {
84
- status: 500,
85
- headers: { "Content-Type": "application/json" },
86
- })
87
- }
88
- }
89
-
90
- async function updateMockInFile(
91
- filePath: string,
92
- mock: SaveMockRequest["mock"],
93
- ): Promise<{ success: boolean; error?: string }> {
94
- try {
95
- const project = new Project()
96
- const sourceFile = project.addSourceFileAtPath(filePath)
97
-
98
- // Find the defineScreen call
99
- const callExpressions = sourceFile.getDescendantsOfKind(
100
- SyntaxKind.CallExpression,
101
- )
102
- const defineScreenCall = callExpressions.find((call) => {
103
- const expression = call.getExpression()
104
- return expression.getText() === "defineScreen"
105
- })
106
-
107
- if (!defineScreenCall) {
108
- return { success: false, error: "defineScreen() call not found in file" }
109
- }
110
-
111
- // Get the object literal argument
112
- const args = defineScreenCall.getArguments()
113
- const firstArg = args[0]
114
- if (!firstArg) {
115
- return { success: false, error: "defineScreen() has no arguments" }
116
- }
117
-
118
- if (firstArg.getKind() !== SyntaxKind.ObjectLiteralExpression) {
119
- return {
120
- success: false,
121
- error: "defineScreen() argument is not an object literal",
122
- }
123
- }
124
-
125
- const obj = firstArg.asKind(SyntaxKind.ObjectLiteralExpression)
126
- if (!obj) {
127
- return { success: false, error: "Failed to parse object literal" }
128
- }
129
-
130
- // Find existing mock property
131
- const mockProperty = obj.getProperty("mock")
132
-
133
- // Generate mock code string
134
- const mockCode = generateMockCode(mock)
135
-
136
- if (mockProperty) {
137
- // Update existing mock property
138
- mockProperty.replaceWithText(`mock: ${mockCode}`)
139
- } else {
140
- // Add new mock property at the end
141
- obj.addPropertyAssignment({
142
- name: "mock",
143
- initializer: mockCode,
144
- })
145
- }
146
-
147
- // Save the file
148
- await sourceFile.save()
149
-
150
- return { success: true }
151
- } catch (error) {
152
- const message = error instanceof Error ? error.message : "Unknown error"
153
- return { success: false, error: message }
154
- }
155
- }
156
-
157
- function generateMockCode(mock: SaveMockRequest["mock"]): string {
158
- const sections = mock.sections.map((section) => {
159
- const props: string[] = []
160
-
161
- if (section.title) {
162
- props.push(`title: ${JSON.stringify(section.title)}`)
163
- }
164
- if (section.layout && section.layout !== "vertical") {
165
- props.push(`layout: ${JSON.stringify(section.layout)}`)
166
- }
167
-
168
- const elements = section.elements.map((element) => {
169
- const elementProps = Object.entries(element)
170
- .filter(([_, value]) => value !== undefined && value !== "")
171
- .map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
172
- .join(", ")
173
- return `{ ${elementProps} }`
174
- })
175
-
176
- props.push(`elements: [\n\t\t\t${elements.join(",\n\t\t\t")},\n\t\t]`)
177
-
178
- return `{\n\t\t${props.join(",\n\t\t")},\n\t}`
179
- })
180
-
181
- return `{\n\tsections: [\n\t${sections.join(",\n\t")},\n\t],\n}`
182
- }
@@ -1,399 +0,0 @@
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
- {!coverage ? (
33
- <div class="empty-state">
34
- <svg class="empty-state-icon" aria-hidden="true" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
35
- <path stroke-linecap="round" stroke-linejoin="round" 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" />
36
- </svg>
37
- <h2 class="empty-state-title">No coverage data</h2>
38
- <p class="empty-state-description">
39
- Run the build command to generate coverage data.
40
- </p>
41
- <code class="empty-state-code">
42
- <span class="prompt">$</span> screenbook build
43
- </code>
44
- </div>
45
- ) : (
46
- <>
47
- {/* Main Stats */}
48
- <div class="coverage-hero">
49
- <div class={`coverage-percentage ${getPercentageBg(coverage.percentage)}`}>
50
- <span class={`percentage-value ${getPercentageColor(coverage.percentage)}`}>
51
- {coverage.percentage}%
52
- </span>
53
- <span class="percentage-label">Coverage</span>
54
- </div>
55
- <div class="coverage-stats">
56
- <div class="stat-card">
57
- <div class="stat-value">{coverage.covered}</div>
58
- <div class="stat-label">Documented</div>
59
- </div>
60
- <div class="stat-card">
61
- <div class="stat-value">{coverage.total}</div>
62
- <div class="stat-label">Total Routes</div>
63
- </div>
64
- <div class="stat-card">
65
- <div class="stat-value">{coverage.missing.length}</div>
66
- <div class="stat-label">Missing</div>
67
- </div>
68
- </div>
69
- </div>
70
-
71
- <div class="coverage-grid">
72
- {/* By Owner */}
73
- <div class="coverage-section">
74
- <h2 class="section-title">
75
- <svg aria-hidden="true" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
76
- <path stroke-linecap="round" stroke-linejoin="round" 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" />
77
- </svg>
78
- By Owner
79
- </h2>
80
- <div class="owner-list">
81
- {Object.entries(coverage.byOwner).map(([owner, data]) => (
82
- <div class="owner-item">
83
- <div class="owner-info">
84
- <span class="owner-name">{owner}</span>
85
- <span class="owner-count">{data.count} screen{data.count > 1 ? "s" : ""}</span>
86
- </div>
87
- <div class="owner-bar">
88
- <div
89
- class="owner-bar-fill"
90
- style={`width: ${Math.round((data.count / coverage.covered) * 100)}%`}
91
- ></div>
92
- </div>
93
- </div>
94
- ))}
95
- </div>
96
- </div>
97
-
98
- {/* By Tag */}
99
- <div class="coverage-section">
100
- <h2 class="section-title">
101
- <svg aria-hidden="true" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
102
- <path stroke-linecap="round" stroke-linejoin="round" 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" />
103
- <path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" />
104
- </svg>
105
- By Tag
106
- </h2>
107
- <div class="tags-grid">
108
- {Object.entries(coverage.byTag)
109
- .sort(([, a], [, b]) => b - a)
110
- .map(([tag, count]) => (
111
- <a href={`/?tag=${encodeURIComponent(tag)}`} class="tag-card">
112
- <span class="tag-name">{tag}</span>
113
- <span class="tag-count">{count}</span>
114
- </a>
115
- ))}
116
- </div>
117
- </div>
118
- </div>
119
-
120
- {/* Missing Routes */}
121
- {coverage.missing.length > 0 && (
122
- <div class="coverage-section missing-section">
123
- <h2 class="section-title">
124
- <svg aria-hidden="true" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
125
- <path stroke-linecap="round" stroke-linejoin="round" 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" />
126
- </svg>
127
- Missing Documentation ({coverage.missing.length})
128
- </h2>
129
- <p class="section-description">
130
- These routes don't have a <code>screen.meta.ts</code> file yet.
131
- </p>
132
- <div class="missing-list">
133
- {coverage.missing.map((item) => (
134
- <div class="missing-item">
135
- <div class="missing-route">
136
- <svg aria-hidden="true" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
137
- <path stroke-linecap="round" stroke-linejoin="round" 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" />
138
- </svg>
139
- <span>{item.route}</span>
140
- </div>
141
- <div class="missing-action">
142
- <code class="suggested-path">{item.suggestedPath}</code>
143
- </div>
144
- </div>
145
- ))}
146
- </div>
147
- </div>
148
- )}
149
-
150
- <div class="coverage-footer">
151
- <p class="timestamp">
152
- Last updated: {new Date(coverage.timestamp).toLocaleString()}
153
- </p>
154
- </div>
155
- </>
156
- )}
157
- </div>
158
- </Layout>
159
-
160
- <style>
161
- .coverage-hero {
162
- display: flex;
163
- gap: 32px;
164
- align-items: center;
165
- margin-bottom: 48px;
166
- }
167
-
168
- .coverage-percentage {
169
- display: flex;
170
- flex-direction: column;
171
- align-items: center;
172
- justify-content: center;
173
- width: 180px;
174
- height: 180px;
175
- border-radius: 50%;
176
- border: 3px solid;
177
- flex-shrink: 0;
178
- }
179
-
180
- .percentage-value {
181
- font-size: 3.5rem;
182
- font-weight: 700;
183
- line-height: 1;
184
- }
185
-
186
- .percentage-label {
187
- font-size: var(--text-sm);
188
- color: var(--color-text-secondary);
189
- margin-top: 4px;
190
- }
191
-
192
- .coverage-stats {
193
- display: flex;
194
- gap: 24px;
195
- flex-wrap: wrap;
196
- }
197
-
198
- .stat-card {
199
- background: var(--color-surface);
200
- border: 1px solid var(--color-border);
201
- border-radius: var(--radius-lg);
202
- padding: 24px 32px;
203
- text-align: center;
204
- min-width: 120px;
205
- }
206
-
207
- .stat-value {
208
- font-size: var(--text-3xl);
209
- font-weight: 700;
210
- color: var(--color-text);
211
- }
212
-
213
- .stat-label {
214
- font-size: var(--text-sm);
215
- color: var(--color-text-muted);
216
- margin-top: 4px;
217
- }
218
-
219
- .coverage-grid {
220
- display: grid;
221
- grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
222
- gap: 32px;
223
- margin-bottom: 48px;
224
- }
225
-
226
- .coverage-section {
227
- background: var(--color-surface);
228
- border: 1px solid var(--color-border);
229
- border-radius: var(--radius-lg);
230
- padding: 24px;
231
- }
232
-
233
- .section-title {
234
- display: flex;
235
- align-items: center;
236
- gap: 10px;
237
- font-size: var(--text-lg);
238
- font-weight: 600;
239
- margin-bottom: 20px;
240
- color: var(--color-text);
241
- }
242
-
243
- .section-title svg {
244
- width: 20px;
245
- height: 20px;
246
- color: var(--color-accent);
247
- }
248
-
249
- .section-description {
250
- font-size: var(--text-sm);
251
- color: var(--color-text-muted);
252
- margin-bottom: 16px;
253
- }
254
-
255
- .owner-list {
256
- display: flex;
257
- flex-direction: column;
258
- gap: 16px;
259
- }
260
-
261
- .owner-item {
262
- display: flex;
263
- flex-direction: column;
264
- gap: 8px;
265
- }
266
-
267
- .owner-info {
268
- display: flex;
269
- justify-content: space-between;
270
- align-items: center;
271
- }
272
-
273
- .owner-name {
274
- font-size: var(--text-sm);
275
- font-weight: 500;
276
- color: var(--color-text);
277
- }
278
-
279
- .owner-count {
280
- font-size: var(--text-xs);
281
- color: var(--color-text-muted);
282
- }
283
-
284
- .owner-bar {
285
- height: 8px;
286
- background: var(--color-bg-muted);
287
- border-radius: 4px;
288
- overflow: hidden;
289
- }
290
-
291
- .owner-bar-fill {
292
- height: 100%;
293
- background: var(--color-accent);
294
- border-radius: 4px;
295
- transition: width 0.3s ease;
296
- }
297
-
298
- .tags-grid {
299
- display: flex;
300
- flex-wrap: wrap;
301
- gap: 8px;
302
- }
303
-
304
- .tag-card {
305
- display: flex;
306
- align-items: center;
307
- gap: 8px;
308
- padding: 8px 12px;
309
- background: var(--color-bg-muted);
310
- border-radius: var(--radius-md);
311
- text-decoration: none;
312
- transition: background 0.15s ease;
313
- }
314
-
315
- .tag-card:hover {
316
- background: var(--color-surface-hover);
317
- }
318
-
319
- .tag-name {
320
- font-size: var(--text-sm);
321
- color: var(--color-text);
322
- }
323
-
324
- .tag-count {
325
- font-size: var(--text-xs);
326
- color: var(--color-accent);
327
- font-weight: 600;
328
- background: var(--color-accent-bg);
329
- padding: 2px 6px;
330
- border-radius: 4px;
331
- }
332
-
333
- .missing-section {
334
- grid-column: 1 / -1;
335
- }
336
-
337
- .missing-list {
338
- display: flex;
339
- flex-direction: column;
340
- gap: 12px;
341
- }
342
-
343
- .missing-item {
344
- display: flex;
345
- justify-content: space-between;
346
- align-items: center;
347
- padding: 12px 16px;
348
- background: var(--color-bg-muted);
349
- border-radius: var(--radius-md);
350
- border-left: 3px solid var(--color-warning);
351
- }
352
-
353
- .missing-route {
354
- display: flex;
355
- align-items: center;
356
- gap: 10px;
357
- font-size: var(--text-sm);
358
- color: var(--color-text);
359
- }
360
-
361
- .missing-route svg {
362
- width: 16px;
363
- height: 16px;
364
- color: var(--color-text-muted);
365
- }
366
-
367
- .suggested-path {
368
- font-size: var(--text-xs);
369
- color: var(--color-text-muted);
370
- }
371
-
372
- .coverage-footer {
373
- text-align: center;
374
- padding-top: 24px;
375
- border-top: 1px solid var(--color-border);
376
- }
377
-
378
- .timestamp {
379
- font-size: var(--text-xs);
380
- color: var(--color-text-muted);
381
- }
382
-
383
- @media (max-width: 768px) {
384
- .coverage-hero {
385
- flex-direction: column;
386
- text-align: center;
387
- }
388
-
389
- .coverage-stats {
390
- justify-content: center;
391
- }
392
-
393
- .missing-item {
394
- flex-direction: column;
395
- align-items: flex-start;
396
- gap: 8px;
397
- }
398
- }
399
- </style>
@@ -1,33 +0,0 @@
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>