@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.
- package/README.md +30 -0
- package/dist/client/_astro/coverage.BzPU-EGZ.css +1 -0
- package/dist/server/entry.mjs +1 -1
- package/dist/server/{manifest_smcahUO6.mjs → manifest_BGl49hHW.mjs} +1 -1
- package/dist/server/pages/coverage.astro.mjs +1 -1
- package/dist/server/pages/editor.astro.mjs +1 -1
- package/dist/server/pages/graph.astro.mjs +1 -1
- package/dist/server/pages/impact.astro.mjs +1 -1
- package/dist/server/pages/index.astro.mjs +1 -1
- package/dist/server/pages/screen/_id_.astro.mjs +1 -1
- package/package.json +5 -1
- package/.astro/content-assets.mjs +0 -1
- package/.astro/content-modules.mjs +0 -1
- package/.astro/content.d.ts +0 -199
- package/.astro/types.d.ts +0 -2
- package/.prettierrc +0 -15
- package/CHANGELOG.md +0 -77
- package/astro.config.mjs +0 -20
- package/dist/client/_astro/coverage.DLKSOM4m.css +0 -1
- package/public/logo.svg +0 -5
- package/src/components/MockFormEditor.tsx +0 -1280
- package/src/components/MockPreview.astro +0 -811
- package/src/layouts/Layout.astro +0 -77
- package/src/pages/api/save-mock.ts +0 -182
- package/src/pages/coverage.astro +0 -399
- package/src/pages/editor.astro +0 -33
- package/src/pages/graph.astro +0 -368
- package/src/pages/impact.astro +0 -462
- package/src/pages/index.astro +0 -176
- package/src/pages/screen/[id].astro +0 -195
- package/src/styles/global.css +0 -904
- package/src/styles/mock-editor.css +0 -1351
- package/src/utils/impactAnalysis.ts +0 -304
- package/src/utils/loadCoverage.ts +0 -30
- package/src/utils/loadScreens.ts +0 -18
- package/tsconfig.json +0 -10
- /package/dist/server/chunks/{loadScreens_CkCqdbH2.mjs → loadScreens_B8bVK3q5.mjs} +0 -0
package/src/layouts/Layout.astro
DELETED
|
@@ -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
|
-
}
|
package/src/pages/coverage.astro
DELETED
|
@@ -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>
|
package/src/pages/editor.astro
DELETED
|
@@ -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>
|