@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,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
+ }