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
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* copied as links; the click handler takes over to scroll smoothly and keep
|
|
13
13
|
* the URL in step.
|
|
14
14
|
*
|
|
15
|
-
* Pair with useActiveSection (`glass/motion/
|
|
15
|
+
* Pair with useActiveSection (`glass/motion/assets/useScrollTick.ts`) for the scroll spy, and
|
|
16
16
|
* give every heading a scroll-margin-top just under its READING_LINE.
|
|
17
17
|
*/
|
|
18
18
|
|
|
@@ -59,11 +59,8 @@ function ChevronDownIcon() {
|
|
|
59
59
|
/**
|
|
60
60
|
* One row.
|
|
61
61
|
*
|
|
62
|
-
* Top-level rows sit flush and carry no marker
|
|
63
|
-
*
|
|
64
|
-
* signalled by colour alone (WCAG 1.4.1). Weight is a genuine second channel:
|
|
65
|
-
* it survives greyscale, colour-blindness and low-contrast displays, and it's
|
|
66
|
-
* cheaper than a background pill in a list that's meant to be quiet.
|
|
62
|
+
* Top-level rows sit flush and carry no marker; the active one takes weight
|
|
63
|
+
* 500 as well as colour, so state is never on colour alone (type law 5).
|
|
67
64
|
*
|
|
68
65
|
* Nested rows carry a horizontal marker that extends and darkens when active,
|
|
69
66
|
* and which doubles as their indent. It sits in a FIXED-WIDTH box so growing
|
|
@@ -3,43 +3,15 @@
|
|
|
3
3
|
───────────────────────────────────────────────────────────────────────── */
|
|
4
4
|
|
|
5
5
|
@theme {
|
|
6
|
-
/*
|
|
7
|
-
|
|
8
|
-
~90 characters per line (85–95): past the comfortable range, which is the
|
|
9
|
-
right place for a site that expects sustained reading (long lines are
|
|
10
|
-
efficient once the reader is committed, and 1.75 leading pays for the
|
|
11
|
-
longer return sweep).
|
|
12
|
-
|
|
13
|
-
EVERY page uses this. The payoff isn't visual consistency, it's
|
|
14
|
-
positional continuity: clicking from a summary into its detail view
|
|
15
|
-
doesn't move the text, so the transition reads as expansion rather than
|
|
16
|
-
as arriving at a different document. */
|
|
6
|
+
/* 640px outer, 32px padding → 576px of text, on every page. Why:
|
|
7
|
+
glass/layout/measure-and-rhythm.md. */
|
|
17
8
|
--container-col: 640px;
|
|
18
|
-
|
|
19
|
-
/* The outer frame that rails anchor to, alongside the column. Anchoring
|
|
20
|
-
both to the SAME frame is what makes the gap between them hold steady
|
|
21
|
-
from 1280px up — a rail positioned against the viewport instead drifts
|
|
22
|
-
away from the content it indexes as the window grows. */
|
|
23
|
-
--container-frame: 1280px;
|
|
24
|
-
--rail-width: 196px;
|
|
25
9
|
}
|
|
26
10
|
|
|
27
11
|
@layer base {
|
|
28
|
-
/*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
drops abruptly to zero where it meets the straight edge. The eye reads
|
|
32
|
-
that discontinuity as a slight hardness. A superellipse varies curvature
|
|
33
|
-
continuously into the edge.
|
|
34
|
-
|
|
35
|
-
superellipse(k) uses exponent 2^k — k=1 is a normal round corner, k=2 a
|
|
36
|
-
full iOS squircle, so 1.5 lands about halfway. Not 2, because a full
|
|
37
|
-
squircle is noticeable at small sizes (it starts to look like a swollen
|
|
38
|
-
rectangle); halfway reads as "well-drawn corner" rather than as an
|
|
39
|
-
effect.
|
|
40
|
-
|
|
41
|
-
Unsupported browsers ignore it and fall back to normal rounding. Keep it
|
|
42
|
-
that way — nothing else in the layout should depend on it. */
|
|
12
|
+
/* Continuous corners on every rounded element. 1.5 sits halfway between a
|
|
13
|
+
round corner (1) and a full squircle (2), which swells at small sizes.
|
|
14
|
+
Unsupported browsers fall back to normal rounding; nothing depends on it. */
|
|
43
15
|
*,
|
|
44
16
|
*::before,
|
|
45
17
|
*::after {
|
|
@@ -54,9 +26,8 @@
|
|
|
54
26
|
padding: 0 32px;
|
|
55
27
|
}
|
|
56
28
|
|
|
57
|
-
/* Skip link
|
|
58
|
-
|
|
59
|
-
page needs a matching id="main". */
|
|
29
|
+
/* Skip link: parked with a transform, never display:none (which drops it
|
|
30
|
+
from the tab order). Every page needs a matching id="main". */
|
|
60
31
|
.skip {
|
|
61
32
|
position: absolute;
|
|
62
33
|
left: 16px;
|
|
@@ -69,20 +40,14 @@
|
|
|
69
40
|
text-decoration: none;
|
|
70
41
|
border-radius: 6px;
|
|
71
42
|
transform: translateY(-200%);
|
|
72
|
-
transition: transform
|
|
43
|
+
transition: transform var(--duration-state) var(--ease-geist);
|
|
73
44
|
}
|
|
74
45
|
.skip:focus {
|
|
75
46
|
transform: translateY(0);
|
|
76
47
|
}
|
|
77
48
|
}
|
|
78
49
|
|
|
79
|
-
/*
|
|
80
|
-
Print. A real presentation, not an afterthought.
|
|
81
|
-
|
|
82
|
-
The .reveal reset is the one people forget: scroll-triggered content is at
|
|
83
|
-
opacity: 0 until an IntersectionObserver fires, and printing never fires
|
|
84
|
-
it — so the page prints blank without this.
|
|
85
|
-
───────────────────────────────────────────────────────────────────────── */
|
|
50
|
+
/* Print. The .reveal reset lives in glass/motion/assets/motion-tokens.css. */
|
|
86
51
|
@media print {
|
|
87
52
|
nav,
|
|
88
53
|
footer,
|
|
@@ -96,11 +61,6 @@
|
|
|
96
61
|
font-size: 11pt;
|
|
97
62
|
}
|
|
98
63
|
|
|
99
|
-
.reveal {
|
|
100
|
-
opacity: 1;
|
|
101
|
-
transform: none;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
64
|
/* An underlined URL-less link is noise on paper. */
|
|
105
65
|
a {
|
|
106
66
|
color: inherit;
|
|
@@ -108,94 +68,5 @@
|
|
|
108
68
|
}
|
|
109
69
|
}
|
|
110
70
|
|
|
111
|
-
/*
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
── The radius ladder ──────────────────────────────────────────────────
|
|
115
|
-
Nesting rule: INNER = OUTER − the padding between them. Geometry, not
|
|
116
|
-
taste — two concentric rounded rectangles share a centre of curvature
|
|
117
|
-
only if their radii differ by exactly the gap between their edges.
|
|
118
|
-
|
|
119
|
-
rounded-full pills, tags, dots, markers
|
|
120
|
-
rounded-[5px] a lettermark box
|
|
121
|
-
rounded-md 6 tooltips, the skip link
|
|
122
|
-
rounded-[9px] 9 small cards in a stack
|
|
123
|
-
rounded-[10px]10 avatar inside a button · a list highlight bar
|
|
124
|
-
rounded-xl 12 cards, hover panels, disclosures, icon buttons
|
|
125
|
-
rounded-2xl 16 outer containers — a dock, a sheet, a pocket
|
|
126
|
-
|
|
127
|
-
Worked example:
|
|
128
|
-
Dock rounded-2xl (16) p-1 (4)
|
|
129
|
-
Icon button rounded-[12px] 16 − 4 = 12 ✓
|
|
130
|
-
Avatar rounded-[10px] 12 − 2 = 10 ✓
|
|
131
|
-
|
|
132
|
-
Radius scales with SIZE, not importance — and sub-proportionally, because
|
|
133
|
-
a large element with a proportional radius reads as a lozenge.
|
|
134
|
-
|
|
135
|
-
── Hairline weights ───────────────────────────────────────────────────
|
|
136
|
-
The choice is about what the line is DOING:
|
|
137
|
-
|
|
138
|
-
0.5px internal articulation — a divider between icon groups inside
|
|
139
|
-
one dock. Renders as a true sub-pixel hairline on retina and
|
|
140
|
-
rounds UP to 1px elsewhere, so it degrades rather than vanishes.
|
|
141
|
-
1px a real edge — where a card ends.
|
|
142
|
-
2px a focus ring; imported Markdown's blockquote rule.
|
|
143
|
-
3px the authored pull quote's bar — a rounded flex child, not a
|
|
144
|
-
border, since a border can't be rounded on its own.
|
|
145
|
-
|
|
146
|
-
On translucent surfaces use the ALPHA ramp (border-gray-alpha-400). A
|
|
147
|
-
solid border on glass doesn't participate in what's behind it and reads as
|
|
148
|
-
a pasted-on frame.
|
|
149
|
-
|
|
150
|
-
An <hr> needs border-0 before you give it a background, or you get the UA
|
|
151
|
-
border AND your line. Non-semantic dividers get aria-hidden.
|
|
152
|
-
|
|
153
|
-
── The z-index ladder ─────────────────────────────────────────────────
|
|
154
|
-
Ordered by PERMANENCE, not importance. Gaps of 10 so something can be
|
|
155
|
-
inserted without renumbering.
|
|
156
|
-
|
|
157
|
-
100 skip link — must beat everything, including modals
|
|
158
|
-
60 a once-a-session overlay — over the dock, under the skip link
|
|
159
|
-
50 fixed navigation
|
|
160
|
-
40 tap-outside catcher — under the nav, over the page
|
|
161
|
-
30 a pocket front, above the cards it contains
|
|
162
|
-
20 hover cards, dropdowns
|
|
163
|
-
10 a hovered item lifting above its siblings
|
|
164
|
-
-10 a sliding highlight, behind content inside `isolate`
|
|
165
|
-
|
|
166
|
-
60 is what the gaps were for: a greeting overlay arrived long after this
|
|
167
|
-
ladder was set and slotted in without touching an existing value.
|
|
168
|
-
|
|
169
|
-
Two pairs to understand rather than copy. 40/50: a full-screen catcher
|
|
170
|
-
must be above the page (to catch the click) and below the menu it
|
|
171
|
-
dismisses (or it eats the menu's own clicks). 60/100: a splash overlay
|
|
172
|
-
covers the navigation — or it isn't covering the page — but must never
|
|
173
|
-
cover the skip link, because a keyboard user tabbing during a two-second
|
|
174
|
-
greeting has to be able to get past it.
|
|
175
|
-
|
|
176
|
-
── Two techniques ─────────────────────────────────────────────────────
|
|
177
|
-
isolate + negative z: a highlight bar behind icons but in front of its
|
|
178
|
-
container's background. `-z-10` alone drops it behind the container;
|
|
179
|
-
`isolate` scopes the negative z inside.
|
|
180
|
-
|
|
181
|
-
pointer-events-none on a full-width strip, auto on the content: a fixed
|
|
182
|
-
nav strip spans the viewport for centring but must not swallow every click
|
|
183
|
-
along the bottom of the page.
|
|
184
|
-
|
|
185
|
-
And the strip carries NO TRANSFORM. A transform makes an element the
|
|
186
|
-
containing block for position:fixed descendants — a full-bleed w-screen
|
|
187
|
-
overlay inside it would be trapped in the strip's bounds. Same class of
|
|
188
|
-
problem as transforms weakening backdrop-filter: when something positioned
|
|
189
|
-
or filtered behaves inexplicably, check every ancestor for transform,
|
|
190
|
-
filter, perspective, contain, and will-change.
|
|
191
|
-
|
|
192
|
-
── Viewport units ─────────────────────────────────────────────────────
|
|
193
|
-
Use dvh, never vh. `vh` refers to the viewport WITHOUT mobile browser
|
|
194
|
-
chrome, so a 100vh element extends under the address bar and its bottom is
|
|
195
|
-
unreachable. `dvh` tracks the actual visible area.
|
|
196
|
-
|
|
197
|
-
── Bottom padding ─────────────────────────────────────────────────────
|
|
198
|
-
Anything with fixed bottom chrome needs pb-28 (112px) audited page by
|
|
199
|
-
page, or the last line sits under the nav. The most common "looks fine on
|
|
200
|
-
my long page" bug.
|
|
201
|
-
───────────────────────────────────────────────────────────────────────── */
|
|
71
|
+
/* The radius ladder, hairlines, z-index ladder, dvh and pb-28 are not classes:
|
|
72
|
+
glass/layout/guide.md and glass/layout/radius-and-elevation.md. */
|
|
@@ -229,17 +229,11 @@ Where a custom animation rules out `<details>`, glass collapses height to zero (
|
|
|
229
229
|
|
|
230
230
|
The content stays in the served HTML, for crawlers, `llms.txt` and find-in-page.
|
|
231
231
|
|
|
232
|
-
|
|
233
|
-
@media print {
|
|
234
|
-
nav, footer, .skip { display: none; }
|
|
235
|
-
.reveal { opacity: 1; transform: none; }
|
|
236
|
-
a { color: inherit; text-decoration: none; }
|
|
237
|
-
}
|
|
238
|
-
```
|
|
232
|
+
Print: `glass/layout/assets/layout-tokens.css`, with the `.reveal` reset in `glass/motion/assets/motion-tokens.css`.
|
|
239
233
|
|
|
240
234
|
## Assets
|
|
241
235
|
|
|
242
|
-
- `glass/layout/assets/layout-tokens.css` — column,
|
|
236
|
+
- `glass/layout/assets/layout-tokens.css` — column, continuous corners, skip link, print.
|
|
243
237
|
- `glass/layout/assets/SectionNav.tsx` — two presentations of one tree, with shared row rendering.
|
|
244
238
|
|
|
245
239
|
## Checklist
|
|
@@ -10,8 +10,6 @@ At the 14px prose size that's about 90 characters a line (85–95, measured in A
|
|
|
10
10
|
|
|
11
11
|
The padding does double duty: on a 375px phone the column is `375 − 64 = 311px`, and on desktop the 32px keeps text off the edge of a maximised window.
|
|
12
12
|
|
|
13
|
-
That 311px once forced a responsive type step (a 30px title wrapping to three lines) until an 18px type ceiling removed it: layout law 1's narrowest-measure check, settled in the scale.
|
|
14
|
-
|
|
15
13
|
640px also equals Tailwind's `sm` breakpoint — convenient, not meaningful: below it the column is padding-limited, so the two coincide naturally.
|
|
16
14
|
|
|
17
15
|
The summary and detail pairs it keeps still: home-page role summary → `/experience`, writing index → article.
|
|
@@ -49,7 +49,7 @@ The rule is in §4 of the guide: `superellipse(1.5)` on every element and pseudo
|
|
|
49
49
|
| 2px | `outline-2`, `border-l-2` | Focus rings, an imported Markdown quote's rule |
|
|
50
50
|
| 3px | `w-[3px] rounded-full` | The pull-quote bar |
|
|
51
51
|
|
|
52
|
-
By job (layout law 6): two groups of icons within one dock, 0.5px; where a card ends, 1px; a focus ring, 2px. The authored pull quote's bar is 3px and rounded, so a flex child rather than a border (`glass/type/
|
|
52
|
+
By job (layout law 6): two groups of icons within one dock, 0.5px; where a card ends, 1px; a focus ring, 2px. The authored pull quote's bar is 3px and rounded, so a flex child rather than a border (`glass/type/assets/prose.tsx`); imported Markdown, which can't take classes, keeps a 2px `gray-alpha-300` border.
|
|
53
53
|
|
|
54
54
|
Vertical dividers use `w-[0.5px]` with an explicit height and `self-center`:
|
|
55
55
|
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* pill). All of them grow out of a trigger sitting against one edge, so all
|
|
5
5
|
* of them stretch from that edge.
|
|
6
6
|
*
|
|
7
|
-
* The effect comes from letting two springs DISAGREE
|
|
8
|
-
*
|
|
7
|
+
* The effect comes from letting two springs DISAGREE (motion-presets.ts;
|
|
8
|
+
* the reasoning is glass/motion/springs.md).
|
|
9
9
|
*
|
|
10
10
|
* Two structural decisions worth keeping:
|
|
11
11
|
*
|
|
@@ -16,10 +16,8 @@
|
|
|
16
16
|
* 2. The wrapper is ALWAYS present, so a trigger's aria-controls still
|
|
17
17
|
* resolves to a real element when the menu is closed.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* subtree — the wrapper scales, the child blurs, and they must be different
|
|
22
|
-
* elements.
|
|
19
|
+
* It animates a bare m.div and expects its child to carry the glass
|
|
20
|
+
* (glass/depth/guide.md §3).
|
|
23
21
|
*/
|
|
24
22
|
|
|
25
23
|
import { AnimatePresence, LazyMotion, domAnimation, m, useReducedMotion } from "motion/react";
|
|
@@ -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"
|