basis-design 0.0.0-stage → 0.1.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.
Files changed (147) hide show
  1. package/LICENSE +177 -0
  2. package/NOTICE +7 -0
  3. package/README.md +135 -3
  4. package/bin/basis.js +6 -0
  5. package/index.html +16 -0
  6. package/package.json +66 -4
  7. package/preview.html +12 -0
  8. package/server/agent.ts +845 -0
  9. package/server/brief.ts +355 -0
  10. package/server/chatlog.ts +57 -0
  11. package/server/exec.ts +77 -0
  12. package/server/export.ts +180 -0
  13. package/server/guidelines.md +379 -0
  14. package/server/history.ts +107 -0
  15. package/server/index.ts +414 -0
  16. package/server/paths.ts +57 -0
  17. package/server/previews.ts +71 -0
  18. package/server/project.ts +151 -0
  19. package/server/projects.ts +213 -0
  20. package/server/reach.ts +192 -0
  21. package/server/rebase.ts +46 -0
  22. package/server/reviews.ts +156 -0
  23. package/server/setup.ts +219 -0
  24. package/server/snapshots.ts +67 -0
  25. package/server/workspace.ts +423 -0
  26. package/shared/annotations.ts +43 -0
  27. package/shared/contracts.ts +190 -0
  28. package/shared/contrast.ts +77 -0
  29. package/shared/design.ts +556 -0
  30. package/shared/diff.ts +26 -0
  31. package/shared/fixed-colors.ts +91 -0
  32. package/shared/frame.ts +223 -0
  33. package/shared/pattern-contracts.ts +229 -0
  34. package/shared/protocol.ts +226 -0
  35. package/shared/review.ts +145 -0
  36. package/shared/tokens.ts +102 -0
  37. package/src/App.tsx +22 -0
  38. package/src/Workbench.tsx +341 -0
  39. package/src/app.css +103 -0
  40. package/src/bridge.ts +163 -0
  41. package/src/chat/ChatPanel.module.css +759 -0
  42. package/src/chat/ChatPanel.tsx +326 -0
  43. package/src/chat/Composer.tsx +290 -0
  44. package/src/chat/Markdown.tsx +67 -0
  45. package/src/chat/images.ts +46 -0
  46. package/src/icon.svg +1 -0
  47. package/src/main.tsx +10 -0
  48. package/src/preview/Foundations.module.css +346 -0
  49. package/src/preview/Foundations.tsx +389 -0
  50. package/src/preview/FrameBar.tsx +74 -0
  51. package/src/preview/Gallery.module.css +232 -0
  52. package/src/preview/Gallery.tsx +163 -0
  53. package/src/preview/Impact.tsx +217 -0
  54. package/src/preview/Preview.module.css +103 -0
  55. package/src/preview/Preview.tsx +167 -0
  56. package/src/preview/Review.module.css +192 -0
  57. package/src/preview/Review.tsx +456 -0
  58. package/src/preview/SectionBoundary.tsx +70 -0
  59. package/src/preview/a11y.ts +123 -0
  60. package/src/preview/color.ts +53 -0
  61. package/src/preview/ds-module.d.ts +2 -0
  62. package/src/preview/ds.ts +33 -0
  63. package/src/preview/errors.ts +83 -0
  64. package/src/preview/main.tsx +283 -0
  65. package/src/preview/narrow.ts +95 -0
  66. package/src/preview/picker/PickLayer.module.css +90 -0
  67. package/src/preview/picker/PickLayer.tsx +151 -0
  68. package/src/preview/picker/annotate.ts +93 -0
  69. package/src/preview/preview.css +102 -0
  70. package/src/preview/progressive.ts +62 -0
  71. package/src/preview/specimens.tsx +379 -0
  72. package/src/shell/ChatResizer.tsx +69 -0
  73. package/src/shell/CreateDialog.tsx +132 -0
  74. package/src/shell/ExportDialog.tsx +232 -0
  75. package/src/shell/HistoryMenu.tsx +95 -0
  76. package/src/shell/InspectBanner.tsx +147 -0
  77. package/src/shell/PreviewFrame.tsx +101 -0
  78. package/src/shell/ProjectMenu.tsx +364 -0
  79. package/src/shell/ReviewBar.tsx +224 -0
  80. package/src/shell/Shell.module.css +1067 -0
  81. package/src/shell/Sidebar.module.css +211 -0
  82. package/src/shell/Sidebar.tsx +80 -0
  83. package/src/shell/TopBar.tsx +83 -0
  84. package/src/shell/history.ts +64 -0
  85. package/src/shell/navigation.tsx +150 -0
  86. package/src/shell/prefs.ts +81 -0
  87. package/src/shell/useReview.ts +114 -0
  88. package/template/LICENSE +16 -0
  89. package/template/components/Button.module.css +151 -0
  90. package/template/components/Button.specimen.tsx +74 -0
  91. package/template/components/Button.tsx +41 -0
  92. package/template/components/Controls.module.css +174 -0
  93. package/template/components/Controls.specimen.tsx +54 -0
  94. package/template/components/Controls.tsx +58 -0
  95. package/template/components/Display.module.css +251 -0
  96. package/template/components/Display.specimen.tsx +261 -0
  97. package/template/components/Display.tsx +126 -0
  98. package/template/components/Field.module.css +109 -0
  99. package/template/components/Field.specimen.tsx +138 -0
  100. package/template/components/Field.tsx +94 -0
  101. package/template/components/Overlay.module.css +237 -0
  102. package/template/components/Overlay.specimen.tsx +60 -0
  103. package/template/components/Overlay.tsx +69 -0
  104. package/template/components/Tabs.module.css +140 -0
  105. package/template/components/Tabs.specimen.tsx +51 -0
  106. package/template/components/Tabs.tsx +117 -0
  107. package/template/components/contracts.ts +24 -0
  108. package/template/components/index.ts +7 -0
  109. package/template/components/specimen.ts +88 -0
  110. package/template/components/util.ts +6 -0
  111. package/template/contract-check.ts +22 -0
  112. package/template/patterns/AppShell.module.css +36 -0
  113. package/template/patterns/AppShell.specimen.tsx +114 -0
  114. package/template/patterns/AppShell.tsx +25 -0
  115. package/template/patterns/Chat.module.css +437 -0
  116. package/template/patterns/Chat.specimen.tsx +298 -0
  117. package/template/patterns/Chat.tsx +236 -0
  118. package/template/patterns/Composer.module.css +184 -0
  119. package/template/patterns/Composer.specimen.tsx +106 -0
  120. package/template/patterns/Composer.tsx +183 -0
  121. package/template/patterns/DataTable.module.css +190 -0
  122. package/template/patterns/DataTable.specimen.tsx +189 -0
  123. package/template/patterns/DataTable.tsx +241 -0
  124. package/template/patterns/EmptyState.module.css +57 -0
  125. package/template/patterns/EmptyState.specimen.tsx +71 -0
  126. package/template/patterns/EmptyState.tsx +36 -0
  127. package/template/patterns/FormSection.module.css +69 -0
  128. package/template/patterns/FormSection.specimen.tsx +75 -0
  129. package/template/patterns/FormSection.tsx +40 -0
  130. package/template/patterns/Navbar.module.css +265 -0
  131. package/template/patterns/Navbar.specimen.tsx +118 -0
  132. package/template/patterns/Navbar.tsx +124 -0
  133. package/template/patterns/PageHeader.module.css +98 -0
  134. package/template/patterns/PageHeader.specimen.tsx +75 -0
  135. package/template/patterns/PageHeader.tsx +63 -0
  136. package/template/patterns/SidebarNav.module.css +186 -0
  137. package/template/patterns/SidebarNav.specimen.tsx +98 -0
  138. package/template/patterns/SidebarNav.tsx +81 -0
  139. package/template/patterns/StatCard.module.css +81 -0
  140. package/template/patterns/StatCard.specimen.tsx +61 -0
  141. package/template/patterns/StatCard.tsx +83 -0
  142. package/template/patterns/contracts.ts +25 -0
  143. package/template/patterns/icons.tsx +68 -0
  144. package/template/patterns/index.ts +12 -0
  145. package/template/patterns/specimen-data.ts +112 -0
  146. package/template/views/TeamMembers.specimen.tsx +147 -0
  147. package/vite.config.ts +9 -0
