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,133 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { ClientMessage, VerifyKitResult } from "../../shared/protocol.ts";
|
|
3
|
+
import { BrowseButton } from "./FolderField.tsx";
|
|
4
|
+
import { readPref, writePref } from "./prefs.ts";
|
|
5
|
+
import styles from "./Shell.module.css";
|
|
6
|
+
|
|
7
|
+
export interface VerifyKitProps {
|
|
8
|
+
workspace: string;
|
|
9
|
+
/** The product repo the project designs for: where the kit belongs, unless the user chose a folder before. */
|
|
10
|
+
repo: string | undefined;
|
|
11
|
+
progress: { step: string; done?: number; total?: number } | null;
|
|
12
|
+
result: VerifyKitResult | null;
|
|
13
|
+
send: (message: ClientMessage) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The product folder the kit was last written to, per project. */
|
|
17
|
+
const targetPref = (workspace: string) => `verify-target:${workspace}`;
|
|
18
|
+
|
|
19
|
+
function plural(count: number, one: string, many = `${one}s`): string {
|
|
20
|
+
return `${count} ${count === 1 ? one : many}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The verification kit tab of the Export dialog: builds the kit (a runnable copy of the design, screenshots of
|
|
25
|
+
* every view's screens and the scripts that check an implementation against them) as a zip or into the product's
|
|
26
|
+
* folder.
|
|
27
|
+
*/
|
|
28
|
+
export function VerifyKit({ workspace, repo, progress, result: latest, send }: VerifyKitProps) {
|
|
29
|
+
const [target, setTarget] = useState(() => readPref(targetPref(workspace)) ?? repo ?? "");
|
|
30
|
+
const [browseError, setBrowseError] = useState<string | null>(null);
|
|
31
|
+
// Only show results of kits built while this dialog is open.
|
|
32
|
+
const [stale] = useState(latest);
|
|
33
|
+
const result = latest === stale ? null : latest;
|
|
34
|
+
const busy = progress !== null;
|
|
35
|
+
|
|
36
|
+
const build = (project?: string) => {
|
|
37
|
+
if (project !== undefined) writePref(targetPref(workspace), project);
|
|
38
|
+
send({ type: "verify-kit", project });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className={styles.kit}>
|
|
43
|
+
<p className={styles.kitIntro}>
|
|
44
|
+
Proof that a feature was built as designed. The kit holds a runnable copy of the design, screenshots of every
|
|
45
|
+
view's screens at the widths the view asks for, and two scripts: one captures the implementation the same way,
|
|
46
|
+
the other compares the images pixel for pixel. Give it to the agent that builds the feature; the target is 0%
|
|
47
|
+
difference, and anything allowed to differ is a named exception.
|
|
48
|
+
</p>
|
|
49
|
+
<form
|
|
50
|
+
className={styles.kitWrite}
|
|
51
|
+
onSubmit={(event) => {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
build(target);
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
<label htmlFor="verify-target">Write into the product's folder</label>
|
|
57
|
+
<div className={styles.kitWriteRow}>
|
|
58
|
+
<input
|
|
59
|
+
id="verify-target"
|
|
60
|
+
value={target}
|
|
61
|
+
onChange={(event) => setTarget(event.target.value)}
|
|
62
|
+
placeholder="Browse, or paste the folder's path"
|
|
63
|
+
spellCheck={false}
|
|
64
|
+
/>
|
|
65
|
+
<BrowseButton
|
|
66
|
+
title="Choose the product's folder"
|
|
67
|
+
start={target}
|
|
68
|
+
onPick={setTarget}
|
|
69
|
+
onError={setBrowseError}
|
|
70
|
+
/>
|
|
71
|
+
<button type="submit" className={styles.chromeButton} disabled={!target.trim() || busy}>
|
|
72
|
+
Write kit
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
<span className={styles.kitNote}>
|
|
76
|
+
The kit goes into <code>basis-verify/</code> there. Writing it again replaces what Basis generated and keeps
|
|
77
|
+
the project's targets, exceptions and captures.
|
|
78
|
+
</span>
|
|
79
|
+
{browseError && <p className={styles.kitError}>{browseError}</p>}
|
|
80
|
+
</form>
|
|
81
|
+
{progress && (
|
|
82
|
+
<p className={styles.kitNote} role="status">
|
|
83
|
+
{progress.step}
|
|
84
|
+
{progress.total ? ` ${progress.done ?? 0} of ${progress.total}` : ""}…
|
|
85
|
+
</p>
|
|
86
|
+
)}
|
|
87
|
+
{result && !result.ok && <p className={styles.kitError}>{result.error}</p>}
|
|
88
|
+
{result?.ok && <Summary result={result} />}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function Summary({ result }: { result: Extract<VerifyKitResult, { ok: true }> }) {
|
|
94
|
+
const { summary, dir } = result;
|
|
95
|
+
if (summary.views === 0) {
|
|
96
|
+
return (
|
|
97
|
+
<p className={styles.kitWarning}>
|
|
98
|
+
There are no views to capture. Ask the agent for a view of a screen in the product, then build the kit.
|
|
99
|
+
</p>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return (
|
|
103
|
+
<>
|
|
104
|
+
<p className={styles.kitSuccess}>
|
|
105
|
+
Captured {plural(summary.shots, "shot")} of {plural(summary.screens, "screen")} in{" "}
|
|
106
|
+
{plural(summary.views, "view")} with {summary.browser}.{" "}
|
|
107
|
+
{dir ? (
|
|
108
|
+
<>
|
|
109
|
+
Wrote the kit to <code>{dir}</code>.
|
|
110
|
+
</>
|
|
111
|
+
) : (
|
|
112
|
+
<a href="/export/verification-kit.zip" download>
|
|
113
|
+
Download the .zip
|
|
114
|
+
</a>
|
|
115
|
+
)}
|
|
116
|
+
</p>
|
|
117
|
+
{summary.failed.length > 0 && (
|
|
118
|
+
<p className={styles.kitWarning}>
|
|
119
|
+
{plural(summary.failed.length, "shot")} of screens that failed to render: {summary.failed.join(", ")}.
|
|
120
|
+
</p>
|
|
121
|
+
)}
|
|
122
|
+
{summary.unstable.length > 0 && (
|
|
123
|
+
<p className={styles.kitWarning}>
|
|
124
|
+
{plural(summary.unstable.length, "shot")} came out differently on a second capture, so nothing can match them
|
|
125
|
+
exactly: {summary.unstable.join(", ")}. Look for clocks, random values or animations in those screens.
|
|
126
|
+
</p>
|
|
127
|
+
)}
|
|
128
|
+
{summary.problems.length > 0 && (
|
|
129
|
+
<p className={styles.kitWarning}>Specimens that could not be loaded: {summary.problems.join("; ")}.</p>
|
|
130
|
+
)}
|
|
131
|
+
</>
|
|
132
|
+
);
|
|
133
|
+
}
|
package/template/CHANGES.md
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
# Starter template changes
|
|
2
|
-
|
|
3
|
-
What changed in Basis' starter components, patterns and views, by template version. Basis updates the files a
|
|
4
|
-
workspace never changed by itself; the agent brings these changes into the ones it did change, keeping the
|
|
5
|
-
design system's own look.
|
|
6
|
-
|
|
7
|
-
## 1
|
|
8
|
-
|
|
9
|
-
- Reduced motion in every component and pattern: under `@media (prefers-reduced-motion: reduce)` nothing slides,
|
|
10
|
-
zooms, shimmers or loops (presses do not shrink, cards do not lift, the indeterminate progress bar and the
|
|
11
|
-
skeleton stand still, tooltips, toasts and dialogs only fade, the tab indicator and underlines jump). Spinners
|
|
12
|
-
keep turning, since they are the only sign that work goes on. See the `@media (prefers-reduced-motion: reduce)`
|
|
13
|
-
block at the end of each stylesheet.
|
|
14
|
-
- Field: the label is a `<label for>` above the control, then the hint, then the error, then the control. The hint
|
|
15
|
-
and the error are tied to the control with `aria-describedby`, the control gets `aria-invalid` from the Field's
|
|
16
|
-
error (through a React context, so TextInput, Textarea and Select need no extra props), and the error has an
|
|
17
|
-
icon as well as the danger colour.
|
|
18
|
-
- Navbar responds to its own width, as a size container: from 1024px everything sits in the bar; from 800px the
|
|
19
|
-
search becomes a button that opens the field below the bar, with focus in it; under 800px the links, the search
|
|
20
|
-
and the actions move into a menu behind a menu button. `compact` still forces the narrow layout, and the menu
|
|
21
|
-
keeps its own open state unless `menuOpen` is given.
|
|
22
|
-
- AppShell is a size container named `app-shell`; SidebarNav becomes its icon rail inside a narrow one (under
|
|
23
|
-
640px). The rail's grid column is pinned so its icons stay centred.
|
|
24
|
-
- Small text that used `textSubtle` (SidebarNav section titles, StatCard hints) uses `textMuted`, since subtle is
|
|
25
|
-
held to 3:1 and small text needs 4.5:1.
|
|
1
|
+
# Starter template changes
|
|
2
|
+
|
|
3
|
+
What changed in Basis' starter components, patterns and views, by template version. Basis updates the files a
|
|
4
|
+
workspace never changed by itself; the agent brings these changes into the ones it did change, keeping the
|
|
5
|
+
design system's own look.
|
|
6
|
+
|
|
7
|
+
## 1
|
|
8
|
+
|
|
9
|
+
- Reduced motion in every component and pattern: under `@media (prefers-reduced-motion: reduce)` nothing slides,
|
|
10
|
+
zooms, shimmers or loops (presses do not shrink, cards do not lift, the indeterminate progress bar and the
|
|
11
|
+
skeleton stand still, tooltips, toasts and dialogs only fade, the tab indicator and underlines jump). Spinners
|
|
12
|
+
keep turning, since they are the only sign that work goes on. See the `@media (prefers-reduced-motion: reduce)`
|
|
13
|
+
block at the end of each stylesheet.
|
|
14
|
+
- Field: the label is a `<label for>` above the control, then the hint, then the error, then the control. The hint
|
|
15
|
+
and the error are tied to the control with `aria-describedby`, the control gets `aria-invalid` from the Field's
|
|
16
|
+
error (through a React context, so TextInput, Textarea and Select need no extra props), and the error has an
|
|
17
|
+
icon as well as the danger colour.
|
|
18
|
+
- Navbar responds to its own width, as a size container: from 1024px everything sits in the bar; from 800px the
|
|
19
|
+
search becomes a button that opens the field below the bar, with focus in it; under 800px the links, the search
|
|
20
|
+
and the actions move into a menu behind a menu button. `compact` still forces the narrow layout, and the menu
|
|
21
|
+
keeps its own open state unless `menuOpen` is given.
|
|
22
|
+
- AppShell is a size container named `app-shell`; SidebarNav becomes its icon rail inside a narrow one (under
|
|
23
|
+
640px). The rail's grid column is pinned so its icons stay centred.
|
|
24
|
+
- Small text that used `textSubtle` (SidebarNav section titles, StatCard hints) uses `textMuted`, since subtle is
|
|
25
|
+
held to 3:1 and small text needs 4.5:1.
|
|
@@ -57,7 +57,30 @@ export type SpecimenBlock =
|
|
|
57
57
|
* Whole screens of one flow, for views: the gallery shows one at a time, full width, with a strip to jump
|
|
58
58
|
* between them. The first screen shows first; each moves on with `go(id)`.
|
|
59
59
|
*/
|
|
60
|
-
| {
|
|
60
|
+
| {
|
|
61
|
+
kind: "flow";
|
|
62
|
+
title?: string;
|
|
63
|
+
screens: { id: string; label: string; render: ComponentType<ScreenProps> }[];
|
|
64
|
+
/** How the verification kit captures each screen; Basis' defaults when left out (see Capture). */
|
|
65
|
+
capture?: Capture;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The screenshots a view's screens are checked against during development. Every screen is captured at every
|
|
70
|
+
* width (and theme), and the implementation must match them pixel for pixel, so a screen must render the same
|
|
71
|
+
* every time: no clock, no randomness, no animation that does not settle.
|
|
72
|
+
*/
|
|
73
|
+
export interface Capture {
|
|
74
|
+
/** Viewport widths in CSS pixels; Basis' default is [375, 768, 1440]. */
|
|
75
|
+
widths?: number[];
|
|
76
|
+
/**
|
|
77
|
+
* "full" (the default) captures the whole page, which must then also be as tall as the design. A number
|
|
78
|
+
* captures exactly that viewport height, for screens such as app shells that scroll inside.
|
|
79
|
+
*/
|
|
80
|
+
height?: "full" | number;
|
|
81
|
+
/** Themes to capture; Basis' default is ["light"]. */
|
|
82
|
+
themes?: ("light" | "dark")[];
|
|
83
|
+
}
|
|
61
84
|
|
|
62
85
|
export interface Specimen {
|
|
63
86
|
/** Anchor and sidebar id; defaults to the title in kebab-case. */
|
package/template/history.json
CHANGED
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"components/specimen.ts": [
|
|
55
55
|
"03370daf9bba1c4329d3dac7b76199970cab9c11a6ea2d23e1a484ead80d36d6",
|
|
56
56
|
"1f826f8c8f183e311fd20cca0e5e3c138ac2775c4e156862600c24e9dad7cf7d",
|
|
57
|
+
"2f4b9f188c981a690c271b100cf4174339637d4bd8f27b3daa200ac089b6256e",
|
|
57
58
|
"e04c6fc2a3e85090b7672d229d95ca8c5a42009f6397332230d324357b993169"
|
|
58
59
|
],
|
|
59
60
|
"patterns/AppShell.module.css": [
|