@wtfalch/design 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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/dist/components/Brand.d.ts +10 -0
  4. package/dist/components/Brand.js +212 -0
  5. package/dist/components/Button.d.ts +63 -0
  6. package/dist/components/Button.js +74 -0
  7. package/dist/components/Callout.d.ts +37 -0
  8. package/dist/components/Callout.js +71 -0
  9. package/dist/components/Card.d.ts +42 -0
  10. package/dist/components/Card.js +30 -0
  11. package/dist/components/Checkbox.d.ts +32 -0
  12. package/dist/components/Checkbox.js +31 -0
  13. package/dist/components/DangerZone.d.ts +59 -0
  14. package/dist/components/DangerZone.js +50 -0
  15. package/dist/components/Dialog.d.ts +28 -0
  16. package/dist/components/Dialog.js +29 -0
  17. package/dist/components/Empty.d.ts +45 -0
  18. package/dist/components/Empty.js +35 -0
  19. package/dist/components/Field.d.ts +58 -0
  20. package/dist/components/Field.js +46 -0
  21. package/dist/components/Icon.d.ts +64 -0
  22. package/dist/components/Icon.js +235 -0
  23. package/dist/components/Illustration.d.ts +36 -0
  24. package/dist/components/Illustration.js +48 -0
  25. package/dist/components/Input.d.ts +14 -0
  26. package/dist/components/Input.js +65 -0
  27. package/dist/components/Markdown.d.ts +21 -0
  28. package/dist/components/Markdown.js +29 -0
  29. package/dist/components/Modal.d.ts +59 -0
  30. package/dist/components/Modal.js +72 -0
  31. package/dist/components/Pill.d.ts +40 -0
  32. package/dist/components/Pill.js +41 -0
  33. package/dist/components/Progress.d.ts +35 -0
  34. package/dist/components/Progress.js +27 -0
  35. package/dist/components/Rows.d.ts +101 -0
  36. package/dist/components/Rows.js +55 -0
  37. package/dist/components/Select.d.ts +28 -0
  38. package/dist/components/Select.js +56 -0
  39. package/dist/components/SizeGrid.d.ts +34 -0
  40. package/dist/components/SizeGrid.js +41 -0
  41. package/dist/components/Skeleton.d.ts +45 -0
  42. package/dist/components/Skeleton.js +47 -0
  43. package/dist/components/Slider.d.ts +70 -0
  44. package/dist/components/Slider.js +100 -0
  45. package/dist/components/Table.d.ts +43 -0
  46. package/dist/components/Table.js +13 -0
  47. package/dist/components/Tabs.d.ts +72 -0
  48. package/dist/components/Tabs.js +82 -0
  49. package/dist/components/Textarea.d.ts +9 -0
  50. package/dist/components/Textarea.js +22 -0
  51. package/dist/components/Toast.d.ts +43 -0
  52. package/dist/components/Toast.js +78 -0
  53. package/dist/components/Toggle.d.ts +56 -0
  54. package/dist/components/Toggle.js +189 -0
  55. package/dist/components/Tooltip.d.ts +22 -0
  56. package/dist/components/Tooltip.js +62 -0
  57. package/dist/components/Tour.d.ts +33 -0
  58. package/dist/components/Tour.js +108 -0
  59. package/dist/components/iconNames.d.ts +18 -0
  60. package/dist/components/iconNames.js +60 -0
  61. package/dist/components/tourMarker.d.ts +29 -0
  62. package/dist/components/tourMarker.js +58 -0
  63. package/dist/contrast.d.ts +18 -0
  64. package/dist/contrast.js +27 -0
  65. package/dist/hooks/useTrapFocus.d.ts +24 -0
  66. package/dist/hooks/useTrapFocus.js +67 -0
  67. package/dist/illustrations.d.ts +11 -0
  68. package/dist/illustrations.js +55 -0
  69. package/dist/index.d.ts +72 -0
  70. package/dist/index.js +65 -0
  71. package/dist/styles/index.css +3124 -0
  72. package/dist/themes.d.ts +210 -0
  73. package/dist/themes.js +300 -0
  74. package/dist/tokens.css +251 -0
  75. package/package.json +74 -0
