@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.
- package/LICENSE +21 -0
- package/astro.config.mjs +20 -0
- package/dist/server/entry.mjs +1 -1
- package/dist/server/{manifest_bx_Ion-2.mjs → manifest_DHNGfdfn.mjs} +1 -1
- package/package.json +55 -51
- package/public/logo.svg +5 -0
- package/src/components/LinkIcon.astro +93 -0
- package/src/components/MockFormEditor.tsx +1280 -0
- package/src/components/MockPreview.astro +811 -0
- package/src/layouts/Layout.astro +123 -0
- package/src/pages/api/save-mock.ts +182 -0
- package/src/pages/coverage.astro +465 -0
- package/src/pages/editor.astro +33 -0
- package/src/pages/graph.astro +423 -0
- package/src/pages/impact.astro +555 -0
- package/src/pages/index.astro +227 -0
- package/src/pages/screen/[id].astro +299 -0
- package/src/styles/global.css +904 -0
- package/src/styles/mock-editor.css +1351 -0
- package/src/utils/impactAnalysis.ts +304 -0
- package/src/utils/loadCoverage.ts +30 -0
- package/src/utils/loadScreens.ts +18 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,123 @@
|
|
|
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
|
|
26
|
+
src="/logo.svg"
|
|
27
|
+
alt="Screenbook home"
|
|
28
|
+
class="logo-icon"
|
|
29
|
+
width="24"
|
|
30
|
+
height="24"
|
|
31
|
+
/>
|
|
32
|
+
Screenbook
|
|
33
|
+
</a>
|
|
34
|
+
<nav class="nav" aria-label="Main navigation">
|
|
35
|
+
<a
|
|
36
|
+
href="/"
|
|
37
|
+
class:list={["nav-link", { active: currentPage === "screens" }]}
|
|
38
|
+
aria-current={currentPage === "screens" ? "page" : undefined}
|
|
39
|
+
>
|
|
40
|
+
<svg
|
|
41
|
+
aria-hidden="true"
|
|
42
|
+
fill="none"
|
|
43
|
+
viewBox="0 0 24 24"
|
|
44
|
+
stroke="currentColor"
|
|
45
|
+
stroke-width="2"
|
|
46
|
+
>
|
|
47
|
+
<path
|
|
48
|
+
stroke-linecap="round"
|
|
49
|
+
stroke-linejoin="round"
|
|
50
|
+
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"
|
|
51
|
+
></path>
|
|
52
|
+
</svg>
|
|
53
|
+
Screens
|
|
54
|
+
</a>
|
|
55
|
+
<a
|
|
56
|
+
href="/graph"
|
|
57
|
+
class:list={["nav-link", { active: currentPage === "graph" }]}
|
|
58
|
+
aria-current={currentPage === "graph" ? "page" : undefined}
|
|
59
|
+
>
|
|
60
|
+
<svg
|
|
61
|
+
aria-hidden="true"
|
|
62
|
+
fill="none"
|
|
63
|
+
viewBox="0 0 24 24"
|
|
64
|
+
stroke="currentColor"
|
|
65
|
+
stroke-width="2"
|
|
66
|
+
>
|
|
67
|
+
<path
|
|
68
|
+
stroke-linecap="round"
|
|
69
|
+
stroke-linejoin="round"
|
|
70
|
+
d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"
|
|
71
|
+
></path>
|
|
72
|
+
</svg>
|
|
73
|
+
Graph
|
|
74
|
+
</a>
|
|
75
|
+
<a
|
|
76
|
+
href="/impact"
|
|
77
|
+
class:list={["nav-link", { active: currentPage === "impact" }]}
|
|
78
|
+
aria-current={currentPage === "impact" ? "page" : undefined}
|
|
79
|
+
>
|
|
80
|
+
<svg
|
|
81
|
+
aria-hidden="true"
|
|
82
|
+
fill="none"
|
|
83
|
+
viewBox="0 0 24 24"
|
|
84
|
+
stroke="currentColor"
|
|
85
|
+
stroke-width="2"
|
|
86
|
+
>
|
|
87
|
+
<path
|
|
88
|
+
stroke-linecap="round"
|
|
89
|
+
stroke-linejoin="round"
|
|
90
|
+
d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"
|
|
91
|
+
></path>
|
|
92
|
+
</svg>
|
|
93
|
+
Impact
|
|
94
|
+
</a>
|
|
95
|
+
<a
|
|
96
|
+
href="/coverage"
|
|
97
|
+
class:list={["nav-link", { active: currentPage === "coverage" }]}
|
|
98
|
+
aria-current={currentPage === "coverage" ? "page" : undefined}
|
|
99
|
+
>
|
|
100
|
+
<svg
|
|
101
|
+
aria-hidden="true"
|
|
102
|
+
fill="none"
|
|
103
|
+
viewBox="0 0 24 24"
|
|
104
|
+
stroke="currentColor"
|
|
105
|
+
stroke-width="2"
|
|
106
|
+
>
|
|
107
|
+
<path
|
|
108
|
+
stroke-linecap="round"
|
|
109
|
+
stroke-linejoin="round"
|
|
110
|
+
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"
|
|
111
|
+
></path>
|
|
112
|
+
</svg>
|
|
113
|
+
Coverage
|
|
114
|
+
</a>
|
|
115
|
+
</nav>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</header>
|
|
119
|
+
<main id="main-content">
|
|
120
|
+
<slot />
|
|
121
|
+
</main>
|
|
122
|
+
</body>
|
|
123
|
+
</html>
|
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
}
|