trykeyline 0.1.1 → 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/bin/keyline.mjs +2 -1
- package/package.json +1 -1
- package/plugins/keyline/.claude-plugin/plugin.json +1 -1
- package/plugins/keyline/plugin.json +1 -1
- package/plugins/keyline/skills/keyline/domains/motion/guide.md +1 -0
- package/plugins/keyline/skills/keyline/domains/patterns/guide.md +1 -1
- package/plugins/keyline/skills/keyline/domains/type/guide.md +1 -1
- package/plugins/keyline/skills/keyline/domains/type/ladders.md +1 -1
- package/plugins/keyline/skills/keyline/glass/color/assets/color-tokens.css +34 -145
- package/plugins/keyline/skills/keyline/glass/color/assets/theme.ts +10 -70
- package/plugins/keyline/skills/keyline/glass/color/guide.md +1 -1
- package/plugins/keyline/skills/keyline/glass/color/intent-ramp.md +2 -34
- package/plugins/keyline/skills/keyline/glass/color/theming.md +4 -65
- 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/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 -140
- package/plugins/keyline/skills/keyline/glass/layout/guide.md +2 -8
- package/plugins/keyline/skills/keyline/glass/layout/measure-and-rhythm.md +0 -2
- package/plugins/keyline/skills/keyline/glass/layout/radius-and-elevation.md +1 -1
- 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 +3 -3
- 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/sound.md +10 -83
- package/plugins/keyline/skills/keyline/glass/type/assets/prose.tsx +8 -43
- package/plugins/keyline/skills/keyline/glass/type/assets/type-tokens.css +46 -85
- package/plugins/keyline/skills/keyline/glass/type/guide.md +3 -3
- 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 +2 -2
- 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/guide.md +1 -1
|
@@ -1,85 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The glass surface, as a component
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* one place. (The elevation stack itself is better as the `.dock-shadow`
|
|
6
|
-
* class in glass/depth/assets/glass.css — see the note on the SHADOW constant below.)
|
|
7
|
-
* Glass is four properties plus three depth cues, and the cues are the part
|
|
8
|
-
* people skip: a blurred tinted rectangle with no hairline and no shadow is a
|
|
9
|
-
* patch, not a floating panel.
|
|
10
|
-
*
|
|
11
|
-
* Pairs with glass/depth/assets/glass.css (the materials) and
|
|
2
|
+
* The glass surface, as a component: the wrapper/surface split and the three
|
|
3
|
+
* depth cues — material, hairline, shadow — in one place. Pairs with
|
|
4
|
+
* glass/depth/assets/glass.css (the materials) and
|
|
12
5
|
* glass/color/assets/color-tokens.css (the tints and the ramps).
|
|
13
6
|
*/
|
|
14
7
|
|
|
15
8
|
import type { ReactNode } from "react";
|
|
16
9
|
|
|
17
|
-
/*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
inset 1px white the specular catch where a raised surface faces the
|
|
21
|
-
light. Contributes more to the floating impression
|
|
22
|
-
than any of the shadows, and is the one most often
|
|
23
|
-
omitted — remove it and the surface looks printed on.
|
|
24
|
-
1px/1px @ 2% contact shadow: resting on something, not pasted over.
|
|
25
|
-
8/16 @ 4% near ambient.
|
|
26
|
-
24/32 @ 6% far ambient. Two layers rather than one because a
|
|
27
|
-
single large shadow reads as fog; two fall off like
|
|
28
|
-
real penumbra.
|
|
29
|
-
|
|
30
|
-
Negative spread (-4px, -8px) contracts the shadow before blurring, keeping
|
|
31
|
-
it tucked under the element instead of haloing past its edges. This is the
|
|
32
|
-
detail most hand-written shadows miss, and why stock box-shadow presets
|
|
33
|
-
often look like a glow.
|
|
34
|
-
|
|
35
|
-
Opacities are 2-6%. Elevation should be felt, not seen.
|
|
36
|
-
|
|
37
|
-
Dark mode is a DIFFERENT shadow, not the same one re-tinted:
|
|
38
|
-
· shadows go up ~10x — 4% black on #0b0b0b is mathematically
|
|
39
|
-
invisible; dark needs 30-60% to register at all
|
|
40
|
-
· highlight drops ~6x — 0.6 white on a dark surface reads as a glowing
|
|
41
|
-
edge rather than a catch light
|
|
42
|
-
|
|
43
|
-
Because the STRUCTURE changes and not just the values, elevation cannot be
|
|
44
|
-
a single token — it needs a per-theme pair, and this is the one place raw
|
|
45
|
-
rgba() outside the token file is acceptable.
|
|
46
|
-
|
|
47
|
-
PREFER THE CLASS. `.dock-shadow` in glass/depth/assets/glass.css is the same four
|
|
48
|
-
layers in a form you can actually read and diff against its dark twin.
|
|
49
|
-
The reference implementation started with the constant below and moved
|
|
50
|
-
to the class, for three reasons:
|
|
51
|
-
|
|
52
|
-
· it's ~400 characters either way, and four CSS lines beat one string
|
|
53
|
-
· a constant still gets imported and interpolated at every call site,
|
|
54
|
-
so a grep for the shadow finds five hits rather than one definition
|
|
55
|
-
· the arbitrary-value escaping is a live hazard — underscores for
|
|
56
|
-
spaces, no spaces inside the parens, and a typo produces a class the
|
|
57
|
-
build silently drops: no shadow, no error
|
|
58
|
-
|
|
59
|
-
The constant is kept here for codebases that can't add a stylesheet. */
|
|
10
|
+
/* The elevation stack as a utility string, for codebases that can't add a
|
|
11
|
+
stylesheet. Prefer the `.dock-shadow` class in glass.css; the layers and
|
|
12
|
+
why the class won are in glass/depth/depth-cues.md §2. */
|
|
60
13
|
export const SHADOW =
|
|
61
14
|
"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)] " +
|
|
62
15
|
"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)]";
|
|
63
16
|
|
|
64
|
-
|
|
65
|
-
contact shadow — it's hovering, not resting. */
|
|
66
|
-
export const SHADOW_PANEL =
|
|
67
|
-
"shadow-[0_8px_24px_-8px_rgba(0,0,0,0.12)] dark:shadow-[0_8px_24px_-8px_rgba(0,0,0,0.6)]";
|
|
68
|
-
|
|
69
|
-
/** Tooltips. 30% black in BOTH themes — it sits on bg-gray-1000, which is
|
|
70
|
-
near-black in light and near-white in dark, so the SURFACE sets the
|
|
71
|
-
requirement rather than the page. Elevation is relative to what's
|
|
72
|
-
immediately behind, not to the theme. */
|
|
73
|
-
export const SHADOW_TIP = "shadow-[0_4px_10px_-4px_rgba(0,0,0,0.3)]";
|
|
74
|
-
|
|
75
|
-
/* ── The hairline ─────────────────────────────────────────────────────────
|
|
76
|
-
From the TRANSLUCENT ramp, not a solid grey. A solid border doesn't
|
|
77
|
-
participate in what's behind it, so over a photograph it reads as a
|
|
78
|
-
pasted-on frame. And because the alpha ramp inverts to white-based in dark
|
|
79
|
-
mode, this is automatically lighter than its surface there and darker here.
|
|
80
|
-
|
|
81
|
-
0.5px renders as a true sub-pixel hairline on retina and rounds UP to 1px
|
|
82
|
-
elsewhere — it degrades to the next weight rather than disappearing. */
|
|
17
|
+
/* The translucent-ramp hairline (glass/depth/depth-cues.md §1). */
|
|
83
18
|
const HAIRLINE = "border-[0.5px] border-gray-alpha-400";
|
|
84
19
|
|
|
85
20
|
export type GlassTier = "nav" | "panel";
|
|
@@ -87,18 +22,9 @@ export type GlassTier = "nav" | "panel";
|
|
|
87
22
|
/**
|
|
88
23
|
* A glass surface with all three depth cues attached.
|
|
89
24
|
*
|
|
90
|
-
* `tier` picks the material
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* nav 20px blur, 72% tint. Fixed chrome that arbitrary content scrolls
|
|
94
|
-
* under.
|
|
95
|
-
* panel 24px blur, 97% tint. A floating menu that must stay legible over
|
|
96
|
-
* anything — including over other glass.
|
|
97
|
-
*
|
|
98
|
-
* `isolate` is on by default because these surfaces usually contain a sliding
|
|
99
|
-
* highlight at -z-10 (`glass/motion/guide.md`). Without it, -z-10
|
|
100
|
-
* would drop the highlight behind the container's own tint instead of just
|
|
101
|
-
* behind the icons.
|
|
25
|
+
* `tier` picks the material (nav 20px / 72%, panel 24px / 97%:
|
|
26
|
+
* glass/depth/guide.md §1). `isolate` is on by default so a sliding highlight
|
|
27
|
+
* at -z-10 stays in front of the tint (glass/depth/depth-cues.md §4).
|
|
102
28
|
*/
|
|
103
29
|
export function GlassSurface({
|
|
104
30
|
tier = "panel",
|
|
@@ -126,46 +52,13 @@ export function GlassSurface({
|
|
|
126
52
|
);
|
|
127
53
|
}
|
|
128
54
|
|
|
129
|
-
/*
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
A transform on an ancestor weakens or breaks backdrop-filter in its
|
|
133
|
-
subtree: the blur samples the wrong region, or nothing. So an animating
|
|
134
|
-
menu must be TWO elements — the wrapper animates, the surface blurs, and
|
|
135
|
-
they are never the same node:
|
|
55
|
+
/* The wrapper / surface split: the wrapper animates, the surface blurs, and
|
|
56
|
+
they are never the same node (glass/depth/guide.md §3); closed menus
|
|
57
|
+
unmount (glass/depth/performance-and-fallbacks.md).
|
|
136
58
|
|
|
137
59
|
<DropMenu open={open}> // animates scale + y. No filter.
|
|
138
60
|
<GlassSurface tier="panel"> // filters. Never transformed.
|
|
139
61
|
…
|
|
140
62
|
</GlassSurface>
|
|
141
63
|
</DropMenu>
|
|
142
|
-
|
|
143
|
-
Where you can't split them (a surface that must animate itself), fall back
|
|
144
|
-
to glass-panel's 97% tint and treat the blur as decoration.
|
|
145
|
-
|
|
146
|
-
This is also a performance win, not just a correctness one: the wrapper
|
|
147
|
-
animates on the compositor (transform and opacity only), and the glass
|
|
148
|
-
child's filter isn't being recomputed against a moving containing block
|
|
149
|
-
every frame.
|
|
150
|
-
|
|
151
|
-
Same root cause, second symptom: a transform also makes an element the
|
|
152
|
-
containing block for position: fixed descendants. In the reference
|
|
153
|
-
implementation the fixed nav strip deliberately carries NO transform, so
|
|
154
|
-
the mobile sheet's w-screen full-bleed overlay can span the viewport
|
|
155
|
-
instead of being trapped inside the strip's bounds.
|
|
156
|
-
|
|
157
|
-
When something filtered or positioned behaves inexplicably, check every
|
|
158
|
-
ancestor for: transform, filter, perspective, contain, will-change.
|
|
159
|
-
|
|
160
|
-
── And close menus by UNMOUNTING them ─────────────────────────────────
|
|
161
|
-
A backdrop-filter element still pays its full filter pass when it's
|
|
162
|
-
transparent — visibility is decided at composite time, after the filter has
|
|
163
|
-
already run. opacity: 0 and visibility: hidden do NOT save you; unmounting
|
|
164
|
-
(or display: none) is the only thing that does.
|
|
165
|
-
|
|
166
|
-
<AnimatePresence>
|
|
167
|
-
{open && <m.div …>{children}</m.div>}
|
|
168
|
-
</AnimatePresence>
|
|
169
|
-
|
|
170
|
-
Which also keeps a closed menu out of the tab order.
|
|
171
|
-
───────────────────────────────────────────────────────────────────────── */
|
|
64
|
+
*/
|
|
@@ -1,31 +1,14 @@
|
|
|
1
1
|
/* ─────────────────────────────────────────────────────────────────────────
|
|
2
|
-
Glass materials — three tiers, with every fallback.
|
|
2
|
+
Glass materials — three tiers, with every fallback (glass/depth/guide.md).
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
--glass-*-bg tints
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Why the tints live there and not here: they change per theme AND get
|
|
9
|
-
replaced wholesale by the reduced-transparency fallback, so they have to be
|
|
10
|
-
runtime :root custom properties rather than build-time tokens. (One of them
|
|
11
|
-
is also a gradient, which no colour-token system accepts.)
|
|
12
|
-
|
|
13
|
-
Three tiers, not four. The reference implementation also defines a
|
|
14
|
-
`glass-menu` (16px / 1.8 / 72%) for "a menu over a known surface" — and
|
|
15
|
-
nothing uses it. Every menu turned out to need glass-panel's defensive
|
|
16
|
-
tint, either because its wrapper animates or because it opens over other
|
|
17
|
-
glass. Dead CSS in a design system is worse than absent CSS: the next
|
|
18
|
-
person assumes it's load-bearing.
|
|
4
|
+
Pairs with glass/color/assets/color-tokens.css, which declares the
|
|
5
|
+
--glass-*-bg tints, their dark values and the reduced-transparency tint
|
|
6
|
+
swap. Both files are needed.
|
|
19
7
|
───────────────────────────────────────────────────────────────────────── */
|
|
20
8
|
|
|
21
9
|
@layer components {
|
|
22
|
-
/* ── Opaque baselines
|
|
23
|
-
|
|
24
|
-
the correct direction: the opaque version is the DEFAULT and the glass is
|
|
25
|
-
layered on top. Invert it and the non-supporting path never gets tested.
|
|
26
|
-
|
|
27
|
-
Support is good (Firefox unflagged since 103), so this is insurance
|
|
28
|
-
rather than a live concern — but it's cheap insurance. */
|
|
10
|
+
/* ── Opaque baselines: what a browser without backdrop-filter gets. The
|
|
11
|
+
glass is layered on top (depth law 15). */
|
|
29
12
|
.glass-nav {
|
|
30
13
|
background: var(--color-gray-100);
|
|
31
14
|
}
|
|
@@ -36,85 +19,33 @@
|
|
|
36
19
|
background: var(--color-background-100);
|
|
37
20
|
}
|
|
38
21
|
|
|
39
|
-
/* ── The materials
|
|
40
|
-
|
|
41
|
-
years, and Safari is where glass matters most.
|
|
42
|
-
|
|
43
|
-
Only the standard `backdrop-filter` is written below, on purpose. Let the
|
|
44
|
-
build add `-webkit-backdrop-filter` (Lightning CSS in Tailwind v4, or
|
|
45
|
-
Autoprefixer). Lightning CSS keeps only the LAST of a hand-written pair,
|
|
46
|
-
so writing both, standard first, ships the prefixed one alone — and
|
|
47
|
-
Firefox, which only reads the standard property, loses every blur. With
|
|
48
|
-
no build step, write the prefixed line first and the standard one last. */
|
|
22
|
+
/* ── The materials. Only the standard `backdrop-filter` is written: let the
|
|
23
|
+
build add the -webkit- line (glass/depth/performance-and-fallbacks.md). */
|
|
49
24
|
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
|
|
50
|
-
/* Fixed chrome
|
|
51
|
-
|
|
52
|
-
about the backdrop, the more of it has to be suppressed so the
|
|
53
|
-
surface's own icons can be read.
|
|
54
|
-
|
|
55
|
-
saturate() is NOT decoration. Blur is a convolution, averaging colours
|
|
56
|
-
moves them toward grey, so a blurred backdrop is ALWAYS desaturated —
|
|
57
|
-
more so at larger radii. Without the boost you get the cloudy
|
|
58
|
-
"glassmorphism" wash. This one line is the difference between
|
|
59
|
-
translucent and dirty, and it costs nothing: same filter pass. */
|
|
25
|
+
/* Fixed chrome over arbitrary content; saturate() restores what the blur
|
|
26
|
+
greys out (depth law 8). */
|
|
60
27
|
.glass-nav {
|
|
61
28
|
background: var(--glass-nav-bg);
|
|
62
29
|
backdrop-filter: blur(20px) saturate(1.8);
|
|
63
30
|
}
|
|
64
31
|
|
|
65
|
-
/*
|
|
66
|
-
|
|
67
|
-
1. Its wrapper ANIMATES. A transform on an ancestor weakens
|
|
68
|
-
backdrop-filter in its subtree — the blur samples the wrong region,
|
|
69
|
-
or nothing, for the duration. So the TINT carries the legibility and
|
|
70
|
-
the blur is a bonus once it settles. General rule: when you can't
|
|
71
|
-
rely on the blur, thicken the tint.
|
|
72
|
-
|
|
73
|
-
2. It opens over OTHER GLASS (the nav dock). Two surfaces at the same
|
|
74
|
-
tint read as one object — the panel would look like the dock had
|
|
75
|
-
grown taller. The heavy tint buys tier separation.
|
|
76
|
-
|
|
77
|
-
3. At 97% you can actually COMPUTE text contrast against the tint. At
|
|
78
|
-
72% the effective background is whatever happens to be behind it,
|
|
79
|
-
and the ratio is not computable.
|
|
80
|
-
|
|
81
|
-
Where you have the choice, fix it structurally instead: animate the
|
|
82
|
-
wrapper, put the glass on a static child. */
|
|
32
|
+
/* 97%: its wrapper animates and it opens over other glass (§3–4 of the
|
|
33
|
+
guide). Animate the wrapper, put the glass on a static child. */
|
|
83
34
|
.glass-panel {
|
|
84
35
|
background: var(--glass-panel-bg);
|
|
85
36
|
backdrop-filter: blur(24px) saturate(1.8);
|
|
86
37
|
}
|
|
87
38
|
|
|
88
|
-
/* A frosted pocket
|
|
89
|
-
|
|
90
|
-
1.4 rather than 1.8 because those cards are the subject; pushing their
|
|
91
|
-
saturation would make the pocket louder than its own contents.
|
|
92
|
-
|
|
93
|
-
Its tint is a gradient (brighter at the top, where the light hits)
|
|
94
|
-
rather than a flat fill — the same idea as a 1px inset catch light, at
|
|
95
|
-
a scale where a 1px line would be lost. */
|
|
39
|
+
/* A frosted pocket: 8px softens rather than obscures, 1.4 keeps it
|
|
40
|
+
quieter than the cards behind it. The tint is a gradient. */
|
|
96
41
|
.folder-glass {
|
|
97
42
|
background: var(--folder-glass-bg);
|
|
98
43
|
backdrop-filter: blur(8px) saturate(1.4);
|
|
99
44
|
}
|
|
100
45
|
}
|
|
101
46
|
|
|
102
|
-
/* ── prefers-reduced-transparency
|
|
103
|
-
|
|
104
|
-
and translucent surfaces are harder to read. One response serves both —
|
|
105
|
-
replace the material with an opaque one.
|
|
106
|
-
|
|
107
|
-
BOTH halves are mandatory and each is useless alone:
|
|
108
|
-
· filter only → a 72% tint over live content with no blur to suppress
|
|
109
|
-
it. WORSE than the glass was; text overlaps text.
|
|
110
|
-
· tint only → you still pay the whole cost of the blur, which was
|
|
111
|
-
half the reason for the preference.
|
|
112
|
-
|
|
113
|
-
The tint half lives in color-tokens.css (it re-declares the :root
|
|
114
|
-
variables and must come after both theme blocks). This is the other half.
|
|
115
|
-
|
|
116
|
-
Useful side effect: with a solid tint, text contrast becomes real and
|
|
117
|
-
computable — for exactly the users most likely to need it. */
|
|
47
|
+
/* ── prefers-reduced-transparency: the filter half. The tint half is in
|
|
48
|
+
color-tokens.css; each is useless alone (depth law 15). */
|
|
118
49
|
@media (prefers-reduced-transparency: reduce) {
|
|
119
50
|
.glass-nav,
|
|
120
51
|
.glass-panel,
|
|
@@ -123,15 +54,7 @@
|
|
|
123
54
|
}
|
|
124
55
|
}
|
|
125
56
|
|
|
126
|
-
/* ── prefers-contrast: more
|
|
127
|
-
A translucent surface leans on its hairline to define its edge, and an
|
|
128
|
-
8%-alpha hairline is the first thing to disappear under a contrast
|
|
129
|
-
preference. Promote it to primary text colour.
|
|
130
|
-
|
|
131
|
-
Note what is NOT changed: the surface itself. High contrast is about
|
|
132
|
-
edges and text, not about removing the material. Composes correctly with
|
|
133
|
-
reduced-transparency if the user has both — solid surface, strong
|
|
134
|
-
border. */
|
|
57
|
+
/* ── prefers-contrast: more — the edge goes to primary; the surface stays. */
|
|
135
58
|
@media (prefers-contrast: more) {
|
|
136
59
|
.glass-nav,
|
|
137
60
|
.glass-panel,
|
|
@@ -142,70 +65,13 @@
|
|
|
142
65
|
}
|
|
143
66
|
|
|
144
67
|
/* ─────────────────────────────────────────────────────────────────────────
|
|
145
|
-
|
|
146
|
-
PATCH. Three non-transparency cues turn it into an object, and every glass
|
|
147
|
-
surface needs all three. See glass/depth/depth-cues.md.
|
|
148
|
-
|
|
149
|
-
1 · A HAIRLINE, from the TRANSLUCENT ramp:
|
|
150
|
-
|
|
151
|
-
border-[0.5px] border-gray-alpha-400
|
|
152
|
-
|
|
153
|
-
Not solid grey — a solid border doesn't participate in what's behind
|
|
154
|
-
it, so over a photograph it reads as a pasted-on frame. And because
|
|
155
|
-
the alpha ramp inverts to white-based in dark mode, the border is
|
|
156
|
-
automatically lighter than its surface there and darker here.
|
|
157
|
-
|
|
158
|
-
0.5px renders as a true sub-pixel hairline on retina and rounds UP to
|
|
159
|
-
1px elsewhere — it degrades rather than disappears.
|
|
160
|
-
|
|
161
|
-
2 · A LAYERED SHADOW. Four layers, one light source above:
|
|
162
|
-
|
|
163
|
-
inset 0 1px 0 rgba(255,255,255,0.6) top-edge catch light
|
|
164
|
-
0 1px 1px rgba(0,0,0,0.02) contact
|
|
165
|
-
0 8px 16px -4px rgba(0,0,0,0.04) near ambient
|
|
166
|
-
0 24px 32px -8px rgba(0,0,0,0.06) far ambient
|
|
167
|
-
|
|
168
|
-
The inset highlight contributes more than any of the shadows and is
|
|
169
|
-
the one most often omitted — remove it and the surface looks printed
|
|
170
|
-
on rather than raised. Negative spread contracts before blurring, so
|
|
171
|
-
the shadow stays tucked under instead of haloing out. Opacities are
|
|
172
|
-
2–6%: if you can identify the shadow as a shadow, it's too strong.
|
|
173
|
-
|
|
174
|
-
Dark mode is a DIFFERENT shadow, not a re-tint — shadows ~10x stronger
|
|
175
|
-
(4% black on #0b0b0b is mathematically invisible), inset highlight ~6x
|
|
176
|
-
weaker (0.6 white on dark reads as a glowing edge). Because the
|
|
177
|
-
structure changes, this can't be one token — it's a per-theme pair,
|
|
178
|
-
and it belongs in the .dock-shadow class at the foot of this file
|
|
179
|
-
rather than repeated inline at every use.
|
|
180
|
-
|
|
181
|
-
3 · CONTINUOUS CORNERS:
|
|
182
|
-
|
|
183
|
-
*, *::before, *::after { corner-shape: superellipse(1.5); }
|
|
184
|
-
|
|
185
|
-
And nest radii by: INNER = OUTER − the padding between them.
|
|
186
|
-
Dock rounded-2xl (16) p-1 (4) → button rounded-[12px] → avatar 10px
|
|
187
|
-
|
|
188
|
-
── Performance, briefly ──────────────────────────────────────────────
|
|
189
|
-
Cost scales with AREA and re-runs whenever anything behind it changes —
|
|
190
|
-
including every frame of every scroll. Keep surfaces small, keep them few,
|
|
191
|
-
UNMOUNT closed menus (a hidden backdrop-filter element still pays its
|
|
192
|
-
filter pass; opacity: 0 does not save you), never animate blur(), and
|
|
193
|
-
never put glass over video or a flat background.
|
|
194
|
-
───────────────────────────────────────────────────────────────────────── */
|
|
195
|
-
|
|
196
|
-
/* ─────────────────────────────────────────────────────────────────────────
|
|
197
|
-
The elevation stack, as a class.
|
|
198
|
-
|
|
199
|
-
Every floating surface takes this alongside its material class:
|
|
68
|
+
The elevation stack, as a class, beside each material class:
|
|
200
69
|
|
|
201
|
-
<div class="glass-nav dock-shadow rounded-2xl border-[0.5px] …">
|
|
70
|
+
<div class="glass-nav dock-shadow rounded-2xl border-[0.5px] border-gray-alpha-400 …">
|
|
202
71
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
not just the numbers. A class is the only place it can live, and putting
|
|
207
|
-
it in one is what stops a ~400-character string being copied into five
|
|
208
|
-
components with a dark twin each.
|
|
72
|
+
A class, not a token: dark mode changes its structure, not just its
|
|
73
|
+
numbers. The hairline, corners, tiers and why each layer is there:
|
|
74
|
+
glass/depth/depth-cues.md.
|
|
209
75
|
───────────────────────────────────────────────────────────────────────── */
|
|
210
76
|
|
|
211
77
|
@layer components {
|
|
@@ -228,27 +94,10 @@
|
|
|
228
94
|
}
|
|
229
95
|
|
|
230
96
|
/* ─────────────────────────────────────────────────────────────────────────
|
|
231
|
-
A gradient 1px stroke — no wrapper element, no mask.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
show it through.
|
|
236
|
-
|
|
237
|
-
background-origin: border-box IS THE LOAD-BEARING LINE. Without it the
|
|
238
|
-
gradient is SIZED to the padding box while being clipped to the border
|
|
239
|
-
box, so the stroke gets the wrong end of the ramp on all four sides — a
|
|
240
|
-
one-word omission that produces a result which looks almost right.
|
|
241
|
-
|
|
242
|
-
AND THE STOPS ARE IN PIXELS, NOT PERCENTAGES. Percentages are relative to
|
|
243
|
-
the box, so the ramp rescales whenever the box resizes: a 46px closed bar
|
|
244
|
-
ran the entire sweep inside its own height, came out louder than the
|
|
245
|
-
250px panel it opened into, and shimmered through the open animation as
|
|
246
|
-
the ramp re-fitted frame by frame. Anchored in pixels the top of the
|
|
247
|
-
stroke holds still and the colour simply grows downward.
|
|
248
|
-
|
|
249
|
-
Colours come from the Glass colour guide's --sv-* family, which is
|
|
250
|
-
that system's one chromatic exception. Delete this block if you don't
|
|
251
|
-
have such an element.
|
|
97
|
+
A gradient 1px stroke — no wrapper element, no mask (§6 of the guide).
|
|
98
|
+
background-origin: border-box is load-bearing, and the stops are in
|
|
99
|
+
pixels so the ramp holds still as the panel grows. Colours are the --sv-*
|
|
100
|
+
family in color-tokens.css. Delete this block if you have no such element.
|
|
252
101
|
───────────────────────────────────────────────────────────────────────── */
|
|
253
102
|
|
|
254
103
|
@layer components {
|
|
@@ -265,16 +114,12 @@
|
|
|
265
114
|
background-origin: border-box;
|
|
266
115
|
background-clip: padding-box, border-box;
|
|
267
116
|
|
|
268
|
-
/*
|
|
269
|
-
that happens to be blue; the two HORIZONTAL casts are what make it
|
|
270
|
-
read as light coming off the panel. Same negative spread as the
|
|
271
|
-
elevation stack, so the glow stays tucked rather than haloing. */
|
|
117
|
+
/* Two horizontal casts make it read as light, not a blue drop shadow. */
|
|
272
118
|
box-shadow: var(--sv-glow);
|
|
273
119
|
transition: box-shadow 220ms var(--ease-geist);
|
|
274
120
|
}
|
|
275
121
|
|
|
276
|
-
/* Lifts on hover and
|
|
277
|
-
same state the chevron does. */
|
|
122
|
+
/* Lifts on hover and stays lifted while open. */
|
|
278
123
|
.sv-panel:hover,
|
|
279
124
|
.sv-panel[data-open="true"] {
|
|
280
125
|
box-shadow: var(--sv-glow-lift);
|
|
@@ -286,11 +131,7 @@
|
|
|
286
131
|
}
|
|
287
132
|
}
|
|
288
133
|
|
|
289
|
-
/* One pass of light
|
|
290
|
-
repeating shimmer claims the thing is still working, which it isn't.
|
|
291
|
-
Drop the class on animationend so the label goes back to painted text:
|
|
292
|
-
background-clip: text with color: transparent is a text-selection and
|
|
293
|
-
forced-colours hazard to leave in place. */
|
|
134
|
+
/* One pass of light, never a loop; drop the class on animationend. */
|
|
294
135
|
.sv-sheen {
|
|
295
136
|
background-image: linear-gradient(
|
|
296
137
|
100deg,
|
|
@@ -20,22 +20,7 @@ Vertical dividers inside the dock are layout's (`glass/layout/radius-and-elevati
|
|
|
20
20
|
|
|
21
21
|
## 2. The shadow stack
|
|
22
22
|
|
|
23
|
-
`.dock-shadow` lives in `glass/depth/assets/glass.css`, both themes side by side; depth law 4 gives each layer's job. Light
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
inset 0 1px 0 rgba(255,255,255,0.10), /* highlight: 0.6 → 0.10 */
|
|
27
|
-
0 1px 1px rgba(0,0,0,0.3), /* shadows: 0.02 → 0.3 */
|
|
28
|
-
0 8px 16px -4px rgba(0,0,0,0.5), /* 0.04 → 0.5 */
|
|
29
|
-
0 24px 32px -8px rgba(0,0,0,0.6) /* 0.06 → 0.6 */
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
A 4%-black shadow on a `#0b0b0b` surface is invisible; dark mode needs 30–60% black before a shadow registers. It's also why the dark page is `#0b0b0b`, not `#000000` (depth law 5).
|
|
33
|
-
|
|
34
|
-
Because the structure changes and not just the values, elevation **cannot be a single token**. It needs a per-theme pair — the one place raw `rgba()` outside the token file is acceptable — and it belongs in a **CSS class**, not a JS constant:
|
|
35
|
-
|
|
36
|
-
```tsx
|
|
37
|
-
<div className="glass-nav dock-shadow rounded-2xl border-[0.5px] border-gray-alpha-400 p-1">
|
|
38
|
-
```
|
|
23
|
+
`.dock-shadow` lives in `glass/depth/assets/glass.css`, both themes side by side; depth law 4 gives each layer's job. Light is 2–6% black with a 0.6 catch light; dark is 30–60% black with the catch light at 0.10, because a 4%-black shadow on `#0b0b0b` is invisible. It's also why the dark page is `#0b0b0b`, not `#000000` (depth law 5). Because the structure changes and not just the values, elevation **cannot be a single token**: it's a per-theme pair — the one place raw `rgba()` outside the token file is acceptable — in a **CSS class**, not a JS constant.
|
|
39
24
|
|
|
40
25
|
The reference implementation started with an exported constant and moved to the class:
|
|
41
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Performance and fallbacks
|
|
2
2
|
|
|
3
|
-
Glass's budget and fallbacks, for depth laws 15 and 16. The cost model, what to avoid, how to measure, containment and the fallback order in general are in `domains/depth/fallbacks-and-performance.md
|
|
3
|
+
Glass's budget and fallbacks, for depth laws 15 and 16. The cost model, what to avoid, how to measure, containment and the fallback order in general are in `domains/depth/fallbacks-and-performance.md`.
|
|
4
4
|
|
|
5
5
|
## What makes it affordable here
|
|
6
6
|
|
|
@@ -20,17 +20,9 @@ The reference implementation runs three or four glass surfaces and stays smooth
|
|
|
20
20
|
|
|
21
21
|
**The wrapper animates, the surface filters.** The `DropMenu` split in §3 of the guide is a double win: the wrapper animates on the compositor, and the child's filter isn't recomputed against a moving containing block every frame.
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## The fallbacks
|
|
24
24
|
|
|
25
|
-
The substitutions are per material, not blanket-white
|
|
26
|
-
|
|
27
|
-
## `prefers-contrast: more`
|
|
28
|
-
|
|
29
|
-
An 8%-alpha hairline is the first thing to disappear under a contrast preference, so the edge goes to `gray-1000`, primary text colour, and the surface stays.
|
|
30
|
-
|
|
31
|
-
## `@supports`
|
|
32
|
-
|
|
33
|
-
Without the guard, the reference implementation's surfaces get a bare tint where `backdrop-filter` is missing: legible but weak, marginal at 72% over a photograph. Support is very good (Firefox shipped it unflagged in 103, mid-2022), so it's a small risk, not a live bug. `glass/depth/assets/glass.css` in this guide ships with the guard in place.
|
|
25
|
+
The code is in `glass/depth/assets/glass.css` (baselines, `@supports`, the filter half of reduced transparency, `prefers-contrast`) and `glass/color/assets/color-tokens.css` (the tint half). Two decisions it encodes: the substitutions are per material, not blanket-white — chrome goes to `gray-100` so the dock still reads as raised, the decorative pocket to opaque `background-100` — and the `@supports` guard is one the live site lacks, a small risk given support (Firefox unflagged since 103) but cheap to close.
|
|
34
26
|
|
|
35
27
|
## The `-webkit-` prefix on the site
|
|
36
28
|
|
|
@@ -59,65 +59,13 @@ The desktop panel needs no equivalent: it *mounts fresh* on open and reads the v
|
|
|
59
59
|
|
|
60
60
|
## Case 2: the table of contents
|
|
61
61
|
|
|
62
|
-
Same tree, two containers, shared row rendering.
|
|
63
|
-
|
|
64
|
-
```tsx
|
|
65
|
-
/** Shared — used by both presentations. */
|
|
66
|
-
function Rows({ nodes, active, parents, onJump }) { … }
|
|
67
|
-
|
|
68
|
-
/** The sticky rail, in the left margin on wide screens. */
|
|
69
|
-
export function SectionNav({ nodes, active, onJump }) {
|
|
70
|
-
return (
|
|
71
|
-
<nav aria-label="On this page">
|
|
72
|
-
<ul className="flex flex-col">
|
|
73
|
-
<Rows … />
|
|
74
|
-
</ul>
|
|
75
|
-
</nav>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** The narrow-screen disclosure — one line above the article. */
|
|
80
|
-
export function SectionNavCompact({ nodes, active, onJump }) {
|
|
81
|
-
const [open, setOpen] = useState(false);
|
|
82
|
-
const jump = (id: string) => { setOpen(false); onJump(id); };
|
|
83
|
-
return (
|
|
84
|
-
<details open={open} onToggle={(e) => setOpen(e.currentTarget.open)}
|
|
85
|
-
className="group rounded-xl border border-gray-alpha-300">
|
|
86
|
-
<summary className="… [&::-webkit-details-marker]:hidden">
|
|
87
|
-
On this page
|
|
88
|
-
<span className="… group-open:rotate-180 motion-reduce:transition-none">
|
|
89
|
-
<ChevronDownIcon />
|
|
90
|
-
</span>
|
|
91
|
-
</summary>
|
|
92
|
-
<ul className="… border-t border-gray-alpha-300">
|
|
93
|
-
<Rows … onJump={jump} />
|
|
94
|
-
</ul>
|
|
95
|
-
</details>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Both are rendered; CSS picks one:
|
|
101
|
-
|
|
102
|
-
```tsx
|
|
103
|
-
<div className="absolute inset-y-0 left-8 hidden w-[196px] xl:block">
|
|
104
|
-
<div className="sticky top-28 max-h-[calc(100dvh-11rem)] overflow-y-auto">
|
|
105
|
-
<SectionNav {...nav} />
|
|
106
|
-
</div>
|
|
107
|
-
</div>
|
|
108
|
-
|
|
109
|
-
<div className="reveal mt-8 xl:hidden">
|
|
110
|
-
<SectionNavCompact {...nav} />
|
|
111
|
-
</div>
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
The props are memoised so both get a stable object:
|
|
62
|
+
Same tree, two containers, shared row rendering: `glass/layout/assets/SectionNav.tsx`. Both are rendered and CSS picks one — the rail in the 1280px frame from `xl` (§2 of the guide), the compact `<details>` above the article below that (`reveal mt-8 xl:hidden`). The props are memoised so both get one stable object:
|
|
115
63
|
|
|
116
64
|
```tsx
|
|
117
65
|
const nav = useMemo(() => ({ nodes: toc, active, onJump: jump }), [active, jump]);
|
|
118
66
|
```
|
|
119
67
|
|
|
120
|
-
The compact version is a `<details>` (layout law 11)
|
|
68
|
+
The compact version is a `<details>` (layout law 11), and costs **one line** above the article, which is what makes a nav in the reading flow acceptable.
|
|
121
69
|
|
|
122
70
|
## Case 3: two views of one collection, chosen by the reader
|
|
123
71
|
|
|
@@ -125,24 +73,7 @@ The first two cases fork on a *device* capability; the gallery's **wall** and **
|
|
|
125
73
|
|
|
126
74
|
### The state can't live in either component
|
|
127
75
|
|
|
128
|
-
The control and what it controls are siblings, so a
|
|
129
|
-
|
|
130
|
-
```ts
|
|
131
|
-
export type GalleryLayout = "grid" | "roll";
|
|
132
|
-
let current: GalleryLayout = "grid";
|
|
133
|
-
|
|
134
|
-
const listeners = new Set<(layout: GalleryLayout) => void>();
|
|
135
|
-
export function onGalleryLayoutChange(fn: (l: GalleryLayout) => void) {
|
|
136
|
-
listeners.add(fn);
|
|
137
|
-
return () => { listeners.delete(fn); };
|
|
138
|
-
}
|
|
139
|
-
export function setGalleryLayout(layout: GalleryLayout) {
|
|
140
|
-
current = layout;
|
|
141
|
-
for (const fn of listeners) fn(layout);
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
The shape of a theme or sound store, for the same reason: neither consumer can be the other's parent without distorting the layout.
|
|
76
|
+
The control and what it controls are siblings, so a module store owns the value (its code is §6 of the guide), mirrored in a hook:
|
|
146
77
|
|
|
147
78
|
```ts
|
|
148
79
|
// Mirror it in a hook. Read in an EFFECT, not during render: on a
|
|
@@ -175,10 +106,6 @@ Find the item nearest the viewport's centre before the outgoing layout unmounts
|
|
|
175
106
|
|
|
176
107
|
Nothing is cropped, so `object-cover` has nothing to do; nothing shifts on load; and one image travels between layouts undistorted, because both ends compute to the same shape.
|
|
177
108
|
|
|
178
|
-
## Hover in Tailwind
|
|
179
|
-
|
|
180
|
-
Tailwind v4's `hover:` variant already compiles to a `(hover: hover)` guard, so utility hover states are safe by default. Imperative JavaScript needs the explicit check.
|
|
181
|
-
|
|
182
109
|
## The touch-versus-hover audit
|
|
183
110
|
|
|
184
111
|
Glass's answers to the audit in `domains/layout/forks-gating-and-disclosures.md`:
|
|
@@ -213,19 +140,6 @@ The last is a hover card *below* its row on narrow screens and *beside* it at `x
|
|
|
213
140
|
| `sm` | 640px | Mobile dock ↔ desktop dock. Page padding. Masonry columns. Stack columns. |
|
|
214
141
|
| `xl` | 1280px | The TOC rail appears. The hover card moves beside its row. |
|
|
215
142
|
|
|
216
|
-
Two for the whole site, both derived from content. Both are viewport breakpoints only because here the column *is* the page. `sm` coincides with the reading measure: below it the column is padding-limited, the natural place for the layout to change character. `xl` is where a 640px column finally leaves margin for a 196px rail. Type size is *not* on the list
|
|
217
|
-
|
|
218
|
-
## Breakpoints on the container
|
|
219
|
-
|
|
220
|
-
Where a pane shares the screen with something that opens and closes, it breaks on its own width (layout law 8). In Tailwind:
|
|
221
|
-
|
|
222
|
-
```tsx
|
|
223
|
-
<section className="@container"> {/* the pane, not the page */}
|
|
224
|
-
<div className="grid @2xl:grid-cols-[minmax(240px,1fr)_2fr]">
|
|
225
|
-
<List />
|
|
226
|
-
<Detail className="@container" /> {/* sizes its own insides */}
|
|
227
|
-
</div>
|
|
228
|
-
</section>
|
|
229
|
-
```
|
|
143
|
+
Two for the whole site, both derived from content. Both are viewport breakpoints only because here the column *is* the page. `sm` coincides with the reading measure: below it the column is padding-limited, the natural place for the layout to change character. `xl` is where a 640px column finally leaves margin for a 196px rail. Type size is *not* on the list (`glass/type/scale.md`).
|
|
230
144
|
|
|
231
|
-
|
|
145
|
+
Glass uses no container breakpoints; where a pane shares the screen with something that opens and closes, see `domains/layout/forks-gating-and-disclosures.md`.
|