@@ -0,0 +1,251 @@
1
+ /**
2
+ * The token vocabulary.
3
+ *
4
+ * This file is the contract. `styles.css` may only consume these; a theme may
5
+ * only supply values for them. Nothing else is themeable, and that is the
6
+ * point: the moment a theme can ship a selector, every theme becomes a fork of
7
+ * the stylesheet and no change to the app is safe again.
8
+ *
9
+ * What makes a theme expressive is not permission to write rules — it is a
10
+ * vocabulary wide enough that the thing you want to vary is a value. So this
11
+ * covers seven axes, not just colour: a theme can change how the app *moves*
12
+ * and how dense it is without touching a selector.
13
+ *
14
+ * Base values here are today's, exactly. Adding this file changed nothing on
15
+ * screen, which is how a 200-literal refactor stays reviewable.
16
+ *
17
+ * Keys are mirrored in `themes.ts` as `ThemeTokens`. Add one here, add it
18
+ * there — a token a theme cannot name is a token no theme can use.
19
+ */
20
+
21
+ :root {
22
+ /* ---- colour ------------------------------------------------------- */
23
+ /* Low-chroma slate, and a muted teal.
24
+ Neither warm nor cold on purpose: a beige-leaning neutral reads cosy and a
25
+ navy-leaning one reads cold, and this is a tool somebody keeps open all
26
+ day. Teal sits between the two, and unlike a saturated blue it does not
27
+ compete with the text for attention.
28
+ Every pair below is measured rather than chosen -- see the block comment
29
+ above `--border-strong`. */
30
+ --bg: #12151a;
31
+ --panel: #191d24;
32
+ --panel-2: #222831;
33
+ --border: #262c36;
34
+ --text: #e4e8ee;
35
+ --muted: #98a1b0;
36
+ --accent: #4fb3a8;
37
+ --accent-dim: #2f6b66;
38
+ /* Status colours that survive a light background too. The old set was
39
+ dark-mode neon -- `#4ade80` on white is 1.74:1 -- and the light theme never
40
+ overrode them, so a `running` pill was unreadable on Paper. */
41
+ --good: #68c48c;
42
+ --warn: #d9a65a;
43
+ --bad: #e28680;
44
+ /* Information, as a colour of its own. `Callout`'s info tone used to borrow
45
+ the accent for its edge and take no fill, so on the two teal themes an
46
+ "information" box was a teal-edged box and William read it as "isn't
47
+ information blue?" (2026-09-03). It is now: blue, and the fourth status
48
+ colour beside good, warn and bad. */
49
+ --info: #74b3ff;
50
+ /* Text that sits *on* the accent. Was hardcoded `#fff`, which made a primary
51
+ button vanish under any pale accent — white on near-white. */
52
+ /* Near-black, because the accent is light enough to need it. White on the old
53
+ `#5b9dff` was 2.72:1 -- the primary button, the single most important
54
+ control in the app, failed the body-text minimum by a wide margin. */
55
+ --on-accent: #06181a;
56
+
57
+ /* ---- surface ------------------------------------------------------ */
58
+ /* Consumed as `background:`, never `background-color:`, so a theme may hand
59
+ it a gradient or an image rather than only a colour. */
60
+ --app-bg: var(--bg);
61
+ /* A slot. Inert unless a theme fills it — see `body::before` in styles.css.
62
+ This is how a theme gets a grain, a vignette or a wash without shipping a
63
+ rule to create the layer. */
64
+ --app-overlay: none;
65
+ --app-overlay-opacity: 0;
66
+ /* Elevation as a scale rather than one shadow. Black is right on a dark UI
67
+ and muddy on a light one, so this has to be a theme's decision. */
68
+ --shadow-1: 0 6px 20px rgba(0, 0, 0, 0.22);
69
+ --shadow-2: 0 8px 28px rgba(0, 0, 0, 0.28);
70
+ --shadow-3: 0 10px 34px rgba(0, 0, 0, 0.3);
71
+ --scrim: rgba(0, 0, 0, 0.45);
72
+
73
+ /* ---- typography --------------------------------------------------- */
74
+ --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
75
+ --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
76
+ --font-size: 14px;
77
+ /* The scale, because there was not one.
78
+ `styles.css` carried eleven distinct sizes -- 10, 10.5, 11, 12, 13, 14, 15,
79
+ 16, 17, 18, 19 -- and used `--font-size` exactly once. Eleven sizes is not a
80
+ hierarchy, it is eleven separate decisions, and it is most of why the app
81
+ felt uneven: two labels doing the same job would differ by a pixel for no
82
+ reason anybody could state.
83
+ Six steps, and every one of them earns its place: `xs` for the meter row and
84
+ mono chips, `sm` for hints, base for body, then three heading steps. A theme
85
+ can move the whole scale by moving `--font-size`, because they are all
86
+ relative to it.
87
+
88
+ `calc` off the token rather than `rem`, which is relative to the *root*
89
+ element and not to this -- `--font-size` is set on `body`, so a rem scale
90
+ would have silently resolved against the browser's 16px and every step
91
+ would have been wrong by a pixel and a half. */
92
+ /* `2xs` exists so the scale never has to round *up*. The first pass mapped
93
+ 10px and 10.5px onto 11px and 15px onto 16px, and the app visibly grew --
94
+ a scale that inflates is not a scale, it is a resize. */
95
+ --text-2xs: calc(var(--font-size) * 0.714);
96
+ --text-xs: calc(var(--font-size) * 0.786);
97
+ --text-sm: calc(var(--font-size) * 0.857);
98
+ --text-base: var(--font-size);
99
+ --text-md: calc(var(--font-size) * 1.143);
100
+ --text-lg: calc(var(--font-size) * 1.286);
101
+ --text-xl: calc(var(--font-size) * 1.429);
102
+ --line-height: 1.5;
103
+ --tracking: 0;
104
+ --weight: 400;
105
+ --weight-strong: 600;
106
+
107
+ /* ---- shape -------------------------------------------------------- */
108
+ /* There were fifteen distinct radii and a `--radius` token used nine times
109
+ while literal `6px` appeared twelve. Four steps and a pill is the whole
110
+ range anything here actually needed. */
111
+ --radius-sm: 3px;
112
+ --radius: 6px;
113
+ --radius-md: 8px;
114
+ --radius-lg: 12px;
115
+ --radius-pill: 999px;
116
+ --border-width: 1px;
117
+ /* The surface a control sits on: an input, a select, a button that is not
118
+ the primary one. Derived from the two panel tones rather than stated, so a
119
+ theme that moves the panels moves the controls with them -- but themeable,
120
+ because a theme may want a control to read as inset or raised against a
121
+ panel it chose. Same bargain as `--app-bg`.
122
+ It lived on `:root` in `styles.css` and was used seven times, which made it
123
+ a token in every respect except being in the vocabulary a theme can name.
124
+ The audit that found it was looking for something else. */
125
+ --control: color-mix(in srgb, var(--panel-2) 62%, var(--panel));
126
+
127
+ /* Two jobs, two tokens.
128
+ `--border` was drawing both the hairline between two rows and the outline
129
+ of every input, select and button. Those have different requirements: a
130
+ divider is decoration and has no contrast minimum, while a control's
131
+ boundary is what tells you it is a control -- WCAG 1.4.11 wants 3:1 for
132
+ that. One token could satisfy either but not both, and it was set for the
133
+ divider, so every input in the app outlined itself at 1.26:1.
134
+ Solved for the threshold rather than picked: this is the first step on the
135
+ neutral ramp that clears 3:1 against `--panel`. */
136
+ --border-strong: #5f6a7a;
137
+
138
+ /* The paper an illustration is drawn on -- the white fill in an Open Peeps
139
+ figure, as opposed to the ink, which takes `currentColor`. Defaults to the
140
+ panel so a figure sits on its surface without being told.
141
+ It was a slot in every respect but the one that matters: used as
142
+ `var(--illo-paper, var(--panel))` and declared nowhere, so it rendered
143
+ correctly and no theme could fill it. A slot the vocabulary does not
144
+ contain is themeable only in appearance. */
145
+ --illo-paper: var(--panel);
146
+
147
+ /* The checkmark, once. Drawn as a mask so whatever wears it supplies its own
148
+ colour -- the choice row's tick and the select's are the same shape and
149
+ should not be two paths that drift. */
150
+ --tick-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m5 13 4 4 10-10' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
151
+
152
+ /* ---- density ------------------------------------------------------ */
153
+ /* One scale. `--density` multiplies the structural gaps — the grid, the
154
+ modals, the settings panes — so a theme can be roomy or tight without
155
+ restating every padding in the stylesheet. */
156
+ --density: 1;
157
+
158
+ /* One scale, in fours, for padding and margin and gap alike.
159
+ There were twenty-four distinct spacing values in `styles.css` -- every
160
+ integer from 1 to 16, then 18, 20, 22, 24, 26, 28, 40, 60 -- against
161
+ twenty-six uses of a token. That is not a rhythm, it is two hundred and
162
+ twenty-five separate decisions, and it is why two rows doing the same job
163
+ sat a pixel apart.
164
+ Fours because the app's smallest real gap is 4px and everything above it
165
+ was already trying to be a multiple of four and missing: 6 and 10 and 14
166
+ are what you write when there is no scale to reach for.
167
+ `--density` multiplies the structural steps, so a theme can be roomy or
168
+ tight without restating a single padding. */
169
+ --space-1: calc(4px * var(--density));
170
+ --space-2: calc(8px * var(--density));
171
+ --space-3: calc(12px * var(--density));
172
+ --space-4: calc(16px * var(--density));
173
+ --space-5: calc(20px * var(--density));
174
+ --space-6: calc(24px * var(--density));
175
+ --space-8: calc(32px * var(--density));
176
+ --space-10: calc(40px * var(--density));
177
+ --space-15: calc(60px * var(--density));
178
+
179
+ /* Below the scale, and deliberately outside it: a 1px or 2px offset is not
180
+ spacing, it is optical alignment -- nudging a glyph off a baseline, or a
181
+ mark off a cap height. Rounding those up to 4px would move things that were
182
+ placed by eye. */
183
+ --nudge: 2px;
184
+
185
+
186
+ /* ---- motion ------------------------------------------------------- */
187
+ /* Four durations existed, written four ways — `0.12s` and `120ms` were the
188
+ same value spelled differently. A theme sets the character here: calm at
189
+ 320ms ease-out, or snappy at 90ms.
190
+ `--dur-drag` is read from JavaScript by the tile-reorder FLIP animation,
191
+ which is the one piece of motion CSS cannot reach. */
192
+ --dur-fast: 120ms;
193
+ --dur-md: 200ms;
194
+ --dur-slow: 300ms;
195
+ --dur-drag: 180ms;
196
+ --ease: cubic-bezier(0.2, 0, 0, 1);
197
+ --ease-out: ease-out;
198
+ /* A spring, sampled: stiffness 170, damping 26, no overshoot -- what a knob
199
+ does under a thumb rather than what a fade does. Written as `linear()` so
200
+ a consumer needs no animation engine, and the same curve `chef`'s design
201
+ system calls `spring/gentle`; it arrived here 2026-09-05 with the switch's
202
+ async state, so the knob's travel could be copied rather than approximated.
203
+ Played over `--dur-md`. A theme may replace it with any easing. */
204
+ --ease-spring: linear(0, 0.0315, 0.1058, 0.2003, 0.3012, 0.4, 0.4917, 0.5741, 0.6464, 0.7086, 0.7614, 0.8057, 0.8425, 0.8729, 0.8979, 0.9182, 0.9346, 0.9479, 0.9586, 0.9672, 0.9741, 0.9795, 0.9839, 0.9873, 1);
205
+
206
+ /* ---- interaction -------------------------------------------------- */
207
+ --hover-lift: 0px;
208
+ --hover-scale: 1;
209
+ /* Read by every `:active` rule in `styles.css` -- reserved since the
210
+ vocabulary was written, used since 2026-09-03. Small: a control that
211
+ visibly shrinks under the pointer reads as a toy. Reduced motion puts it
212
+ back to 1 below. */
213
+ --press-scale: 0.985;
214
+ /* How much darker a control goes while pressed: the share of `--text` mixed
215
+ into its hover colour. One number, so a button, a row and a tab press
216
+ the same amount. */
217
+ --press-ink: 9%;
218
+ /* The accent itself, not a pale tint of it. `--accent-dim` was 1.90:1 against
219
+ the panel, so the ring marking the focused control was fainter than the
220
+ control. */
221
+ --focus-ring: 0 0 0 2px var(--accent);
222
+
223
+ /* ---- fixed -------------------------------------------------------- */
224
+ /* Not themeable: geometry other things are measured against. The panel head
225
+ is pinned so a head without a cog — the studio's preview — is the same
226
+ height as one with, and the applet gets the same remainder in both. */
227
+ --tile-control: 34px;
228
+ }
229
+
230
+ /* Motion is an accessibility setting before it is a style, so this outranks
231
+ every theme. Declared here rather than in `styles.css` because it is part of
232
+ the vocabulary's contract: a theme may set the durations, it may not decide
233
+ whether they apply. */
234
+ /* `!important`, and it is the one place in the vocabulary that carries it.
235
+ A theme is applied as inline styles on the root, and an inline declaration
236
+ beats a stylesheet rule -- so a theme that set `--dur-md: 260ms` kept it
237
+ under `prefers-reduced-motion: reduce`, measured, and the three built-ins
238
+ only ever passed because none of them sets a duration. "Outranks every
239
+ theme" has to be true of a theme written by somebody who never read this
240
+ file, and the cascade is the only thing that can make it so. */
241
+ @media (prefers-reduced-motion: reduce) {
242
+ :root {
243
+ --dur-fast: 0s !important;
244
+ --dur-md: 0s !important;
245
+ --dur-slow: 0s !important;
246
+ --dur-drag: 0s !important;
247
+ --hover-lift: 0px !important;
248
+ --hover-scale: 1 !important;
249
+ --press-scale: 1 !important;
250
+ }
251
+ }
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@wtfalch/design",
3
+ "version": "0.1.0",
4
+ "description": "The tf design system: themeable components on a fixed token vocabulary.",
5
+ "keywords": [
6
+ "design-system",
7
+ "react",
8
+ "react-aria",
9
+ "components",
10
+ "design-tokens",
11
+ "themes",
12
+ "css-variables",
13
+ "accessibility"
14
+ ],
15
+ "author": "William Tallis Falch",
16
+ "homepage": "https://github.com/wtfalch/design#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/wtfalch/design/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/wtfalch/design",
23
+ "directory": "packages/design"
24
+ },
25
+ "license": "MIT",
26
+ "type": "module",
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "./themes": {
39
+ "types": "./dist/themes.d.ts",
40
+ "default": "./dist/themes.js"
41
+ },
42
+ "./tokens.css": "./dist/tokens.css",
43
+ "./styles.css": "./dist/styles/index.css"
44
+ },
45
+ "sideEffects": [
46
+ "*.css"
47
+ ],
48
+ "engines": {
49
+ "node": ">=22.0.0"
50
+ },
51
+ "peerDependencies": {
52
+ "react": "^19.0.0",
53
+ "react-dom": "^19.0.0"
54
+ },
55
+ "dependencies": {
56
+ "dompurify": "^3.4.13",
57
+ "marked": "^18.0.9",
58
+ "react-aria-components": "^1.21.0"
59
+ },
60
+ "devDependencies": {
61
+ "@types/node": "^22",
62
+ "@types/react": "^19",
63
+ "@types/react-dom": "^19",
64
+ "react": "^19",
65
+ "react-dom": "^19",
66
+ "typescript": "^5.9.0",
67
+ "vitest": "^4.1.6"
68
+ },
69
+ "scripts": {
70
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/copy-css.mjs",
71
+ "typecheck": "tsc --noEmit",
72
+ "test": "vitest run"
73
+ }
74
+ }