@@ -0,0 +1,180 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { strToU8, zipSync } from "fflate";
5
+ import type { Design } from "../shared/design.ts";
6
+ import { PAGES, type SourceFolder } from "../shared/frame.ts";
7
+ import type { ExportFile, ExportWriteResult } from "../shared/protocol.ts";
8
+ import { exportCss, exportJson, exportTailwind } from "../shared/tokens.ts";
9
+
10
+ const repoRoot = path.resolve(import.meta.dirname, "..");
11
+
12
+ export function slug(name: string): string {
13
+ return (
14
+ name
15
+ .toLowerCase()
16
+ .normalize("NFKD")
17
+ .replace(/[^a-z0-9]+/g, "-")
18
+ .replace(/^-+|-+$/g, "") || "design-system"
19
+ );
20
+ }
21
+
22
+ /**
23
+ * Workspace code imports siblings with explicit extensions ("./util.ts"),
24
+ * which needs a TypeScript flag most projects do not set. Exported code uses
25
+ * extensionless relative imports, which every bundler resolves.
26
+ */
27
+ function stripImportExtensions(source: string): string {
28
+ return source.replace(/(\bfrom\s+|\bimport\s*\(\s*|\bimport\s+)(["'])(\.{1,2}\/[^"']+?)\.(tsx?|jsx?)\2/g, "$1$2$3$2");
29
+ }
30
+
31
+ function readme(design: Design, paths: string[]): string {
32
+ const families = paths
33
+ .filter((file) => file.endsWith(".tsx") && !file.includes(".specimen.") && !file.endsWith("icons.tsx"))
34
+ .map((file) => `- \`${file}\``)
35
+ .join("\n");
36
+ return `# ${design.name}
37
+
38
+ ${design.description || "A design system generated with Basis."}
39
+
40
+ ## Contents
41
+
42
+ - \`tokens.css\`: every design token as a CSS custom property. Light by default, dark under
43
+ \`data-theme="dark"\` or the operating system's dark preference. Font imports are included.
44
+ - \`theme.tailwind.css\`: the same tokens as a Tailwind v4 entry stylesheet, for Tailwind projects.
45
+ - \`design-system.json\`: the source tokens, for tooling or for importing back into Basis.
46
+ - \`components/\`: React components styled with CSS Modules and the token variables. The
47
+ \`*.specimen.tsx\` files describe every component in every state; they are living documentation and
48
+ a ready-made source for stories or visual tests, and nothing imports them by default.
49
+ - \`patterns/\`: larger building blocks composed from the components (navigation, data tables, forms,
50
+ empty states, page layouts). They import only from \`../components\`, so you can take what you need.
51
+
52
+ ${families}
53
+
54
+ ## Use it
55
+
56
+ 1. Copy this folder into your project, for example as \`src/design-system/\`.
57
+ 2. Load the tokens once, at the root of the app:
58
+ - without Tailwind: \`import "./design-system/tokens.css";\`
59
+ - with Tailwind v4: use \`theme.tailwind.css\` as the app's main stylesheet instead of \`@import "tailwindcss"\`.
60
+ Utilities such as \`bg-primary\`, \`text-text-muted\` and \`rounded-md\` then use the design tokens.
61
+ 3. Use the components:
62
+
63
+ \`\`\`tsx
64
+ import { Button, Field, TextInput } from "./design-system/components";
65
+
66
+ <Field label="Email">
67
+ <TextInput placeholder="name@company.com" />
68
+ </Field>
69
+ <Button variant="primary">Continue</Button>
70
+ \`\`\`
71
+
72
+ Patterns work the same way: \`import { Navbar, DataTable } from "./design-system/patterns";\`
73
+
74
+ 4. For dark mode, set \`data-theme="dark"\` (or \`"light"\`) on \`<html>\`, or leave it unset to follow the OS.
75
+
76
+ Requirements: React 18 or newer and a bundler with CSS Modules support (Vite, Next.js, Remix and
77
+ webpack's css-loader all qualify). The components have no other dependencies.
78
+
79
+ The \`data-ds\`, \`data-force\` and \`force\` hooks exist so the Basis gallery can show every state at once.
80
+ They are harmless in an app, and \`force\` is handy in your own stories or visual tests.
81
+
82
+ Generated by Basis on ${new Date().toISOString().slice(0, 10)}.
83
+ `;
84
+ }
85
+
86
+ async function sourceFiles(workspaceDir: string, folder: SourceFolder): Promise<ExportFile[]> {
87
+ const root = path.join(workspaceDir, folder);
88
+ if (!existsSync(root)) return [];
89
+ const entries = (await readdir(root, { recursive: true })).map((entry) => entry.split(path.sep).join("/"));
90
+ const files: ExportFile[] = [];
91
+ for (const entry of entries.sort()) {
92
+ const full = path.join(root, entry);
93
+ if (!(await stat(full)).isFile()) continue;
94
+ let content = await readFile(full, "utf8");
95
+ if (/\.(tsx?|jsx?)$/.test(entry)) content = stripImportExtensions(content);
96
+ // The workspace's index.ts talks about the Basis gallery; in a project it is just the public entry point.
97
+ if (folder === "components" && entry === "index.ts") {
98
+ content = content.replace(
99
+ /^\/\/ Every component the Basis gallery renders\..*\n/,
100
+ "// Public entry point of the design system's components.\n",
101
+ );
102
+ }
103
+ files.push({ path: `${folder}/${entry}`, content });
104
+ }
105
+ return files;
106
+ }
107
+
108
+ /** Everything a new project needs: components, patterns, tokens in three forms, and a README. */
109
+ export async function buildStarterKit(design: Design, workspaceDir: string): Promise<ExportFile[]> {
110
+ // Views are prototypes of a product's screens, not part of the design system a project installs.
111
+ const folders = PAGES.filter((page) => page.kind !== "view").map((page) => page.folder);
112
+ const files = (await Promise.all(folders.map((folder) => sourceFiles(workspaceDir, folder)))).flat();
113
+ return [
114
+ {
115
+ path: "README.md",
116
+ content: readme(
117
+ design,
118
+ files.map((file) => file.path),
119
+ ),
120
+ },
121
+ { path: "tokens.css", content: exportCss(design) },
122
+ { path: "theme.tailwind.css", content: exportTailwind(design) },
123
+ { path: "design-system.json", content: exportJson(design) },
124
+ ...files,
125
+ ];
126
+ }
127
+
128
+ /** Re-exporting an unchanged kit only differs in the README's date line; that is not a conflict. */
129
+ function withoutStamp(text: string): string {
130
+ return text.replace(/^Generated by Basis on .*$/m, "");
131
+ }
132
+
133
+ export function zipStarterKit(files: ExportFile[], folder: string): Uint8Array {
134
+ return zipSync(Object.fromEntries(files.map((file) => [`${folder}/${file.path}`, strToU8(file.content)])), {
135
+ level: 6,
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Writes the kit into a folder of the user's choosing. Refuses to touch Basis
141
+ * itself, and reports files that would change instead of overwriting them
142
+ * unless the user confirmed.
143
+ */
144
+ export async function writeStarterKit(
145
+ files: ExportFile[],
146
+ target: string,
147
+ overwrite: boolean,
148
+ ): Promise<ExportWriteResult> {
149
+ const trimmed = target.trim();
150
+ if (!trimmed || !path.isAbsolute(trimmed)) {
151
+ return { ok: false, target: trimmed, written: [], conflicts: [], error: "Enter an absolute folder path." };
152
+ }
153
+ const dir = path.resolve(trimmed);
154
+ const relativeToBasis = path.relative(repoRoot, dir);
155
+ if (!relativeToBasis.startsWith("..") && !path.isAbsolute(relativeToBasis)) {
156
+ return {
157
+ ok: false,
158
+ target: dir,
159
+ written: [],
160
+ conflicts: [],
161
+ error: "That folder is inside Basis itself. Pick a folder in your project.",
162
+ };
163
+ }
164
+ const conflicts: string[] = [];
165
+ for (const file of files) {
166
+ const destination = path.join(dir, file.path);
167
+ if (!existsSync(destination)) continue;
168
+ const current = await readFile(destination, "utf8").catch(() => null);
169
+ if (current === null || withoutStamp(current) !== withoutStamp(file.content)) conflicts.push(file.path);
170
+ }
171
+ if (conflicts.length && !overwrite) return { ok: false, target: dir, written: [], conflicts };
172
+ const written: string[] = [];
173
+ for (const file of files) {
174
+ const destination = path.join(dir, file.path);
175
+ await mkdir(path.dirname(destination), { recursive: true });
176
+ await writeFile(destination, file.content);
177
+ written.push(file.path);
178
+ }
179
+ return { ok: true, target: dir, written, conflicts: [] };
180
+ }
@@ -0,0 +1,379 @@
1
+ # Design guidelines
2
+
3
+ <!-- Generated by Basis on every start (from server/guidelines.md). Edits here are overwritten. -->
4
+
5
+ Distilled from WCAG 2.2, the WAI-ARIA Authoring Practices, Material Design 3, Apple's Human Interface Guidelines,
6
+ GOV.UK Design System, IBM Carbon, Atlassian, Shopify Polaris, Adobe Spectrum, Nielsen Norman Group research and
7
+ practitioners on visual craft (sources at the end). Read the section for what you are about to design before you
8
+ start, and run "Before you reply" on what you made.
9
+
10
+ Labels: **must** is a WCAG A/AA requirement or a consensus every system shares; **default** is the choice Basis makes
11
+ where the big systems disagree, so every design system made here is consistent; everything else is strong practice.
12
+ When the user asks for something else, do what they ask and say which guideline it departs from.
13
+
14
+ Contents: Before you reply · States · Focus, targets and keyboard · Buttons · Text fields and selects · Checkboxes,
15
+ radios and switches · Tabs and navigation · Overlays · Feedback · Data · Forms · Screens and flows · Writing · AI
16
+ assistants · Colour · Dark mode · Depth · Typography · Spacing and shape · Motion · Mood requests · Sources
17
+
18
+ ## Before you reply
19
+
20
+ Go through what you made, in both themes and every state the gallery shows:
21
+
22
+ - One focal point per region: a single primary button per view or panel, one accent hue, one key number per card.
23
+ - Every state reads differently from its neighbours: hover is a hint, pressed a step beyond hover, focus a ring,
24
+ selected/checked/invalid shown by more than colour, disabled clearly unavailable yet legible.
25
+ - The focus ring is visible on the component's own fill and on every surface it sits on, in both themes, and no
26
+ `overflow: hidden` ancestor clips it.
27
+ - Hit areas are at least 24 × 24 px (icon buttons, chip and toast close buttons, row actions, pagination).
28
+ - Boundaries that identify a control (input border, checkbox box, switch track, radio ring, selected-tab
29
+ indicator) are at least 3:1 against what is next to them.
30
+ - Nothing has a fixed height that clips text: use min-height and padding; the component survives a long label,
31
+ a missing value, 200% zoom and a 320 px wide frame.
32
+ - Motion stays under the durations below, never blocks the next click, and has a `prefers-reduced-motion` path.
33
+ - Every effect has a counterpart in the other theme (see Dark mode); no colour is fixed outside the roles.
34
+ - Labels are verbs plus nouns in sentence case; no "Submit", "OK", "Click here", lorem ipsum or "John Doe".
35
+ - Patterns and views show their empty, loading and error states, not only the happy path.
36
+ - It has its own identity: do not reproduce the recognisable look of a well-known product.
37
+
38
+ ## States
39
+
40
+ Applies to every interactive component. Style (variant) and state are separate axes: the variant sets emphasis,
41
+ states move through time on top of it.
42
+
43
+ - Hover: a slight change only, since touch never sees it. Filled components step to their `xHover` role
44
+ (darker in light, usually lighter in dark: the same step size, not the same direction). Transparent and surface
45
+ components (ghost buttons, list rows, menu items, table rows) use a state layer instead: the content colour mixed
46
+ over the container, `color-mix(in oklab, var(--color-text) 8%, transparent)` for hover and 10–12% for pressed.
47
+ Material uses 8% hover, 10% focus and pressed, 16% dragged.
48
+ - Pressed: a step beyond hover (`xActive`), within about 100 ms of the press. Optionally a small physical cue
49
+ (scale 0.97–0.99 or `translateY(1px)`), never one that moves layout.
50
+ - Focus: a ring, never only a colour change (see the next section).
51
+ - Selected, checked, current: colour plus a second cue (weight, indicator bar, check mark, filled shape).
52
+ - Invalid: danger border plus an icon and a text message. Colour alone is not enough (must).
53
+ - Disabled: `disabledBg` and `disabledText`; the control drops its brand colour but its label stays legible.
54
+ Disabled controls are exempt from contrast rules, which is exactly why they hide the reason: prefer an enabled
55
+ control that explains on use, or visible text that says why it is unavailable.
56
+ - Loading: keep the size and the label (for screen readers), block repeat activation, keep focus where it is.
57
+ - Every custom control needs a visible pressed state; without one it feels dead.
58
+
59
+ ## Focus, targets and keyboard
60
+
61
+ - Visible focus on everything focusable (must). Style `:focus-visible` (and `[data-force="focus"]`), not `:focus`.
62
+ - The ring: at least `--focus-ring-width` (2 px) around the whole control, in `focusRing` at 3:1 against the
63
+ surfaces it sits on (must) and clearly apart from the control's own fill. Use `outline` with `--focus-ring-offset`, or
64
+ keep `outline: 2px solid transparent` next to a box-shadow ring so it survives forced-colours mode. An inset ring
65
+ only where a container clips overflow (rows in a scroll area), and then thicker.
66
+ - A focused element must not be hidden under sticky headers, toasts or composers: give scroll containers
67
+ `scroll-padding` equal to those bars (must).
68
+ - Targets at least 24 × 24 px, or spaced so a 24 px circle around each does not touch another (must). 44 px for
69
+ touch-first controls. Grow the hit area with padding or a pseudo-element rather than the glyph.
70
+ - Native elements first: `<button>`, `<a href>`, `<input type=checkbox|radio>`, `<select>`, `<dialog>`, `<table>`.
71
+ A `<div>` with a click handler is never a control.
72
+ - Composite widgets (tabs, radio group, listbox, menu, toolbar, grid) are one Tab stop; arrow keys move inside them.
73
+ - A control's accessible name contains its visible label and starts with it (must). Icon-only controls need an
74
+ `aria-label`, and their SVG `aria-hidden="true"`.
75
+ - Changing a control never navigates, submits or opens a window by itself (must).
76
+ - Anything shown on hover is also shown on focus (`:focus-within`), dismissible with Esc, and stays while the
77
+ pointer moves onto it (must). Row actions that appear only on hover are invisible to keyboard and touch users.
78
+ - Every drag has a click alternative: move buttons or a menu for sortable lists, track clicks and arrow keys for
79
+ sliders (must).
80
+
81
+ ## Buttons
82
+
83
+ - Hierarchy: one primary per view, dialog or panel. More actions step down: secondary, then outline or ghost, then
84
+ an overflow menu. Mark the preferred action by style, not size; buttons in one group share a size.
85
+ - Variants: primary (the main action), secondary (alternatives), outline/ghost (low emphasis, Cancel), danger (only
86
+ for serious, hard-to-undo actions, rarely needed). Danger is never the default or the primary of a dialog and is
87
+ never icon-only; its label says what happens.
88
+ - Sizes: 32 / 40 / 48 px (default 40). Match the height of inputs it sits beside. Small buttons keep a 24 px
89
+ minimum hit area.
90
+ - Labels: verb plus object in sentence case ("Save draft", "Delete project"); single words only for Done, Close,
91
+ Cancel. No punctuation. Wrap long labels rather than truncate.
92
+ - Icons: only when the meaning is established and tied to the action; 16–20 px, in the label's colour; if one
93
+ button in a group has an icon, all do. Icon-only buttons need an accessible name and a tooltip.
94
+ - Loading: keep the width and keep the label in the DOM (spinner over or beside it), `aria-busy`, ignore repeat
95
+ presses, keep focus. Acknowledge the press at once; show the spinner only if it takes over about 1 s. Announce
96
+ the result through a status region, not the button.
97
+ - Disabled: default is to keep buttons enabled and validate when pressed. Never disable without a visible reason.
98
+ - Buttons act on the page, links navigate. Do not dress one as the other.
99
+ - "Premium" or tactile: a medium weight, slightly tighter label tracking, a layered tinted shadow, a top highlight
100
+ and fast press feedback, all theme-aware (see Depth). Not more saturation, not gradients everywhere.
101
+
102
+ ## Text fields and selects
103
+
104
+ - A visible label above every field, short, sentence case, no colon (must). The placeholder is never the label:
105
+ it disappears, fails contrast and is not reliably announced. Use placeholders for format examples at most.
106
+ - Hint text between the label and the field, one short sentence, connected with `aria-describedby`.
107
+ - Default marking: ask only what is needed, so most fields are required; mark the optional ones "(optional)".
108
+ - Width hints at the answer: postcode, year, amount and code fields are narrow; names and addresses wide.
109
+ - Height 40 px by default, matching buttons; the input border is at least 3:1 against its surface (must).
110
+ - Errors: the message sits by the field (between hint and input), says what is wrong and how to fix it in the
111
+ label's words ("Enter a date in the past", not "Invalid date"), with a danger border, an icon and
112
+ `aria-invalid="true"` plus `aria-describedby` pointing at the message. Keep what the user typed.
113
+ - Default validation: on submit; after that, a field that showed an error re-checks as the user types and clears as
114
+ soon as it is valid. Never flag an untouched field or a field while it is first being filled.
115
+ - Prefix and suffix for units and currency, and say the unit in the label too. Prefer a character counter over a
116
+ silent `maxlength`; `inputmode` over `type=number`.
117
+ - Selects: native `<select>` unless the design needs search or rich options; a custom one carries the full
118
+ combobox keyboard model (arrows, Enter, Esc, type-ahead, `aria-expanded`, `aria-activedescendant`).
119
+
120
+ ## Checkboxes, radios and switches
121
+
122
+ - Radio: exactly one of a few options. Checkbox: any number, or one setting saved on submit. Switch: an on/off
123
+ setting that takes effect immediately; never inside a form with a submit button.
124
+ - Stack options vertically, control left of label; the label is part of the click target. Inline only for two
125
+ short options.
126
+ - Groups get a `fieldset` and `legend`, usually phrased as a question; add "Select all that apply" when it helps.
127
+ - Do not preselect; offer an explicit "None" when that is a valid answer.
128
+ - Indeterminate is a parent whose children are partly checked; checking it checks all.
129
+ - Switch labels name what "on" means ("Email notifications") and do not change with the state; state shows by thumb
130
+ position plus colour. The off track and the box/ring of unchecked controls need 3:1 (must).
131
+
132
+ ## Tabs and navigation
133
+
134
+ - Selected tab: two cues, such as an indicator bar plus stronger text, or a filled pill. One row, labels of one or
135
+ two words, no all caps. Tabs switch content within a page; page navigation is links with
136
+ `aria-current="page"`.
137
+ - Keyboard: Tab lands on the active tab, arrows move between tabs, the next Tab goes into the panel.
138
+ - Do not hide content users must compare or read in order behind tabs.
139
+ - Navigation always shows where the user is (active item matching the page heading) and how to get back
140
+ (breadcrumb or Back link on detail screens).
141
+ - Application navigation on the left on wide screens; do not hide it behind a hamburger on desktop. Submenus open
142
+ on click. Icons support labels, they do not replace them.
143
+ - Segmented controls: two to five short options of equal weight, with a clear selected fill.
144
+
145
+ ## Overlays
146
+
147
+ - Modal dialogs only for a critical warning, required input or a short focused task. Not for details (use a side
148
+ panel so the list stays visible), not for field errors, not to confirm routine actions (offer Undo instead).
149
+ - Dialog: a title that matches the button that opened it, at most three buttons with the primary last on the
150
+ trailing side, only the body scrolls. Focus moves in (first field, or the least destructive button for confirms),
151
+ is trapped, Esc closes, focus returns to the trigger. `role="dialog"`, `aria-modal`, labelled by its title.
152
+ - Confirmations name the item and the consequence and label buttons with outcomes ("Delete 3 invoices" /
153
+ "Keep invoices"), never Yes/No; the destructive button is never the default.
154
+ - Popovers and menus open on click, close on Esc and outside click, return focus to their trigger, and sit on
155
+ `surfaceRaised` with the overlay shadow. Action menus use the menu keyboard model; site navigation does not.
156
+ - Tooltips: short supplementary text on hover and focus, Esc dismisses, never on disabled controls, never the only
157
+ place information lives, nothing interactive inside.
158
+
159
+ ## Feedback
160
+
161
+ - Badges and status labels pair colour with text (and an icon where the meaning is established), sentence case.
162
+ Status colours have matched lightness so none shouts louder than another.
163
+ - Inline alerts sit next to what they concern, do not auto-dismiss and have at most one action. Info, success,
164
+ warning and error each use their role (`xSubtle` background, `xStrong` text, `x` icon or border).
165
+ - Toasts are for passive confirmations only, never errors or anything essential. At least 5 s, paused on hover
166
+ and focus; a toast with an action (Undo) stays until the user acts, and its action is reachable elsewhere too.
167
+ `role="status"` in a region that exists before the message arrives. One consistent position per product.
168
+ - Loading: under about 1 s show nothing extra beyond the pressed state; up to 10 s a spinner for one element or a
169
+ skeleton that mirrors the real layout for a view; over 10 s determinate progress with a way to cancel.
170
+ Skeleton shimmer is subtle and stops under reduced motion.
171
+ - Empty states are designed, never blank, and never shown while data is still loading. First use: what will appear
172
+ here and a button to create the first item. No results: the query, and a way to broaden it or clear filters.
173
+ - Errors say what happened and what to do next, keep the valid data on screen, and match prominence to severity:
174
+ inline for a field, a banner for a section or page, a dialog only when the user cannot continue.
175
+
176
+ ## Data
177
+
178
+ - Tables: the first column is a human-readable identifier (a name, an invoice number), never an internal id.
179
+ Numbers right-aligned with `font-variant-numeric: tabular-nums`, text left-aligned, headers aligned with their
180
+ data. Row heights 32 / 40 / 48 px by density.
181
+ - Sorting has three states; only the sorted column keeps its arrow visible; sortable headers are buttons with
182
+ `aria-sort` on the `th`. Long headers wrap to two lines before truncating.
183
+ - One or two row actions inline; more go behind checkbox selection and a bulk action bar. Row actions stay visible
184
+ on focus, not only on hover.
185
+ - Sticky header (and first column on wide tables), row hover, skeleton rows while loading, and the empty, no-results
186
+ and error states.
187
+ - Pagination or "Load more" when users look for or compare items; infinite scroll only for browsing feeds.
188
+ - Stat cards and dashboards: every number answers a question and shows a comparison (target, previous period,
189
+ threshold) and links to the records behind it. Bars and lines, not pies, donuts, gauges or 3D. Fit what is
190
+ monitored on one screen. Invented trends must add up.
191
+
192
+ ## Forms
193
+
194
+ - One column; only short related fields share a row (city and postcode). Group fields under short headings.
195
+ - Spacing: about 24–32 px between fields on a page, 16–24 px in dialogs and panels, about 48 px before the buttons.
196
+ - Default button placement: in page forms the primary sits at the start (left), Cancel or Back after it; in dialogs,
197
+ side panels and wizards the primary is last on the trailing side.
198
+ - On a failed submit, an error summary at the top titled "There is a problem" takes focus and links each item to
199
+ its field, using the same words as the field's message.
200
+ - Multi-step: one question or decision per step, a Back link at the top, "Continue" as the button, a check-answers
201
+ step before the final submit, and a confirmation that says what happens next.
202
+ - Ask only what you need; prefill what the user already gave in the flow; allow paste and password managers.
203
+
204
+ ## Screens and flows
205
+
206
+ These apply to views and to patterns that make up a screen.
207
+
208
+ - One primary action per screen. Spend contrast on one thing per region; everything else steps down in size,
209
+ weight and colour.
210
+ - Headings carry the information (users scan headings, then dip in), lead with the important words, and match the
211
+ navigation label that leads there.
212
+ - Group by proximity and enclosure: space inside a group is clearly smaller than between groups (1:2 or more).
213
+ - Every action shows feedback within 0.1 s; anything over 1 s shows progress where it happens; over 10 s shows
214
+ determinate progress and a way out.
215
+ - Every modal, drawer, step and edit mode has a labelled way out; destructive results can be undone. Prefer Undo to
216
+ confirmation; confirm only what is irreversible and costly.
217
+ - Before a commit, the screen shows the current state: draft or published, unsaved changes, selection count,
218
+ active filters.
219
+ - List and detail: a side panel or split view on wide screens, push navigation on narrow ones. Keep the same content
220
+ and features at every size; change the layout, not the substance.
221
+ - Design all the states a screen meets: first use, populated, loading, error, no results, success.
222
+ - Every interactive element leads somewhere in the flow or is visibly out of scope; nothing invented leads nowhere.
223
+ - Progressive disclosure for rare options: at most two levels, behind a clearly named control.
224
+
225
+ ## Writing
226
+
227
+ - Sentence case everywhere: buttons, headings, labels, menus, tabs. No all caps for emphasis (small uppercase
228
+ labels are a typographic style with added tracking, not emphasis).
229
+ - Buttons are verb plus noun naming the outcome, the same verb throughout a flow ("Add product" ends in "Add
230
+ product", not "Create item"). Links make sense on their own; no "Learn more" without what about.
231
+ - One term per concept across list, detail, form and messages.
232
+ - Errors: what happened and how to fix it, specific, plain, no blame ("invalid", "illegal"), no humour, no codes
233
+ unless they help; apologise only for real system failures.
234
+ - Short sentences, important words first, "you" for the user. Headings and labels without end punctuation. No
235
+ exclamation marks in success messages.
236
+ - Numbers, dates and money follow the product's locale, identical on every screen. In Swedish: "4 juni 2025",
237
+ "9 000 kr", "12,5 %". In English: "4 June 2025", "9,000", "£75.50".
238
+ - Sample data is realistic and from the product's domain and language: plausible names and ids, amounts that add
239
+ up, dates in order, statuses spread realistically. Include edge cases: a very long name, a missing optional
240
+ value, zero and negative amounts, one item and many, non-ASCII characters (å, ä, ö, é).
241
+
242
+ ## AI assistants
243
+
244
+ For chat and assistant patterns and views.
245
+
246
+ - The empty state says what the assistant can do here, without overpromising, with suggested prompts as buttons.
247
+ - Distinct states: waiting for the first token, streaming, working on a named step ("Searching invoices…"), done,
248
+ failed. Stop is prominent while generating and keeps the partial answer.
249
+ - Do not auto-scroll past the start of a new answer while it streams.
250
+ - Per message: copy, regenerate, edit the prompt, feedback (thumbs with an optional reason); labelled controls,
251
+ grouped.
252
+ - Answers lead with the answer; sources are linked and say what the answer is based on ("Based on 14 invoices
253
+ from March").
254
+ - Before the assistant changes data it says what will change; afterwards it offers Undo.
255
+ - Errors explain plainly, keep the prompt for a retry and offer a manual path.
256
+ - One assistant entry point; do not rebuild search, filters or navigation as chat.
257
+
258
+ ## Colour
259
+
260
+ - Components read semantic roles only; palette changes happen in the roles' values. "Warmer" or "calmer" changes
261
+ the neutrals' hue and chroma and maybe the accent hue, in both themes; the role mapping stays.
262
+ - One accent hue for the primary action, selection, focus and links; neutrals carry about 90% of the pixels.
263
+ Several competing accents are the most common amateur signal.
264
+ - Tint neutrals slightly toward the brand or a warm/cool direction (OKLCH chroma about 0.005–0.02). It is the
265
+ cheapest lever for mood.
266
+ - Work in OKLCH: keep the hue, vary lightness, lower chroma at the light and dark ends. Relative colour
267
+ (`oklch(from var(--color-primary) calc(l - 0.06) c h)`) derives steps that follow the theme.
268
+ - State steps: about 0.04–0.08 OKLCH lightness for hover on large fills, about double for pressed.
269
+ - Status colours (info, success, warning, danger) at matched lightness and chroma, each with a subtle background,
270
+ strong text, solid fill and on-colour, in both themes.
271
+ - Muted text still needs 4.5:1; `textSubtle` is for large or non-essential text only.
272
+
273
+ ## Dark mode
274
+
275
+ Both themes are first-class siblings: same hues, same number of surface levels, same radius, spacing and type, same
276
+ accent-to-neutral ratio. Only lightness, chroma and alpha change. If light has an effect, dark has a translated
277
+ version of it, not an absence.
278
+
279
+ - No pure black or white: a dark grey page (around OKLCH L 0.15–0.2) and off-white text (L 0.93–0.97).
280
+ - Elevation by lightness: in dark, each raised level a little lighter (about 0.02–0.05 OKLCH L per step), in order
281
+ `surfaceSunken` < `bg` < `surface` < `surfaceRaised`. In light, raised surfaces are white or near-white and lean
282
+ on the shadow; the page and sunken wells step down by about 0.01–0.03.
283
+ - Shadows nearly vanish on dark surfaces: the `shadow` role is stronger in dark, and the lighter surface or a faint
284
+ border carries the depth. Design so the element still separates with the shadow barely visible.
285
+ - Borders in dark: a step lighter than the surface or a translucent light mix (6–12%), never bright lines.
286
+ - Brand and status colours lighter and less saturated in dark (keep the hue, drop chroma 20–40%, raise L) when
287
+ used as text or icons; a solid primary fill may stay saturated.
288
+ - Hover and pressed: same step size as light, usually the opposite direction (lighter).
289
+ - Light text on dark looks bolder: avoid hairline weights.
290
+ - Images and illustrations: contain them, dim slightly or provide dark versions; icons use `currentColor`.
291
+
292
+ ## Depth
293
+
294
+ - One light source, from above: all shadows share direction; highlights sit on the top edge.
295
+ - Elevation levels: sunken (wells, tracks), flat (page content; cards with a border or a lightness step), raised
296
+ (emphasised or draggable cards), overlay (menus, popovers, dialogs, toasts). As elevation rises, offset and blur
297
+ grow and opacity falls.
298
+ - Layered shadows (two to four layers, offsets and blurs doubling: 1, 2, 4, 8 px) read more naturally than one big
299
+ blur. Mix them from the `shadow` role, never plain black: `0 2px 4px color-mix(in srgb, var(--color-shadow) 20%,
300
+ transparent)`.
301
+ - One edge cue per surface: a border, a shadow or a lightness step; combine only a hairline with a soft shadow.
302
+ - Tactile or "premium" controls: a top inner highlight, an optional bottom inner shade and a tight outer shadow, all
303
+ derived from the fill or the roles. A highlight that is 15–30% light on a light-theme fill must drop to about
304
+ 4–10% in dark or it glows; the bottom shade usually disappears in dark. Pressed removes the highlight and shrinks
305
+ the outer shadow.
306
+ - Do not animate layered `box-shadow`; fade a pseudo-element's shadow or animate transform instead.
307
+
308
+ ## Typography
309
+
310
+ - Five to seven sizes. Product UI scales by about 1.125–1.2 per step; editorial and marketing by 1.25–1.333.
311
+ - Body at least 14 px in dense product UI and 16 px for reading; labels and captions at least 11–12 px.
312
+ - Line height falls as size rises: about 1.5 for body, 1.2–1.3 for headings, 1.0–1.1 for display.
313
+ - Measure 45–90 characters, ideally 60–75 for body copy (`max-width: 65ch`).
314
+ - Letter-spacing: normal for body; +0.05–0.12em for all caps and small labels; -0.01 to -0.03em for large display.
315
+ - Two or three weights. Hierarchy from size, weight and colour together, not weight alone; not everything bold.
316
+ - Tabular numbers for tables, prices, timers and anything that updates or aligns.
317
+
318
+ ## Spacing and shape
319
+
320
+ - A 4 px base with an 8 px rhythm (the `space` tokens). Small steps inside components, large steps for layout.
321
+ - Components carry no outer margins; parents set gaps (`gap`, stacks). Padding inside a container is at least the
322
+ gap between its children.
323
+ - Density moves control and row heights one step (32 / 40 / 48 px); type size stays.
324
+ - Left-align text in product UI; centre only short standalone blocks (empty states, hero headings).
325
+ - A small radius scale tied to size: small controls smaller, containers larger. Nested corners are concentric:
326
+ inner radius = outer radius − padding − border width.
327
+ - Patterns respond to their container (container queries), so they work in any column width and at 320 px.
328
+
329
+ ## Motion
330
+
331
+ - Durations: micro feedback (toggles, button states) about 70–120 ms; small expansions and dropdowns about
332
+ 150–250 ms; dialogs, sheets and large moves about 250–400 ms. Over 500 ms feels slow; frequent motion is shorter.
333
+ - Easing: entering decelerates, exiting accelerates and runs 20–30% shorter, things that stay use a standard
334
+ ease-in-out; linear only for spinners and progress. Use the `ease` tokens.
335
+ - Animate `transform` and `opacity`; never `transition: all` or layout properties.
336
+ - Press feedback almost instant; hover-in about 100–200 ms. Never make the user wait for an animation to finish.
337
+ - Reduced motion (default): under `@media (prefers-reduced-motion: reduce)` remove slides, zooms, parallax, shimmer
338
+ and loops; keep state changes as short fades or instant switches.
339
+ - Anything that moves or updates by itself for more than 5 s has a way to pause it, and nothing flashes more than
340
+ three times a second (must).
341
+
342
+ ## Mood requests
343
+
344
+ Taste, built from the levers above; always in both themes.
345
+
346
+ - Warmer: neutrals toward hue 60–80 at chroma 0.01–0.02, warmer shadow tint, perhaps a warmer accent.
347
+ - Calmer: lower chroma throughout, one restrained accent, more space, flatter elevation, slower but short motion.
348
+ - Premium: restraint. Fewer colours, tighter tracking on labels, medium weights, layered tinted shadows, a subtle
349
+ theme-aware highlight, precise alignment and fast press feedback.
350
+ - Playful: a brighter accent, rounder radius, springier but still short motion, more saturated status colours.
351
+ - Fintech or data-heavy: low-chroma neutrals, one blue or green accent, tabular numbers, compact density options,
352
+ productive motion (100–240 ms), status colours with matched lightness.
353
+ - Editorial: a serif or high-contrast display face, a larger type ratio, generous measure and leading, few borders.
354
+
355
+ ## Sources
356
+
357
+ - WCAG 2.2 and its Understanding documents: https://www.w3.org/WAI/WCAG22/Understanding/
358
+ - WAI-ARIA Authoring Practices, patterns: https://www.w3.org/WAI/ARIA/apg/patterns/
359
+ - Focus indicators (Sara Soueidan): https://www.sarasoueidan.com/blog/focus-indicators/
360
+ - Toasts and accessibility (Scott O'Hara): https://www.scottohara.me/blog/2019/07/08/a-toast-to-a11y-toasts.html
361
+ - Material Design 3: https://m3.material.io/ (state layers, colour roles, elevation, motion)
362
+ - Apple Human Interface Guidelines: https://developer.apple.com/design/human-interface-guidelines/
363
+ - GOV.UK Design System and Service Manual: https://design-system.service.gov.uk/,
364
+ https://www.gov.uk/service-manual/design/writing-for-user-interfaces
365
+ - IBM Carbon: https://carbondesignsystem.com/ (buttons, forms, data table, notifications, spacing, motion)
366
+ - Atlassian Design System: https://atlassian.design/ (buttons, lozenges, elevation)
367
+ - Shopify Polaris: https://polaris-react.shopify.com/
368
+ - Adobe React Spectrum: https://react-spectrum.adobe.com/ (button loading, toasts)
369
+ - Nielsen Norman Group: https://www.nngroup.com/articles/ (heuristics, response times, button states, forms,
370
+ error messages, toasts, tooltips, dialogs, tabs, menus, data tables, dashboards, empty states, skeletons, AI chat)
371
+ - Laws of UX: https://lawsofux.com/
372
+ - Microsoft HAX guidelines: https://www.microsoft.com/en-us/haxtoolkit/library/
373
+ - Google PAIR guidebook: https://pair.withgoogle.com/
374
+ - Radix Colors, the 12-step scale: https://www.radix-ui.com/colors/docs/palette-composition/understanding-the-scale
375
+ - OKLCH in CSS (Evil Martians): https://evilmartians.com/chronicles/oklch-in-css-why-quit-rgb-hsl
376
+ - Designing shadows (Josh W. Comeau): https://www.joshwcomeau.com/css/designing-shadows/
377
+ - Practical Typography: https://practicaltypography.com/
378
+ - Utopia, fluid type and space: https://utopia.fyi/
379
+ - Nested radius: https://cloudfour.com/thinks/the-math-behind-nesting-rounded-corners/