@waukeshamakerspace/design-tokens 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 +194 -0
- package/assets/apple-touch-icon.png +0 -0
- package/assets/favicon.ico +0 -0
- package/assets/icon-192.png +0 -0
- package/assets/icon-512.png +0 -0
- package/assets/logo-white.png +0 -0
- package/assets/logo.png +0 -0
- package/css/fonts.css +13 -0
- package/css/tailwind.css +64 -0
- package/css/tokens.css +115 -0
- package/index.d.ts +40 -0
- package/index.js +101 -0
- package/mui.d.ts +6 -0
- package/mui.js +76 -0
- package/package.json +73 -0
- package/tailwind-preset.cjs +84 -0
- package/theme-init.d.ts +20 -0
- package/theme-init.js +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Waukesha Makerspace, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# @waukeshamakerspace/design-tokens
|
|
2
|
+
|
|
3
|
+
Waukesha Makerspace brand + semantic design tokens, extracted from the token
|
|
4
|
+
system pioneered in `makers-cms`. One package, consumed by every WMI UI, so
|
|
5
|
+
the brand hexes live in exactly one place.
|
|
6
|
+
|
|
7
|
+
- **Brand layer** (fixed): Burnt Orange `#E8600A`, Charcoal `#2B2B2B`, Golden
|
|
8
|
+
Yellow `#F5A623`, Warm White `#F7F3EE` — from
|
|
9
|
+
`marketing/brand/brand-guidelines.md`.
|
|
10
|
+
- **Semantic layer** (themable): `surface`, `foreground`, `border`, `primary`,
|
|
11
|
+
`secondary`, `accent`, `link`, `ring`, `warning`/`success`/`danger` — each
|
|
12
|
+
with light **and** dark values. Components use these, never brand hexes.
|
|
13
|
+
- **Fonts**: Oswald (headings) + Open Sans (body), wired via `--font-heading`
|
|
14
|
+
/ `--font-body`.
|
|
15
|
+
|
|
16
|
+
Cross-cutting UI rules (mobile-first, device-theme behavior, accessibility)
|
|
17
|
+
live in the architecture repo: `waukesha-makers-architecture/shared/ui-guidelines.md`.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install @waukeshamakerspace/design-tokens
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Enable Renovate or Dependabot in consuming apps so new token releases arrive
|
|
26
|
+
as automatic bump PRs.
|
|
27
|
+
|
|
28
|
+
## Dark mode model
|
|
29
|
+
|
|
30
|
+
`css/tokens.css` resolves the theme in this order:
|
|
31
|
+
|
|
32
|
+
1. `.dark` / `.light` class on `<html>` wins (set pre-paint by the theme-init
|
|
33
|
+
script, or by an app-level toggle).
|
|
34
|
+
2. No class present → the OS `prefers-color-scheme` applies via media query.
|
|
35
|
+
|
|
36
|
+
So plain-CSS consumers follow the device theme with **zero JS**. Apps using
|
|
37
|
+
class-based styling (Tailwind `dark:`, MUI color schemes) inline the
|
|
38
|
+
theme-init script so the class follows the device too:
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
// Next.js: in app/layout.tsx <head>, before first paint
|
|
42
|
+
import { themeInitScript } from '@waukeshamakerspace/design-tokens/theme-init';
|
|
43
|
+
|
|
44
|
+
<script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// SPA: top of the entry module
|
|
49
|
+
import { initTheme } from '@waukeshamakerspace/design-tokens/theme-init';
|
|
50
|
+
initTheme();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### On-the-fly theme switching
|
|
54
|
+
|
|
55
|
+
Every app must expose a simple toggle so both modes can be exercised during
|
|
56
|
+
development (see `shared/ui-guidelines.md`). Build it on the exported
|
|
57
|
+
helpers so it coordinates with the OS-change listener instead of fighting it:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { toggleTheme, setTheme, getTheme } from '@waukeshamakerspace/design-tokens/theme-init';
|
|
61
|
+
|
|
62
|
+
<button onClick={() => toggleTheme()}>Theme</button>
|
|
63
|
+
// or explicit: setTheme('dark') / setTheme('light') / setTheme('system')
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
A toggle choice is session-only (not persisted): it suppresses OS-change
|
|
67
|
+
syncing until `setTheme('system')` or a reload, which return the app to
|
|
68
|
+
following the device theme.
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
### Tailwind v4 (makers-cms style)
|
|
73
|
+
|
|
74
|
+
```css
|
|
75
|
+
/* globals.css */
|
|
76
|
+
@import "tailwindcss";
|
|
77
|
+
@import "@waukeshamakerspace/design-tokens/css/tailwind.css";
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Gives every token as a utility (`bg-surface`, `text-foreground-muted`,
|
|
81
|
+
`bg-primary hover:bg-primary-hover`, `font-heading`) plus a class-based
|
|
82
|
+
`dark:` variant.
|
|
83
|
+
|
|
84
|
+
### Tailwind v3
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
// tailwind.config.js
|
|
88
|
+
module.exports = {
|
|
89
|
+
presets: [require('@waukeshamakerspace/design-tokens/tailwind-preset')],
|
|
90
|
+
// ...
|
|
91
|
+
};
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```css
|
|
95
|
+
/* global stylesheet */
|
|
96
|
+
@import "@waukeshamakerspace/design-tokens/css/tokens.css";
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Same utility names as v4. Caveat: opacity modifiers (`bg-primary/50`) don't
|
|
100
|
+
work on var()-based colors in v3.
|
|
101
|
+
|
|
102
|
+
### MUI v6+
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import { createMakersTheme } from '@waukeshamakerspace/design-tokens/mui';
|
|
106
|
+
|
|
107
|
+
const theme = createMakersTheme(); // pass overrides to deep-merge
|
|
108
|
+
<ThemeProvider theme={theme}>...</ThemeProvider>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Light/dark schemes flip on the same `.light`/`.dark` class (via MUI
|
|
112
|
+
`cssVariables.colorSchemeSelector`), so MUI stays in sync with the CSS tokens.
|
|
113
|
+
`themeOptions` is also exported if you'd rather call `createTheme` yourself.
|
|
114
|
+
|
|
115
|
+
### Plain CSS / anything else
|
|
116
|
+
|
|
117
|
+
```css
|
|
118
|
+
@import "@waukeshamakerspace/design-tokens/css/tokens.css";
|
|
119
|
+
|
|
120
|
+
.card {
|
|
121
|
+
background: var(--surface-raised);
|
|
122
|
+
color: var(--foreground);
|
|
123
|
+
border: 1px solid var(--border);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Fonts
|
|
128
|
+
|
|
129
|
+
Next.js apps: load Oswald + Open Sans with `next/font` and map them to
|
|
130
|
+
`--font-heading` / `--font-body` (see makers-cms `layout.tsx`).
|
|
131
|
+
Everything else: `@import "@waukeshamakerspace/design-tokens/css/fonts.css";`
|
|
132
|
+
(loads Google Fonts and wires the variables).
|
|
133
|
+
|
|
134
|
+
### Brand assets (logos + favicons)
|
|
135
|
+
|
|
136
|
+
The package ships the logo masters and a favicon set generated from them:
|
|
137
|
+
|
|
138
|
+
| Path | What it is |
|
|
139
|
+
| --- | --- |
|
|
140
|
+
| `assets/logo.png` | Full logo, dark artwork, transparent, 1500×1500 |
|
|
141
|
+
| `assets/logo-white.png` | Full logo, white artwork, transparent, 1500×1500 |
|
|
142
|
+
| `assets/favicon.ico` | 16/32/48 multi-size, warm-white plate |
|
|
143
|
+
| `assets/apple-touch-icon.png` | 180×180, warm-white plate |
|
|
144
|
+
| `assets/icon-192.png` / `assets/icon-512.png` | PWA / web-manifest icons, transparent |
|
|
145
|
+
|
|
146
|
+
**Bundler apps (Vite, webpack, Next `<Image>`/`metadata`):** import the file —
|
|
147
|
+
the bundler hashes the URL, so rebrands bust caches automatically.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import logo from '@waukeshamakerspace/design-tokens/assets/logo.png';
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// Next.js app router: app/layout.tsx
|
|
155
|
+
import favicon from '@waukeshamakerspace/design-tokens/assets/favicon.ico';
|
|
156
|
+
import appleIcon from '@waukeshamakerspace/design-tokens/assets/apple-touch-icon.png';
|
|
157
|
+
|
|
158
|
+
export const metadata = {
|
|
159
|
+
icons: { icon: favicon.src, apple: appleIcon.src },
|
|
160
|
+
};
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Anything that needs the files in `public/`:** copy at build time, e.g.
|
|
164
|
+
|
|
165
|
+
```jsonc
|
|
166
|
+
// package.json
|
|
167
|
+
"prebuild": "cp node_modules/@waukeshamakerspace/design-tokens/assets/favicon.ico public/"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Token values in JS
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
import { brand, light, dark, fonts } from '@waukeshamakerspace/design-tokens';
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Editing tokens
|
|
177
|
+
|
|
178
|
+
`index.js` is the single source of truth for values; `assets/logo.png` /
|
|
179
|
+
`assets/logo-white.png` are the masters for imagery. Edit those, then:
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
npm run build # regenerates css/tokens.css + the assets/ favicon set
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Never edit `css/tokens.css` or the generated icons (`favicon.ico`,
|
|
186
|
+
`apple-touch-icon.png`, `icon-*.png`) by hand. Brand-layer changes must go
|
|
187
|
+
through `marketing/brand/brand-guidelines.md` first.
|
|
188
|
+
|
|
189
|
+
## Releasing
|
|
190
|
+
|
|
191
|
+
Automated by release-please. Write [Conventional Commits](https://www.conventionalcommits.org)
|
|
192
|
+
(`fix:` → patch, `feat:` → minor, `feat!:` → major); pushes to main accumulate
|
|
193
|
+
into a release PR, and merging it publishes to npm. Don't bump `version` by
|
|
194
|
+
hand.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/assets/logo.png
ADDED
|
Binary file
|
package/css/fonts.css
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/* Optional: brand fonts for apps NOT using next/font.
|
|
2
|
+
*
|
|
3
|
+
* Loads Oswald + Open Sans from Google Fonts and wires the font tokens.
|
|
4
|
+
* Next.js apps should skip this file and set --font-heading / --font-body
|
|
5
|
+
* via next/font instead (self-hosted, no layout shift).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
@import url('https://fonts.googleapis.com/css2?family=Oswald:wght@400;500;600;700&family=Open+Sans:ital,wght@0,400;0,600;0,700;1,400&display=swap');
|
|
9
|
+
|
|
10
|
+
:root {
|
|
11
|
+
--font-heading: 'Oswald', 'Arial Narrow', sans-serif;
|
|
12
|
+
--font-body: 'Open Sans', Arial, Helvetica, sans-serif;
|
|
13
|
+
}
|
package/css/tailwind.css
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/* Tailwind v4 entry point. In your app's globals.css:
|
|
2
|
+
*
|
|
3
|
+
* @import "tailwindcss";
|
|
4
|
+
* @import "@waukeshamakerspace/design-tokens/css/tailwind.css";
|
|
5
|
+
*
|
|
6
|
+
* Provides the token variables, a class-based `dark:` variant, and Tailwind
|
|
7
|
+
* utilities for every token (bg-surface, text-foreground-muted, bg-primary,
|
|
8
|
+
* font-heading, …).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
@import "./tokens.css";
|
|
12
|
+
|
|
13
|
+
/* `dark:` follows the .dark class on <html> (kept in sync with the OS theme
|
|
14
|
+
by the theme-init script). */
|
|
15
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
16
|
+
|
|
17
|
+
@theme inline {
|
|
18
|
+
/* Brand utilities — use only when you mean the literal color in both themes */
|
|
19
|
+
--color-burnt-orange: var(--brand-burnt-orange);
|
|
20
|
+
--color-charcoal: var(--brand-charcoal);
|
|
21
|
+
--color-golden-yellow: var(--brand-golden-yellow);
|
|
22
|
+
--color-warm-white: var(--brand-warm-white);
|
|
23
|
+
|
|
24
|
+
/* Semantic utilities — use these everywhere */
|
|
25
|
+
--color-surface: var(--surface);
|
|
26
|
+
--color-surface-muted: var(--surface-muted);
|
|
27
|
+
--color-surface-raised: var(--surface-raised);
|
|
28
|
+
--color-surface-inverse: var(--surface-inverse);
|
|
29
|
+
|
|
30
|
+
--color-foreground: var(--foreground);
|
|
31
|
+
--color-foreground-muted: var(--foreground-muted);
|
|
32
|
+
--color-foreground-subtle: var(--foreground-subtle);
|
|
33
|
+
--color-foreground-inverse: var(--foreground-inverse);
|
|
34
|
+
|
|
35
|
+
--color-border: var(--border);
|
|
36
|
+
--color-border-strong: var(--border-strong);
|
|
37
|
+
|
|
38
|
+
--color-primary: var(--primary);
|
|
39
|
+
--color-primary-hover: var(--primary-hover);
|
|
40
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
41
|
+
|
|
42
|
+
--color-secondary: var(--secondary);
|
|
43
|
+
--color-secondary-hover: var(--secondary-hover);
|
|
44
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
45
|
+
|
|
46
|
+
--color-accent: var(--accent);
|
|
47
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
48
|
+
|
|
49
|
+
--color-link: var(--link);
|
|
50
|
+
--color-link-hover: var(--link-hover);
|
|
51
|
+
|
|
52
|
+
--color-ring: var(--ring);
|
|
53
|
+
|
|
54
|
+
--color-warning: var(--warning);
|
|
55
|
+
--color-warning-foreground: var(--warning-foreground);
|
|
56
|
+
--color-success: var(--success);
|
|
57
|
+
--color-success-foreground: var(--success-foreground);
|
|
58
|
+
--color-danger: var(--danger);
|
|
59
|
+
--color-danger-foreground: var(--danger-foreground);
|
|
60
|
+
|
|
61
|
+
/* Fonts — apps define --font-heading / --font-body (next/font or fonts.css) */
|
|
62
|
+
--font-heading: var(--font-heading);
|
|
63
|
+
--font-body: var(--font-body);
|
|
64
|
+
}
|
package/css/tokens.css
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/* GENERATED FILE — do not edit. Edit index.js and run `npm run build`. */
|
|
2
|
+
|
|
3
|
+
/* ────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
Waukesha Makerspace design tokens.
|
|
5
|
+
|
|
6
|
+
Two layers:
|
|
7
|
+
1. BRAND (--brand-*) — fixed colors from marketing/brand/brand-guidelines.md.
|
|
8
|
+
Reach for these only when you mean the literal color regardless of theme.
|
|
9
|
+
2. SEMANTIC — themable, role-based (surface, foreground, primary, link, …).
|
|
10
|
+
Components should use these by default; they flip for dark mode.
|
|
11
|
+
|
|
12
|
+
Dark mode resolution order:
|
|
13
|
+
- `.dark` / `.light` class on <html> wins (set pre-paint by the
|
|
14
|
+
theme-init script exported from '@waukeshamakerspace/design-tokens/theme-init').
|
|
15
|
+
- With no class present, the OS preference applies via media query.
|
|
16
|
+
──────────────────────────────────────────────────────────────────────── */
|
|
17
|
+
|
|
18
|
+
:root {
|
|
19
|
+
--brand-burnt-orange: #e8600a;
|
|
20
|
+
--brand-charcoal: #2b2b2b;
|
|
21
|
+
--brand-golden-yellow: #f5a623;
|
|
22
|
+
--brand-warm-white: #f7f3ee;
|
|
23
|
+
|
|
24
|
+
--surface: #ffffff;
|
|
25
|
+
--surface-muted: var(--brand-warm-white);
|
|
26
|
+
--surface-raised: #ffffff;
|
|
27
|
+
--surface-inverse: var(--brand-charcoal);
|
|
28
|
+
--foreground: var(--brand-charcoal);
|
|
29
|
+
--foreground-muted: #6b6b6b;
|
|
30
|
+
--foreground-subtle: #9a9a9a;
|
|
31
|
+
--foreground-inverse: var(--brand-warm-white);
|
|
32
|
+
--border: #e5e5e5;
|
|
33
|
+
--border-strong: #c5c5c5;
|
|
34
|
+
--primary: var(--brand-burnt-orange);
|
|
35
|
+
--primary-hover: #c0500a;
|
|
36
|
+
--primary-foreground: var(--brand-warm-white);
|
|
37
|
+
--secondary: var(--brand-charcoal);
|
|
38
|
+
--secondary-hover: #1a1a1a;
|
|
39
|
+
--secondary-foreground: var(--brand-warm-white);
|
|
40
|
+
--accent: var(--brand-golden-yellow);
|
|
41
|
+
--accent-foreground: var(--brand-charcoal);
|
|
42
|
+
--link: var(--brand-burnt-orange);
|
|
43
|
+
--link-hover: #c0500a;
|
|
44
|
+
--ring: var(--brand-burnt-orange);
|
|
45
|
+
--warning: var(--brand-golden-yellow);
|
|
46
|
+
--warning-foreground: var(--brand-charcoal);
|
|
47
|
+
--success: #16a34a;
|
|
48
|
+
--success-foreground: #ffffff;
|
|
49
|
+
--danger: #dc2626;
|
|
50
|
+
--danger-foreground: #ffffff;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Dark theme — explicit class */
|
|
54
|
+
:root.dark {
|
|
55
|
+
--surface: #161616;
|
|
56
|
+
--surface-muted: #232323;
|
|
57
|
+
--surface-raised: #2a2a2a;
|
|
58
|
+
--surface-inverse: var(--brand-warm-white);
|
|
59
|
+
--foreground: var(--brand-warm-white);
|
|
60
|
+
--foreground-muted: #a8a8a8;
|
|
61
|
+
--foreground-subtle: #707070;
|
|
62
|
+
--foreground-inverse: var(--brand-charcoal);
|
|
63
|
+
--border: #3a3a3a;
|
|
64
|
+
--border-strong: #5a5a5a;
|
|
65
|
+
--primary: #ff7a2e;
|
|
66
|
+
--primary-hover: #ff9050;
|
|
67
|
+
--primary-foreground: var(--brand-warm-white);
|
|
68
|
+
--secondary: var(--brand-warm-white);
|
|
69
|
+
--secondary-hover: #ffffff;
|
|
70
|
+
--secondary-foreground: var(--brand-charcoal);
|
|
71
|
+
--accent: var(--brand-golden-yellow);
|
|
72
|
+
--accent-foreground: var(--brand-charcoal);
|
|
73
|
+
--link: #ff7a2e;
|
|
74
|
+
--link-hover: #ff9050;
|
|
75
|
+
--ring: #ff7a2e;
|
|
76
|
+
--warning: var(--brand-golden-yellow);
|
|
77
|
+
--warning-foreground: var(--brand-charcoal);
|
|
78
|
+
--success: #22c55e;
|
|
79
|
+
--success-foreground: #ffffff;
|
|
80
|
+
--danger: #ef4444;
|
|
81
|
+
--danger-foreground: #ffffff;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Dark theme — OS preference, when no explicit class overrides it */
|
|
85
|
+
@media (prefers-color-scheme: dark) {
|
|
86
|
+
:root:not(.light) {
|
|
87
|
+
--surface: #161616;
|
|
88
|
+
--surface-muted: #232323;
|
|
89
|
+
--surface-raised: #2a2a2a;
|
|
90
|
+
--surface-inverse: var(--brand-warm-white);
|
|
91
|
+
--foreground: var(--brand-warm-white);
|
|
92
|
+
--foreground-muted: #a8a8a8;
|
|
93
|
+
--foreground-subtle: #707070;
|
|
94
|
+
--foreground-inverse: var(--brand-charcoal);
|
|
95
|
+
--border: #3a3a3a;
|
|
96
|
+
--border-strong: #5a5a5a;
|
|
97
|
+
--primary: #ff7a2e;
|
|
98
|
+
--primary-hover: #ff9050;
|
|
99
|
+
--primary-foreground: var(--brand-warm-white);
|
|
100
|
+
--secondary: var(--brand-warm-white);
|
|
101
|
+
--secondary-hover: #ffffff;
|
|
102
|
+
--secondary-foreground: var(--brand-charcoal);
|
|
103
|
+
--accent: var(--brand-golden-yellow);
|
|
104
|
+
--accent-foreground: var(--brand-charcoal);
|
|
105
|
+
--link: #ff7a2e;
|
|
106
|
+
--link-hover: #ff9050;
|
|
107
|
+
--ring: #ff7a2e;
|
|
108
|
+
--warning: var(--brand-golden-yellow);
|
|
109
|
+
--warning-foreground: var(--brand-charcoal);
|
|
110
|
+
--success: #22c55e;
|
|
111
|
+
--success-foreground: #ffffff;
|
|
112
|
+
--danger: #ef4444;
|
|
113
|
+
--danger-foreground: #ffffff;
|
|
114
|
+
}
|
|
115
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type BrandColorName =
|
|
2
|
+
| 'burnt-orange'
|
|
3
|
+
| 'charcoal'
|
|
4
|
+
| 'golden-yellow'
|
|
5
|
+
| 'warm-white';
|
|
6
|
+
|
|
7
|
+
export type SemanticTokenName =
|
|
8
|
+
| 'surface'
|
|
9
|
+
| 'surface-muted'
|
|
10
|
+
| 'surface-raised'
|
|
11
|
+
| 'surface-inverse'
|
|
12
|
+
| 'foreground'
|
|
13
|
+
| 'foreground-muted'
|
|
14
|
+
| 'foreground-subtle'
|
|
15
|
+
| 'foreground-inverse'
|
|
16
|
+
| 'border'
|
|
17
|
+
| 'border-strong'
|
|
18
|
+
| 'primary'
|
|
19
|
+
| 'primary-hover'
|
|
20
|
+
| 'primary-foreground'
|
|
21
|
+
| 'secondary'
|
|
22
|
+
| 'secondary-hover'
|
|
23
|
+
| 'secondary-foreground'
|
|
24
|
+
| 'accent'
|
|
25
|
+
| 'accent-foreground'
|
|
26
|
+
| 'link'
|
|
27
|
+
| 'link-hover'
|
|
28
|
+
| 'ring'
|
|
29
|
+
| 'warning'
|
|
30
|
+
| 'warning-foreground'
|
|
31
|
+
| 'success'
|
|
32
|
+
| 'success-foreground'
|
|
33
|
+
| 'danger'
|
|
34
|
+
| 'danger-foreground';
|
|
35
|
+
|
|
36
|
+
export declare const brand: Record<BrandColorName, string>;
|
|
37
|
+
export declare const light: Record<SemanticTokenName, string>;
|
|
38
|
+
export declare const dark: Record<SemanticTokenName, string>;
|
|
39
|
+
export declare const fonts: { heading: string; body: string };
|
|
40
|
+
export declare const semanticTokenNames: SemanticTokenName[];
|
package/index.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waukesha Makerspace design tokens — single source of truth for values.
|
|
3
|
+
*
|
|
4
|
+
* Brand layer comes from marketing/brand/brand-guidelines.md. The semantic
|
|
5
|
+
* layer (including the dark palette) was designed in makers-cms and is
|
|
6
|
+
* canonized here; makers-cms and other apps consume it from this package.
|
|
7
|
+
*
|
|
8
|
+
* `npm run build` regenerates css/tokens.css from these objects — edit here,
|
|
9
|
+
* never in the generated CSS.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Fixed brand palette. Use only when you mean the literal color in both themes. */
|
|
13
|
+
export const brand = {
|
|
14
|
+
'burnt-orange': '#e8600a',
|
|
15
|
+
charcoal: '#2b2b2b',
|
|
16
|
+
'golden-yellow': '#f5a623',
|
|
17
|
+
'warm-white': '#f7f3ee',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Semantic tokens — light theme. Components should reach for these, not brand. */
|
|
21
|
+
export const light = {
|
|
22
|
+
surface: '#ffffff',
|
|
23
|
+
'surface-muted': brand['warm-white'],
|
|
24
|
+
'surface-raised': '#ffffff',
|
|
25
|
+
'surface-inverse': brand.charcoal,
|
|
26
|
+
|
|
27
|
+
foreground: brand.charcoal,
|
|
28
|
+
'foreground-muted': '#6b6b6b',
|
|
29
|
+
'foreground-subtle': '#9a9a9a',
|
|
30
|
+
'foreground-inverse': brand['warm-white'],
|
|
31
|
+
|
|
32
|
+
border: '#e5e5e5',
|
|
33
|
+
'border-strong': '#c5c5c5',
|
|
34
|
+
|
|
35
|
+
primary: brand['burnt-orange'],
|
|
36
|
+
'primary-hover': '#c0500a',
|
|
37
|
+
'primary-foreground': brand['warm-white'],
|
|
38
|
+
|
|
39
|
+
secondary: brand.charcoal,
|
|
40
|
+
'secondary-hover': '#1a1a1a',
|
|
41
|
+
'secondary-foreground': brand['warm-white'],
|
|
42
|
+
|
|
43
|
+
accent: brand['golden-yellow'],
|
|
44
|
+
'accent-foreground': brand.charcoal,
|
|
45
|
+
|
|
46
|
+
link: brand['burnt-orange'],
|
|
47
|
+
'link-hover': '#c0500a',
|
|
48
|
+
|
|
49
|
+
ring: brand['burnt-orange'],
|
|
50
|
+
|
|
51
|
+
warning: brand['golden-yellow'],
|
|
52
|
+
'warning-foreground': brand.charcoal,
|
|
53
|
+
success: '#16a34a',
|
|
54
|
+
'success-foreground': '#ffffff',
|
|
55
|
+
danger: '#dc2626',
|
|
56
|
+
'danger-foreground': '#ffffff',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Semantic tokens — dark theme. Same keys as `light`. */
|
|
60
|
+
export const dark = {
|
|
61
|
+
...light,
|
|
62
|
+
surface: '#161616',
|
|
63
|
+
'surface-muted': '#232323',
|
|
64
|
+
'surface-raised': '#2a2a2a',
|
|
65
|
+
'surface-inverse': brand['warm-white'],
|
|
66
|
+
|
|
67
|
+
foreground: brand['warm-white'],
|
|
68
|
+
'foreground-muted': '#a8a8a8',
|
|
69
|
+
'foreground-subtle': '#707070',
|
|
70
|
+
'foreground-inverse': brand.charcoal,
|
|
71
|
+
|
|
72
|
+
border: '#3a3a3a',
|
|
73
|
+
'border-strong': '#5a5a5a',
|
|
74
|
+
|
|
75
|
+
primary: '#ff7a2e', // brightened burnt-orange for AA contrast on dark surfaces
|
|
76
|
+
'primary-hover': '#ff9050',
|
|
77
|
+
|
|
78
|
+
secondary: brand['warm-white'],
|
|
79
|
+
'secondary-hover': '#ffffff',
|
|
80
|
+
'secondary-foreground': brand.charcoal,
|
|
81
|
+
|
|
82
|
+
link: '#ff7a2e',
|
|
83
|
+
'link-hover': '#ff9050',
|
|
84
|
+
|
|
85
|
+
ring: '#ff7a2e',
|
|
86
|
+
|
|
87
|
+
success: '#22c55e',
|
|
88
|
+
danger: '#ef4444',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Brand font stacks. Apps wire these to `--font-heading` / `--font-body` —
|
|
93
|
+
* via next/font (Next.js) or by importing css/fonts.css (everything else).
|
|
94
|
+
*/
|
|
95
|
+
export const fonts = {
|
|
96
|
+
heading: "'Oswald', 'Arial Narrow', sans-serif",
|
|
97
|
+
body: "'Open Sans', Arial, Helvetica, sans-serif",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Every semantic token name, for adapters that need to enumerate them. */
|
|
101
|
+
export const semanticTokenNames = Object.keys(light);
|
package/mui.d.ts
ADDED
package/mui.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MUI v6+ adapter. Builds the Waukesha Makerspace theme from the token
|
|
3
|
+
* values in index.js, with light/dark color schemes flipped by the
|
|
4
|
+
* `.light` / `.dark` class on <html> (same mechanism as the CSS tokens —
|
|
5
|
+
* use the theme-init script to set the class from the device theme).
|
|
6
|
+
*
|
|
7
|
+
* import { createMakersTheme } from '@waukeshamakerspace/design-tokens/mui';
|
|
8
|
+
* const theme = createMakersTheme(); // or createMakersTheme({ ...overrides })
|
|
9
|
+
*/
|
|
10
|
+
import { createTheme } from '@mui/material/styles';
|
|
11
|
+
import { light, dark, fonts } from './index.js';
|
|
12
|
+
|
|
13
|
+
const palette = (t) => ({
|
|
14
|
+
primary: {
|
|
15
|
+
main: t.primary,
|
|
16
|
+
dark: t['primary-hover'],
|
|
17
|
+
contrastText: t['primary-foreground'],
|
|
18
|
+
},
|
|
19
|
+
secondary: { main: t.secondary, contrastText: t['secondary-foreground'] },
|
|
20
|
+
success: { main: t.success, contrastText: t['success-foreground'] },
|
|
21
|
+
error: { main: t.danger, contrastText: t['danger-foreground'] },
|
|
22
|
+
warning: { main: t.warning, contrastText: t['warning-foreground'] },
|
|
23
|
+
background: { default: t.surface, paper: t['surface-raised'] },
|
|
24
|
+
text: { primary: t.foreground, secondary: t['foreground-muted'] },
|
|
25
|
+
divider: t.border,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const heading = `var(--font-heading, ${fonts.heading})`;
|
|
29
|
+
const body = `var(--font-body, ${fonts.body})`;
|
|
30
|
+
|
|
31
|
+
export const themeOptions = {
|
|
32
|
+
cssVariables: { colorSchemeSelector: 'class' },
|
|
33
|
+
colorSchemes: {
|
|
34
|
+
light: { palette: palette(light) },
|
|
35
|
+
dark: { palette: palette(dark) },
|
|
36
|
+
},
|
|
37
|
+
shape: { borderRadius: 12 },
|
|
38
|
+
typography: {
|
|
39
|
+
fontFamily: body,
|
|
40
|
+
h1: { fontFamily: heading, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.01em' },
|
|
41
|
+
h2: { fontFamily: heading, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.01em' },
|
|
42
|
+
h3: { fontFamily: heading, fontWeight: 700, textTransform: 'uppercase' },
|
|
43
|
+
h4: { fontFamily: heading, fontWeight: 600, textTransform: 'uppercase' },
|
|
44
|
+
h5: { fontFamily: heading, fontWeight: 600 },
|
|
45
|
+
h6: { fontFamily: heading, fontWeight: 600 },
|
|
46
|
+
button: { fontFamily: heading, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.03em' },
|
|
47
|
+
},
|
|
48
|
+
components: {
|
|
49
|
+
MuiCard: {
|
|
50
|
+
styleOverrides: {
|
|
51
|
+
root: { borderRadius: 14, backgroundImage: 'none' },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
MuiButton: {
|
|
55
|
+
defaultProps: { disableElevation: true },
|
|
56
|
+
styleOverrides: {
|
|
57
|
+
root: { borderRadius: 12 },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
MuiAppBar: {
|
|
61
|
+
defaultProps: { elevation: 0, color: 'default' },
|
|
62
|
+
styleOverrides: {
|
|
63
|
+
root: {
|
|
64
|
+
backgroundColor: 'var(--mui-palette-background-paper)',
|
|
65
|
+
color: 'var(--mui-palette-text-primary)',
|
|
66
|
+
borderBottom: '1px solid var(--mui-palette-divider)',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Create the brand theme; extra args deep-merge as overrides. */
|
|
74
|
+
export function createMakersTheme(...overrides) {
|
|
75
|
+
return createTheme(themeOptions, ...overrides);
|
|
76
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@waukeshamakerspace/design-tokens",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Waukesha Makerspace brand + semantic design tokens: CSS variables with automatic light/dark theming, Tailwind (v3 + v4) and MUI adapters",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/WaukeshaMakerspace/design-tokens.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./index.js",
|
|
12
|
+
"types": "./index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./index.d.ts",
|
|
16
|
+
"default": "./index.js"
|
|
17
|
+
},
|
|
18
|
+
"./css": "./css/tokens.css",
|
|
19
|
+
"./css/tokens.css": "./css/tokens.css",
|
|
20
|
+
"./css/tailwind.css": "./css/tailwind.css",
|
|
21
|
+
"./css/fonts.css": "./css/fonts.css",
|
|
22
|
+
"./tailwind-preset": "./tailwind-preset.cjs",
|
|
23
|
+
"./mui": {
|
|
24
|
+
"types": "./mui.d.ts",
|
|
25
|
+
"default": "./mui.js"
|
|
26
|
+
},
|
|
27
|
+
"./theme-init": {
|
|
28
|
+
"types": "./theme-init.d.ts",
|
|
29
|
+
"default": "./theme-init.js"
|
|
30
|
+
},
|
|
31
|
+
"./assets/*": "./assets/*"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"index.js",
|
|
35
|
+
"index.d.ts",
|
|
36
|
+
"css/",
|
|
37
|
+
"assets/",
|
|
38
|
+
"tailwind-preset.cjs",
|
|
39
|
+
"mui.js",
|
|
40
|
+
"mui.d.ts",
|
|
41
|
+
"theme-init.js",
|
|
42
|
+
"theme-init.d.ts"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "npm run build:css && npm run build:icons",
|
|
46
|
+
"build:css": "node scripts/build-css.mjs",
|
|
47
|
+
"build:icons": "node scripts/build-icons.mjs",
|
|
48
|
+
"prepublishOnly": "npm run build"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@mui/material": ">=6",
|
|
52
|
+
"tailwindcss": ">=3"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"@mui/material": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"tailwindcss": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"keywords": [
|
|
63
|
+
"design-tokens",
|
|
64
|
+
"waukesha-makerspace",
|
|
65
|
+
"theme",
|
|
66
|
+
"tailwind",
|
|
67
|
+
"mui"
|
|
68
|
+
],
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"png-to-ico": "^3.0.1",
|
|
71
|
+
"sharp": "^0.35.3"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind v3 preset. Maps token *names* to the CSS variables from
|
|
3
|
+
* css/tokens.css — import that file (or '@waukeshamakerspace/design-tokens/css')
|
|
4
|
+
* in your global stylesheet, then:
|
|
5
|
+
*
|
|
6
|
+
* // tailwind.config.js
|
|
7
|
+
* module.exports = {
|
|
8
|
+
* presets: [require('@waukeshamakerspace/design-tokens/tailwind-preset')],
|
|
9
|
+
* ...
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* Gives the same utility names as the Tailwind v4 layer (bg-surface,
|
|
13
|
+
* text-foreground-muted, bg-primary, font-heading, …).
|
|
14
|
+
*
|
|
15
|
+
* Caveat: because values are var() references, Tailwind v3 opacity modifiers
|
|
16
|
+
* (e.g. bg-primary/50) do not work on these colors.
|
|
17
|
+
*/
|
|
18
|
+
module.exports = {
|
|
19
|
+
darkMode: 'class',
|
|
20
|
+
theme: {
|
|
21
|
+
extend: {
|
|
22
|
+
colors: {
|
|
23
|
+
// Brand — literal colors, both themes
|
|
24
|
+
'burnt-orange': 'var(--brand-burnt-orange)',
|
|
25
|
+
charcoal: 'var(--brand-charcoal)',
|
|
26
|
+
'golden-yellow': 'var(--brand-golden-yellow)',
|
|
27
|
+
'warm-white': 'var(--brand-warm-white)',
|
|
28
|
+
|
|
29
|
+
// Semantic — use these everywhere
|
|
30
|
+
surface: {
|
|
31
|
+
DEFAULT: 'var(--surface)',
|
|
32
|
+
muted: 'var(--surface-muted)',
|
|
33
|
+
raised: 'var(--surface-raised)',
|
|
34
|
+
inverse: 'var(--surface-inverse)',
|
|
35
|
+
},
|
|
36
|
+
foreground: {
|
|
37
|
+
DEFAULT: 'var(--foreground)',
|
|
38
|
+
muted: 'var(--foreground-muted)',
|
|
39
|
+
subtle: 'var(--foreground-subtle)',
|
|
40
|
+
inverse: 'var(--foreground-inverse)',
|
|
41
|
+
},
|
|
42
|
+
border: {
|
|
43
|
+
DEFAULT: 'var(--border)',
|
|
44
|
+
strong: 'var(--border-strong)',
|
|
45
|
+
},
|
|
46
|
+
primary: {
|
|
47
|
+
DEFAULT: 'var(--primary)',
|
|
48
|
+
hover: 'var(--primary-hover)',
|
|
49
|
+
foreground: 'var(--primary-foreground)',
|
|
50
|
+
},
|
|
51
|
+
secondary: {
|
|
52
|
+
DEFAULT: 'var(--secondary)',
|
|
53
|
+
hover: 'var(--secondary-hover)',
|
|
54
|
+
foreground: 'var(--secondary-foreground)',
|
|
55
|
+
},
|
|
56
|
+
accent: {
|
|
57
|
+
DEFAULT: 'var(--accent)',
|
|
58
|
+
foreground: 'var(--accent-foreground)',
|
|
59
|
+
},
|
|
60
|
+
link: {
|
|
61
|
+
DEFAULT: 'var(--link)',
|
|
62
|
+
hover: 'var(--link-hover)',
|
|
63
|
+
},
|
|
64
|
+
ring: 'var(--ring)',
|
|
65
|
+
warning: {
|
|
66
|
+
DEFAULT: 'var(--warning)',
|
|
67
|
+
foreground: 'var(--warning-foreground)',
|
|
68
|
+
},
|
|
69
|
+
success: {
|
|
70
|
+
DEFAULT: 'var(--success)',
|
|
71
|
+
foreground: 'var(--success-foreground)',
|
|
72
|
+
},
|
|
73
|
+
danger: {
|
|
74
|
+
DEFAULT: 'var(--danger)',
|
|
75
|
+
foreground: 'var(--danger-foreground)',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
fontFamily: {
|
|
79
|
+
heading: ['var(--font-heading)', 'sans-serif'],
|
|
80
|
+
body: ['var(--font-body)', 'sans-serif'],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
package/theme-init.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type ThemeMode = 'light' | 'dark';
|
|
2
|
+
|
|
3
|
+
/** Sync <html> with the device theme now and on every OS-theme change. */
|
|
4
|
+
export declare function initTheme(win?: Window): void;
|
|
5
|
+
|
|
6
|
+
/** The theme currently applied to <html>. */
|
|
7
|
+
export declare function getTheme(win?: Window): ThemeMode;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Switch theme on the fly. 'light' / 'dark' override the device theme for
|
|
11
|
+
* this page session; 'system' clears the override and resumes following the
|
|
12
|
+
* device.
|
|
13
|
+
*/
|
|
14
|
+
export declare function setTheme(mode: ThemeMode | 'system', win?: Window): void;
|
|
15
|
+
|
|
16
|
+
/** Flip between light and dark; returns the new mode. */
|
|
17
|
+
export declare function toggleTheme(win?: Window): ThemeMode;
|
|
18
|
+
|
|
19
|
+
/** Same behavior as initTheme(), as a string to inline in <head> pre-paint. */
|
|
20
|
+
export declare const themeInitScript: string;
|
package/theme-init.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-theme sync + on-the-fly theme switching.
|
|
3
|
+
*
|
|
4
|
+
* Follows prefers-color-scheme by applying `.dark` or `.light` to <html>,
|
|
5
|
+
* and keeps it in sync when the OS theme changes. Apps must also expose a
|
|
6
|
+
* simple toggle (see setTheme/toggleTheme below) so either mode can be
|
|
7
|
+
* exercised on the fly — per shared/ui-guidelines.md. A toggle choice is
|
|
8
|
+
* session-only (not persisted): it suppresses OS-change syncing until
|
|
9
|
+
* setTheme('system') or a reload returns the app to following the device.
|
|
10
|
+
*
|
|
11
|
+
* Next.js / SSR — inline the script in <head> so the class is set before
|
|
12
|
+
* first paint (no flash of the wrong theme):
|
|
13
|
+
*
|
|
14
|
+
* import { themeInitScript } from '@waukeshamakerspace/design-tokens/theme-init';
|
|
15
|
+
* <script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
|
|
16
|
+
*
|
|
17
|
+
* SPAs — call initTheme() at the top of your entry module instead.
|
|
18
|
+
*
|
|
19
|
+
* Note: the CSS tokens also carry a prefers-color-scheme media-query
|
|
20
|
+
* fallback, so pages render the right theme even without this script; the
|
|
21
|
+
* script exists so class-based styling (Tailwind `dark:`, MUI color schemes)
|
|
22
|
+
* follows the device too.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// The manual-override flag lives on <html> (data-theme-override) rather than
|
|
26
|
+
// in module state so the inline <head> script and this module coordinate
|
|
27
|
+
// without sharing scope.
|
|
28
|
+
|
|
29
|
+
const apply = (root, isDark) => {
|
|
30
|
+
root.classList.toggle('dark', isDark);
|
|
31
|
+
root.classList.toggle('light', !isDark);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Sync <html> with the device theme now and on every OS-theme change. */
|
|
35
|
+
export function initTheme(win = window) {
|
|
36
|
+
const media = win.matchMedia('(prefers-color-scheme: dark)');
|
|
37
|
+
const root = win.document.documentElement;
|
|
38
|
+
apply(root, media.matches);
|
|
39
|
+
media.addEventListener('change', (e) => {
|
|
40
|
+
if (!root.dataset.themeOverride) apply(root, e.matches);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The theme currently applied to <html>. */
|
|
45
|
+
export function getTheme(win = window) {
|
|
46
|
+
return win.document.documentElement.classList.contains('dark') ? 'dark' : 'light';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Switch theme on the fly. 'light' / 'dark' override the device theme for
|
|
51
|
+
* this page session; 'system' clears the override and resumes following the
|
|
52
|
+
* device.
|
|
53
|
+
*/
|
|
54
|
+
export function setTheme(mode, win = window) {
|
|
55
|
+
const root = win.document.documentElement;
|
|
56
|
+
if (mode === 'system') {
|
|
57
|
+
delete root.dataset.themeOverride;
|
|
58
|
+
apply(root, win.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
59
|
+
} else {
|
|
60
|
+
root.dataset.themeOverride = mode;
|
|
61
|
+
apply(root, mode === 'dark');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Flip between light and dark; returns the new mode. */
|
|
66
|
+
export function toggleTheme(win = window) {
|
|
67
|
+
const next = getTheme(win) === 'dark' ? 'light' : 'dark';
|
|
68
|
+
setTheme(next, win);
|
|
69
|
+
return next;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Same behavior as initTheme(), as a string to inline in <head> pre-paint. */
|
|
73
|
+
export const themeInitScript = `(function () {
|
|
74
|
+
var media = window.matchMedia('(prefers-color-scheme: dark)');
|
|
75
|
+
var root = document.documentElement;
|
|
76
|
+
function apply(isDark) {
|
|
77
|
+
root.classList.toggle('dark', isDark);
|
|
78
|
+
root.classList.toggle('light', !isDark);
|
|
79
|
+
}
|
|
80
|
+
apply(media.matches);
|
|
81
|
+
media.addEventListener('change', function (e) {
|
|
82
|
+
if (!root.dataset.themeOverride) apply(e.matches);
|
|
83
|
+
});
|
|
84
|
+
})();`;
|