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.
@@ -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
+ }
@@ -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
- | { kind: "flow"; title?: string; screens: { id: string; label: string; render: ComponentType<ScreenProps> }[] };
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. */
@@ -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": [