basis-design 0.1.1 → 0.2.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/README.md +2 -1
- package/capture.html +12 -0
- package/package.json +72 -70
- package/server/brief.ts +14 -3
- package/server/capture.ts +128 -0
- package/server/folders.ts +125 -0
- package/server/index.ts +112 -1
- package/server/project.ts +7 -1
- package/server/template.ts +168 -168
- package/server/verify-kit/capture.mjs +206 -0
- package/server/verify-kit/compare.d.mts +39 -0
- package/server/verify-kit/compare.mjs +307 -0
- package/server/verify.ts +383 -0
- package/shared/capture.ts +75 -0
- package/shared/protocol.ts +29 -1
- package/src/Workbench.tsx +1 -0
- package/src/bridge.ts +173 -163
- package/src/capture/capture.css +50 -0
- package/src/capture/main.tsx +146 -0
- package/src/preview/specimens.tsx +21 -0
- package/src/shell/ExportDialog.tsx +41 -4
- package/src/shell/FolderField.tsx +131 -0
- package/src/shell/ProjectMenu.tsx +20 -18
- package/src/shell/Shell.module.css +1146 -1076
- package/src/shell/VerifyKit.tsx +133 -0
- package/template/CHANGES.md +25 -25
- package/template/components/specimen.ts +24 -1
- package/template/history.json +1 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Capturing views for verification: each screen of a view is photographed at fixed sizes, and the implementation
|
|
3
|
+
* of the feature is later checked against those screenshots pixel for pixel. Shared by the capture page (which
|
|
4
|
+
* renders one screen) and the server (which takes the screenshots).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type CaptureTheme = "light" | "dark";
|
|
8
|
+
|
|
9
|
+
/** What a flow block may say about capturing (template/components/specimen.ts, `Capture`). */
|
|
10
|
+
export interface CaptureOptions {
|
|
11
|
+
widths?: number[];
|
|
12
|
+
height?: "full" | number;
|
|
13
|
+
themes?: CaptureTheme[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** One screenshot of one screen. */
|
|
17
|
+
export interface CaptureShot {
|
|
18
|
+
width: number;
|
|
19
|
+
/** The viewport's height. With `fullPage` the image is as tall as the page; without, exactly this tall. */
|
|
20
|
+
viewportHeight: number;
|
|
21
|
+
fullPage: boolean;
|
|
22
|
+
theme: CaptureTheme;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_CAPTURE_WIDTHS = [375, 768, 1440];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The viewport height that goes with a width, for full-page captures too: elements sized in vh (an app shell,
|
|
29
|
+
* a full-height hero) take it, so the design and the implementation must be photographed with the same one.
|
|
30
|
+
*/
|
|
31
|
+
export function viewportHeightFor(width: number): number {
|
|
32
|
+
if (width <= 480) return 812;
|
|
33
|
+
if (width <= 1024) return 1024;
|
|
34
|
+
return 900;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const MIN_SIZE = 200;
|
|
38
|
+
const MAX_SIZE = 4000;
|
|
39
|
+
|
|
40
|
+
/** The screenshots a flow block asks for, with Basis' defaults filled in and nonsense left out. */
|
|
41
|
+
export function captureShots(options: CaptureOptions | undefined): CaptureShot[] {
|
|
42
|
+
const widths = [...new Set((options?.widths?.length ? options.widths : DEFAULT_CAPTURE_WIDTHS).map(Math.round))]
|
|
43
|
+
.filter((width) => width >= MIN_SIZE && width <= MAX_SIZE)
|
|
44
|
+
.sort((a, b) => a - b);
|
|
45
|
+
const themes = [...new Set(options?.themes?.length ? options.themes : (["light"] as CaptureTheme[]))].filter(
|
|
46
|
+
(theme) => theme === "light" || theme === "dark",
|
|
47
|
+
);
|
|
48
|
+
const fixed = typeof options?.height === "number" ? Math.round(options.height) : null;
|
|
49
|
+
const height = fixed !== null && fixed >= MIN_SIZE && fixed <= MAX_SIZE ? fixed : null;
|
|
50
|
+
return themes.flatMap((theme) =>
|
|
51
|
+
widths.map((width) => ({
|
|
52
|
+
width,
|
|
53
|
+
viewportHeight: height ?? viewportHeightFor(width),
|
|
54
|
+
fullPage: height === null,
|
|
55
|
+
theme,
|
|
56
|
+
})),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The file name of a shot inside the kit: "screens/<view>/<screen>@<width>[-dark].png". */
|
|
61
|
+
export function shotFile(view: string, screen: string, shot: CaptureShot): string {
|
|
62
|
+
const theme = shot.theme === "light" ? "" : `-${shot.theme}`;
|
|
63
|
+
const height = shot.fullPage ? "" : `x${shot.viewportHeight}`;
|
|
64
|
+
return `screens/${view}/${screen}@${shot.width}${height}${theme}.png`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** What the capture page reports about the views it can render (`?list`). */
|
|
68
|
+
export interface CaptureView {
|
|
69
|
+
id: string;
|
|
70
|
+
title: string;
|
|
71
|
+
screens: { id: string; label: string; shots: CaptureShot[] }[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The attribute the capture page sets on <html> once a screen has settled and can be photographed. */
|
|
75
|
+
export const READY_ATTRIBUTE = "data-basis-ready";
|
package/shared/protocol.ts
CHANGED
|
@@ -115,6 +115,29 @@ export interface ExportWriteResult {
|
|
|
115
115
|
error?: string;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/** What a finished verification kit holds, for the Export dialog to report. */
|
|
119
|
+
export interface VerifyKitSummary {
|
|
120
|
+
/** The browser that took the screenshots, with its version. */
|
|
121
|
+
browser: string;
|
|
122
|
+
views: number;
|
|
123
|
+
screens: number;
|
|
124
|
+
shots: number;
|
|
125
|
+
/** Shots whose two captures differed: those screens do not render the same twice. */
|
|
126
|
+
unstable: string[];
|
|
127
|
+
/** Shots of screens that threw while rendering. */
|
|
128
|
+
failed: string[];
|
|
129
|
+
/** Specimen files that could not be loaded. */
|
|
130
|
+
problems: string[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type VerifyKitResult =
|
|
134
|
+
| {
|
|
135
|
+
ok: true;
|
|
136
|
+
summary: VerifyKitSummary;
|
|
137
|
+
/** Where it was written; absent for a zip, which is then ready to download. */ dir?: string;
|
|
138
|
+
}
|
|
139
|
+
| { ok: false; error: string };
|
|
140
|
+
|
|
118
141
|
export interface DesignState {
|
|
119
142
|
design: Design;
|
|
120
143
|
/** Validation errors for the file on disk; null when it is valid. The preview keeps the last valid design. */
|
|
@@ -186,7 +209,10 @@ export type ServerMessage =
|
|
|
186
209
|
| { type: "chat-remove"; id: string }
|
|
187
210
|
| { type: "snapshot"; snapshot: Snapshot }
|
|
188
211
|
| { type: "export-manifest"; folder: string; files: { path: string; bytes: number }[] }
|
|
189
|
-
| { type: "export-result"; result: ExportWriteResult }
|
|
212
|
+
| { type: "export-result"; result: ExportWriteResult }
|
|
213
|
+
/** How far the verification kit has come; `done`/`total` while it takes screenshots. */
|
|
214
|
+
| { type: "verify-progress"; step: string; done?: number; total?: number }
|
|
215
|
+
| { type: "verify-result"; result: VerifyKitResult };
|
|
190
216
|
|
|
191
217
|
export type ClientMessage =
|
|
192
218
|
/** With `alternatives` (2 or 3), the agent makes that many takes on the prompt to choose between. */
|
|
@@ -218,6 +244,8 @@ export type ClientMessage =
|
|
|
218
244
|
| { type: "preview-errors"; errors: string[]; visible: boolean; checks?: string[]; checking?: boolean }
|
|
219
245
|
| { type: "export-manifest" }
|
|
220
246
|
| { type: "export-write"; target: string; overwrite: boolean }
|
|
247
|
+
/** Builds the verification kit: into `<project>/basis-verify/` with `project`, else as a zip to download. */
|
|
248
|
+
| { type: "verify-kit"; project?: string }
|
|
221
249
|
| { type: "project-switch"; id: string }
|
|
222
250
|
/** Creates a project from Basis' defaults, or as a copy of the open project's design system, and opens it. */
|
|
223
251
|
/**
|
package/src/Workbench.tsx
CHANGED
|
@@ -336,6 +336,7 @@ export function Workbench({ state, send }: { state: BridgeState; send: (message:
|
|
|
336
336
|
result={state.exportResult}
|
|
337
337
|
send={send}
|
|
338
338
|
onClose={() => setExporting(false)}
|
|
339
|
+
verify={{ repo: state.project?.repo, progress: state.verifyProgress, result: state.verifyResult }}
|
|
339
340
|
/>
|
|
340
341
|
)}
|
|
341
342
|
</div>
|
package/src/bridge.ts
CHANGED
|
@@ -1,163 +1,173 @@
|
|
|
1
|
-
import { useCallback, useEffect, useReducer, useRef } from "react";
|
|
2
|
-
import type {
|
|
3
|
-
AgentState,
|
|
4
|
-
Baseline,
|
|
5
|
-
ChatItem,
|
|
6
|
-
ClientMessage,
|
|
7
|
-
DesignState,
|
|
8
|
-
HistoryEntry,
|
|
9
|
-
ExportWriteResult,
|
|
10
|
-
ProjectEntry,
|
|
11
|
-
ServerMessage,
|
|
12
|
-
Snapshot,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
project
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
snapshots
|
|
32
|
-
|
|
33
|
-
review
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
case "
|
|
99
|
-
return { ...state,
|
|
100
|
-
case "
|
|
101
|
-
return { ...state,
|
|
102
|
-
case "
|
|
103
|
-
return { ...state,
|
|
104
|
-
case "
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return { ...state, chat };
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return { ...state,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
1
|
+
import { useCallback, useEffect, useReducer, useRef } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
AgentState,
|
|
4
|
+
Baseline,
|
|
5
|
+
ChatItem,
|
|
6
|
+
ClientMessage,
|
|
7
|
+
DesignState,
|
|
8
|
+
HistoryEntry,
|
|
9
|
+
ExportWriteResult,
|
|
10
|
+
ProjectEntry,
|
|
11
|
+
ServerMessage,
|
|
12
|
+
Snapshot,
|
|
13
|
+
VerifyKitResult,
|
|
14
|
+
} from "../shared/protocol.ts";
|
|
15
|
+
import type { ReviewState } from "../shared/review.ts";
|
|
16
|
+
|
|
17
|
+
export interface BridgeState {
|
|
18
|
+
connected: boolean;
|
|
19
|
+
/** The open project, and every project on the list. */
|
|
20
|
+
project: ProjectEntry | null;
|
|
21
|
+
projects: ProjectEntry[];
|
|
22
|
+
/** The project being opened, while Basis switches to it. */
|
|
23
|
+
switching: ProjectEntry | null;
|
|
24
|
+
/** Why the last project request failed, until the next one. */
|
|
25
|
+
/** The last failed project request's answer; a new object each time, so a repeated message is a new answer. */
|
|
26
|
+
projectError: { message: string } | null;
|
|
27
|
+
design: DesignState | null;
|
|
28
|
+
history: HistoryEntry[];
|
|
29
|
+
agent: AgentState | null;
|
|
30
|
+
chat: ChatItem[];
|
|
31
|
+
/** History snapshots fetched for inspection, by entry id. */
|
|
32
|
+
snapshots: Record<string, Snapshot>;
|
|
33
|
+
/** The open review, if any. */
|
|
34
|
+
review: ReviewState | null;
|
|
35
|
+
/** Past versions materialised for reviews, by commit id. */
|
|
36
|
+
baselines: Record<string, Baseline>;
|
|
37
|
+
/** Port of the sandboxed preview origin, from the server. */
|
|
38
|
+
previewPort: number | null;
|
|
39
|
+
exportManifest: { folder: string; files: { path: string; bytes: number }[] } | null;
|
|
40
|
+
exportResult: ExportWriteResult | null;
|
|
41
|
+
/** The verification kit being built (its current step), or null. */
|
|
42
|
+
verifyProgress: { step: string; done?: number; total?: number } | null;
|
|
43
|
+
verifyResult: VerifyKitResult | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const initial: BridgeState = {
|
|
47
|
+
connected: false,
|
|
48
|
+
project: null,
|
|
49
|
+
projects: [],
|
|
50
|
+
switching: null,
|
|
51
|
+
projectError: null,
|
|
52
|
+
design: null,
|
|
53
|
+
history: [],
|
|
54
|
+
agent: null,
|
|
55
|
+
chat: [],
|
|
56
|
+
snapshots: {},
|
|
57
|
+
review: null,
|
|
58
|
+
baselines: {},
|
|
59
|
+
previewPort: null,
|
|
60
|
+
exportManifest: null,
|
|
61
|
+
exportResult: null,
|
|
62
|
+
verifyProgress: null,
|
|
63
|
+
verifyResult: null,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type Action = ServerMessage | { type: "connected"; connected: boolean };
|
|
67
|
+
|
|
68
|
+
function reduce(state: BridgeState, action: Action): BridgeState {
|
|
69
|
+
switch (action.type) {
|
|
70
|
+
case "connected":
|
|
71
|
+
return { ...state, connected: action.connected };
|
|
72
|
+
case "hello": {
|
|
73
|
+
// A hello for another project replaces everything that belonged to the previous one.
|
|
74
|
+
const same = state.project?.id === action.project.id;
|
|
75
|
+
return {
|
|
76
|
+
...(same ? state : { ...initial, projectError: state.projectError }),
|
|
77
|
+
connected: true,
|
|
78
|
+
project: action.project,
|
|
79
|
+
projects: action.projects,
|
|
80
|
+
switching: null,
|
|
81
|
+
previewPort: action.previewPort,
|
|
82
|
+
design: action.design,
|
|
83
|
+
history: action.history,
|
|
84
|
+
agent: action.agent,
|
|
85
|
+
chat: action.chat,
|
|
86
|
+
review: action.review,
|
|
87
|
+
// The server may have restarted and removed its copies of past versions; ask for them again.
|
|
88
|
+
baselines: {},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
case "projects":
|
|
92
|
+
// The open project's entry can change too (a product repo linked).
|
|
93
|
+
return {
|
|
94
|
+
...state,
|
|
95
|
+
projects: action.projects,
|
|
96
|
+
project: action.projects.find((entry) => entry.id === state.project?.id) ?? state.project,
|
|
97
|
+
};
|
|
98
|
+
case "project-switching":
|
|
99
|
+
return { ...state, switching: action.project, projectError: null };
|
|
100
|
+
case "project-error":
|
|
101
|
+
return { ...state, projectError: { message: action.message } };
|
|
102
|
+
case "design":
|
|
103
|
+
return { ...state, design: action.design };
|
|
104
|
+
case "history":
|
|
105
|
+
return { ...state, history: action.history };
|
|
106
|
+
case "agent":
|
|
107
|
+
return { ...state, agent: action.agent };
|
|
108
|
+
case "chat-reset":
|
|
109
|
+
return { ...state, chat: [] };
|
|
110
|
+
case "chat-upsert": {
|
|
111
|
+
const index = state.chat.findIndex((item) => item.id === action.item.id);
|
|
112
|
+
if (index === -1) return { ...state, chat: [...state.chat, action.item] };
|
|
113
|
+
const chat = state.chat.slice();
|
|
114
|
+
chat[index] = action.item;
|
|
115
|
+
return { ...state, chat };
|
|
116
|
+
}
|
|
117
|
+
case "export-manifest":
|
|
118
|
+
return { ...state, exportManifest: { folder: action.folder, files: action.files } };
|
|
119
|
+
case "export-result":
|
|
120
|
+
return { ...state, exportResult: action.result };
|
|
121
|
+
case "verify-progress":
|
|
122
|
+
return { ...state, verifyProgress: { step: action.step, done: action.done, total: action.total } };
|
|
123
|
+
case "verify-result":
|
|
124
|
+
return { ...state, verifyProgress: null, verifyResult: action.result };
|
|
125
|
+
case "review":
|
|
126
|
+
// A review that ends takes the server's copies of past versions with it.
|
|
127
|
+
return { ...state, review: action.review, baselines: action.review ? state.baselines : {} };
|
|
128
|
+
case "chat-remove":
|
|
129
|
+
return { ...state, chat: state.chat.filter((item) => item.id !== action.id) };
|
|
130
|
+
case "baseline":
|
|
131
|
+
return { ...state, baselines: { ...state.baselines, [action.baseline.id]: action.baseline } };
|
|
132
|
+
case "snapshot":
|
|
133
|
+
return { ...state, snapshots: { ...state.snapshots, [action.snapshot.id]: action.snapshot } };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Keeps a WebSocket to the Basis server open and mirrors its state. */
|
|
138
|
+
export function useBridge(): { state: BridgeState; send: (message: ClientMessage) => void } {
|
|
139
|
+
const [state, dispatch] = useReducer(reduce, initial);
|
|
140
|
+
const socket = useRef<WebSocket | null>(null);
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
let closed = false;
|
|
144
|
+
let retry: ReturnType<typeof setTimeout> | undefined;
|
|
145
|
+
const connect = () => {
|
|
146
|
+
const ws = new WebSocket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/bridge`);
|
|
147
|
+
socket.current = ws;
|
|
148
|
+
ws.onmessage = (event) => dispatch(JSON.parse(String(event.data)) as ServerMessage);
|
|
149
|
+
ws.onclose = () => {
|
|
150
|
+
dispatch({ type: "connected", connected: false });
|
|
151
|
+
if (!closed) retry = setTimeout(connect, 1000);
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
connect();
|
|
155
|
+
return () => {
|
|
156
|
+
closed = true;
|
|
157
|
+
clearTimeout(retry);
|
|
158
|
+
const ws = socket.current;
|
|
159
|
+
if (!ws) return;
|
|
160
|
+
ws.onmessage = null;
|
|
161
|
+
ws.onclose = null;
|
|
162
|
+
// Closing a socket that is still connecting logs a browser warning, so let it open first.
|
|
163
|
+
if (ws.readyState === WebSocket.CONNECTING) ws.onopen = () => ws.close();
|
|
164
|
+
else ws.close();
|
|
165
|
+
};
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
const send = useCallback((message: ClientMessage) => {
|
|
169
|
+
if (socket.current?.readyState === WebSocket.OPEN) socket.current.send(JSON.stringify(message));
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
return { state, send };
|
|
173
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The page a captured screen renders on, and nothing else: no gallery around it, no transitions, no caret. The
|
|
3
|
+
* base below is what a screen inherits; an implementation that is checked against the screenshots needs the
|
|
4
|
+
* same (the kit's manifest repeats it as `base`).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
*,
|
|
8
|
+
*::before,
|
|
9
|
+
*::after {
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
transition: none !important;
|
|
12
|
+
animation: none !important;
|
|
13
|
+
caret-color: transparent !important;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
html,
|
|
17
|
+
body {
|
|
18
|
+
margin: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
html {
|
|
22
|
+
scrollbar-width: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
html::-webkit-scrollbar {
|
|
26
|
+
display: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.ds-root {
|
|
30
|
+
min-height: 100vh;
|
|
31
|
+
background: var(--color-bg);
|
|
32
|
+
color: var(--color-text);
|
|
33
|
+
font-family: var(--font-sans);
|
|
34
|
+
font-size: var(--text-base);
|
|
35
|
+
line-height: var(--leading-normal);
|
|
36
|
+
color-scheme: light;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.ds-root[data-theme="dark"] {
|
|
40
|
+
color-scheme: dark;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.capture-problem {
|
|
44
|
+
padding: 24px;
|
|
45
|
+
font:
|
|
46
|
+
14px/1.5 ui-monospace,
|
|
47
|
+
Consolas,
|
|
48
|
+
monospace;
|
|
49
|
+
color: #b42318;
|
|
50
|
+
}
|