@wtfalch/design 0.2.0 → 0.3.1

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 (43) hide show
  1. package/README.md +81 -35
  2. package/dist/components/Brand.d.ts +12 -1
  3. package/dist/components/Brand.js +15 -3
  4. package/dist/components/Callout.d.ts +1 -1
  5. package/dist/components/Callout.js +1 -1
  6. package/dist/components/Card.d.ts +1 -1
  7. package/dist/components/Card.js +1 -1
  8. package/dist/components/DangerZone.js +3 -3
  9. package/dist/components/Dialog.js +1 -1
  10. package/dist/components/Empty.d.ts +2 -2
  11. package/dist/components/Empty.js +2 -2
  12. package/dist/components/Icon.d.ts +2 -2
  13. package/dist/components/Icon.js +5 -228
  14. package/dist/components/Illustration.d.ts +1 -1
  15. package/dist/components/Illustration.js +2 -3
  16. package/dist/components/Input.js +1 -1
  17. package/dist/components/Modal.js +2 -2
  18. package/dist/components/Toast.js +1 -1
  19. package/dist/components/Tour.js +2 -2
  20. package/dist/components/brandMarks.d.ts +40 -1
  21. package/dist/components/brandMarks.js +14 -19
  22. package/dist/components/icons.d.ts +37 -0
  23. package/dist/components/icons.js +253 -0
  24. package/dist/index.d.ts +56 -46
  25. package/dist/index.js +44 -35
  26. package/dist/products/index.d.ts +69 -0
  27. package/dist/products/index.js +54 -0
  28. package/dist/products/tf.d.ts +32 -0
  29. package/dist/products/tf.js +104 -0
  30. package/dist/products/valet.d.ts +23 -0
  31. package/dist/products/valet.js +103 -0
  32. package/dist/tf.css +3362 -0
  33. package/dist/tf.d.ts +18 -0
  34. package/dist/tf.js +17 -0
  35. package/dist/themes/css.d.ts +45 -0
  36. package/dist/themes/css.js +78 -0
  37. package/dist/{themes.d.ts → themes/index.d.ts} +11 -13
  38. package/dist/{themes.js → themes/index.js} +12 -89
  39. package/dist/tokens.css +17 -0
  40. package/dist/valet.css +3366 -0
  41. package/dist/valet.d.ts +18 -0
  42. package/dist/valet.js +17 -0
  43. package/package.json +14 -4
