mirror of
https://github.com/Nutlope/hallmark.git
synced 2026-08-14 12:35:33 +02:00
Scroll choreography: four earned patterns replace a blanket ban
motion.md banned scroll-scrubbed animation 'unless there is a specific reason' while tier-c sanctioned animation-timeline outright - a live contradiction with no vocabulary between them. references/ scroll-choreography.md now IS the specific reason: four named patterns (pinned chapter, scrub-linked headline, progress spine, sticky diptych) with Baseline-2025 recipes, each inside @supports + reduced-motion + min-width guards so the base page is always the finished page. Two correctness rules are baked in: the animation shorthand resets animation-timeline (declare after), and pinned-pane children cannot drive their own view() (steps ride a named timeline on the tall track). Budget: one pattern per page (sloplint F8 will count), never on body text, linear easing for scrub, hero orchestration untouched, JS fallback is the IO reveal-once only. motion.md reroutes to the vocabulary, tier-c's decision row defers to it, macros 14/16 and f2 point at it, and SKILL.md loads it conditionally.
This commit is contained in:
@@ -236,7 +236,7 @@ A custom system is complete (palette + pairing + axes), never a colour swap; its
|
||||
|
||||
**Every build:** [`references/typography.md`](references/typography.md) · [`references/color.md`](references/color.md) · [`references/layout-and-space.md`](references/layout-and-space.md) · [`references/motion.md`](references/motion.md) · [`references/copy.md`](references/copy.md) · [`references/anti-patterns.md`](references/anti-patterns.md).
|
||||
|
||||
**Conditionally (be honest, no defensive pre-loads):** [`references/microinteractions.md`](references/microinteractions.md) when anything is interactive (most pages); [`references/interaction-and-states.md`](references/interaction-and-states.md) for stateful UI; [`references/responsive.md`](references/responsive.md) when mobile is in scope; [`references/structure.md`](references/structure.md) only when deviating from a named macrostructure; [`references/assets.md`](references/assets.md) only when an enrichment needs an external asset; [`references/texture.md`](references/texture.md) only when the picked theme earns texture (Riso, Carnival, Arcade, faint Newsprint) or a custom draw has print lineage; [`references/custom-theme.md`](references/custom-theme.md) only on the custom route; [`references/design-md.md`](references/design-md.md) only when the user asks to lock the system; [`references/preview-examples.md`](references/preview-examples.md) only if the Step 5 spec is not scaffolding enough.
|
||||
**Conditionally (be honest, no defensive pre-loads):** [`references/microinteractions.md`](references/microinteractions.md) when anything is interactive (most pages); [`references/interaction-and-states.md`](references/interaction-and-states.md) for stateful UI; [`references/responsive.md`](references/responsive.md) when mobile is in scope; [`references/structure.md`](references/structure.md) only when deviating from a named macrostructure; [`references/assets.md`](references/assets.md) only when an enrichment needs an external asset; [`references/texture.md`](references/texture.md) only when the picked theme earns texture (Riso, Carnival, Arcade, faint Newsprint) or a custom draw has print lineage; [`references/scroll-choreography.md`](references/scroll-choreography.md) only when the brief asks for scroll story / cinematic pacing or the macro is Feature-stack / Narrative Workflow; [`references/custom-theme.md`](references/custom-theme.md) only on the custom route; [`references/design-md.md`](references/design-md.md) only when the user asks to lock the system; [`references/preview-examples.md`](references/preview-examples.md) only if the Step 5 spec is not scaffolding enough.
|
||||
|
||||
**At the end only:** [`references/slop-test.md`](references/slop-test.md) strictly at Step 7 (pre-loading it costs thousands of tokens for nothing; `anti-patterns.md` is the pre-emit list); [`references/contract.md`](references/contract.md) at handoff; [`references/export-formats.md`](references/export-formats.md) only on `design.md` projects.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
### F2 · Sticky-scroll stack
|
||||
Sticky left pane, scrolling right pane that cycles through related screenshots.
|
||||
*Use when:* feature has multiple sub-states worth showing in sequence.
|
||||
*Don't confuse with:* F4 Step sequence (which is linearly numbered, not synced).
|
||||
*Don't confuse with:* F4 Step sequence (which is linearly numbered, not synced). Scroll-scrub choreography on top of the pin lives in [`scroll-choreography.md`](../scroll-choreography.md) § Sticky diptych.
|
||||
|
||||
```html
|
||||
<section class="sticky-stack">
|
||||
|
||||
@@ -79,7 +79,7 @@ The browser handles the cross-fade. No animation libraries needed for state chan
|
||||
```
|
||||
Single element, simple motion → CSS keyframes / @property
|
||||
Multiple elements, orchestrated entrance → Motion (React) or GSAP (vanilla / complex)
|
||||
Scroll-progress-linked → animation-timeline (CSS) — or GSAP ScrollTrigger if complex
|
||||
Scroll-progress-linked → ONE pattern from scroll-choreography.md (animation-timeline CSS); GSAP ScrollTrigger only when that pattern genuinely needs JS
|
||||
State change between two layouts → View Transitions API
|
||||
A list reflows in React → AutoAnimate
|
||||
A complex hero narrative with scrubbing → GSAP timeline + ScrollTrigger
|
||||
|
||||
@@ -7,7 +7,7 @@ Numbered stages tell the story of how the user uses the product over time. Each
|
||||
- **Divider:** thick numbered rule between stages.
|
||||
- **Button:** stage-internal links; one global "Start at stage 1 →" at the foot.
|
||||
- **Image:** small product capture per stage, often annotated.
|
||||
- **Reveal:** sweep horizontal as stages enter the viewport.
|
||||
- **Reveal:** sweep horizontal as stages enter the viewport (a scrub-linked stage sequence may use [`scroll-choreography.md`](../scroll-choreography.md) § Pinned chapter instead - one pattern).
|
||||
|
||||
Reach for it for products with explicit workflows — project management, design-to-dev pipelines, writing tools.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Sticky left pane (label / description) + scroll-synced right pane (screenshots c
|
||||
- **Divider:** section bands; the sticky pane re-anchors per section.
|
||||
- **Button:** in the sticky pane, set when the user reaches the section's detail count.
|
||||
- **Image:** the scrolling-right column is mostly imagery.
|
||||
- **Reveal:** none of the spatial-fade kind; the sticky/scroll IS the motion.
|
||||
- **Reveal:** none of the spatial-fade kind; the sticky/scroll IS the motion (choreograph it via [`scroll-choreography.md`](../scroll-choreography.md) § Sticky diptych or § Pinned chapter - one pattern).
|
||||
|
||||
Reach for it for premium products, complex feature stories, anything where you want to control pacing as the user scrolls.
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ Cap total stagger at ~500ms. Beyond that the page feels slow to settle.
|
||||
## Scroll-linked motion
|
||||
|
||||
- Use IntersectionObserver, **never** `scroll` event listeners.
|
||||
- Use it only for *reveal once* effects. No parallax. No scroll-scrubbed animations unless there is a specific reason.
|
||||
- Use it only for *reveal once* effects. No parallax. Scroll-scrubbed motion exists ONLY as the named patterns in [`scroll-choreography.md`](scroll-choreography.md), one per page; anything outside that vocabulary is banned.
|
||||
- Every scroll-triggered motion must have a reduced-motion fallback.
|
||||
|
||||
## State transitions
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# Scroll choreography - the one earned pattern
|
||||
|
||||
The default is still none: motion.md's IntersectionObserver reveal-once is the page norm. This file is the exception path - a scroll-linked pattern used ONCE, on structure, when the content genuinely has a sequence worth pinning or scrubbing. sloplint check F8 counts pattern families and flags a second one.
|
||||
|
||||
## Budget (non-negotiable)
|
||||
|
||||
- **One pattern per page.** Two scroll behaviours compete for the same attention.
|
||||
- **Never on body text.** Targets are structural: a pinned section, ONE display element, a 2px spine, figures. Prose never moves under the reader.
|
||||
- **Hero entrance rules unchanged.** The page-load orchestration in motion.md stays as is; scroll choreography never replaces it, and no element carries both.
|
||||
- **Static end-state mandatory.** Base CSS shows the finished layout. The choreography lives entirely inside `@supports (animation-timeline: view())` + `@media (prefers-reduced-motion: no-preference) and (min-width: 40rem)` - reduced-motion, small screens, and non-supporting browsers get the complete page.
|
||||
- **Order footgun:** the `animation` shorthand RESETS `animation-timeline` and `animation-range`. Declare them AFTER `animation`, always.
|
||||
- **`linear` is correct here** - scrubbing maps scroll distance to progress 1:1; an easing curve would make the page feel detached from the thumb.
|
||||
- A pattern counts as one of the page's three animation primitives (microinteractions.md).
|
||||
|
||||
## 1 · Pinned chapter - steps reveal while the section is pinned
|
||||
|
||||
The tall track owns a named view-timeline; the pinned pane's steps consume it in slices. (Elements inside a stuck pane cannot drive their own `view()` - they are not moving.)
|
||||
|
||||
```html
|
||||
<section class="chapter">
|
||||
<div class="chapter__pin">
|
||||
<h2>How it works</h2>
|
||||
<ol class="chapter__steps"><li>…</li><li>…</li><li>…</li></ol>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
```css
|
||||
.chapter { min-height: 280vh; } /* track length = pin duration */
|
||||
.chapter__pin { position: sticky; top: calc(var(--banner-height, 0px) + var(--space-xl)); }
|
||||
@supports (animation-timeline: view()) {
|
||||
@media (prefers-reduced-motion: no-preference) and (min-width: 40rem) {
|
||||
.chapter { view-timeline: --chapter block; }
|
||||
.chapter__steps li { animation: chapter-step linear both; animation-timeline: --chapter; }
|
||||
.chapter__steps li:nth-child(1) { animation-range: contain 5% contain 30%; }
|
||||
.chapter__steps li:nth-child(2) { animation-range: contain 30% contain 55%; }
|
||||
.chapter__steps li:nth-child(3) { animation-range: contain 55% contain 80%; }
|
||||
@keyframes chapter-step { from { opacity: 0.2; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2 · Scrub-linked headline - ONE display element, clip wipe
|
||||
|
||||
For a mid-page statement head (Ft5 / T3 register), never the hero h1 (that is load-orchestration territory). One element per page, ever.
|
||||
|
||||
```css
|
||||
.statement__head { /* base: fully drawn */ }
|
||||
@supports (animation-timeline: view()) {
|
||||
@media (prefers-reduced-motion: no-preference) and (min-width: 40rem) {
|
||||
.statement__head {
|
||||
animation: head-wipe linear both;
|
||||
animation-timeline: view();
|
||||
animation-range: entry 20% cover 45%;
|
||||
}
|
||||
@keyframes head-wipe {
|
||||
from { clip-path: inset(0 100% 0 0); transform: translateX(-0.25em); }
|
||||
to { clip-path: inset(0 0 0 0); transform: none; }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3 · Progress spine - scroll(root) scaling a 2px rule
|
||||
|
||||
Reading progress as a hairline, not a chrome bar. A functional indicator, so `linear` is canon. Hidden below 40rem (it eats the mobile gutter).
|
||||
|
||||
```css
|
||||
.spine { position: fixed; inset-block: 0; left: max(1rem, 2vw); width: 2px; background: var(--color-rule); }
|
||||
.spine::after { content: ""; position: absolute; inset: 0; background: var(--color-accent);
|
||||
transform-origin: top; transform: scaleY(0); }
|
||||
@media (max-width: 40rem) { .spine { display: none; } }
|
||||
@supports (animation-timeline: scroll()) {
|
||||
@media (prefers-reduced-motion: no-preference) and (min-width: 40rem) {
|
||||
.spine::after { animation: spine-grow linear both; animation-timeline: scroll(root); }
|
||||
@keyframes spine-grow { to { transform: scaleY(1); } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4 · Sticky diptych - alternating two-pane pin
|
||||
|
||||
Structure and pin mechanics come from [`components/f2-sticky-scroll-stack.md`](components/f2-sticky-scroll-stack.md) - markup, `--banner-height` offset, and mobile collapse are defined there; do not restate them. This recipe adds only the choreography: the scrolling figures fade in on their own `view()` (they move, so self-timelines work), and consecutive diptychs alternate the pinned side.
|
||||
|
||||
```css
|
||||
/* f2 base: .sticky-stack grid + .pane-sticky pin, per the component file */
|
||||
.sticky-stack:nth-of-type(even) .pane-sticky { order: 2; } /* alternate the pin side */
|
||||
@supports (animation-timeline: view()) {
|
||||
@media (prefers-reduced-motion: no-preference) and (min-width: 40rem) {
|
||||
.pane-scroll figure { animation: fig-in linear both; animation-timeline: view();
|
||||
animation-range: entry 0% entry 60%; }
|
||||
@keyframes fig-in { from { opacity: 0.3; transform: translateY(12px); } to { opacity: 1; transform: none; } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## JS fallback (optional, shared)
|
||||
|
||||
When JS is present and `animation-timeline` is not, the one sanctioned fallback is motion.md's reveal-once - never a scroll listener, never a scrub polyfill:
|
||||
|
||||
```html
|
||||
<script>
|
||||
if (!CSS.supports('animation-timeline: view()')) {
|
||||
const io = new IntersectionObserver((es) => es.forEach((e) => {
|
||||
if (e.isIntersecting) { e.target.classList.add('is-seen'); io.unobserve(e.target); }
|
||||
}), { rootMargin: '0px 0px -20%' });
|
||||
document.querySelectorAll('[data-reveal]').forEach((el) => io.observe(el));
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Pair with a 420ms `--ease-out` transition to the end state. Without JS and without support, the base static layout is already the finished page - that is the contract.
|
||||
Reference in New Issue
Block a user