@gusnips/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 +85 -0
- package/package.json +58 -0
- package/src/base.css +121 -0
- package/src/index.css +46 -0
- package/src/theme.css +117 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gustavo Salomé
|
|
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,85 @@
|
|
|
1
|
+
# @gusnips/tokens
|
|
2
|
+
|
|
3
|
+
The colour names a Tailwind 4 app uses, and the dark-mode switch behind them. You bring the
|
|
4
|
+
colours.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
bun add @gusnips/tokens
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```css
|
|
11
|
+
@import "tailwindcss";
|
|
12
|
+
@import "@gusnips/tokens/index.css";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Two lines, and you can write `bg-card text-card-foreground` anywhere. It renders in light mode
|
|
16
|
+
and in dark mode, and no component ever writes a `dark:` variant.
|
|
17
|
+
|
|
18
|
+
## The mechanism
|
|
19
|
+
|
|
20
|
+
Twenty names — `background`, `foreground`, `card`, `popover`, `primary`, `secondary`, `muted`,
|
|
21
|
+
`accent`, `destructive`, their `-foreground` pairs, plus `border`, `input`, `ring` and `scrim`.
|
|
22
|
+
|
|
23
|
+
Dark mode rebinds **the same names** under a `.dark` class. `bg-card` is one utility that
|
|
24
|
+
resolves to a different colour below a `.dark` ancestor. That is the whole trick.
|
|
25
|
+
|
|
26
|
+
## Your colours
|
|
27
|
+
|
|
28
|
+
The values that ship are a plain grey scale — readable, and deliberately characterless, so the
|
|
29
|
+
app looks like nothing until you decide what it looks like. Override after the import, and set
|
|
30
|
+
both halves:
|
|
31
|
+
|
|
32
|
+
```css
|
|
33
|
+
@theme {
|
|
34
|
+
--color-primary: #7c3aed;
|
|
35
|
+
}
|
|
36
|
+
.dark {
|
|
37
|
+
--color-primary: #a78bfa;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Skip the second line and your daytime purple stays on screen at night.
|
|
42
|
+
|
|
43
|
+
## Two names you cannot pick freely
|
|
44
|
+
|
|
45
|
+
A build check measures both, so a bad value fails your build instead of reaching someone.
|
|
46
|
+
|
|
47
|
+
- **`--color-input` needs 3:1 against `--color-background`.** It is a field border, and WCAG
|
|
48
|
+
1.4.11 asks 3:1 of anything that outlines a control. This is the one people miss: a grey
|
|
49
|
+
picked to sit nicely next to `--color-border` will not clear it. One codebase this came from
|
|
50
|
+
pointed `input` at its divider grey — 1.6:1 — so every text field in two apps had a border
|
|
51
|
+
some people cannot see.
|
|
52
|
+
- **`--color-primary` needs its own dark value.** Holding one brand colour across both modes is
|
|
53
|
+
the trap. A fill chosen against a white page is dark, and on a near-black card it lands _on_
|
|
54
|
+
the 3:1 line instead of clear of it. Lift it for dark, and flip `--color-primary-foreground`
|
|
55
|
+
with it — which is why nothing in this family writes `text-white`.
|
|
56
|
+
|
|
57
|
+
## What is not here
|
|
58
|
+
|
|
59
|
+
Fonts, shadows, radii, type scale, motion. Radii and type sizes already have Tailwind's own
|
|
60
|
+
`--radius-*` and `--text-*` names, so retune those in your `@theme` and every `rounded-lg` in
|
|
61
|
+
every component follows. The rest is what your product looks like, and it stays in your product.
|
|
62
|
+
|
|
63
|
+
One trap worth knowing before you write your own `@theme`: **`--duration-*` is not a Tailwind
|
|
64
|
+
namespace.** `--duration-standard: 250ms` compiles to no utility at all, so `duration-standard`
|
|
65
|
+
in a `className` is dead text and the transition quietly runs at Tailwind's default. `--ease-*`
|
|
66
|
+
beside it _does_ work, which is what hides it — the easing lands, the duration does not. Write
|
|
67
|
+
`duration-250`, or keep the token and put `transition-duration: var(--duration-standard)` in a
|
|
68
|
+
real rule. One codebase had 61 of these.
|
|
69
|
+
|
|
70
|
+
## The base rules
|
|
71
|
+
|
|
72
|
+
`index.css` also brings a handful of rules that every app needs and nobody remembers:
|
|
73
|
+
|
|
74
|
+
- one `:focus-visible` outline, drawn with `--color-ring`, so it follows your theme into dark
|
|
75
|
+
- `cursor: pointer` on buttons, which browsers do not give you
|
|
76
|
+
- `color-scheme`, which stops a two-tone seam where a phone toolbar retracts
|
|
77
|
+
- reduced motion clamped to `0.01ms` rather than `none`, so animations still land on their end
|
|
78
|
+
pose instead of never appearing — including the two delay properties, which are the half
|
|
79
|
+
people forget
|
|
80
|
+
- a thin scrollbar whose thumb reads `--color-input`, and `.scrollbar-none` for a tab strip
|
|
81
|
+
|
|
82
|
+
Want them separately? `@gusnips/tokens/theme.css` is the names with no rules,
|
|
83
|
+
`@gusnips/tokens/base.css` the rules with no names.
|
|
84
|
+
|
|
85
|
+
MIT · part of [frontkit](https://github.com/gusnips/frontkit)
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gusnips/tokens",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One Tailwind 4 @theme file: the semantic token contract, the dark-mode mechanism, and the base rules every app needs. Colours stay per-brand.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Gustavo Salomé",
|
|
8
|
+
"homepage": "https://github.com/gusnips/frontkit/tree/main/tokens#readme",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/gusnips/frontkit/issues"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
"./theme.css": "./src/theme.css",
|
|
14
|
+
"./base.css": "./src/base.css",
|
|
15
|
+
"./index.css": "./src/index.css"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"sideEffects": [
|
|
23
|
+
"*.css"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"tailwindcss",
|
|
30
|
+
"tailwind-v4",
|
|
31
|
+
"design-tokens",
|
|
32
|
+
"theme",
|
|
33
|
+
"dark-mode",
|
|
34
|
+
"css"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "bun run scripts/compile-check.ts",
|
|
38
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
39
|
+
"test": "bun run scripts/compile-check.ts",
|
|
40
|
+
"lint": "eslint scripts",
|
|
41
|
+
"sync:docs": "cp ../LICENSE .",
|
|
42
|
+
"prepublishOnly": "bun run lint && bun run typecheck && bun run test && bun run sync:docs",
|
|
43
|
+
"release:patch": "bun pm version patch && bun publish --access public",
|
|
44
|
+
"release:minor": "bun pm version minor && bun publish --access public",
|
|
45
|
+
"release:major": "bun pm version major && bun publish --access public"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@tailwindcss/cli": "^4.3.3",
|
|
49
|
+
"@types/node": "^24.12.0",
|
|
50
|
+
"tailwindcss": "^4.3.3",
|
|
51
|
+
"typescript": "^5.9.3"
|
|
52
|
+
},
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/gusnips/frontkit.git",
|
|
56
|
+
"directory": "tokens"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/base.css
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The base rules every app needs and nobody remembers to write.
|
|
3
|
+
*
|
|
4
|
+
* Each rule below is here because a product shipped without it and something broke. No
|
|
5
|
+
* colours and no brand: every value reads a token from theme.css, so import that first —
|
|
6
|
+
* or import index.css, which does both in the right order.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
@layer base {
|
|
10
|
+
/* The browser paints two areas itself: the overscroll rubber-band, and the strip a
|
|
11
|
+
100svh surface leaves uncovered when a mobile toolbar retracts. With no explicit
|
|
12
|
+
`color-scheme` it paints them its own default near-black, a hair off our surface —
|
|
13
|
+
a hard two-tone seam across every full-height loading and error screen. Painting
|
|
14
|
+
`html` (not just `body`) and declaring the scheme makes those UA-painted areas match.
|
|
15
|
+
`color-scheme` also fixes form controls, scrollbars and the caret, which is the
|
|
16
|
+
second half of the same win. */
|
|
17
|
+
html {
|
|
18
|
+
color-scheme: light;
|
|
19
|
+
background-color: var(--color-background);
|
|
20
|
+
color: var(--color-foreground);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Matches wherever the class lives. Products put it on <html>, which is what makes the
|
|
24
|
+
canvas follow; on <body> it still corrects controls and scrollbars in the subtree. */
|
|
25
|
+
.dark {
|
|
26
|
+
color-scheme: dark;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* A browser's default cursor for <button> is the arrow — links get the hand for free,
|
|
30
|
+
buttons never do — and Tailwind's preflight does not say otherwise. So nothing built
|
|
31
|
+
out of a Button primitive read as clickable on hover. Saying it once here beats
|
|
32
|
+
sprinkling `cursor-pointer` across every primitive and every call site.
|
|
33
|
+
In `@layer base` on purpose: cursor utilities live in the utilities layer, which the
|
|
34
|
+
cascade puts after base, so a component that deliberately asks for `cursor-default`
|
|
35
|
+
or `cursor-not-allowed` on an enabled button still wins. `:not(:disabled)` leaves the
|
|
36
|
+
disabled cursor alone, since that element sets its own. */
|
|
37
|
+
button:not(:disabled),
|
|
38
|
+
[role="button"]:not([aria-disabled="true"]) {
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* One focus ring for the whole app, drawn with the ring token so it follows the theme
|
|
43
|
+
into dark mode with no second rule. No `border-radius` here: an outline already
|
|
44
|
+
traces the element's own radius, and forcing a value squares a pill or a segmented
|
|
45
|
+
control back into a rectangle. */
|
|
46
|
+
:focus-visible {
|
|
47
|
+
outline: 2px solid var(--color-ring);
|
|
48
|
+
outline-offset: 2px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* The ring reaches text fields too, and nothing here strips it from them. That is
|
|
52
|
+
deliberate, and it is the one donor rule that had to INVERT on the way into a package.
|
|
53
|
+
The app it came from follows this with `outline: none` on every input, textarea, select
|
|
54
|
+
and combobox, because its own fields each draw a border-plus-ring on focus and the
|
|
55
|
+
global outline landed on top as a doubled, heavy frame. There, removing it is a fix.
|
|
56
|
+
Here it would be the reverse: we add a ring the adopter never had, then take it away
|
|
57
|
+
again from exactly the elements that most need one. An app with plain `<input>`s would
|
|
58
|
+
import this file and come out with no keyboard focus indicator on any field — a WCAG
|
|
59
|
+
2.4.7 failure introduced by us, in an app that was fine before. A shared layer does not
|
|
60
|
+
get to make that trade, and a comment telling adopters to put the ring back is not a
|
|
61
|
+
fix, because it only works for the ones who read it.
|
|
62
|
+
A control that genuinely draws its own focus treatment opts out on itself, with
|
|
63
|
+
`focus-visible:outline-none` on the element. Utilities sit in a later cascade layer
|
|
64
|
+
than base, so it wins with no specificity fight, and the opt-out stays local to the one
|
|
65
|
+
component that earned it. */
|
|
66
|
+
|
|
67
|
+
/* A thin scrollbar, because the OS default is a wide light bar that stays light on a
|
|
68
|
+
dark app. The thumb reads `--color-input`, the one token defined to clear 3:1 against
|
|
69
|
+
the surface behind it — exactly what a scrollbar needs — and it is rebound in `.dark`,
|
|
70
|
+
so the thumb follows the theme without a second rule.
|
|
71
|
+
6px is a choice, not a contract — override the width in your own stylesheet if your
|
|
72
|
+
app wants a chunkier bar. What is load-bearing is the token the thumb reads.
|
|
73
|
+
WebKit pseudo-elements only, deliberately: setting the standard `scrollbar-width`
|
|
74
|
+
makes Chrome ignore these rules entirely, so asking for both gets neither. */
|
|
75
|
+
::-webkit-scrollbar {
|
|
76
|
+
width: 6px;
|
|
77
|
+
height: 6px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
::-webkit-scrollbar-track {
|
|
81
|
+
background: transparent;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
::-webkit-scrollbar-thumb {
|
|
85
|
+
background: var(--color-input);
|
|
86
|
+
border-radius: 3px;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@layer utilities {
|
|
91
|
+
/* Hide the bar, keep the scroll — for a tab strip or filter row that scrolls sideways,
|
|
92
|
+
where the scrollbar is chrome nobody drags. Here `scrollbar-width` is the right tool
|
|
93
|
+
and it deliberately overrides the rule above; the WebKit line covers Safari before
|
|
94
|
+
18.2, which does not support the standard property. */
|
|
95
|
+
.scrollbar-none {
|
|
96
|
+
scrollbar-width: none;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.scrollbar-none::-webkit-scrollbar {
|
|
100
|
+
display: none;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Reduced motion. Clamped to 0.01ms rather than `none` so every animation still FIRES and
|
|
105
|
+
lands on its end pose — an element whose visible state is a keyframe's `to` block would
|
|
106
|
+
otherwise never appear at all.
|
|
107
|
+
The two delay properties are the half that is easy to miss: without them a staggered
|
|
108
|
+
list still waits out its full stagger before anything shows up, which is the delay with
|
|
109
|
+
none of the motion that explained it. One of the three donor themes clamped the
|
|
110
|
+
durations and forgot the delays. */
|
|
111
|
+
@media (prefers-reduced-motion: reduce) {
|
|
112
|
+
*,
|
|
113
|
+
*::before,
|
|
114
|
+
*::after {
|
|
115
|
+
animation-duration: 0.01ms !important;
|
|
116
|
+
animation-iteration-count: 1 !important;
|
|
117
|
+
animation-delay: 0.01ms !important;
|
|
118
|
+
transition-duration: 0.01ms !important;
|
|
119
|
+
transition-delay: 0.01ms !important;
|
|
120
|
+
}
|
|
121
|
+
}
|
package/src/index.css
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The tokens and the base rules together. Import this unless you have a reason not to.
|
|
3
|
+
*
|
|
4
|
+
* An app entry is three lines:
|
|
5
|
+
*
|
|
6
|
+
* @import "tailwindcss";
|
|
7
|
+
* @import "@gusnips/tokens/index.css";
|
|
8
|
+
* @source "../../../packages/ui/src"; // one per package Tailwind must scan for classes
|
|
9
|
+
*
|
|
10
|
+
* Then your own colours, after those. Override a name in `@theme` for light and in `.dark`
|
|
11
|
+
* for dark — both halves, every time, or the light value stays on screen at night:
|
|
12
|
+
*
|
|
13
|
+
* @theme { --color-primary: #7c3aed; }
|
|
14
|
+
* .dark { --color-primary: #a78bfa; }
|
|
15
|
+
*
|
|
16
|
+
* That is two lines, and it changes every component that ever wrote `bg-primary`.
|
|
17
|
+
*
|
|
18
|
+
* Keep this import AFTER `@import "tailwindcss"`. It uses `@theme`, `@layer` and
|
|
19
|
+
* `@custom-variant`, and Tailwind only understands those once it has been imported.
|
|
20
|
+
*
|
|
21
|
+
* You do not need to declare `@custom-variant dark` yourself — theme.css ships it.
|
|
22
|
+
*
|
|
23
|
+
* Take the halves separately if you need to: `@gusnips/tokens/theme.css` is the tokens
|
|
24
|
+
* with no rules, `@gusnips/tokens/base.css` the rules with no tokens.
|
|
25
|
+
*
|
|
26
|
+
* Not here, on purpose: fonts, shadows, radii, a type scale, motion, keyframes. Radii,
|
|
27
|
+
* type sizes and easings already have Tailwind's own `--radius-*`, `--text-*` and
|
|
28
|
+
* `--ease-*` names — retune those in your `@theme` and every `rounded-lg` in every
|
|
29
|
+
* component follows. The rest is what your product looks like, and it stays in your
|
|
30
|
+
* product.
|
|
31
|
+
*
|
|
32
|
+
* Migrating a theme that has its own radii: keep `--radius-sm/md/lg/xl` and retune the
|
|
33
|
+
* values — they are already Tailwind's names. Drop `--radius-full` and `--radius-pill`.
|
|
34
|
+
* `rounded-full` is a built-in utility that needs no token at all, and it resolves to
|
|
35
|
+
* `calc(infinity * 1px)`, which stays round on an element taller than the `9999px` both
|
|
36
|
+
* donor themes redefined it to. A theme that spells it `pill` renames every
|
|
37
|
+
* `rounded-pill` to `rounded-full` and deletes the token.
|
|
38
|
+
*
|
|
39
|
+
* One trap worth knowing before you write your own `@theme`: `--duration-*` is NOT a
|
|
40
|
+
* Tailwind namespace. `duration-standard` compiles to nothing at all, silently, and the
|
|
41
|
+
* transition falls back to 150ms. `duration-250` works; so does `var(--duration-standard)`
|
|
42
|
+
* inside a hand-written rule. The compile check pins this so nobody rediscovers it.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
@import "./theme.css";
|
|
46
|
+
@import "./base.css";
|
package/src/theme.css
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The colour contract, and the dark-mode mechanism.
|
|
3
|
+
*
|
|
4
|
+
* Twenty names. Nineteen of them were already defined, independently, in three separate
|
|
5
|
+
* product themes — that agreement is what makes this a contract rather than a preference.
|
|
6
|
+
* A component writes `bg-card text-card-foreground` once and renders correctly in both
|
|
7
|
+
* modes, in every product that imports this file.
|
|
8
|
+
*
|
|
9
|
+
* Dark mode is class-strategy: the SAME names are rebound under `.dark`, so `bg-card`
|
|
10
|
+
* resolves differently below a `.dark` ancestor and no component ever writes a `dark:`
|
|
11
|
+
* variant. That is the entire mechanism, and it is the reason the names are worth sharing.
|
|
12
|
+
* The alternative one product tried — a parallel `--color-dark-*` namespace — makes every
|
|
13
|
+
* component spell out both halves, which is a `dark:` variant with extra steps.
|
|
14
|
+
*
|
|
15
|
+
* The VALUES are placeholders: a plain grey scale, accessible and deliberately
|
|
16
|
+
* characterless. A product overrides them in its own `@theme` block AFTER importing this
|
|
17
|
+
* one. Colour is where a product lives; it does not belong in a shared package.
|
|
18
|
+
*
|
|
19
|
+
* Two traps, both of which cost real time to find:
|
|
20
|
+
* - Do NOT change this to `@theme inline`. Inline bakes each value into the utility, and
|
|
21
|
+
* the `.dark` rebinding below silently stops working.
|
|
22
|
+
* - A name added here must be added to `.dark` too. Forgetting leaves one colour stuck at
|
|
23
|
+
* its light value in dark mode, on one screen, which is how a theme rots. The compile
|
|
24
|
+
* check fails on that.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/* `:where()` keeps the variant at zero specificity, so `dark:bg-card` and `bg-card` weigh
|
|
28
|
+
the same and source order decides — the ordinary Tailwind behaviour every other variant
|
|
29
|
+
has. Without it the dark variant would quietly outrank hover and focus styles. */
|
|
30
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
31
|
+
|
|
32
|
+
@theme {
|
|
33
|
+
/* ─── Light ───
|
|
34
|
+
Contrast measured, not guessed: foreground on background 17.2:1, muted-foreground on
|
|
35
|
+
background 7.5:1 and on muted 7.2:1, every `-foreground` on its own surface above
|
|
36
|
+
14:1. Two of those are floors the compile check enforces — see `input` below. */
|
|
37
|
+
--color-background: #fafafa;
|
|
38
|
+
--color-foreground: #171717;
|
|
39
|
+
--color-card: #ffffff;
|
|
40
|
+
--color-card-foreground: #171717;
|
|
41
|
+
--color-popover: #ffffff;
|
|
42
|
+
--color-popover-foreground: #171717;
|
|
43
|
+
--color-primary: #171717;
|
|
44
|
+
--color-primary-foreground: #fafafa;
|
|
45
|
+
--color-secondary: #f5f5f5;
|
|
46
|
+
--color-secondary-foreground: #171717;
|
|
47
|
+
--color-muted: #f5f5f5;
|
|
48
|
+
--color-muted-foreground: #525252;
|
|
49
|
+
/* `accent` is the NEUTRAL highlight — a menu row under the cursor, a hovered list item.
|
|
50
|
+
Brand tinting is always explicit, never the ambient default. Two of the three donor
|
|
51
|
+
themes say this in their own words, and the third had to fix it later. */
|
|
52
|
+
--color-accent: #e5e5e5;
|
|
53
|
+
--color-accent-foreground: #171717;
|
|
54
|
+
/* The one placeholder that is not grey. `destructive` is a MEANING, not a brand choice:
|
|
55
|
+
a grey delete button is indistinguishable from a secondary one, so a product that
|
|
56
|
+
never overrides this still gets a warning that reads as a warning. 4.8:1 under white. */
|
|
57
|
+
--color-destructive: #dc2626;
|
|
58
|
+
--color-destructive-foreground: #ffffff;
|
|
59
|
+
--color-border: #e5e5e5;
|
|
60
|
+
/* `input` is a control BOUNDARY, not decoration, so 3:1 against `--color-background` is
|
|
61
|
+
a FLOOR and not a preference (WCAG 1.4.11). The compile check fails below it, in both
|
|
62
|
+
modes. If you override this token, measure it: a grey picked to sit nicely beside
|
|
63
|
+
`border` will not clear 3:1, which is exactly how one donor ended up pointing `input`
|
|
64
|
+
at the same rung it uses for a passive divider — 1.6:1 in light, 2.2:1 in dark, so
|
|
65
|
+
every field border in two of its apps is invisible to somebody.
|
|
66
|
+
`border` has no floor. It is a hairline between two surfaces, and nothing depends on
|
|
67
|
+
seeing it; that is why the two tokens exist separately and why this one is so much
|
|
68
|
+
darker. Here: 3.2:1 on background, 3.4:1 on card. */
|
|
69
|
+
--color-input: #8c8c8c;
|
|
70
|
+
--color-ring: #171717;
|
|
71
|
+
/* What a dialog dims the page with. A token rather than `black/40`, because the two
|
|
72
|
+
modes need different scrims: over a light ground a wash of ink reads as shade, and
|
|
73
|
+
over a dark ground the same wash reads as nothing at all. */
|
|
74
|
+
--color-scrim: rgb(23 23 23 / 0.5);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
* Dark. Stays after `@theme` on purpose: `@theme` compiles to a `:root` rule, and `:root`
|
|
79
|
+
* and `.dark` carry equal specificity, so source order is the only thing making the dark
|
|
80
|
+
* values win.
|
|
81
|
+
*
|
|
82
|
+
* Elevation here is lightness, not shadow — a drop shadow barely reads on a near-black
|
|
83
|
+
* ground. background → card → accent climb in even steps so a popover lifts off the page
|
|
84
|
+
* without needing one.
|
|
85
|
+
*/
|
|
86
|
+
.dark {
|
|
87
|
+
--color-background: #0a0a0a;
|
|
88
|
+
--color-foreground: #fafafa;
|
|
89
|
+
--color-card: #171717;
|
|
90
|
+
--color-card-foreground: #fafafa;
|
|
91
|
+
--color-popover: #171717;
|
|
92
|
+
--color-popover-foreground: #fafafa;
|
|
93
|
+
/* Primary LIFTS for dark, and its label flips dark with it. Copy the strategy, not the
|
|
94
|
+
greys. The tempting version — hold one brand colour across both modes, keep a white
|
|
95
|
+
label — is the trap: a fill chosen to look right on a white page is dark, and on a
|
|
96
|
+
near-black card it lands ON the 3:1 a UI component needs rather than clear of it. One
|
|
97
|
+
donor's held violet measures 3.08:1 against its own dark card, which passes with 0.08
|
|
98
|
+
to spare; the donor that lifts the same kind of hue for dark measures 6.6:1. The
|
|
99
|
+
margin is the whole difference, and it disappears the first time someone lightens a
|
|
100
|
+
card. The label must flip because a lifted fill is light — white on it is unreadable. */
|
|
101
|
+
--color-primary: #fafafa;
|
|
102
|
+
--color-primary-foreground: #171717;
|
|
103
|
+
--color-secondary: #262626;
|
|
104
|
+
--color-secondary-foreground: #fafafa;
|
|
105
|
+
--color-muted: #262626;
|
|
106
|
+
--color-muted-foreground: #a3a3a3;
|
|
107
|
+
--color-accent: #333333;
|
|
108
|
+
--color-accent-foreground: #fafafa;
|
|
109
|
+
/* Inverted, not darkened: a saturated red on a near-black ground is unreadable, so the
|
|
110
|
+
fill lifts and the label goes dark. 5.8:1. */
|
|
111
|
+
--color-destructive: #f87171;
|
|
112
|
+
--color-destructive-foreground: #450a0a;
|
|
113
|
+
--color-border: #262626;
|
|
114
|
+
--color-input: #6b6b6b; /* 3.7:1 on background, 3.4:1 on card */
|
|
115
|
+
--color-ring: #fafafa;
|
|
116
|
+
--color-scrim: rgb(0 0 0 / 0.65);
|
|
117
|
+
}
|