@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.
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/components/Brand.d.ts +10 -0
- package/dist/components/Brand.js +212 -0
- package/dist/components/Button.d.ts +63 -0
- package/dist/components/Button.js +74 -0
- package/dist/components/Callout.d.ts +37 -0
- package/dist/components/Callout.js +71 -0
- package/dist/components/Card.d.ts +42 -0
- package/dist/components/Card.js +30 -0
- package/dist/components/Checkbox.d.ts +32 -0
- package/dist/components/Checkbox.js +31 -0
- package/dist/components/DangerZone.d.ts +59 -0
- package/dist/components/DangerZone.js +50 -0
- package/dist/components/Dialog.d.ts +28 -0
- package/dist/components/Dialog.js +29 -0
- package/dist/components/Empty.d.ts +45 -0
- package/dist/components/Empty.js +35 -0
- package/dist/components/Field.d.ts +58 -0
- package/dist/components/Field.js +46 -0
- package/dist/components/Icon.d.ts +64 -0
- package/dist/components/Icon.js +235 -0
- package/dist/components/Illustration.d.ts +36 -0
- package/dist/components/Illustration.js +48 -0
- package/dist/components/Input.d.ts +14 -0
- package/dist/components/Input.js +65 -0
- package/dist/components/Markdown.d.ts +21 -0
- package/dist/components/Markdown.js +29 -0
- package/dist/components/Modal.d.ts +59 -0
- package/dist/components/Modal.js +72 -0
- package/dist/components/Pill.d.ts +40 -0
- package/dist/components/Pill.js +41 -0
- package/dist/components/Progress.d.ts +35 -0
- package/dist/components/Progress.js +27 -0
- package/dist/components/Rows.d.ts +101 -0
- package/dist/components/Rows.js +55 -0
- package/dist/components/Select.d.ts +28 -0
- package/dist/components/Select.js +56 -0
- package/dist/components/SizeGrid.d.ts +34 -0
- package/dist/components/SizeGrid.js +41 -0
- package/dist/components/Skeleton.d.ts +45 -0
- package/dist/components/Skeleton.js +47 -0
- package/dist/components/Slider.d.ts +70 -0
- package/dist/components/Slider.js +100 -0
- package/dist/components/Table.d.ts +43 -0
- package/dist/components/Table.js +13 -0
- package/dist/components/Tabs.d.ts +72 -0
- package/dist/components/Tabs.js +82 -0
- package/dist/components/Textarea.d.ts +9 -0
- package/dist/components/Textarea.js +22 -0
- package/dist/components/Toast.d.ts +43 -0
- package/dist/components/Toast.js +78 -0
- package/dist/components/Toggle.d.ts +56 -0
- package/dist/components/Toggle.js +189 -0
- package/dist/components/Tooltip.d.ts +22 -0
- package/dist/components/Tooltip.js +62 -0
- package/dist/components/Tour.d.ts +33 -0
- package/dist/components/Tour.js +108 -0
- package/dist/components/iconNames.d.ts +18 -0
- package/dist/components/iconNames.js +60 -0
- package/dist/components/tourMarker.d.ts +29 -0
- package/dist/components/tourMarker.js +58 -0
- package/dist/contrast.d.ts +18 -0
- package/dist/contrast.js +27 -0
- package/dist/hooks/useTrapFocus.d.ts +24 -0
- package/dist/hooks/useTrapFocus.js +67 -0
- package/dist/illustrations.d.ts +11 -0
- package/dist/illustrations.js +55 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +65 -0
- package/dist/styles/index.css +3124 -0
- package/dist/themes.d.ts +210 -0
- package/dist/themes.js +300 -0
- package/dist/tokens.css +251 -0
- package/package.json +74 -0
package/dist/themes.d.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The themes, and the vocabulary they are allowed to speak.
|
|
3
|
+
*
|
|
4
|
+
* A theme is a `Partial<ThemeTokens>`: name the tokens you change, inherit the
|
|
5
|
+
* rest from `tokens.css`. Without that, every theme is a forty-six line chore
|
|
6
|
+
* and every token added later means editing all of them.
|
|
7
|
+
*
|
|
8
|
+
* The type is the enforcement. A typo in a token name is a compile error rather
|
|
9
|
+
* than a property that silently does nothing — which is exactly the failure a
|
|
10
|
+
* string-keyed map would produce, at run time, invisibly. That property is the
|
|
11
|
+
* reason this file is exported from the package at all: a consumer writing a
|
|
12
|
+
* theme gets the same compile error tf gets, and a theme that ships as JSON or
|
|
13
|
+
* as a hand-written CSS file does not.
|
|
14
|
+
*
|
|
15
|
+
* ---
|
|
16
|
+
*
|
|
17
|
+
* **A token is themeable, derived, or fixed, and the three are separate lists.**
|
|
18
|
+
*
|
|
19
|
+
* The old contract had one list, and it had drifted from the stylesheet in both
|
|
20
|
+
* directions: it declared `--pad-1`…`--pad-5`, which `tokens.css` stopped
|
|
21
|
+
* defining when the spacing scale was renamed to `--space-*`, so five keys a
|
|
22
|
+
* theme could name resolved to nothing at all. And it named none of the sixteen
|
|
23
|
+
* derived tokens, so there was no way to tell a token deliberately withheld
|
|
24
|
+
* from a token somebody forgot.
|
|
25
|
+
*
|
|
26
|
+
* Splitting the list is what makes that a decision rather than an omission:
|
|
27
|
+
*
|
|
28
|
+
* - **themeable** — a theme may set it. Forty-six of them.
|
|
29
|
+
* - **derived** — computed from a themeable token, and a theme must NOT set it.
|
|
30
|
+
* `--text-*` are `calc()` off `--font-size` and `--space-*` are `calc()` off
|
|
31
|
+
* `--density`, so overriding one step with a literal is how you get a type
|
|
32
|
+
* scale that no longer scales. Move the input, not the output.
|
|
33
|
+
* - **fixed** — not themeable at all. Geometry other things are measured
|
|
34
|
+
* against, and one glyph.
|
|
35
|
+
*
|
|
36
|
+
* `tokens.test.ts` holds all three to `tokens.css` in both directions, so a
|
|
37
|
+
* token added to the stylesheet and to no list fails the suite rather than
|
|
38
|
+
* becoming a fourth, undocumented category.
|
|
39
|
+
*/
|
|
40
|
+
/** Every token a theme may set. */
|
|
41
|
+
export interface ThemeTokens {
|
|
42
|
+
'--bg': string;
|
|
43
|
+
'--panel': string;
|
|
44
|
+
'--panel-2': string;
|
|
45
|
+
'--border': string;
|
|
46
|
+
/** A control's outline, which WCAG 1.4.11 wants at 3:1 — a different job
|
|
47
|
+
* from `--border`, which draws hairline dividers and has no minimum. They
|
|
48
|
+
* shared a value once and it could only ever be right for one of them. */
|
|
49
|
+
'--border-strong': string;
|
|
50
|
+
'--text': string;
|
|
51
|
+
'--muted': string;
|
|
52
|
+
'--accent': string;
|
|
53
|
+
'--accent-dim': string;
|
|
54
|
+
'--good': string;
|
|
55
|
+
'--warn': string;
|
|
56
|
+
'--bad': string;
|
|
57
|
+
'--info': string;
|
|
58
|
+
/** Text that sits *on* the accent. Never assume white: on a pale accent a
|
|
59
|
+
* primary button vanishes into its own background. */
|
|
60
|
+
'--on-accent': string;
|
|
61
|
+
/** Consumed as `background:`, never `background-color:`, so a theme may hand
|
|
62
|
+
* it a gradient or an image rather than only a colour. */
|
|
63
|
+
'--app-bg': string;
|
|
64
|
+
/** A slot. Inert unless a theme fills it — this is how a theme gets a grain,
|
|
65
|
+
* a vignette or a wash without shipping a rule to create the layer. */
|
|
66
|
+
'--app-overlay': string;
|
|
67
|
+
'--app-overlay-opacity': string;
|
|
68
|
+
/** The surface a control sits on. Derived from the panel tones by default,
|
|
69
|
+
* and settable, the same bargain `--app-bg` makes. */
|
|
70
|
+
'--control': string;
|
|
71
|
+
/** The paper an illustration is drawn on; its ink takes `currentColor`. */
|
|
72
|
+
'--illo-paper': string;
|
|
73
|
+
'--shadow-1': string;
|
|
74
|
+
'--shadow-2': string;
|
|
75
|
+
'--shadow-3': string;
|
|
76
|
+
'--scrim': string;
|
|
77
|
+
'--font': string;
|
|
78
|
+
'--font-mono': string;
|
|
79
|
+
/** The one input to the type scale. Every `--text-*` step is `calc()` off
|
|
80
|
+
* this, so moving it moves all seven together. */
|
|
81
|
+
'--font-size': string;
|
|
82
|
+
'--line-height': string;
|
|
83
|
+
'--tracking': string;
|
|
84
|
+
'--weight': string;
|
|
85
|
+
'--weight-strong': string;
|
|
86
|
+
'--radius-sm': string;
|
|
87
|
+
'--radius': string;
|
|
88
|
+
'--radius-md': string;
|
|
89
|
+
'--radius-lg': string;
|
|
90
|
+
'--radius-pill': string;
|
|
91
|
+
'--border-width': string;
|
|
92
|
+
/** The one input to the spacing scale. Every `--space-*` step is `calc()`
|
|
93
|
+
* off this, so a theme can be roomy or tight without restating a single
|
|
94
|
+
* padding. */
|
|
95
|
+
'--density': string;
|
|
96
|
+
'--dur-fast': string;
|
|
97
|
+
'--dur-md': string;
|
|
98
|
+
'--dur-slow': string;
|
|
99
|
+
'--dur-drag': string;
|
|
100
|
+
'--ease': string;
|
|
101
|
+
'--ease-out': string;
|
|
102
|
+
'--ease-spring': string;
|
|
103
|
+
'--hover-lift': string;
|
|
104
|
+
'--hover-scale': string;
|
|
105
|
+
'--press-scale': string;
|
|
106
|
+
/** How much darker a control goes while pressed: the share of `--text` mixed
|
|
107
|
+
* into its hover colour. One number, so a button, a row and a tab press the
|
|
108
|
+
* same amount. */
|
|
109
|
+
'--press-ink': string;
|
|
110
|
+
'--focus-ring': string;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Computed from a themeable token, and not settable.
|
|
114
|
+
*
|
|
115
|
+
* A theme that sets `--text-lg` directly gets a scale with a step that no
|
|
116
|
+
* longer moves when `--font-size` does, which is the same as having no scale.
|
|
117
|
+
* Move `--font-size` and `--density`; these follow.
|
|
118
|
+
*/
|
|
119
|
+
export declare const DERIVED_TOKENS: readonly ["--text-2xs", "--text-xs", "--text-sm", "--text-base", "--text-md", "--text-lg", "--text-xl", "--space-1", "--space-2", "--space-3", "--space-4", "--space-5", "--space-6", "--space-8", "--space-10", "--space-15"];
|
|
120
|
+
/**
|
|
121
|
+
* Not themeable, and each for its own reason.
|
|
122
|
+
*
|
|
123
|
+
* `--tile-control` is geometry other things are measured against — a panel head
|
|
124
|
+
* without a cog has to be the same height as one with, or the content below it
|
|
125
|
+
* gets a different remainder.
|
|
126
|
+
*
|
|
127
|
+
* `--nudge` is optical alignment, not spacing: a 2px offset that nudges a glyph
|
|
128
|
+
* off a baseline does not scale with density, because the glyph did not.
|
|
129
|
+
*
|
|
130
|
+
* `--tick-mask` is the checkmark, drawn once as a mask so whatever wears it
|
|
131
|
+
* supplies its own colour. It stays fixed because an arbitrary SVG data URI
|
|
132
|
+
* from a theme is a theme shipping markup, which is the one thing the whole
|
|
133
|
+
* design refuses.
|
|
134
|
+
*/
|
|
135
|
+
export declare const FIXED_TOKENS: readonly ["--tile-control", "--nudge", "--tick-mask"];
|
|
136
|
+
/**
|
|
137
|
+
* The themeable vocabulary, at run time.
|
|
138
|
+
*
|
|
139
|
+
* `ThemeTokens` is a type and disappears at compile time, but applying a theme
|
|
140
|
+
* — or sending one into an applet's frame — means enumerating the keys with the
|
|
141
|
+
* program running. The check below keeps the two from drifting: adding a token
|
|
142
|
+
* to the interface without adding it here is a compile error, which is the same
|
|
143
|
+
* bargain the interface itself makes.
|
|
144
|
+
*/
|
|
145
|
+
export declare const TOKEN_KEYS: readonly ["--bg", "--panel", "--panel-2", "--border", "--border-strong", "--text", "--muted", "--accent", "--accent-dim", "--good", "--warn", "--bad", "--info", "--on-accent", "--app-bg", "--app-overlay", "--app-overlay-opacity", "--control", "--illo-paper", "--shadow-1", "--shadow-2", "--shadow-3", "--scrim", "--font", "--font-mono", "--font-size", "--line-height", "--tracking", "--weight", "--weight-strong", "--radius-sm", "--radius", "--radius-md", "--radius-lg", "--radius-pill", "--border-width", "--density", "--dur-fast", "--dur-md", "--dur-slow", "--dur-drag", "--ease", "--ease-out", "--ease-spring", "--hover-lift", "--hover-scale", "--press-scale", "--press-ink", "--focus-ring"];
|
|
146
|
+
export interface Theme {
|
|
147
|
+
/** What the picker calls it. */
|
|
148
|
+
name: string;
|
|
149
|
+
/** One line, shown under the name. */
|
|
150
|
+
note: string;
|
|
151
|
+
/**
|
|
152
|
+
* Whether the palette reads light or dark, for `color-scheme`.
|
|
153
|
+
*
|
|
154
|
+
* Native controls and scrollbars follow this. Getting it wrong gives you a
|
|
155
|
+
* white scrollbar down a black page, and there is no other signal that tells
|
|
156
|
+
* the browser which way round the page is.
|
|
157
|
+
*/
|
|
158
|
+
scheme: 'light' | 'dark';
|
|
159
|
+
tokens: Partial<ThemeTokens>;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Declare a theme.
|
|
163
|
+
*
|
|
164
|
+
* The only thing this adds over an object literal is that the literal is
|
|
165
|
+
* checked *at the point it is written* rather than wherever it is first used —
|
|
166
|
+
* so a consumer gets the red squiggle under the typo, not under the import.
|
|
167
|
+
*/
|
|
168
|
+
export declare function defineTheme(theme: Theme): Theme;
|
|
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.
|
|
183
|
+
*/
|
|
184
|
+
export declare const THEMES: Record<string, Theme>;
|
|
185
|
+
export declare const DEFAULT_THEME = "system";
|
|
186
|
+
export declare function isTheme(name: unknown): name is string;
|
|
187
|
+
/**
|
|
188
|
+
* Put a theme on an element.
|
|
189
|
+
*
|
|
190
|
+
* **The element is an argument, and that is the whole difference between this
|
|
191
|
+
* and the version that lived in the app.** tf owns its document, so writing to
|
|
192
|
+
* `documentElement` was free. A package does not: mounted inside a page it did
|
|
193
|
+
* not build, it has to be able to theme a subtree and leave the rest alone.
|
|
194
|
+
* Defaulting to `documentElement` keeps the app case a no-op.
|
|
195
|
+
*
|
|
196
|
+
* Every themeable key is written, including the ones the theme did not name —
|
|
197
|
+
* cleared to `''` so they fall back to `tokens.css`. Setting only what a theme
|
|
198
|
+
* declares leaves the *previous* theme's values behind on every key it happens
|
|
199
|
+
* not to mention, which reads as two themes at once and is very hard to see.
|
|
200
|
+
*/
|
|
201
|
+
export declare function applyTheme(theme: string | Theme, el?: HTMLElement): void;
|
|
202
|
+
/**
|
|
203
|
+
* The theme as *values*, resolved against the document.
|
|
204
|
+
*
|
|
205
|
+
* A theme is a sparse map, so reading it tells you what it overrode rather than
|
|
206
|
+
* what is on screen. Anything that has to send the palette somewhere it cannot
|
|
207
|
+
* follow a CSS variable — an applet's canvas, a chart library, a screenshot
|
|
208
|
+
* runner — needs the resolved set instead.
|
|
209
|
+
*/
|
|
210
|
+
export declare function resolvedTokens(el?: HTMLElement): Partial<ThemeTokens>;
|
package/dist/themes.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The themes, and the vocabulary they are allowed to speak.
|
|
3
|
+
*
|
|
4
|
+
* A theme is a `Partial<ThemeTokens>`: name the tokens you change, inherit the
|
|
5
|
+
* rest from `tokens.css`. Without that, every theme is a forty-six line chore
|
|
6
|
+
* and every token added later means editing all of them.
|
|
7
|
+
*
|
|
8
|
+
* The type is the enforcement. A typo in a token name is a compile error rather
|
|
9
|
+
* than a property that silently does nothing — which is exactly the failure a
|
|
10
|
+
* string-keyed map would produce, at run time, invisibly. That property is the
|
|
11
|
+
* reason this file is exported from the package at all: a consumer writing a
|
|
12
|
+
* theme gets the same compile error tf gets, and a theme that ships as JSON or
|
|
13
|
+
* as a hand-written CSS file does not.
|
|
14
|
+
*
|
|
15
|
+
* ---
|
|
16
|
+
*
|
|
17
|
+
* **A token is themeable, derived, or fixed, and the three are separate lists.**
|
|
18
|
+
*
|
|
19
|
+
* The old contract had one list, and it had drifted from the stylesheet in both
|
|
20
|
+
* directions: it declared `--pad-1`…`--pad-5`, which `tokens.css` stopped
|
|
21
|
+
* defining when the spacing scale was renamed to `--space-*`, so five keys a
|
|
22
|
+
* theme could name resolved to nothing at all. And it named none of the sixteen
|
|
23
|
+
* derived tokens, so there was no way to tell a token deliberately withheld
|
|
24
|
+
* from a token somebody forgot.
|
|
25
|
+
*
|
|
26
|
+
* Splitting the list is what makes that a decision rather than an omission:
|
|
27
|
+
*
|
|
28
|
+
* - **themeable** — a theme may set it. Forty-six of them.
|
|
29
|
+
* - **derived** — computed from a themeable token, and a theme must NOT set it.
|
|
30
|
+
* `--text-*` are `calc()` off `--font-size` and `--space-*` are `calc()` off
|
|
31
|
+
* `--density`, so overriding one step with a literal is how you get a type
|
|
32
|
+
* scale that no longer scales. Move the input, not the output.
|
|
33
|
+
* - **fixed** — not themeable at all. Geometry other things are measured
|
|
34
|
+
* against, and one glyph.
|
|
35
|
+
*
|
|
36
|
+
* `tokens.test.ts` holds all three to `tokens.css` in both directions, so a
|
|
37
|
+
* token added to the stylesheet and to no list fails the suite rather than
|
|
38
|
+
* becoming a fourth, undocumented category.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Computed from a themeable token, and not settable.
|
|
42
|
+
*
|
|
43
|
+
* A theme that sets `--text-lg` directly gets a scale with a step that no
|
|
44
|
+
* longer moves when `--font-size` does, which is the same as having no scale.
|
|
45
|
+
* Move `--font-size` and `--density`; these follow.
|
|
46
|
+
*/
|
|
47
|
+
export const DERIVED_TOKENS = [
|
|
48
|
+
'--text-2xs',
|
|
49
|
+
'--text-xs',
|
|
50
|
+
'--text-sm',
|
|
51
|
+
'--text-base',
|
|
52
|
+
'--text-md',
|
|
53
|
+
'--text-lg',
|
|
54
|
+
'--text-xl',
|
|
55
|
+
'--space-1',
|
|
56
|
+
'--space-2',
|
|
57
|
+
'--space-3',
|
|
58
|
+
'--space-4',
|
|
59
|
+
'--space-5',
|
|
60
|
+
'--space-6',
|
|
61
|
+
'--space-8',
|
|
62
|
+
'--space-10',
|
|
63
|
+
'--space-15',
|
|
64
|
+
];
|
|
65
|
+
/**
|
|
66
|
+
* Not themeable, and each for its own reason.
|
|
67
|
+
*
|
|
68
|
+
* `--tile-control` is geometry other things are measured against — a panel head
|
|
69
|
+
* without a cog has to be the same height as one with, or the content below it
|
|
70
|
+
* gets a different remainder.
|
|
71
|
+
*
|
|
72
|
+
* `--nudge` is optical alignment, not spacing: a 2px offset that nudges a glyph
|
|
73
|
+
* off a baseline does not scale with density, because the glyph did not.
|
|
74
|
+
*
|
|
75
|
+
* `--tick-mask` is the checkmark, drawn once as a mask so whatever wears it
|
|
76
|
+
* supplies its own colour. It stays fixed because an arbitrary SVG data URI
|
|
77
|
+
* from a theme is a theme shipping markup, which is the one thing the whole
|
|
78
|
+
* design refuses.
|
|
79
|
+
*/
|
|
80
|
+
export const FIXED_TOKENS = ['--tile-control', '--nudge', '--tick-mask'];
|
|
81
|
+
/**
|
|
82
|
+
* The themeable vocabulary, at run time.
|
|
83
|
+
*
|
|
84
|
+
* `ThemeTokens` is a type and disappears at compile time, but applying a theme
|
|
85
|
+
* — or sending one into an applet's frame — means enumerating the keys with the
|
|
86
|
+
* program running. The check below keeps the two from drifting: adding a token
|
|
87
|
+
* to the interface without adding it here is a compile error, which is the same
|
|
88
|
+
* bargain the interface itself makes.
|
|
89
|
+
*/
|
|
90
|
+
export const TOKEN_KEYS = [
|
|
91
|
+
'--bg',
|
|
92
|
+
'--panel',
|
|
93
|
+
'--panel-2',
|
|
94
|
+
'--border',
|
|
95
|
+
'--border-strong',
|
|
96
|
+
'--text',
|
|
97
|
+
'--muted',
|
|
98
|
+
'--accent',
|
|
99
|
+
'--accent-dim',
|
|
100
|
+
'--good',
|
|
101
|
+
'--warn',
|
|
102
|
+
'--bad',
|
|
103
|
+
'--info',
|
|
104
|
+
'--on-accent',
|
|
105
|
+
'--app-bg',
|
|
106
|
+
'--app-overlay',
|
|
107
|
+
'--app-overlay-opacity',
|
|
108
|
+
'--control',
|
|
109
|
+
'--illo-paper',
|
|
110
|
+
'--shadow-1',
|
|
111
|
+
'--shadow-2',
|
|
112
|
+
'--shadow-3',
|
|
113
|
+
'--scrim',
|
|
114
|
+
'--font',
|
|
115
|
+
'--font-mono',
|
|
116
|
+
'--font-size',
|
|
117
|
+
'--line-height',
|
|
118
|
+
'--tracking',
|
|
119
|
+
'--weight',
|
|
120
|
+
'--weight-strong',
|
|
121
|
+
'--radius-sm',
|
|
122
|
+
'--radius',
|
|
123
|
+
'--radius-md',
|
|
124
|
+
'--radius-lg',
|
|
125
|
+
'--radius-pill',
|
|
126
|
+
'--border-width',
|
|
127
|
+
'--density',
|
|
128
|
+
'--dur-fast',
|
|
129
|
+
'--dur-md',
|
|
130
|
+
'--dur-slow',
|
|
131
|
+
'--dur-drag',
|
|
132
|
+
'--ease',
|
|
133
|
+
'--ease-out',
|
|
134
|
+
'--ease-spring',
|
|
135
|
+
'--hover-lift',
|
|
136
|
+
'--hover-scale',
|
|
137
|
+
'--press-scale',
|
|
138
|
+
'--press-ink',
|
|
139
|
+
'--focus-ring',
|
|
140
|
+
];
|
|
141
|
+
// Compile error if a token exists in the type and not in the list above.
|
|
142
|
+
const _covers = true;
|
|
143
|
+
void _covers;
|
|
144
|
+
// And the other direction, which the old check did not have: a key in the list
|
|
145
|
+
// that the type does not declare.
|
|
146
|
+
const _exact = true;
|
|
147
|
+
void _exact;
|
|
148
|
+
/**
|
|
149
|
+
* Declare a theme.
|
|
150
|
+
*
|
|
151
|
+
* The only thing this adds over an object literal is that the literal is
|
|
152
|
+
* checked *at the point it is written* rather than wherever it is first used —
|
|
153
|
+
* so a consumer gets the red squiggle under the typo, not under the import.
|
|
154
|
+
*/
|
|
155
|
+
export function defineTheme(theme) {
|
|
156
|
+
return theme;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The dark palette, written out rather than inherited.
|
|
160
|
+
*
|
|
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.
|
|
193
|
+
*
|
|
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.
|
|
199
|
+
*/
|
|
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
|
+
};
|
|
251
|
+
export const DEFAULT_THEME = 'system';
|
|
252
|
+
export function isTheme(name) {
|
|
253
|
+
return typeof name === 'string' && name in THEMES;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Put a theme on an element.
|
|
257
|
+
*
|
|
258
|
+
* **The element is an argument, and that is the whole difference between this
|
|
259
|
+
* and the version that lived in the app.** tf owns its document, so writing to
|
|
260
|
+
* `documentElement` was free. A package does not: mounted inside a page it did
|
|
261
|
+
* not build, it has to be able to theme a subtree and leave the rest alone.
|
|
262
|
+
* Defaulting to `documentElement` keeps the app case a no-op.
|
|
263
|
+
*
|
|
264
|
+
* Every themeable key is written, including the ones the theme did not name —
|
|
265
|
+
* cleared to `''` so they fall back to `tokens.css`. Setting only what a theme
|
|
266
|
+
* declares leaves the *previous* theme's values behind on every key it happens
|
|
267
|
+
* not to mention, which reads as two themes at once and is very hard to see.
|
|
268
|
+
*/
|
|
269
|
+
export function applyTheme(theme, el = document.documentElement) {
|
|
270
|
+
/* A registered name, or a `Theme` object straight from `defineTheme`.
|
|
271
|
+
The second is how an app that is not tf applies its own palette without
|
|
272
|
+
first pushing it into a registry it does not own -- `THEMES` is the
|
|
273
|
+
built-ins, and a consumer's theme is theirs. */
|
|
274
|
+
const resolved = typeof theme === 'string' ? (THEMES[theme] ?? THEMES[DEFAULT_THEME]) : theme;
|
|
275
|
+
const name = typeof theme === 'string' ? theme : resolved.name.toLowerCase().replace(/\s+/g, '-');
|
|
276
|
+
for (const key of TOKEN_KEYS) {
|
|
277
|
+
const value = resolved.tokens[key];
|
|
278
|
+
if (value === undefined)
|
|
279
|
+
el.style.removeProperty(key);
|
|
280
|
+
else
|
|
281
|
+
el.style.setProperty(key, value);
|
|
282
|
+
}
|
|
283
|
+
el.dataset.theme = name;
|
|
284
|
+
el.style.colorScheme = resolved.scheme;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* The theme as *values*, resolved against the document.
|
|
288
|
+
*
|
|
289
|
+
* A theme is a sparse map, so reading it tells you what it overrode rather than
|
|
290
|
+
* what is on screen. Anything that has to send the palette somewhere it cannot
|
|
291
|
+
* follow a CSS variable — an applet's canvas, a chart library, a screenshot
|
|
292
|
+
* runner — needs the resolved set instead.
|
|
293
|
+
*/
|
|
294
|
+
export function resolvedTokens(el = document.documentElement) {
|
|
295
|
+
const style = getComputedStyle(el);
|
|
296
|
+
const out = {};
|
|
297
|
+
for (const key of TOKEN_KEYS)
|
|
298
|
+
out[key] = style.getPropertyValue(key).trim();
|
|
299
|
+
return out;
|
|
300
|
+
}
|