@wtfalch/design 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -37
- package/dist/components/Brand.d.ts +35 -6
- package/dist/components/Brand.js +26 -201
- package/dist/components/Icon.d.ts +1 -1
- package/dist/components/Icon.js +5 -228
- package/dist/components/Illustration.js +1 -2
- package/dist/components/Tour.d.ts +3 -1
- package/dist/components/Tour.js +3 -3
- package/dist/components/brandMarks.d.ts +71 -0
- package/dist/components/brandMarks.js +26 -0
- package/dist/components/icons.d.ts +37 -0
- package/dist/components/icons.js +253 -0
- package/dist/components/tourMarker.d.ts +14 -3
- package/dist/components/tourMarker.js +11 -7
- package/dist/index.d.ts +13 -1
- package/dist/index.js +11 -1
- package/dist/products/index.d.ts +69 -0
- package/dist/products/index.js +54 -0
- package/dist/products/tf.d.ts +32 -0
- package/dist/products/tf.js +104 -0
- package/dist/products/valet.d.ts +23 -0
- package/dist/products/valet.js +103 -0
- package/dist/styles/index.css +23 -57
- package/dist/tf.css +3362 -0
- package/dist/tf.d.ts +18 -0
- package/dist/tf.js +17 -0
- package/dist/themes/css.d.ts +45 -0
- package/dist/themes/css.js +78 -0
- package/dist/{themes.d.ts → themes/index.d.ts} +11 -13
- package/dist/{themes.js → themes/index.js} +12 -89
- package/dist/tokens.css +17 -0
- package/dist/valet.css +3366 -0
- package/dist/valet.d.ts +18 -0
- package/dist/valet.js +17 -0
- package/package.json +15 -5
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';
|
|
10
|
+
export declare const product: import("./products").Product;
|
|
11
|
+
export declare const Brand: (props: {
|
|
12
|
+
name?: import("./index").BrandName;
|
|
13
|
+
title?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
}) => import("react").JSX.Element;
|
|
16
|
+
export declare const THEMES: Record<string, import("./themes").Theme>;
|
|
17
|
+
export declare const DEFAULT_THEME: string;
|
|
18
|
+
export declare const applyTheme: (theme?: string | import("./themes").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';
|
|
10
|
+
import { bindProduct } from './products';
|
|
11
|
+
import { tf } from './products/tf';
|
|
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';
|
|
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
|
+
* No imports but a type, on purpose: `build-products.mjs` loads the compiled
|
|
19
|
+
* copy of this file in plain Node, which cannot follow the package's
|
|
20
|
+
* extensionless relative imports.
|
|
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
|
+
* No imports but a type, on purpose: `build-products.mjs` loads the compiled
|
|
18
|
+
* copy of this file in plain Node, which cannot follow the package's
|
|
19
|
+
* extensionless relative imports.
|
|
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
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
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';
|
|
41
|
+
import { VALET_THEMES } from '../products/valet';
|
|
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
|
-
*
|
|
161
|
+
* Every theme the package knows, keyed by the name a consumer applies.
|
|
160
162
|
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
*
|
|
195
|
-
*
|
|
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;
|