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
|
@@ -1,24 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Motion presets —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Two vocabularies appear here on purpose:
|
|
6
|
-
* bounce + duration — a designer's controls; predictable arrival
|
|
7
|
-
* stiffness + damping + mass — needed when the RELATIONSHIP between two
|
|
8
|
-
* springs is the effect (see GROW / CROSS)
|
|
9
|
-
*
|
|
10
|
-
* The one rule that applies to all of them: opacity never springs. Position
|
|
11
|
-
* wants physics, opacity wants a curve. A spring on opacity overshoots past
|
|
12
|
-
* its clamp and hitches on the way back — a visible flicker at the tail of
|
|
13
|
-
* every fade.
|
|
2
|
+
* Motion presets — the springs and tweens glass uses. Why each number is what
|
|
3
|
+
* it is: glass/motion/springs.md. Opacity never springs (motion law 6).
|
|
14
4
|
*/
|
|
15
5
|
|
|
16
|
-
|
|
17
|
-
A highlight or tooltip sliding to a new item. bounce is near the floor of
|
|
18
|
-
"perceptibly springy" on purpose: this is a thing MOVING, not a thing
|
|
19
|
-
ARRIVING. Bounce implies a moment of settling, and a highlight that
|
|
20
|
-
settles at every stop draws attention to itself instead of to the item
|
|
21
|
-
it's marking. */
|
|
6
|
+
/** Shared highlight or tooltip re-targeting: moving, not arriving. */
|
|
22
7
|
export const SLIDE = {
|
|
23
8
|
type: "spring",
|
|
24
9
|
bounce: 0.2,
|
|
@@ -26,184 +11,25 @@ export const SLIDE = {
|
|
|
26
11
|
opacity: { duration: 0.16, ease: "easeOut" },
|
|
27
12
|
} as const;
|
|
28
13
|
|
|
29
|
-
|
|
30
|
-
Cards rising out of a folder on hover. 40% more bounce and 25% more
|
|
31
|
-
duration than anything else in the system. That it's the ONLY such moment
|
|
32
|
-
is why it works: against a baseline of restraint, one exuberant
|
|
33
|
-
interaction reads as craft. Three of them read as a template. */
|
|
14
|
+
/** Cards rising out of a folder — the one playful moment. */
|
|
34
15
|
export const LIFT = { type: "spring", bounce: 0.28, duration: 0.5 } as const;
|
|
35
16
|
|
|
36
|
-
|
|
37
|
-
Two springs of deliberately different character, applied to the two axes
|
|
38
|
-
of one panel. Every difference pushes the same direction — GROW is fast
|
|
39
|
-
and loose, CROSS is slow and controlled:
|
|
40
|
-
|
|
41
|
-
stiffness 620 vs 380 GROW pulls harder, moves sooner
|
|
42
|
-
damping 19 vs 30 GROW resists less, overshoots
|
|
43
|
-
mass 0.85 vs 1 GROW is lighter, accelerates faster
|
|
44
|
-
|
|
45
|
-
Through the overshoot the panel is briefly TALLER and NARROWER than it
|
|
46
|
-
ends up, then settles. Squash and stretch emerging from physics rather
|
|
47
|
-
than a hand-authored timeline — interruptible and velocity-aware for free.
|
|
48
|
-
|
|
49
|
-
Generalises: when two axes of one element move together, giving them
|
|
50
|
-
different springs creates deformation, and deformation reads as mass. */
|
|
17
|
+
/** Squash and stretch: GROW on scaleY and y, CROSS on scaleX (glass/motion/guide.md §4). */
|
|
51
18
|
export const GROW = { type: "spring", stiffness: 620, damping: 19, mass: 0.85 } as const;
|
|
52
19
|
export const CROSS = { type: "spring", stiffness: 380, damping: 30 } as const;
|
|
53
20
|
|
|
54
|
-
|
|
55
|
-
A tween, not a spring. easeIn, so it accelerates away rather than
|
|
56
|
-
decelerating in. 130ms, well under the state tier.
|
|
57
|
-
|
|
58
|
-
A bounce on the way out reads as the interface being reluctant, and it
|
|
59
|
-
stands between the user and whatever they're reaching for next.
|
|
60
|
-
Entrances can have character; exits should get out of the way. */
|
|
21
|
+
/** Closing: a flat tween that gets out of the way. */
|
|
61
22
|
export const CLOSE = { duration: 0.13, ease: "easeIn" } as const;
|
|
62
23
|
|
|
63
|
-
/*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
preference for a static interface. A user who sets it still needs to know
|
|
67
|
-
that a menu opened.
|
|
68
|
-
|
|
69
|
-
Flat, state-tier, no overshoot. The state change stays legible; the
|
|
70
|
-
physics goes away. */
|
|
71
|
-
export const FLAT = { duration: 0.15 } as const;
|
|
72
|
-
|
|
73
|
-
/** Pick the spring or its flat substitute. `reduce` from useReducedMotion(). */
|
|
74
|
-
export function motionOr<T>(reduce: boolean | null, animated: T) {
|
|
75
|
-
return reduce ? FLAT : animated;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/* ── Duration ladder, for inline JS use ──────────────────────────────────
|
|
79
|
-
Mirrors motion-tokens.css. Seconds, because that's what Motion takes. */
|
|
80
|
-
export const DURATION = {
|
|
81
|
-
micro: 0.1,
|
|
82
|
-
state: 0.15,
|
|
83
|
-
popover: 0.2,
|
|
84
|
-
route: 0.26,
|
|
85
|
-
overlay: 0.3,
|
|
86
|
-
theme: 0.4,
|
|
87
|
-
} as const;
|
|
88
|
-
|
|
89
|
-
/* ── Off the ladder, deliberately ────────────────────────────────────────
|
|
90
|
-
The ladder is for STATE: something changed and the user has to follow it.
|
|
91
|
-
Two categories aren't state, and squeezing them onto the ladder makes
|
|
92
|
-
them worse rather than tighter.
|
|
93
|
-
|
|
94
|
-
THE SUBJECT OF THE MOMENT. When the thing animating IS the content, the
|
|
95
|
-
user is looking directly at it and the motion has to read in its own
|
|
96
|
-
right. Note the discipline in the pair below: same component, two
|
|
97
|
-
durations, decided by whether the motion is the subject or the report. */
|
|
98
|
-
export const SUBJECT = {
|
|
99
|
-
/** Stepping between images — a noise-thresholded dissolve, not a cut. */
|
|
100
|
-
dissolve: 0.62,
|
|
101
|
-
/** The same image moving between two layouts. On screen at both ends, so
|
|
102
|
-
it only has to be followable — back inside the overlay tier. */
|
|
103
|
-
flight: 0.42,
|
|
104
|
-
} as const;
|
|
105
|
-
|
|
106
|
-
/* A SET PIECE gets its own budget by never appearing again: once per tab,
|
|
107
|
-
never blocking a deep link, never played under reduced motion. See §14 of
|
|
108
|
-
glass/motion/guide.md for the gating. */
|
|
109
|
-
export const SET_PIECE = {
|
|
110
|
-
/** The first open of a disclosure, per line. */
|
|
111
|
-
theatreLine: 0.34,
|
|
112
|
-
/** Every subsequent open of the same thing. */
|
|
113
|
-
quietLine: 0.16,
|
|
114
|
-
/** Between staggered children on the first open only. */
|
|
115
|
-
stagger: 0.09,
|
|
116
|
-
} as const;
|
|
117
|
-
|
|
118
|
-
/* ── The curve for a track that also drives a filter ─────────────────────
|
|
119
|
-
NOT --ease-geist. The house curve overshoots past 1, and a blur animating
|
|
120
|
-
to 0px would have to pass through a NEGATIVE radius to settle back.
|
|
121
|
-
Browsers clamp rather than crash, so the symptom isn't an error — it's a
|
|
122
|
-
value stuck at zero for the tail while everything else is still moving,
|
|
123
|
-
which reads as the animation stalling right at the end.
|
|
124
|
-
|
|
125
|
-
Applies to any property with a hard floor: blur, brightness, a radius
|
|
126
|
-
going to 0, a scale you've promised won't invert. Expo-out has the same
|
|
127
|
-
"arrives decisively" character without ever leaving [0, 1]. */
|
|
128
|
-
export const EASE_FILTER = [0.16, 1, 0.3, 1] as const;
|
|
129
|
-
|
|
130
|
-
/* ── Blur crossfade ──────────────────────────────────────────────────────
|
|
131
|
-
Text swapping in place — a wordmark revealing its tagline on hover, say.
|
|
132
|
-
Note the asymmetry (280 in / 180 out): same principle as the springs.
|
|
133
|
-
|
|
134
|
-
Blur is the vestibular trigger; the fade isn't. So reduced motion swaps
|
|
135
|
-
only the blur amount to 0px and keeps the crossfade. Animating
|
|
136
|
-
blur(0px) → blur(0px) is a no-op the browser optimises away, so this needs
|
|
137
|
-
no second code path. */
|
|
138
|
-
export function blurCrossfade(reduce: boolean | null) {
|
|
139
|
-
const blur = reduce ? "blur(0px)" : "blur(4px)";
|
|
140
|
-
return {
|
|
141
|
-
initial: { opacity: 0, filter: blur },
|
|
142
|
-
animate: { opacity: 1, filter: "blur(0px)", transition: { duration: 0.28, ease: "easeOut" } },
|
|
143
|
-
exit: { opacity: 0, filter: blur, transition: { duration: 0.18, ease: "easeIn" } },
|
|
144
|
-
} as const;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/* ── First-open theatre ──────────────────────────────────────────────────
|
|
148
|
-
Staggered lines behind a blur, on the FIRST open only; every subsequent
|
|
149
|
-
open is a quick fade. `theatre` comes from sessionStorage, so it is false
|
|
150
|
-
on the server for everyone and true on the client for most.
|
|
151
|
-
|
|
152
|
-
Which is why the shut state below is a CONSTANT. Branch a rendered style
|
|
153
|
-
on a client-only flag and React hands you two different first paints and
|
|
154
|
-
says so in the console. Only the transition reads the flags — a transition
|
|
155
|
-
isn't a style, so there's nothing for hydration to disagree about, and a
|
|
156
|
-
reader who asked for less motion gets a zero-length transition to the same
|
|
157
|
-
end state rather than a different starting position.
|
|
158
|
-
|
|
159
|
-
Record "seen" on animation COMPLETE, not on start: setting it earlier
|
|
160
|
-
lands in the same render batch as the open and cancels the very animation
|
|
161
|
-
it's recording. */
|
|
162
|
-
export function theatreLine(reduce: boolean | null, theatre: boolean) {
|
|
163
|
-
return {
|
|
164
|
-
shut: { opacity: 0, y: 6, filter: "blur(4px)" },
|
|
165
|
-
open: {
|
|
166
|
-
opacity: 1,
|
|
167
|
-
y: 0,
|
|
168
|
-
filter: "blur(0px)",
|
|
169
|
-
transition: {
|
|
170
|
-
duration: reduce ? 0 : theatre ? SET_PIECE.theatreLine : SET_PIECE.quietLine,
|
|
171
|
-
// A blur is on this track — see EASE_FILTER above.
|
|
172
|
-
ease: EASE_FILTER,
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
} as const;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** The parent's stagger. Zero after the first open, so a re-open is instant. */
|
|
179
|
-
export function theatreList(theatre: boolean) {
|
|
180
|
-
return {
|
|
181
|
-
open: {
|
|
182
|
-
transition: {
|
|
183
|
-
staggerChildren: theatre ? SET_PIECE.stagger : 0,
|
|
184
|
-
delayChildren: theatre ? 0.06 : 0,
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
} as const;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/* ── Icon fill ───────────────────────────────────────────────────────────
|
|
191
|
-
Fill means CHOSEN — the page you're on, or the one you're pointing at.
|
|
192
|
-
Nothing is filled because it looks better.
|
|
193
|
-
|
|
194
|
-
The filled variant is not a second drawing: it's the same outline with its
|
|
195
|
-
interior painted in, layered OVER the stroke at the same coordinates. The
|
|
196
|
-
silhouette never moves, so the two crossfade in place — no pop, no reflow.
|
|
197
|
-
|
|
198
|
-
ease-out, not the house curve: the overshoot is right for travel and wrong
|
|
199
|
-
for a fade. Same split the springs make between position and opacity. */
|
|
24
|
+
/* Icon fill: the filled layer sits over the outline at the same coordinates
|
|
25
|
+
and cross-fades in place (glass/motion/guide.md §13). ease-out, not the
|
|
26
|
+
house curve. */
|
|
200
27
|
export const fillFade = (on: boolean) =>
|
|
201
28
|
`transition-opacity duration-200 ease-out motion-reduce:transition-none ${
|
|
202
29
|
on ? "opacity-100" : "opacity-0"
|
|
203
30
|
}`;
|
|
204
31
|
|
|
205
|
-
/** The inverse — interior lines
|
|
206
|
-
divider, a document's rules) have to leave as the fill arrives. */
|
|
32
|
+
/** The inverse — interior lines the fill swallows (a briefcase's divider). */
|
|
207
33
|
export const detailFade = (on: boolean) =>
|
|
208
34
|
`transition-opacity duration-200 ease-out motion-reduce:transition-none ${
|
|
209
35
|
on ? "opacity-0" : "opacity-100"
|
|
@@ -1,56 +1,20 @@
|
|
|
1
1
|
/* ─────────────────────────────────────────────────────────────────────────
|
|
2
2
|
Interface motion — tokens and CSS-driven effects
|
|
3
3
|
Drop-in for Tailwind v4 (@theme) or plain CSS (the :root fallback below).
|
|
4
|
+
The ladder and the reasons for each value: glass/motion/guide.md §1–2.
|
|
4
5
|
───────────────────────────────────────────────────────────────────────── */
|
|
5
6
|
|
|
6
7
|
@theme {
|
|
7
|
-
/*
|
|
8
|
-
|
|
9
|
-
feel physical, not enough to read as bounce. A single shared curve is
|
|
10
|
-
why unrelated elements moving at once still feel like one mechanism.
|
|
11
|
-
|
|
12
|
-
Do NOT add a second bezier "because this needs a different feel." If an
|
|
13
|
-
element needs distinct character it needs a spring — that's the axis
|
|
14
|
-
where character belongs without fragmenting the system. */
|
|
8
|
+
/* One curve for the whole system; the 1.1 overshoots ~10% and settles.
|
|
9
|
+
Character beyond it belongs to springs, never a second bezier. */
|
|
15
10
|
--ease-geist: cubic-bezier(0.175, 0.885, 0.32, 1.1);
|
|
16
11
|
|
|
17
|
-
/*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
--duration-
|
|
21
|
-
--duration-
|
|
22
|
-
--duration-
|
|
23
|
-
--duration-route: 260ms; /* whole viewport, on the critical path */
|
|
24
|
-
--duration-overlay: 300ms; /* large region entering or collapsing */
|
|
25
|
-
--duration-theme: 400ms; /* literally everything, at once */
|
|
26
|
-
|
|
27
|
-
/* ── Off the ladder, deliberately ───────────────────────────────────────
|
|
28
|
-
The ladder is for STATE. Two things aren't state, and squeezing them
|
|
29
|
-
onto it makes them worse rather than tighter.
|
|
30
|
-
|
|
31
|
-
THE SUBJECT OF THE MOMENT — the thing animating IS the content, so the
|
|
32
|
-
user is looking directly at it and the motion has to read in its own
|
|
33
|
-
right. Note the pair: the dissolve is the subject, the flight is a
|
|
34
|
-
report about it, so the flight stays inside the overlay tier. */
|
|
35
|
-
--duration-dissolve: 620ms; /* stepping between images */
|
|
36
|
-
--duration-flight: 420ms; /* one image moving between two layouts */
|
|
37
|
-
|
|
38
|
-
/* A SET PIECE earns its budget by never appearing again — once per tab,
|
|
39
|
-
never blocking a deep link, never played under reduced motion. */
|
|
40
|
-
--duration-setpiece-in: 560ms;
|
|
41
|
-
--duration-setpiece-out: 320ms;
|
|
42
|
-
|
|
43
|
-
/* ── The curve for a track that also drives a filter ────────────────────
|
|
44
|
-
NOT --ease-geist. The house curve overshoots past 1, and a blur
|
|
45
|
-
animating to 0px would have to pass through a NEGATIVE radius to settle
|
|
46
|
-
back. Browsers clamp rather than crash, so the symptom is a value stuck
|
|
47
|
-
at zero for the tail of the transition — which reads as the animation
|
|
48
|
-
stalling right at the end, and is very hard to attribute.
|
|
49
|
-
|
|
50
|
-
Applies to any property with a hard floor: blur, brightness, a radius
|
|
51
|
-
going to 0, a scale you've promised won't invert. Expo-out has the same
|
|
52
|
-
"arrives decisively" character without ever leaving [0, 1]. */
|
|
53
|
-
--ease-filter: cubic-bezier(0.16, 1, 0.3, 1);
|
|
12
|
+
/* The state tiers these rules use. The full ladder, and the off-ladder
|
|
13
|
+
subject and set-piece timings, are in the guide. */
|
|
14
|
+
--duration-state: 150ms;
|
|
15
|
+
--duration-route: 260ms;
|
|
16
|
+
--duration-overlay: 300ms;
|
|
17
|
+
--duration-theme: 400ms;
|
|
54
18
|
}
|
|
55
19
|
|
|
56
20
|
/* Plain-CSS fallback if you're not on Tailwind v4. */
|
|
@@ -60,24 +24,20 @@
|
|
|
60
24
|
|
|
61
25
|
@layer components {
|
|
62
26
|
/* ── Reveal on scroll ────────────────────────────────────────────────────
|
|
63
|
-
CSS animates; JS only sets `data-in` (
|
|
64
|
-
|
|
65
|
-
doesn't feel like it's sliding around. */
|
|
27
|
+
CSS animates; JS only sets `data-in` (useReveal.ts, which also holds the
|
|
28
|
+
<noscript> reset this needs). 8px of travel. */
|
|
66
29
|
.reveal {
|
|
67
30
|
opacity: 0;
|
|
68
31
|
transform: translateY(8px);
|
|
69
32
|
transition: opacity var(--duration-overlay) var(--ease-geist),
|
|
70
33
|
transform var(--duration-overlay) var(--ease-geist);
|
|
71
34
|
}
|
|
72
|
-
/* An attribute, not a class — see useReveal.ts for why a class can't survive. */
|
|
73
35
|
.reveal[data-in] {
|
|
74
36
|
opacity: 1;
|
|
75
37
|
transform: translateY(0);
|
|
76
38
|
}
|
|
77
39
|
|
|
78
|
-
/*
|
|
79
|
-
degrade to. Note it resolves to opacity: 1; the danger with any
|
|
80
|
-
reveal-on-scroll is leaving content stuck at opacity: 0. */
|
|
40
|
+
/* Resolves to opacity: 1 — never leave reveal content stuck at 0. */
|
|
81
41
|
@media (prefers-reduced-motion: reduce) {
|
|
82
42
|
.reveal {
|
|
83
43
|
opacity: 1;
|
|
@@ -86,8 +46,8 @@
|
|
|
86
46
|
}
|
|
87
47
|
}
|
|
88
48
|
|
|
89
|
-
/*
|
|
90
|
-
|
|
49
|
+
/* Scroll-triggered content prints blank without this. The one copy of the
|
|
50
|
+
print reset; layout-tokens.css's print block relies on it. */
|
|
91
51
|
@media print {
|
|
92
52
|
.reveal {
|
|
93
53
|
opacity: 1;
|
|
@@ -95,29 +55,9 @@
|
|
|
95
55
|
}
|
|
96
56
|
}
|
|
97
57
|
|
|
98
|
-
/* THE THIRD GUARD IS NOT CSS. A feature check can't catch "JavaScript
|
|
99
|
-
never ran at all": the rules above ship, `.reveal` applies, and nothing
|
|
100
|
-
arrives to add `.in`. The reader gets a correct document they cannot
|
|
101
|
-
see — a blank page rather than a plain one. Put this in the head:
|
|
102
|
-
|
|
103
|
-
<noscript>
|
|
104
|
-
<style>.reveal { opacity: 1 !important; transform: none !important; }</style>
|
|
105
|
-
</noscript>
|
|
106
|
-
|
|
107
|
-
The general rule: any CSS that hides content pending JavaScript needs a
|
|
108
|
-
<noscript> reset. */
|
|
109
|
-
|
|
110
58
|
/* ── Route transition ───────────────────────────────────────────────────
|
|
111
|
-
Opacity only
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
260ms rather than 300: a page transition sits directly between "I
|
|
115
|
-
clicked" and "I can read," and that latency reads as the site being
|
|
116
|
-
slow. 260 is the shortest crossfade that isn't mistaken for a flicker.
|
|
117
|
-
|
|
118
|
-
`both` holds the from-state before the animation starts, preventing a
|
|
119
|
-
one-frame flash of opaque content. Key the wrapper on the pathname to
|
|
120
|
-
replay it on each navigation. */
|
|
59
|
+
Opacity only, 260ms; `both` holds the from-state so there's no one-frame
|
|
60
|
+
flash. Key the wrapper on the pathname (glass/motion/easing-and-duration.md). */
|
|
121
61
|
.route-fade {
|
|
122
62
|
animation: routeFade var(--duration-route) var(--ease-geist) both;
|
|
123
63
|
}
|
|
@@ -130,21 +70,10 @@
|
|
|
130
70
|
}
|
|
131
71
|
|
|
132
72
|
/* ── Theme crossfade ────────────────────────────────────────────────────
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
stops it poisoning everything else.
|
|
138
|
-
|
|
139
|
-
400ms because this is the largest possible change: every surface, text
|
|
140
|
-
colour, border, icon fill and shadow simultaneously.
|
|
141
|
-
|
|
142
|
-
Plain `ease`, not the house curve — overshoot on a position is momentum;
|
|
143
|
-
overshoot on a colour interpolation means the background travels past
|
|
144
|
-
its target and comes back, which reads as a wobble in the light.
|
|
145
|
-
|
|
146
|
-
The whole rule sits inside no-preference, so it's opt-in. Reduced-motion
|
|
147
|
-
users still get the theme change, just instantly. */
|
|
73
|
+
setThemeChoice (glass/color/assets/theme.ts) adds the class on an
|
|
74
|
+
explicit toggle for ~440ms, then removes it; the !important beats every
|
|
75
|
+
component's own duration for that window only. Plain `ease`, and opt-in
|
|
76
|
+
under no-preference. Why: glass/motion/easing-and-duration.md. */
|
|
148
77
|
@media (prefers-reduced-motion: no-preference) {
|
|
149
78
|
:root.theme-transition,
|
|
150
79
|
:root.theme-transition *,
|
|
@@ -159,18 +88,5 @@
|
|
|
159
88
|
}
|
|
160
89
|
}
|
|
161
90
|
|
|
162
|
-
/*
|
|
163
|
-
Parked off-screen with a transform (not display:none, which would take
|
|
164
|
-
it out of the tab order) and slid in on focus at the state tier. */
|
|
165
|
-
.skip {
|
|
166
|
-
position: absolute;
|
|
167
|
-
left: 16px;
|
|
168
|
-
top: 16px;
|
|
169
|
-
z-index: 100;
|
|
170
|
-
transform: translateY(-200%);
|
|
171
|
-
transition: transform var(--duration-state) var(--ease-geist);
|
|
172
|
-
}
|
|
173
|
-
.skip:focus {
|
|
174
|
-
transform: translateY(0);
|
|
175
|
-
}
|
|
91
|
+
/* The skip link lives in glass/layout/assets/layout-tokens.css. */
|
|
176
92
|
}
|
|
@@ -33,12 +33,7 @@ The gap between 150 and 200 maps to *whether other things move too*:
|
|
|
33
33
|
|
|
34
34
|
## Why route transitions are 260ms
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
.route-fade { animation: routeFade 260ms var(--ease-geist) both; }
|
|
38
|
-
@keyframes routeFade { from { opacity: 0 } to { opacity: 1 } }
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
260ms is the shortest crossfade that still reads as intentional; below ~200ms a full-viewport opacity change looks like a rendering artefact (law 3 caps it). Opacity only, because a directional transition implies a spatial relationship between routes, and the site map (home, experience, writing, playground) is flat.
|
|
36
|
+
The rule is `.route-fade` in `glass/motion/assets/motion-tokens.css`. 260ms is the shortest crossfade that still reads as intentional; below ~200ms a full-viewport opacity change looks like a rendering artefact (law 3 caps it). Opacity only, because a directional transition implies a spatial relationship between routes, and the site map (home, experience, writing, playground) is flat.
|
|
42
37
|
|
|
43
38
|
Keying the wrapper on the pathname replays the CSS animation on every navigation:
|
|
44
39
|
|
|
@@ -50,33 +45,9 @@ Keying the wrapper on the pathname replays the CSS animation on every navigation
|
|
|
50
45
|
|
|
51
46
|
## Why the theme crossfade breaks both rules
|
|
52
47
|
|
|
53
|
-
```css
|
|
54
|
-
@media (prefers-reduced-motion: no-preference) {
|
|
55
|
-
:root.theme-transition,
|
|
56
|
-
:root.theme-transition *,
|
|
57
|
-
:root.theme-transition *::before,
|
|
58
|
-
:root.theme-transition *::after {
|
|
59
|
-
transition: background-color 400ms ease, color 400ms ease, border-color 400ms ease,
|
|
60
|
-
fill 400ms ease, stroke 400ms ease, box-shadow 400ms ease !important;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
48
|
**400ms, longest on the ladder**: every surface, text colour, border, icon fill and shadow at once. At 150ms it reads as a jarring flash; at 400ms, as the lights coming up. **Plain `ease`, not the house curve**, because colour has no mass (law 5).
|
|
66
49
|
|
|
67
|
-
The
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
export function setThemeChoice(choice: ThemeChoice): void {
|
|
71
|
-
localStorage.setItem(KEY, choice);
|
|
72
|
-
const root = document.documentElement;
|
|
73
|
-
root.classList.add("theme-transition");
|
|
74
|
-
applyTheme(choice);
|
|
75
|
-
window.setTimeout(() => root.classList.remove("theme-transition"), 440);
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
It exists for ~440ms, the duration plus a 40ms margin, and its `!important` beats every component's own `transition-colors duration-150` for that window. Why it's scoped this way is in `domains/motion/patterns.md`, theme cross-fade.
|
|
50
|
+
The `.theme-transition` rule is in `glass/motion/assets/motion-tokens.css`; `setThemeChoice` in `glass/color/assets/theme.ts` adds it only on an explicit toggle and removes it after ~440ms, the duration plus a 40ms margin, so its `!important` beats every component's own `transition-colors duration-150` for that window only. Why it's scoped this way is in `domains/motion/patterns.md`, theme cross-fade.
|
|
80
51
|
|
|
81
52
|
## The overshoot in the house curve
|
|
82
53
|
|
|
@@ -38,7 +38,7 @@ Glass uses two of law 4's classes:
|
|
|
38
38
|
|
|
39
39
|
One curve for the whole site. The `1.1` end control point overshoots slightly and settles back, physical but not bouncy. The exceptions:
|
|
40
40
|
|
|
41
|
-
- **
|
|
41
|
+
- **100ms opacity hovers and icon fills** (§13): `ease-out`. Opacity entrances (`.reveal`, the route fade) keep the house curve on purpose: on opacity the overshoot clamps at 1.
|
|
42
42
|
- **The theme crossfade**: plain `ease`.
|
|
43
43
|
- **Anything spring-driven**: no bezier at all.
|
|
44
44
|
- **Any track that also drives a filter**, or any property floored at zero (law 5): expo-out, `cubic-bezier(0.16, 1, 0.3, 1)`, which never exceeds 1. It is the same intent in a curve legal for the property, not a second character.
|
|
@@ -116,11 +116,7 @@ The details that keep it from flickering are in `domains/motion/patterns.md`, th
|
|
|
116
116
|
|
|
117
117
|
### When the DOM element isn't enough
|
|
118
118
|
|
|
119
|
-
A *photograph* travelling between layouts escalates to the GPU (law 13)
|
|
120
|
-
|
|
121
|
-
**Carry the reading position across**: open the other layout on the tile nearest the viewport centre, measured before React commits it, and give each end its own rect around a shared centre (law 8).
|
|
122
|
-
|
|
123
|
-
→ The measurement (`centreFrame`) and the WebGL notes: `glass/motion/shared-element.md`, escalating to the GPU
|
|
119
|
+
A *photograph* travelling between layouts escalates to the GPU (law 13), opening on the tile nearest the viewport centre (law 8): `glass/motion/shared-element.md`, escalating to the GPU
|
|
124
120
|
|
|
125
121
|
## 7. Reduced motion
|
|
126
122
|
|
|
@@ -152,34 +148,11 @@ One passive listener and one rAF per behaviour (law 12; the shape is in `domains
|
|
|
152
148
|
|
|
153
149
|
## 9. Reveal on scroll
|
|
154
150
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
```css
|
|
158
|
-
.reveal { opacity: 0; transform: translateY(8px);
|
|
159
|
-
transition: opacity 300ms var(--ease-geist), transform 300ms var(--ease-geist); }
|
|
160
|
-
.reveal[data-in] { opacity: 1; transform: translateY(0); }
|
|
161
|
-
|
|
162
|
-
@media (prefers-reduced-motion: reduce) {
|
|
163
|
-
.reveal { opacity: 1; transform: none; transition: none; }
|
|
164
|
-
}
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
`threshold: 0` with `rootMargin: "0px 0px -40px 0px"`, one-shot, re-run on the pathname. 8px of travel registers without a page of paragraphs seeming to slide. Every guard is law 11 and `domains/motion/guards.md`; the two the site hit:
|
|
168
|
-
|
|
169
|
-
- **A fractional threshold on a tall element.** Every rendered SKILL.md on the site, eight to thirteen thousand pixels in one `.reveal`, once sat invisible at opacity 0 behind `0.1`.
|
|
170
|
-
- **A class the framework owns.** Tag-filtered rows went blank as the pointer left, when React rewrote a state-dependent `className` and wiped an `in` added with `classList.add`. Hence `data-in`.
|
|
171
|
-
|
|
172
|
-
The no-JavaScript reset, in the head:
|
|
173
|
-
|
|
174
|
-
```html
|
|
175
|
-
<noscript>
|
|
176
|
-
<style>.reveal { opacity: 1 !important; transform: none !important; }</style>
|
|
177
|
-
</noscript>
|
|
178
|
-
```
|
|
151
|
+
300ms on the house curve, 8px of travel, `threshold: 0`, `rootMargin: "0px 0px -40px 0px"`, marked by `data-in`; the guards are law 11, whose two bugs are this site's. → `glass/motion/assets/motion-tokens.css`, `glass/motion/assets/useReveal.ts`
|
|
179
152
|
|
|
180
153
|
## 10. Performance: `m` not `motion`
|
|
181
154
|
|
|
182
|
-
`m.div` inside `<LazyMotion features={domAnimation} strict>`, never a bare `motion.*` (`domains/motion/performance.md`). The menu
|
|
155
|
+
`m.div` inside `<LazyMotion features={domAnimation} strict>`, never a bare `motion.*` (`domains/motion/performance.md`). The menu's wrapper scales and its `glass-panel` child carries the blur (law 13; `glass/depth/guide.md` §3).
|
|
183
156
|
|
|
184
157
|
## 11. Hover intent
|
|
185
158
|
|
|
@@ -204,11 +177,7 @@ active:scale-[0.92] /* 8% — felt, not seen */
|
|
|
204
177
|
|
|
205
178
|
On `pointerdown` (law 3).
|
|
206
179
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
**Sound can be a phrase, not just an event.** The opening screen's run, a note per photograph, is written out in A major pentatonic, where no two notes clash, so a run cut short by a failed image still resolves; the landing note, the only accented one, sits a fifth *below* the start, closing the phrase rather than adding a step.
|
|
210
|
-
|
|
211
|
-
Sound is *on* by default here, which is defensible only on a personal site and only if you're confident. → Synthesis walkthrough and delegation pattern: `glass/motion/sound.md`
|
|
180
|
+
Sound is *on* by default here, defensible only on a personal site: a synthesised "tok" on hover and click, muteable; `[data-quiet-hover]` silences hover on dense surfaces. → `glass/motion/sound.md`; the rules: `domains/motion/patterns.md`, sound
|
|
212
181
|
|
|
213
182
|
## 13. Fill means chosen
|
|
214
183
|
|
|
@@ -223,7 +192,7 @@ export const detailFade = (on: boolean) =>
|
|
|
223
192
|
`transition-opacity duration-200 ease-out motion-reduce:transition-none ${on ? "opacity-0" : "opacity-100"}`;
|
|
224
193
|
```
|
|
225
194
|
|
|
226
|
-
**200ms `ease-out`, not the house curve
|
|
195
|
+
**200ms `ease-out`, not the house curve** (§2). The knockout mask for interior lines is in `domains/motion/patterns.md`, fill means chosen.
|
|
227
196
|
|
|
228
197
|
**How the marks that can't fill signal instead:**
|
|
229
198
|
|
|
@@ -235,7 +204,7 @@ export const detailFade = (on: boolean) =>
|
|
|
235
204
|
|
|
236
205
|
## 14. Set pieces play once
|
|
237
206
|
|
|
238
|
-
Two set pieces, each once a session (law 15): the opening screen, and a summary panel's first reveal.
|
|
207
|
+
Two set pieces, each once a session (law 15): the opening screen, and a summary panel's first reveal. The first open runs each line at 340ms, its children staggered 90ms after a 60ms delay; every later open runs 160ms with no stagger, and reduced motion runs none.
|
|
239
208
|
|
|
240
209
|
```ts
|
|
241
210
|
// Once a session, per item. The theatre is a welcome; a re-run is a delay.
|
|
@@ -265,8 +234,8 @@ Collapsibles animate height and stay mounted (law 11), so the content stays in t
|
|
|
265
234
|
|
|
266
235
|
## Assets
|
|
267
236
|
|
|
268
|
-
- `glass/motion/assets/motion-tokens.css` — easing curve,
|
|
269
|
-
- `glass/motion/assets/motion-presets.ts` —
|
|
237
|
+
- `glass/motion/assets/motion-tokens.css` — easing curve, state durations, `.reveal`, route fade, theme crossfade.
|
|
238
|
+
- `glass/motion/assets/motion-presets.ts` — the springs, the close and the icon-fill fades.
|
|
270
239
|
- `glass/motion/assets/DropMenu.tsx` — the squash-and-stretch menu.
|
|
271
240
|
- `glass/motion/assets/useScrollTick.ts` — rAF-throttled scroll with hysteresis.
|
|
272
241
|
- `glass/motion/assets/useReveal.ts` — the IntersectionObserver hook and its guards.
|
|
@@ -277,7 +246,7 @@ The domain checklist first; then glass's values:
|
|
|
277
246
|
|
|
278
247
|
- [ ] Every duration on the ladder: no 250ms, 500ms or 1s.
|
|
279
248
|
- [ ] Off-ladder timing is the subject of the moment (the 620ms dissolve) or a once-a-session set piece, and says which.
|
|
280
|
-
- [ ] `--ease-geist` everywhere, plus `ease-out` for opacity
|
|
249
|
+
- [ ] `--ease-geist` everywhere, opacity entrances included, plus `ease-out` for 100ms opacity hovers and icon fills, plain `ease` for the theme fade, and expo-out on any track driving a filter.
|
|
281
250
|
- [ ] Menus open on the `GROW`/`CROSS` pair and close on `CLOSE`.
|
|
282
251
|
- [ ] Hover timed by area: a link 100ms, a card-sized wash 200ms.
|
|
283
252
|
- [ ] No translate on hover, bar the folder's rising cards.
|
|
@@ -60,13 +60,7 @@ One variable turns a blur-crossfade into a plain crossfade; `blur(0px)` → `blu
|
|
|
60
60
|
|
|
61
61
|
## In CSS
|
|
62
62
|
|
|
63
|
-
The `.reveal`
|
|
64
|
-
|
|
65
|
-
```css
|
|
66
|
-
@media (prefers-reduced-motion: reduce) {
|
|
67
|
-
.route-fade { animation: none; }
|
|
68
|
-
}
|
|
69
|
-
```
|
|
63
|
+
The `.reveal` reset, the route fade's `animation: none` and the theme crossfade's `no-preference` opt-in are all in `glass/motion/assets/motion-tokens.css`.
|
|
70
64
|
|
|
71
65
|
### Tailwind's `motion-reduce:` variant
|
|
72
66
|
|
|
@@ -18,40 +18,11 @@ const ANCHOR = "scroll-mt-28"; // 112px — matched to READING_LINE = 120
|
|
|
18
18
|
|
|
19
19
|
Every heading carries `scroll-mt-28`, so a jump lands it 112px below the viewport top, just above the 120px line.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Deep links scroll with `scrollIntoView({ behavior: reduce ? "auto" : "smooth" })` and set the hash with `history.replaceState` (`domains/motion/performance.md`, scroll spies).
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
el.scrollIntoView({ behavior: reduce ? "auto" : "smooth", block: "start" });
|
|
25
|
-
window.history.replaceState(null, "", `#${id}`);
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
The URL stays copyable without a second scroll fighting the first.
|
|
29
|
-
|
|
30
|
-
## The `1fr` ↔ `0fr` grid trick, in Tailwind
|
|
31
|
-
|
|
32
|
-
Collapsing a region to zero width without animating `width`:
|
|
33
|
-
|
|
34
|
-
```tsx
|
|
35
|
-
<li
|
|
36
|
-
className="grid transition-[grid-template-columns] duration-300 ease-geist motion-reduce:transition-none"
|
|
37
|
-
style={{ gridTemplateColumns: expanded ? "1fr" : "0fr" }}
|
|
38
|
-
>
|
|
39
|
-
<div className="flex min-w-0 items-center overflow-hidden">{children}</div>
|
|
40
|
-
</li>
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
`min-w-0` on the child is required (flex children default to `min-width: auto` and won't shrink below their content); `overflow-hidden` clips during the collapse.
|
|
44
|
-
|
|
45
|
-
## `backdrop-filter` and transforms
|
|
46
|
-
|
|
47
|
-
The animating wrapper and the glass surface are different elements (law 13):
|
|
48
|
-
|
|
49
|
-
```tsx
|
|
50
|
-
<DropMenu open={open}> {/* animates scale + y */}
|
|
51
|
-
<ul className="glass-panel …"> {/* carries the blur, never transformed */}
|
|
52
|
-
```
|
|
23
|
+
## The dock's collapse
|
|
53
24
|
|
|
54
|
-
|
|
25
|
+
The dock's icon row collapses on `grid-template-columns` `1fr` ↔ `0fr`, 300ms on the house curve, with `min-w-0 overflow-hidden` on the child (the technique: `domains/motion/performance.md`, what to animate). The menu's wrapper/surface split is §3 of `glass/depth/guide.md`.
|
|
55
26
|
|
|
56
27
|
## Timers
|
|
57
28
|
|
|
@@ -114,7 +114,7 @@ export type Rect = { x: number; y: number; width: number; height: number };
|
|
|
114
114
|
export function lerpRect(a: Rect, b: Rect, t: number): Rect { /* … */ }
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
The rules are law 13 and `domains/motion/performance.md`, escalating to the GPU. Here the wall stays DOM and the open photo is `alt=""`, its labels in the strip beside it.
|
|
117
|
+
The rules are law 13 and `domains/motion/performance.md`, escalating to the GPU. Here the wall stays DOM, since a hundred tiles as textures is ~590MB before atlasing, and the open photo is `alt=""`, its labels in the strip beside it.
|
|
118
118
|
|
|
119
119
|
**Failing returns `null`:**
|
|
120
120
|
|