trykeyline 0.1.0 → 0.1.2
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/.claude-plugin/marketplace.json +1 -1
- package/LICENSE +0 -16
- package/bin/keyline.mjs +2 -1
- package/package.json +1 -1
- package/plugins/keyline/.claude-plugin/plugin.json +1 -1
- package/plugins/keyline/plugin.json +22 -0
- package/plugins/keyline/skills/keyline/SKILL.md +22 -32
- package/plugins/keyline/skills/keyline/commands/audit.md +8 -3
- package/plugins/keyline/skills/keyline/commands/critique.md +3 -26
- package/plugins/keyline/skills/keyline/commands/init.md +5 -4
- package/plugins/keyline/skills/keyline/commands/polish.md +2 -2
- package/plugins/keyline/skills/keyline/commands/refine.md +6 -16
- package/plugins/keyline/skills/keyline/commands/sync.md +1 -3
- package/plugins/keyline/skills/keyline/commands/ux-laws.md +26 -0
- package/plugins/keyline/skills/keyline/domains/color/deriving-a-ramp.md +1 -1
- package/plugins/keyline/skills/keyline/domains/color/guide.md +10 -46
- package/plugins/keyline/skills/keyline/domains/color/status-and-exceptions.md +2 -2
- package/plugins/keyline/skills/keyline/domains/depth/elevation-recipes.md +2 -8
- package/plugins/keyline/skills/keyline/domains/depth/guide.md +12 -41
- package/plugins/keyline/skills/keyline/domains/layout/guide.md +31 -56
- package/plugins/keyline/skills/keyline/domains/layout/measure-and-rhythm.md +16 -16
- package/plugins/keyline/skills/keyline/domains/layout/radius-lines-and-layers.md +2 -2
- package/plugins/keyline/skills/keyline/domains/motion/guide.md +12 -47
- package/plugins/keyline/skills/keyline/domains/motion/patterns.md +1 -1
- package/plugins/keyline/skills/keyline/domains/motion/performance.md +1 -1
- package/plugins/keyline/skills/keyline/domains/patterns/guide.md +20 -62
- package/plugins/keyline/skills/keyline/domains/patterns/iteration.md +1 -9
- package/plugins/keyline/skills/keyline/domains/type/guide.md +13 -37
- package/plugins/keyline/skills/keyline/domains/type/ladders.md +1 -1
- package/plugins/keyline/skills/keyline/domains/type/type-audit.md +5 -8
- package/plugins/keyline/skills/keyline/glass/color/assets/color-tokens.css +34 -165
- package/plugins/keyline/skills/keyline/glass/color/assets/theme.ts +10 -70
- package/plugins/keyline/skills/keyline/glass/color/guide.md +3 -30
- package/plugins/keyline/skills/keyline/glass/color/intent-ramp.md +2 -34
- package/plugins/keyline/skills/keyline/glass/color/theming.md +5 -72
- package/plugins/keyline/skills/keyline/glass/depth/assets/GlassPanel.tsx +14 -121
- package/plugins/keyline/skills/keyline/glass/depth/assets/glass.css +29 -188
- package/plugins/keyline/skills/keyline/glass/depth/depth-cues.md +1 -16
- package/plugins/keyline/skills/keyline/glass/depth/guide.md +4 -73
- package/plugins/keyline/skills/keyline/glass/depth/performance-and-fallbacks.md +3 -11
- package/plugins/keyline/skills/keyline/glass/layout/adaptive-presentations.md +5 -91
- package/plugins/keyline/skills/keyline/glass/layout/assets/SectionNav.tsx +3 -6
- package/plugins/keyline/skills/keyline/glass/layout/assets/layout-tokens.css +11 -138
- package/plugins/keyline/skills/keyline/glass/layout/guide.md +10 -15
- package/plugins/keyline/skills/keyline/glass/layout/measure-and-rhythm.md +4 -6
- package/plugins/keyline/skills/keyline/glass/layout/radius-and-elevation.md +3 -2
- package/plugins/keyline/skills/keyline/glass/motion/assets/DropMenu.tsx +4 -6
- package/plugins/keyline/skills/keyline/glass/motion/assets/motion-presets.ts +10 -184
- package/plugins/keyline/skills/keyline/glass/motion/assets/motion-tokens.css +21 -105
- package/plugins/keyline/skills/keyline/glass/motion/easing-and-duration.md +2 -31
- package/plugins/keyline/skills/keyline/glass/motion/guide.md +10 -41
- package/plugins/keyline/skills/keyline/glass/motion/reduced-motion.md +1 -7
- package/plugins/keyline/skills/keyline/glass/motion/scroll-performance.md +3 -32
- package/plugins/keyline/skills/keyline/glass/motion/shared-element.md +1 -1
- package/plugins/keyline/skills/keyline/glass/motion/sound.md +10 -80
- package/plugins/keyline/skills/keyline/glass/type/assets/prose.tsx +8 -43
- package/plugins/keyline/skills/keyline/glass/type/assets/type-tokens.css +49 -86
- package/plugins/keyline/skills/keyline/glass/type/guide.md +5 -7
- package/plugins/keyline/skills/keyline/glass/type/long-form-prose.md +9 -74
- package/plugins/keyline/skills/keyline/glass/type/scale.md +2 -6
- package/plugins/keyline/skills/keyline/glass/type/weight-and-tracking.md +4 -20
- package/plugins/keyline/skills/keyline/scripts/check/css.mjs +5 -14
- package/plugins/keyline/skills/keyline/scripts/check/design.mjs +3 -1
- package/plugins/keyline/skills/keyline/scripts/check/files.mjs +70 -26
- package/plugins/keyline/skills/keyline/scripts/check/markup.mjs +3 -3
- package/plugins/keyline/skills/keyline/scripts/check/rules.mjs +9 -1
- package/plugins/keyline/skills/keyline/scripts/check.mjs +6 -3
- package/plugins/keyline/skills/keyline/scripts/contrast.mjs +2 -0
- package/plugins/keyline/skills/keyline/scripts/scan.mjs +29 -45
- package/plugins/keyline/skills/keyline/system/design-md.md +4 -4
- package/plugins/keyline/skills/keyline/system/guide.md +4 -8
- package/plugins/keyline/skills/keyline/system/presets.md +16 -16
- package/plugins/keyline/skills/keyline/system/roles.md +8 -8
|
@@ -5,39 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
@import "tailwindcss";
|
|
7
7
|
|
|
8
|
-
/*
|
|
9
|
-
|
|
10
|
-
expressed a preference the OS shouldn't win.
|
|
11
|
-
|
|
12
|
-
:where() keeps specificity at zero, so a dark: utility doesn't outrank an
|
|
13
|
-
unprefixed one purely by virtue of the attribute selector. */
|
|
8
|
+
/* `dark:` keys off data-theme (set by theme.ts), not the OS; :where() keeps
|
|
9
|
+
specificity at zero. */
|
|
14
10
|
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
|
|
15
11
|
|
|
16
12
|
@theme {
|
|
17
|
-
/* ── The solid ramp
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
200 that surface, hovered · inert placeholder fill
|
|
22
|
-
300 that surface, active/pressed
|
|
23
|
-
400 BORDER
|
|
24
|
-
500 border hovered · decorative marks (bullets, dots, rail markers)
|
|
25
|
-
600 border active
|
|
26
|
-
700 solid fill
|
|
27
|
-
800 solid fill hovered
|
|
28
|
-
900 SECONDARY TEXT
|
|
29
|
-
1000 PRIMARY TEXT · inverted-element fill
|
|
30
|
-
|
|
31
|
-
NOTE 500-800 ARE NOT TEXT COLOURS. A border needs 3:1 against its
|
|
32
|
-
surface; text needs 4.5:1. On a light theme these mid steps are light
|
|
33
|
-
greys on white — 500 is 1.66:1, 600 is 2.38:1, 800 is 4.12:1 — so all
|
|
34
|
-
of them fail as text. Only 900 and 1000 are readable. Dark mode hides
|
|
35
|
-
this completely, because the same indices sit far lighter against a
|
|
36
|
-
near-black page. Text tiers below.
|
|
37
|
-
|
|
38
|
-
Note 400 is LIGHTER than 300 in this theme. Not a mistake — a border
|
|
39
|
-
sits beside content and must stay quieter than a pressed surface, which
|
|
40
|
-
sits under it. Intent beats monotonicity. */
|
|
13
|
+
/* ── The solid ramp, indexed by intent: 100–300 surface states, 400–600
|
|
14
|
+
border states, 700–800 solid fill, 900 secondary text, 1000 primary.
|
|
15
|
+
500–800 are NOT text colours, and 400 is lighter than 300 on purpose:
|
|
16
|
+
glass/color/intent-ramp.md. */
|
|
41
17
|
--color-gray-100: #f2f2f2;
|
|
42
18
|
--color-gray-200: #ebebeb;
|
|
43
19
|
--color-gray-300: #e6e6e6;
|
|
@@ -49,11 +25,7 @@
|
|
|
49
25
|
--color-gray-900: #4d4d4d;
|
|
50
26
|
--color-gray-1000: #171717;
|
|
51
27
|
|
|
52
|
-
/* ── The translucent ramp
|
|
53
|
-
Same indices, alpha instead of solid. Use these for ANYTHING LAYERED:
|
|
54
|
-
borders on glass, dividers, hairlines, overlays. A solid border on a
|
|
55
|
-
translucent panel doesn't participate in what's behind it and reads as
|
|
56
|
-
a pasted-on frame. */
|
|
28
|
+
/* ── The translucent ramp: anything layered (glass/color/guide.md §2). */
|
|
57
29
|
--color-gray-alpha-100: #0000000d;
|
|
58
30
|
--color-gray-alpha-200: #00000015;
|
|
59
31
|
--color-gray-alpha-300: #0000001a;
|
|
@@ -61,41 +33,19 @@
|
|
|
61
33
|
--color-gray-alpha-500: #00000036;
|
|
62
34
|
--color-gray-alpha-600: #0000003d;
|
|
63
35
|
|
|
64
|
-
/* ── Surfaces
|
|
65
|
-
|
|
66
|
-
a component surface. background-100 is also the TEXT colour for anything
|
|
67
|
-
inverted (a tooltip), which is why it's a token and not a literal white. */
|
|
36
|
+
/* ── Surfaces, held out of the ramp. background-100 is also the text of
|
|
37
|
+
anything inverted. */
|
|
68
38
|
--color-background-100: #ffffff;
|
|
69
39
|
--color-background-200: #fafafa;
|
|
70
40
|
|
|
71
|
-
/* ── Text tiers
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
both. Ratios against background-100:
|
|
75
|
-
|
|
76
|
-
gray-1000 primary 17.93:1 body copy, headings, active state
|
|
77
|
-
gray-900 secondary 8.45:1 emphasised, ancestor rows
|
|
78
|
-
muted 6.48:1 deks, supporting copy, <em>
|
|
79
|
-
quiet 5.49:1 resting icons, state labels, <cite>
|
|
80
|
-
faint 4.61:1 dates, read times, meta
|
|
81
|
-
|
|
82
|
-
Worst real pairing is muted on gray-100 (a subtle surface) at 5.79:1.
|
|
83
|
-
|
|
84
|
-
The ladder is STRICTLY DESCENDING on purpose. Once every tier has to
|
|
85
|
-
clear 4.5:1 they crowd together and the hierarchy collapses — check the
|
|
86
|
-
ordering, not just the thresholds, or you pass WCAG and lose the design.
|
|
87
|
-
|
|
88
|
-
Icons ride `quiet` rather than getting their own token: they only need
|
|
89
|
-
3:1, but they usually sit beside a label and two tokens that must stay
|
|
90
|
-
visually matched is more coupling than it saves. */
|
|
41
|
+
/* ── Text tiers, apart from the ramp (glass/color/guide.md §4). Against
|
|
42
|
+
background-100: gray-1000 17.93 · gray-900 8.45 · muted 6.48 · quiet
|
|
43
|
+
5.49 · faint 4.61. Strictly descending; icons ride `quiet`. */
|
|
91
44
|
--color-muted: #5e5e5e;
|
|
92
45
|
--color-quiet: #696969;
|
|
93
46
|
--color-faint: #757575;
|
|
94
47
|
|
|
95
|
-
/* ── The one accent
|
|
96
|
-
Focus ring only. Links stay monochrome — in a monochrome interface
|
|
97
|
-
colour IS attention, so spend it on the one thing that must be
|
|
98
|
-
unmissable: where the keyboard is. */
|
|
48
|
+
/* ── The one accent: the focus ring only. */
|
|
99
49
|
--color-blue-700: #006bff;
|
|
100
50
|
|
|
101
51
|
/* Elevated surface for sliding highlights and active pills — white here,
|
|
@@ -103,30 +53,18 @@
|
|
|
103
53
|
--color-highlight: #ffffff;
|
|
104
54
|
}
|
|
105
55
|
|
|
106
|
-
/* ── Runtime surfaces
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
fallback at runtime — and because one of them is a gradient, which no
|
|
110
|
-
colour token system accepts.
|
|
111
|
-
|
|
112
|
-
Rule: a value that changes by CONTEXT belongs in :root; a value that's
|
|
113
|
-
part of the vocabulary belongs in @theme. */
|
|
56
|
+
/* ── Runtime surfaces: plain custom properties, not @theme tokens, because
|
|
57
|
+
they re-declare per theme and under reduced transparency, and one is a
|
|
58
|
+
gradient (glass/color/guide.md §6). */
|
|
114
59
|
:root {
|
|
115
60
|
color-scheme: light;
|
|
116
61
|
--glass-nav-bg: rgba(242, 242, 242, 0.72);
|
|
117
62
|
--glass-panel-bg: rgba(243, 243, 243, 0.97);
|
|
118
63
|
--folder-glass-bg: linear-gradient(to bottom, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0.34));
|
|
119
64
|
|
|
120
|
-
/* ── The one chromatic exception
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
collapsed summary panel: it's an OFFER rather than content ("do you want
|
|
124
|
-
the three-line version?"), which makes it the one element allowed to
|
|
125
|
-
announce itself, and means the exception can't spread, because nothing
|
|
126
|
-
else on the site is an offer.
|
|
127
|
-
|
|
128
|
-
Delete this block if you don't have such an element. An exception with a
|
|
129
|
-
weak justification becomes a palette within two components. */
|
|
65
|
+
/* ── The one chromatic exception: the collapsed summary panel, an offer
|
|
66
|
+
rather than content (glass/color/guide.md §3). Delete this block if you
|
|
67
|
+
don't have such an element. */
|
|
130
68
|
--sv-edge-top: #16276b; /* deep navy at the top of the 1px stroke */
|
|
131
69
|
--sv-edge-mid: #2f5ac4;
|
|
132
70
|
--sv-edge-bottom: #6d97ea; /* running to a lighter blue at the bottom */
|
|
@@ -135,9 +73,7 @@
|
|
|
135
73
|
--sv-accent: #2f5ac4; /* the MARK, and only the mark */
|
|
136
74
|
--sv-sheen-hi: #8fb2f2;
|
|
137
75
|
|
|
138
|
-
/*
|
|
139
|
-
that happens to be blue; weighting it to the two SIDES is what makes it
|
|
140
|
-
read as light coming off the panel. See the Glass depth guide (`glass/depth/guide.md`). */
|
|
76
|
+
/* Weighted to the sides so it reads as light (glass/depth/guide.md §6). */
|
|
141
77
|
--sv-glow:
|
|
142
78
|
0 1px 2px rgba(22, 39, 107, 0.05),
|
|
143
79
|
-10px 0 20px -18px rgba(47, 90, 196, 0.38),
|
|
@@ -150,15 +86,8 @@
|
|
|
150
86
|
0 14px 34px -16px rgba(47, 90, 196, 0.6);
|
|
151
87
|
}
|
|
152
88
|
|
|
153
|
-
/* ── Dark theme
|
|
154
|
-
|
|
155
|
-
no per-component dark: classes at all. Semantics hold: low steps are
|
|
156
|
-
surfaces, high steps are text, "border" still means 400.
|
|
157
|
-
|
|
158
|
-
Not a mechanical inversion. The bottom is compressed (100→400 spans only
|
|
159
|
-
twenty units) because the eye separates near-black surfaces easily, while
|
|
160
|
-
the middle jumps forty units at once (400→500) to pull "border" clear of
|
|
161
|
-
"text". 700 is the one step identical in both themes. */
|
|
89
|
+
/* ── Dark theme: the same names re-tinted, not inverted
|
|
90
|
+
(glass/color/intent-ramp.md, dark-mode re-tinting). */
|
|
162
91
|
:root[data-theme="dark"] {
|
|
163
92
|
color-scheme: dark;
|
|
164
93
|
|
|
@@ -173,10 +102,7 @@
|
|
|
173
102
|
--color-gray-900: #c4c4c4;
|
|
174
103
|
--color-gray-1000: #ededed;
|
|
175
104
|
|
|
176
|
-
/* The
|
|
177
|
-
A dark-mode border is lighter than its surface, so a black overlay would
|
|
178
|
-
make it disappear. "A little more contrast than the surface" means black
|
|
179
|
-
in light mode and white in dark. */
|
|
105
|
+
/* The alpha ramp inverts to white-based. */
|
|
180
106
|
--color-gray-alpha-100: #ffffff0d;
|
|
181
107
|
--color-gray-alpha-200: #ffffff15;
|
|
182
108
|
--color-gray-alpha-300: #ffffff1f;
|
|
@@ -184,43 +110,27 @@
|
|
|
184
110
|
--color-gray-alpha-500: #ffffff36;
|
|
185
111
|
--color-gray-alpha-600: #ffffff45;
|
|
186
112
|
|
|
187
|
-
/* Near-black, not #000
|
|
188
|
-
produces harsh contrast with light text. */
|
|
113
|
+
/* Near-black, not #000. */
|
|
189
114
|
--color-background-100: #0b0b0b;
|
|
190
115
|
--color-background-200: #161616;
|
|
191
116
|
|
|
192
|
-
/*
|
|
193
|
-
to change — it keeps a value that already passed at 7.53:1. The contrast
|
|
194
|
-
failure this ladder exists to fix was overwhelmingly a LIGHT-theme
|
|
195
|
-
failure; only `faint` was genuinely broken in dark too (3.97:1).
|
|
196
|
-
Ratios against background-100: muted 7.53 · quiet 5.48 · faint 4.59. */
|
|
117
|
+
/* Re-picked for the dark surface: muted 7.53 · quiet 5.48 · faint 4.59. */
|
|
197
118
|
--color-muted: #a0a0a0;
|
|
198
119
|
--color-quiet: #878787;
|
|
199
120
|
--color-faint: #7a7a7a;
|
|
200
121
|
|
|
201
|
-
/*
|
|
202
|
-
less on near-black — any accent needs a per-theme value. */
|
|
122
|
+
/* Every accent takes a per-theme value. */
|
|
203
123
|
--color-blue-700: #3b82f6;
|
|
204
124
|
|
|
205
125
|
--color-highlight: #2c2c2c;
|
|
206
126
|
|
|
207
127
|
--glass-nav-bg: rgba(26, 26, 26, 0.72);
|
|
208
128
|
--glass-panel-bg: rgba(22, 22, 22, 0.97);
|
|
209
|
-
/* Grey-based
|
|
210
|
-
rather than glass. */
|
|
129
|
+
/* Grey-based: a white gradient over a dark page reads as fog. */
|
|
211
130
|
--folder-glass-bg: linear-gradient(to bottom, rgba(48, 48, 48, 0.5), rgba(30, 30, 30, 0.34));
|
|
212
131
|
|
|
213
|
-
/*
|
|
214
|
-
|
|
215
|
-
stroke would simply go missing down its darker half. The instinct is to
|
|
216
|
-
lighten only the dark end, and that's the mistake: it pinches the ramp
|
|
217
|
-
and it stops reading as one gradient. A gradient is a relationship
|
|
218
|
-
between its stops, so re-picking it for a theme means re-picking all of
|
|
219
|
-
them — exactly as the accent rule says.
|
|
220
|
-
|
|
221
|
-
The fill is TINTED, not darkened. It sits at gray-100's weight so the
|
|
222
|
-
panel doesn't punch a hole in the page: "raised" means "more contrast
|
|
223
|
-
against the page", and the direction flips between themes. */
|
|
132
|
+
/* Every stop lifted by about the same amount, so it stays one gradient;
|
|
133
|
+
the fill is tinted to gray-100's weight, not darkened. */
|
|
224
134
|
--sv-edge-top: #24407f;
|
|
225
135
|
--sv-edge-mid: #3f6ed0;
|
|
226
136
|
--sv-edge-bottom: #86adf7;
|
|
@@ -240,9 +150,8 @@
|
|
|
240
150
|
0 14px 36px -16px rgba(90, 140, 240, 0.7);
|
|
241
151
|
}
|
|
242
152
|
|
|
243
|
-
/* Reduced transparency
|
|
244
|
-
theme blocks
|
|
245
|
-
so source order decides. */
|
|
153
|
+
/* Reduced transparency: the tint half (glass.css drops the filter). MUST
|
|
154
|
+
come after both theme blocks — same specificity, so source order decides. */
|
|
246
155
|
@media (prefers-reduced-transparency: reduce) {
|
|
247
156
|
:root,
|
|
248
157
|
:root[data-theme="dark"] {
|
|
@@ -264,10 +173,7 @@
|
|
|
264
173
|
color: var(--color-background-100);
|
|
265
174
|
}
|
|
266
175
|
|
|
267
|
-
/* :focus-visible, never :focus
|
|
268
|
-
outline-offset puts a gap of page colour between element and ring, so
|
|
269
|
-
the ring reads as a distinct object rather than a border. Links get more
|
|
270
|
-
because inline text has ragged edges the ring shouldn't crowd. */
|
|
176
|
+
/* :focus-visible, never :focus; the offset leaves a gap of page colour. */
|
|
271
177
|
:focus-visible {
|
|
272
178
|
outline: 2px solid var(--color-blue-700);
|
|
273
179
|
outline-offset: 2px;
|
|
@@ -278,42 +184,5 @@
|
|
|
278
184
|
}
|
|
279
185
|
}
|
|
280
186
|
|
|
281
|
-
/*
|
|
282
|
-
|
|
283
|
-
glass/depth/assets/glass.css — the blur and saturation values, the
|
|
284
|
-
@supports guard with an opaque baseline, and the other half of the
|
|
285
|
-
reduced-transparency fallback (dropping backdrop-filter, which this file's
|
|
286
|
-
tint swap does not do on its own).
|
|
287
|
-
|
|
288
|
-
Both files are needed for working glass. They're split because the tints
|
|
289
|
-
are colour tokens that re-tint per theme, while the materials are surface
|
|
290
|
-
craft — different guides own them, and neither should duplicate the other.
|
|
291
|
-
───────────────────────────────────────────────────────────────────────── */
|
|
292
|
-
|
|
293
|
-
/* ─────────────────────────────────────────────────────────────────────────
|
|
294
|
-
Elevation, for reference — apply in components, not here. A shadow that
|
|
295
|
-
changes STRUCTURE between themes can't be a single token, so it needs a
|
|
296
|
-
dark: variant. Extract it once to a shared constant and import it.
|
|
297
|
-
|
|
298
|
-
Four layers, one light source:
|
|
299
|
-
inset 1px white top-edge catch light — does more for "floating" than
|
|
300
|
-
any of the shadows
|
|
301
|
-
1px/1px @ 2% contact shadow: resting on something, not pasted over
|
|
302
|
-
8/16 @ 4% near ambient
|
|
303
|
-
24/32 @ 6% far ambient — two layers, because one large shadow
|
|
304
|
-
reads as fog while two fall off like real penumbra
|
|
305
|
-
|
|
306
|
-
Negative spread (-4px, -8px) contracts before blurring, keeping the shadow
|
|
307
|
-
tucked under the element instead of haloing past its edges. This is the
|
|
308
|
-
detail most hand-written shadows miss.
|
|
309
|
-
|
|
310
|
-
Opacities are 2–6%. Elevation should be felt, not seen.
|
|
311
|
-
|
|
312
|
-
const SHADOW =
|
|
313
|
-
"shadow-[inset_0_1px_0_rgba(255,255,255,0.6),0_1px_1px_rgba(0,0,0,0.02),0_8px_16px_-4px_rgba(0,0,0,0.04),0_24px_32px_-8px_rgba(0,0,0,0.06)] " +
|
|
314
|
-
"dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.10),0_1px_1px_rgba(0,0,0,0.3),0_8px_16px_-4px_rgba(0,0,0,0.5),0_24px_32px_-8px_rgba(0,0,0,0.6)]";
|
|
315
|
-
|
|
316
|
-
Dark inverts BOTH ways: shadows ~10x stronger (4% black on #0b0b0b is
|
|
317
|
-
mathematically invisible), inset highlight ~6x weaker (0.6 white on a dark
|
|
318
|
-
surface reads as a glowing edge, not a catch light).
|
|
319
|
-
───────────────────────────────────────────────────────────────────────── */
|
|
187
|
+
/* The .glass-* material classes and .dock-shadow live in
|
|
188
|
+
glass/depth/assets/glass.css; both files are needed for working glass. */
|
|
@@ -1,33 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Theme persistence. Two explicit modes, Light or Dark; Light is the default.
|
|
3
3
|
* The choice sticks in localStorage and is reflected on <html> via data-theme,
|
|
4
|
-
* which the dark token overrides key off (
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* getThemeChoice for initial React state, applyTheme for the DOM write,
|
|
8
|
-
* setThemeChoice for the user action. Keeping them apart is what lets the
|
|
9
|
-
* toggle animate while the initial paint doesn't.
|
|
10
|
-
*
|
|
11
|
-
* ─────────────────────────────────────────────────────────────────────────
|
|
12
|
-
* PAIR THIS WITH THE HEAD SCRIPT AT THE BOTTOM OF THIS FILE. Without it,
|
|
13
|
-
* every load flashes white before React mounts — the single most noticeable
|
|
14
|
-
* bug a themed site can have.
|
|
15
|
-
* ─────────────────────────────────────────────────────────────────────────
|
|
4
|
+
* which the dark token overrides key off (color-tokens.css). Three functions
|
|
5
|
+
* for three callers: initial React state, the DOM write, the user action.
|
|
6
|
+
* Pair it with the head script at the bottom of this file.
|
|
16
7
|
*/
|
|
17
8
|
|
|
18
9
|
export type ThemeChoice = "light" | "dark";
|
|
19
10
|
|
|
20
11
|
const KEY = "theme";
|
|
21
12
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
* stale value, a corrupted entry, a value from an older version of the site —
|
|
25
|
-
* falls through to light.
|
|
26
|
-
*
|
|
27
|
-
* Every localStorage access is wrapped. It throws in Safari private browsing
|
|
28
|
-
* and under some cookie policies. A preference that doesn't persist is a
|
|
29
|
-
* minor annoyance; an uncaught throw is a broken page.
|
|
30
|
-
*/
|
|
13
|
+
/** `=== "dark"`, not a parse: anything unrecognised falls through to light.
|
|
14
|
+
Every localStorage access is wrapped; it throws in some private modes. */
|
|
31
15
|
export function getThemeChoice(): ThemeChoice {
|
|
32
16
|
try {
|
|
33
17
|
if (localStorage.getItem(KEY) === "dark") return "dark";
|
|
@@ -48,12 +32,8 @@ export function setThemeChoice(choice: ThemeChoice): void {
|
|
|
48
32
|
/* preference just won't persist */
|
|
49
33
|
}
|
|
50
34
|
|
|
51
|
-
/*
|
|
52
|
-
|
|
53
|
-
on the site — every link, every icon, every row becomes a 400ms colour
|
|
54
|
-
fade and the whole interface goes sluggish.
|
|
55
|
-
|
|
56
|
-
440ms = the 400ms transition plus a 40ms margin so nothing is cut off. */
|
|
35
|
+
/* Crossfade on explicit toggles only: the class (motion-tokens.css) is
|
|
36
|
+
added, then removed after 440ms — the 400ms fade plus a 40ms margin. */
|
|
57
37
|
const root = document.documentElement;
|
|
58
38
|
root.classList.add("theme-transition");
|
|
59
39
|
applyTheme(choice);
|
|
@@ -73,47 +53,7 @@ export function setThemeChoice(choice: ThemeChoice): void {
|
|
|
73
53
|
})();
|
|
74
54
|
</script>
|
|
75
55
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
· BLOCKING — no async, no defer, no type="module" (which defers
|
|
80
|
-
implicitly); it must run before the first paint
|
|
81
|
-
· BEFORE CSS — so the attribute is set when the stylesheet applies
|
|
82
|
-
· try/catch — an uncaught throw here blocks all subsequent scripts
|
|
83
|
-
|
|
84
|
-
For SSR, render the attribute server-side from a cookie instead —
|
|
85
|
-
localStorage isn't readable on the server, so a cookie is the only way to
|
|
86
|
-
get the value into the initial HTML.
|
|
87
|
-
───────────────────────────────────────────────────────────────────────── */
|
|
88
|
-
|
|
89
|
-
/* ─────────────────────────────────────────────────────────────────────────
|
|
90
|
-
Adding a third "system" state, if you want it.
|
|
91
|
-
|
|
92
|
-
The reference implementation ships two explicit modes because a theme
|
|
93
|
-
control should be predictable — "System" means the theme changes when the
|
|
94
|
-
OS crosses a sunset threshold, which is surprising if you picked it from a
|
|
95
|
-
site's own menu. But respecting the OS on a first visit is the more
|
|
96
|
-
considerate default. Both positions are defensible.
|
|
97
|
-
|
|
98
|
-
If you go to three, you owe the user two extra things:
|
|
99
|
-
|
|
100
|
-
export type ThemeChoice = "light" | "dark" | "system";
|
|
101
|
-
|
|
102
|
-
function resolve(choice: ThemeChoice): "light" | "dark" {
|
|
103
|
-
if (choice !== "system") return choice;
|
|
104
|
-
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
1. The head script must resolve "system" too, so it needs the matchMedia
|
|
108
|
-
call inlined as well.
|
|
109
|
-
|
|
110
|
-
2. A live listener, so the page follows the OS WHILE OPEN:
|
|
111
|
-
|
|
112
|
-
window.matchMedia("(prefers-color-scheme: dark)").addEventListener(
|
|
113
|
-
"change",
|
|
114
|
-
() => { if (getThemeChoice() === "system") applyTheme(resolve("system")); }
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
Skipping this is the common bug: the theme follows the OS on load and
|
|
118
|
-
then silently stops, which is worse than not offering the option.
|
|
56
|
+
Inline, blocking, before the stylesheet, in try/catch; for SSR, render
|
|
57
|
+
the attribute from a cookie. Why each, and what a third "system" state
|
|
58
|
+
would add: domains/color/theming-mechanics.md.
|
|
119
59
|
───────────────────────────────────────────────────────────────────────── */
|
|
@@ -109,7 +109,7 @@ A `#16276b` stop on a `#0b0b0b` page loses the stroke's darker half, so the whol
|
|
|
109
109
|
|
|
110
110
|
### Status colour is not an exception
|
|
111
111
|
|
|
112
|
-
Nothing on the site reports state, so Glass ships no status pair; if you add one, it follows domain law 6 and `domains/color/status-and-exceptions.md
|
|
112
|
+
Nothing on the site reports state, so Glass ships no status pair; if you add one, it follows domain law 6 and `domains/color/status-and-exceptions.md`. Every destructive or session-ending action (delete, clear, sign out) shares one red treatment, wherever it sits.
|
|
113
113
|
|
|
114
114
|
## 4. Text is a five-tier ladder — and it is *not* the grey ramp
|
|
115
115
|
|
|
@@ -156,20 +156,7 @@ Utilities read the variables, so most of the site needs **no per-component `dark
|
|
|
156
156
|
|
|
157
157
|
### No flash of wrong theme
|
|
158
158
|
|
|
159
|
-
A blocking
|
|
160
|
-
|
|
161
|
-
```html
|
|
162
|
-
<script>
|
|
163
|
-
(function () {
|
|
164
|
-
try {
|
|
165
|
-
document.documentElement.dataset.theme =
|
|
166
|
-
localStorage.getItem("theme") === "dark" ? "dark" : "light";
|
|
167
|
-
} catch (e) {}
|
|
168
|
-
})();
|
|
169
|
-
</script>
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
→ Persistence, toggle crossfade, light/dark/system trade-offs: `glass/color/theming.md`
|
|
159
|
+
A blocking `<head>` script sets the attribute before paint (domain law 9): `glass/color/assets/theme.ts`. → Two states, Light by default, and the toggle's 400ms plain-`ease` crossfade: `glass/color/theming.md`
|
|
173
160
|
|
|
174
161
|
## 6. Some colours are runtime, not build-time
|
|
175
162
|
|
|
@@ -183,24 +170,10 @@ The ramps and surfaces are vocabulary, in `@theme`; the **glass tints** are plai
|
|
|
183
170
|
}
|
|
184
171
|
```
|
|
185
172
|
|
|
186
|
-
|
|
173
|
+
Runtime because the reduced-transparency fallback swaps them, after both theme blocks (`glass/depth/guide.md` §8), and one isn't a colour at all; the `--sv-*` gradients sit in `:root` too.
|
|
187
174
|
|
|
188
175
|
The nav tint is `#f2f2f2` (`gray-100`), not white: grey chrome reads as an object, not a hole in the page. Dark tints are *lighter* than the dark page (domain law 8).
|
|
189
176
|
|
|
190
|
-
The reduced-transparency swap comes **after** both theme blocks and names both selectors (domain law 10):
|
|
191
|
-
|
|
192
|
-
```css
|
|
193
|
-
@media (prefers-reduced-transparency: reduce) {
|
|
194
|
-
:root, :root[data-theme="dark"] {
|
|
195
|
-
--glass-nav-bg: var(--color-gray-100);
|
|
196
|
-
--glass-panel-bg: var(--color-gray-100);
|
|
197
|
-
--folder-glass-bg: var(--color-background-100);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
Dropping the `backdrop-filter` is the other half, and both are required. Blur, saturation, elevation shadows and text contrast over glass belong to `glass/depth/guide.md`.
|
|
203
|
-
|
|
204
177
|
## Assets
|
|
205
178
|
|
|
206
179
|
- `glass/color/assets/color-tokens.css` — ramps, themes, glass tints and their fallback swap, focus ring; pairs with `glass/depth/assets/glass.css` (material classes).
|
|
@@ -58,20 +58,7 @@ Held out of the ramp because a page background is categorically different from a
|
|
|
58
58
|
|
|
59
59
|
## Dark-mode re-tinting
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
:root[data-theme="dark"] {
|
|
63
|
-
--color-gray-100: #1a1a1a; /* 100–400: dark surfaces, rising slowly */
|
|
64
|
-
--color-gray-200: #212121;
|
|
65
|
-
--color-gray-300: #282828;
|
|
66
|
-
--color-gray-400: #2e2e2e;
|
|
67
|
-
--color-gray-500: #707070; /* 500–800: the mid greys jump */
|
|
68
|
-
--color-gray-600: #7d7d7d;
|
|
69
|
-
--color-gray-700: #8f8f8f;
|
|
70
|
-
--color-gray-800: #a0a0a0;
|
|
71
|
-
--color-gray-900: #c4c4c4; /* 900–1000: light text */
|
|
72
|
-
--color-gray-1000: #ededed;
|
|
73
|
-
}
|
|
74
|
-
```
|
|
61
|
+
The values are the dark block of `glass/color/assets/color-tokens.css`.
|
|
75
62
|
|
|
76
63
|
**It's not an inversion** (domain law 8). Inverting the light ramp would put 100 at `#0d0d0d` and 1000 at `#e8e8e8`, evenly spaced. The real dark ramp is *compressed at the bottom* (100→400 spans only `#1a1a1a`→`#2e2e2e`, twenty units) and *expanded in the middle* (400→500 jumps forty units), because dark surfaces must stay distinguishable at very low lightness, where the eye is more sensitive to small differences, while mid-greys need a big jump to separate "border" from "text."
|
|
77
64
|
|
|
@@ -98,18 +85,7 @@ Against the near-black page the same indices pass — 800 is 7.53:1, 700 is 6.09
|
|
|
98
85
|
|
|
99
86
|
### The fix: text tiers as their own tokens
|
|
100
87
|
|
|
101
|
-
|
|
102
|
-
:root {
|
|
103
|
-
--color-muted: #5e5e5e; /* 6.48:1 — deks, supporting copy, <em> */
|
|
104
|
-
--color-quiet: #696969; /* 5.49:1 — resting icons, state labels, <cite> */
|
|
105
|
-
--color-faint: #757575; /* 4.61:1 — dates, read times, meta */
|
|
106
|
-
}
|
|
107
|
-
:root[data-theme="dark"] {
|
|
108
|
-
--color-muted: #a0a0a0; /* 7.53:1 */
|
|
109
|
-
--color-quiet: #878787; /* 5.48:1 */
|
|
110
|
-
--color-faint: #7a7a7a; /* 4.59:1 */
|
|
111
|
-
}
|
|
112
|
-
```
|
|
88
|
+
`muted`, `quiet` and `faint`, per theme, are in `glass/color/assets/color-tokens.css`; their jobs are §4 of `glass/color/guide.md`.
|
|
113
89
|
|
|
114
90
|
**Every tier clears 4.5:1 in both themes**, including the worst real pairing: muted on `gray-100`, a subtle surface, at 5.79:1.
|
|
115
91
|
|
|
@@ -126,11 +102,3 @@ Against the near-black page the same indices pass — 800 is 7.53:1, 700 is 6.09
|
|
|
126
102
|
**A border step as text.** The one this guide got wrong for a whole revision (above).
|
|
127
103
|
|
|
128
104
|
**"Something between 500 and 600"**, and **auditing contrast in dark mode only**: domain laws 1 and 3. Here the worst surface is a subtle `gray-100`, not `background-100`.
|
|
129
|
-
|
|
130
|
-
## Adding a semantic colour
|
|
131
|
-
|
|
132
|
-
The reference implementation has none, because nothing on the site reports state. Check that before you add them. If you do, they get their own pairs outside the grey ramp, kept out of the focus ring's territory, and follow domain law 6 and `domains/color/status-and-exceptions.md`: shape carries the state, red means loss and never a live state, destructive actions take a red ground (solid danger with `background-100` text when highlighted), and small marks clear 3:1 on every ground in each theme.
|
|
133
|
-
|
|
134
|
-
## Spending your one chromatic exception
|
|
135
|
-
|
|
136
|
-
Glass spends it once, on the collapsed summary panel: an offer, not content (domain law 7). Its containment rules — only the mark takes the accent; every stop re-picked for dark — are in §3 of `glass/color/guide.md`.
|
|
@@ -10,74 +10,13 @@ Glass's theme switch, persistence and crossfade, with the values it ships. The m
|
|
|
10
10
|
|
|
11
11
|
Tailwind v4 drives `dark:` off the OS media query by default; Glass has a toggle, so `data-theme` on `<html>` is the mechanism (domain law 9). For a permanently-light region inside a dark page, re-declare the custom properties on its container rather than nesting the attribute (`domains/color/theming-mechanics.md`, **Theme islands**).
|
|
12
12
|
|
|
13
|
-
## No flash of wrong theme
|
|
14
|
-
|
|
15
|
-
```html
|
|
16
|
-
<head>
|
|
17
|
-
<!-- Before the stylesheet. Blocking on purpose. -->
|
|
18
|
-
<script>
|
|
19
|
-
(function () {
|
|
20
|
-
try {
|
|
21
|
-
document.documentElement.dataset.theme =
|
|
22
|
-
localStorage.getItem("theme") === "dark" ? "dark" : "light";
|
|
23
|
-
} catch (e) {}
|
|
24
|
-
})();
|
|
25
|
-
</script>
|
|
26
|
-
<link rel="stylesheet" href="…" />
|
|
27
|
-
</head>
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Inline, blocking, before the stylesheet, in `try/catch`; it duplicates a few lines of the theme module, with a comment in each place pointing at the other (`domains/color/theming-mechanics.md`, **No flash of wrong theme**).
|
|
31
|
-
|
|
32
|
-
## Persistence and the crossfade
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
export type ThemeChoice = "light" | "dark";
|
|
36
|
-
|
|
37
|
-
const KEY = "theme";
|
|
38
|
-
|
|
39
|
-
export function getThemeChoice(): ThemeChoice {
|
|
40
|
-
try {
|
|
41
|
-
if (localStorage.getItem(KEY) === "dark") return "dark";
|
|
42
|
-
} catch { /* localStorage unavailable */ }
|
|
43
|
-
return "light";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function applyTheme(choice: ThemeChoice): void {
|
|
47
|
-
document.documentElement.dataset.theme = choice;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function setThemeChoice(choice: ThemeChoice): void {
|
|
51
|
-
try {
|
|
52
|
-
localStorage.setItem(KEY, choice);
|
|
53
|
-
} catch { /* preference just won't persist */ }
|
|
13
|
+
## No flash of wrong theme, persistence and the crossfade
|
|
54
14
|
|
|
55
|
-
|
|
56
|
-
const root = document.documentElement;
|
|
57
|
-
root.classList.add("theme-transition");
|
|
58
|
-
applyTheme(choice);
|
|
59
|
-
window.setTimeout(() => root.classList.remove("theme-transition"), 440);
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Three functions for three callers — `getThemeChoice` for initial React state, `applyTheme` for the DOM write, `setThemeChoice` for the user action (`domains/color/theming-mechanics.md`, **Persistence**). The comparison is `=== "dark"`, so anything unrecognised falls through to light.
|
|
64
|
-
|
|
65
|
-
### The 440ms class
|
|
66
|
-
|
|
67
|
-
```css
|
|
68
|
-
@media (prefers-reduced-motion: no-preference) {
|
|
69
|
-
:root.theme-transition,
|
|
70
|
-
:root.theme-transition *,
|
|
71
|
-
:root.theme-transition *::before,
|
|
72
|
-
:root.theme-transition *::after {
|
|
73
|
-
transition: background-color 400ms ease, color 400ms ease, border-color 400ms ease,
|
|
74
|
-
fill 400ms ease, stroke 400ms ease, box-shadow 400ms ease !important;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
```
|
|
15
|
+
The code lives in `glass/color/assets/theme.ts` — the three functions and, at the bottom, the inline head script — and the 440ms `.theme-transition` rule in `glass/motion/assets/motion-tokens.css`. The head script duplicates a few lines of the theme module, with a comment in each place pointing at the other. The mechanism, the head script's four requirements, the three callers and why the crossfade class is temporary and `!important`: `domains/color/theming-mechanics.md`.
|
|
78
16
|
|
|
79
|
-
|
|
17
|
+
Glass's values:
|
|
80
18
|
|
|
19
|
+
- **The comparison is `=== "dark"`**, so anything unrecognised falls through to light.
|
|
81
20
|
- **440ms**: 400ms plus a 40ms margin so nothing is cut off. For those 440ms it has to beat every component's own `transition-colors duration-150`.
|
|
82
21
|
- **400ms**: the longest duration in the system, for its largest change — every surface, text colour, border, icon fill and shadow at once.
|
|
83
22
|
- **Plain `ease`, not the house curve.** The house easing overshoots, and on a colour that sends the background past its target and back — a wobble in the light.
|
|
@@ -99,13 +38,7 @@ The reference implementation ships **two** explicit modes, Light and Dark, with
|
|
|
99
38
|
|
|
100
39
|
Most components need no theme branching; the test for when one does is in `domains/color/theming-mechanics.md`. Glass's cases:
|
|
101
40
|
|
|
102
|
-
**Multi-layer shadows.** A shadow whose *structure* changes between themes (the inset highlight weakening while the ambient shadows strengthen 10×) can't live in one colour token
|
|
103
|
-
|
|
104
|
-
```tsx
|
|
105
|
-
const SHADOW =
|
|
106
|
-
"shadow-[inset_0_1px_0_rgba(255,255,255,0.6),0_1px_1px_rgba(0,0,0,0.02),…] " +
|
|
107
|
-
"dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.10),0_1px_1px_rgba(0,0,0,0.3),…]";
|
|
108
|
-
```
|
|
41
|
+
**Multi-layer shadows.** A shadow whose *structure* changes between themes (the inset highlight weakening while the ambient shadows strengthen 10×) can't live in one colour token, but Glass keeps it out of `dark:` too: it's one class, `.dock-shadow`, with its `:root[data-theme="dark"]` twin beside it (`glass/depth/depth-cues.md`, §2), not a JS constant.
|
|
109
42
|
|
|
110
43
|
**Ring/border colours as alpha of a specific hue.** `ring-black/[0.06] dark:ring-white/[0.08]` encodes what the alpha ramp does, where the utility won't take a token. Prefer `gray-alpha-*` where you can; use the pair where you can't.
|
|
111
44
|
|