package/dist/tf.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * tf's entry: the package, as tf.
3
+ *
4
+ * Everything the main entry exports, and on top of it `Brand`, `THEMES`,
5
+ * `DEFAULT_THEME` and `applyTheme` bound to tf -- a local export shadows
6
+ * the same name from `export *`. A site is one product, so it imports this
7
+ * and `@wtfalch/design/tf.css` and nothing of anyone else's.
8
+ */
9
+ export * from './index.js';
10
+ export declare const product: import("./products/index.js").Product;
11
+ export declare const Brand: (props: {
12
+ name?: import("./index.js").BrandName;
13
+ title?: string;
14
+ className?: string;
15
+ }) => import("react").JSX.Element;
16
+ export declare const THEMES: Record<string, import("./themes/index.js").Theme>;
17
+ export declare const DEFAULT_THEME: string;
18
+ export declare const applyTheme: (theme?: string | import("./themes/index.js").Theme, el?: HTMLElement) => void;
package/dist/tf.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * tf's entry: the package, as tf.
3
+ *
4
+ * Everything the main entry exports, and on top of it `Brand`, `THEMES`,
5
+ * `DEFAULT_THEME` and `applyTheme` bound to tf -- a local export shadows
6
+ * the same name from `export *`. A site is one product, so it imports this
7
+ * and `@wtfalch/design/tf.css` and nothing of anyone else's.
8
+ */
9
+ export * from './index.js';
10
+ import { bindProduct } from './products/index.js';
11
+ import { tf } from './products/tf.js';
12
+ const bound = bindProduct(tf);
13
+ export const product = bound.product;
14
+ export const Brand = bound.Brand;
15
+ export const THEMES = bound.THEMES;
16
+ export const DEFAULT_THEME = bound.DEFAULT_THEME;
17
+ export const applyTheme = bound.applyTheme;
@@ -0,0 +1,45 @@
1
+ import type { Theme, ThemeTokens } from './index.js';
2
+ /**
3
+ * A theme as CSS, for the paint before React.
4
+ *
5
+ * `applyTheme` writes a theme as inline custom properties at run time, and
6
+ * React mounts after the first paint, so a theme applied in an effect flashes
7
+ * the base palette. A consumer wants the same values as a stylesheet rule it
8
+ * can ship in `<head>`, and it must not write that rule by hand, because a
9
+ * copy drifts from the object the contrast test measures. So the rule is
10
+ * generated from the object: at build time for the products here, or by a
11
+ * consumer for a theme of its own.
12
+ *
13
+ * The selector is `:root[data-theme='<id>']`, the attribute `applyTheme` sets,
14
+ * so the two mechanisms agree on which theme is on. A theme with no tokens,
15
+ * which is `system`, gets no block: its palette is the base, and its light
16
+ * half is the `prefers-color-scheme` block `base.css` scopes to it.
17
+ *
18
+ * `build-products.mjs` loads the compiled copy of this file in plain Node.
19
+ * Until 0.3.1 that meant it could import nothing but a type, because the build
20
+ * emitted relative imports without extensions; the build writes them now.
21
+ */
22
+ /** The name `applyTheme` derives for a theme object. */
23
+ export declare function themeId(theme: Theme): string;
24
+ export declare function themeCss(id: string, theme: Theme): string;
25
+ /** Every theme of one product, one rule each, ids checked against the names. */
26
+ export declare function productCss(themes: Record<string, Theme>): string;
27
+ /**
28
+ * A product's stylesheet, whole: the one file a site imports.
29
+ *
30
+ * In cascade order: the vocabulary; the product's identity on `:root`, so it
31
+ * wins over the base values by coming later at the same specificity; the
32
+ * default theme on `:root:not([data-theme])`, so the first paint is right with
33
+ * no attribute at all; the components; and one rule per theme. A default with
34
+ * no tokens -- tf's `system`, which is a media query rather than a palette --
35
+ * writes no default rule, and neither does an empty identity.
36
+ *
37
+ * Structural, not typed to `Product`: that type lives beside the React
38
+ * binding, and this module has to stay loadable in plain Node for the build.
39
+ */
40
+ export declare function productStylesheet(product: {
41
+ name: string;
42
+ identity: Partial<ThemeTokens>;
43
+ themes: Record<string, Theme>;
44
+ defaultTheme: string;
45
+ }, tokens: string, components: string): string;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * A theme as CSS, for the paint before React.
3
+ *
4
+ * `applyTheme` writes a theme as inline custom properties at run time, and
5
+ * React mounts after the first paint, so a theme applied in an effect flashes
6
+ * the base palette. A consumer wants the same values as a stylesheet rule it
7
+ * can ship in `<head>`, and it must not write that rule by hand, because a
8
+ * copy drifts from the object the contrast test measures. So the rule is
9
+ * generated from the object: at build time for the products here, or by a
10
+ * consumer for a theme of its own.
11
+ *
12
+ * The selector is `:root[data-theme='<id>']`, the attribute `applyTheme` sets,
13
+ * so the two mechanisms agree on which theme is on. A theme with no tokens,
14
+ * which is `system`, gets no block: its palette is the base, and its light
15
+ * half is the `prefers-color-scheme` block `base.css` scopes to it.
16
+ *
17
+ * `build-products.mjs` loads the compiled copy of this file in plain Node.
18
+ * Until 0.3.1 that meant it could import nothing but a type, because the build
19
+ * emitted relative imports without extensions; the build writes them now.
20
+ */
21
+ /** The name `applyTheme` derives for a theme object. */
22
+ export function themeId(theme) {
23
+ return theme.name.toLowerCase().replace(/\s+/g, '-');
24
+ }
25
+ /** The declarations of a token map, as one line. */
26
+ function declarations(tokens) {
27
+ return Object.entries(tokens)
28
+ .map(([key, value]) => `${key}:${value}`)
29
+ .join(';');
30
+ }
31
+ export function themeCss(id, theme) {
32
+ const rules = declarations(theme.tokens);
33
+ if (!rules)
34
+ return '';
35
+ return `:root[data-theme='${id}']{${rules};color-scheme:${theme.scheme}}`;
36
+ }
37
+ /** Every theme of one product, one rule each, ids checked against the names. */
38
+ export function productCss(themes) {
39
+ return Object.entries(themes)
40
+ .map(([id, theme]) => {
41
+ if (themeId(theme) !== id) {
42
+ throw new Error(`theme "${theme.name}" is keyed "${id}", but applyTheme calls it "${themeId(theme)}"`);
43
+ }
44
+ return themeCss(id, theme);
45
+ })
46
+ .filter(Boolean)
47
+ .join('\n');
48
+ }
49
+ /**
50
+ * A product's stylesheet, whole: the one file a site imports.
51
+ *
52
+ * In cascade order: the vocabulary; the product's identity on `:root`, so it
53
+ * wins over the base values by coming later at the same specificity; the
54
+ * default theme on `:root:not([data-theme])`, so the first paint is right with
55
+ * no attribute at all; the components; and one rule per theme. A default with
56
+ * no tokens -- tf's `system`, which is a media query rather than a palette --
57
+ * writes no default rule, and neither does an empty identity.
58
+ *
59
+ * Structural, not typed to `Product`: that type lives beside the React
60
+ * binding, and this module has to stay loadable in plain Node for the build.
61
+ */
62
+ export function productStylesheet(product, tokens, components) {
63
+ const parts = ['/* ---- tokens.css ---- */', tokens.trimEnd()];
64
+ const identity = declarations(product.identity);
65
+ if (identity) {
66
+ parts.push(`/* ---- ${product.name}: the identity, under every theme ---- */`, `:root{${identity}}`);
67
+ }
68
+ const fallback = product.themes[product.defaultTheme];
69
+ if (!fallback) {
70
+ throw new Error(`product "${product.name}" defaults to "${product.defaultTheme}", which is not one of its themes`);
71
+ }
72
+ const rules = declarations(fallback.tokens);
73
+ if (rules) {
74
+ parts.push(`/* ---- ${product.name}: ${product.defaultTheme}, until a theme is picked ---- */`, `:root:not([data-theme]){${rules};color-scheme:${fallback.scheme}}`);
75
+ }
76
+ parts.push('/* ---- styles.css ---- */', components.trimEnd(), `/* ---- ${product.name}: the themes ---- */`, productCss(product.themes));
77
+ return `${parts.join('\n')}\n`;
78
+ }
@@ -167,19 +167,17 @@ export interface Theme {
167
167
  */
168
168
  export declare function defineTheme(theme: Theme): Theme;
169
169
  /**
170
- * The built-ins, which are **examples and not the menu**.
171
- *
172
- * tf ships these four; an app that installs this package is expected to bring
173
- * its own and is not expected to look like tf. They are here because a package
174
- * that ships a token vocabulary and no theme written in it leaves the first
175
- * consumer guessing at how wide the vocabulary really is and because the
176
- * contrast test needs something concrete to measure.
177
- *
178
- * `system` is a theme, not a mode. A `prefers-color-scheme` block that
179
- * overrides `:root` unconditionally means choosing a dark theme on a
180
- * light-mode laptop gets silently repainted; that media query is scoped to this
181
- * theme, so following the OS is a choice among the others rather than a rule
182
- * above them.
170
+ * Every theme the package knows, keyed by the name a consumer applies.
171
+ *
172
+ * Per product since 0.3.0. A theme is part of a product's identity the way its
173
+ * mark is, and 0.2.0 already made `Brand` the home of every product's mark by
174
+ * name; keeping each product's palette in its own repo meant each repo
175
+ * re-deriving first paint, the contrast measurement and a page to look at it
176
+ * on. So `products/tf.ts` holds tf's three and `products/valet.ts` valet's two,
177
+ * this is the union, and the contrast test and the gallery read the union.
178
+ *
179
+ * A consumer that wants only its own imports the product module, or the CSS
180
+ * `build-products.mjs` generates from it, and bundles nobody else's.
183
181
  */
184
182
  export declare const THEMES: Record<string, Theme>;
185
183
  export declare const DEFAULT_THEME = "system";
@@ -37,6 +37,8 @@
37
37
  * token added to the stylesheet and to no list fails the suite rather than
38
38
  * becoming a fourth, undocumented category.
39
39
  */
40
+ import { TF_THEMES } from '../products/tf.js';
41
+ import { VALET_THEMES } from '../products/valet.js';
40
42
  /**
41
43
  * Computed from a themeable token, and not settable.
42
44
  *
@@ -156,98 +158,19 @@ export function defineTheme(theme) {
156
158
  return theme;
157
159
  }
158
160
  /**
159
- * The dark palette, written out rather than inherited.
161
+ * Every theme the package knows, keyed by the name a consumer applies.
160
162
  *
161
- * `night` was `tokens: {}` once "whatever the base is" and that was wrong
162
- * twice over. It made the theme depend on a file it does not own, and it made
163
- * its swatch in the picker preview *the theme currently applied*, because an
164
- * empty map falls back to the live values.
165
- */
166
- const NIGHT = {
167
- '--bg': '#0f1115',
168
- '--panel': '#161a21',
169
- '--panel-2': '#1c222b',
170
- '--border': '#262d38',
171
- '--text': '#e6e9ef',
172
- '--muted': '#8b94a4',
173
- '--accent': '#5b9dff',
174
- '--accent-dim': '#2a4877',
175
- /* No `--on-accent` here on purpose, so it inherits the base near-black.
176
- Night used to set `#ffffff`, which is 2.72:1 on this accent -- the exact
177
- pair `tokens.css` records as the reason the token exists at all. The base
178
- was fixed and the theme carrying the old palette was never revisited, so
179
- the primary button in the app's fixed dark theme failed the body-text
180
- minimum by a wide margin for as long as the token had been "fixed".
181
- `#06181a` on `#5b9dff` is 6.69:1. Found by the contrast scan, which is the
182
- argument for shipping the measurement rather than the rule. */
183
- '--app-bg': '#0f1115',
184
- };
185
- /**
186
- * The built-ins, which are **examples and not the menu**.
187
- *
188
- * tf ships these four; an app that installs this package is expected to bring
189
- * its own and is not expected to look like tf. They are here because a package
190
- * that ships a token vocabulary and no theme written in it leaves the first
191
- * consumer guessing at how wide the vocabulary really is — and because the
192
- * contrast test needs something concrete to measure.
163
+ * Per product since 0.3.0. A theme is part of a product's identity the way its
164
+ * mark is, and 0.2.0 already made `Brand` the home of every product's mark by
165
+ * name; keeping each product's palette in its own repo meant each repo
166
+ * re-deriving first paint, the contrast measurement and a page to look at it
167
+ * on. So `products/tf.ts` holds tf's three and `products/valet.ts` valet's two,
168
+ * this is the union, and the contrast test and the gallery read the union.
193
169
  *
194
- * `system` is a theme, not a mode. A `prefers-color-scheme` block that
195
- * overrides `:root` unconditionally means choosing a dark theme on a
196
- * light-mode laptop gets silently repainted; that media query is scoped to this
197
- * theme, so following the OS is a choice among the others rather than a rule
198
- * above them.
170
+ * A consumer that wants only its own imports the product module, or the CSS
171
+ * `build-products.mjs` generates from it, and bundles nobody else's.
199
172
  */
200
- export const THEMES = {
201
- system: {
202
- name: 'System',
203
- note: 'Follows your OS between light and dark',
204
- scheme: 'dark',
205
- // Empty on purpose: this is the one theme that must *not* state a palette,
206
- // because the `prefers-color-scheme` block is scoped to it and needs the
207
- // base values to fall through. Its swatch is a special case.
208
- tokens: {},
209
- },
210
- night: {
211
- name: 'Night',
212
- note: 'The dark palette, fixed — ignores the OS',
213
- scheme: 'dark',
214
- tokens: NIGHT,
215
- },
216
- paper: {
217
- name: 'Paper',
218
- note: 'Light, with shadows that suit it',
219
- scheme: 'light',
220
- tokens: {
221
- '--bg': '#f6f7f9',
222
- '--panel': '#ffffff',
223
- '--panel-2': '#f0f2f5',
224
- '--border': '#e4e8ec',
225
- '--border-strong': '#8792a1',
226
- '--text': '#191d23',
227
- '--muted': '#5d6773',
228
- '--accent': '#0e7872',
229
- '--accent-dim': '#7fbdb8',
230
- '--on-accent': '#ffffff',
231
- // The base set is tuned for a dark panel. Unoverridden, `--good` was
232
- // 1.74:1 on white -- a `running` pill nobody could read.
233
- '--good': '#1c7a4a',
234
- '--warn': '#8a6216',
235
- '--bad': '#b3312c',
236
- // `#2f7fe6` was 3.96:1 on this theme's white panel -- the "information is
237
- // blue" colour was measured on the dark base when it was added and never
238
- // on a light one. Found by contrast.test.ts on its first run. This is the
239
- // lightest step on the same hue that clears 4.5:1 on the panel *and* the
240
- // page, with room: 5.24 on white, 4.88 on the page.
241
- '--info': '#216bc9',
242
- // Black shadows are right on a dark UI and muddy on a light one. This is
243
- // the whole reason elevation had to become a token.
244
- '--shadow-1': '0 4px 14px rgba(16, 24, 40, 0.08)',
245
- '--shadow-2': '0 8px 24px rgba(16, 24, 40, 0.10)',
246
- '--shadow-3': '0 12px 32px rgba(16, 24, 40, 0.12)',
247
- '--scrim': 'rgba(16, 24, 40, 0.32)',
248
- },
249
- },
250
- };
173
+ export const THEMES = { ...TF_THEMES, ...VALET_THEMES };
251
174
  export const DEFAULT_THEME = 'system';
252
175
  export function isTheme(name) {
253
176
  return typeof name === 'string' && name in THEMES;
package/dist/tokens.css CHANGED
@@ -238,6 +238,23 @@
238
238
  only ever passed because none of them sets a duration. "Outranks every
239
239
  theme" has to be true of a theme written by somebody who never read this
240
240
  file, and the cascade is the only thing that can make it so. */
241
+ /*
242
+ * Three tokens derived from colour tokens, re-derived wherever a theme lands.
243
+ *
244
+ * Declared on `:root` alone, `--control`, `--illo-paper` and `--focus-ring`
245
+ * resolve their `var()` against the root's palette and are inherited as
246
+ * finished values. A theme applied to a subtree with `applyTheme(theme, el)`
247
+ * then gets the root's control surface and the root's focus ring under its own
248
+ * panels: found on 2026-09-05 as black inputs and a teal ring on a light theme
249
+ * painted beside a dark one. `data-theme` is the attribute `applyTheme` sets,
250
+ * so a themed element derives them again from its own values.
251
+ */
252
+ [data-theme] {
253
+ --control: color-mix(in srgb, var(--panel-2) 62%, var(--panel));
254
+ --illo-paper: var(--panel);
255
+ --focus-ring: 0 0 0 2px var(--accent);
256
+ }
257
+
241
258
  @media (prefers-reduced-motion: reduce) {
242
259
  :root {
243
260
  --dur-fast: 0s !important;