Skip to content

Commit 4225433

Browse files
feat: falling autumn leaves and even wordmark spacing / 左侧秋叶飘落装饰与字标间距修复 (#1247)
* feat: add Halloween header wordmark with pumpkin overlay 中文:为页眉字标添加万圣节配色和南瓜叠加装饰,并覆盖多主题及响应式测试。 * feat: falling autumn leaves on the left edge and even wordmark spacing Adds a CSS-only strip of leaves drifting down the left page gutter for October. It is aria-hidden, ignores pointer input, sits under the sticky header, shrinks on phones, rests in place under prefers-reduced-motion, and is hidden in forced-colors mode and on embed routes. Also fixes uneven letter spacing in the Halloween wordmark and trims the pumpkin padding. 中文:在页面左侧边距添加缓慢飘落的秋叶装饰(纯 CSS,aria-hidden,不拦截点击, 位于固定页头下方,手机端缩小,开启“减少动态效果”时静止显示,在强制颜色模式和 embed 路由中隐藏);同时修复万圣节字标字母间距不均的问题,并收紧南瓜留白。 * fix: keep autumn leaves within responsive gutters 中文:限制落叶装饰在响应式留白内,缩小窄屏叶片和摆幅,并添加七种宽度的回归测试。 --------- Co-authored-by: functionstackx <47992694+functionstackx@users.noreply.github.com>
1 parent f705137 commit 4225433

6 files changed

Lines changed: 317 additions & 1 deletion

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { AutumnLeaves } from '@/components/autumn-leaves';
2+
3+
describe('Autumn leaf gutter regression', () => {
4+
for (const width of [320, 640, 768, 1024, 1280, 1440, 1536]) {
5+
it(`does not paint over content at ${width}px`, () => {
6+
cy.viewport(width, 900);
7+
cy.mount(
8+
<>
9+
<AutumnLeaves />
10+
<div className="container mx-auto px-4 lg:px-8">
11+
<button data-testid="content-edge" onClick={cy.stub().as('clicked')}>
12+
Content starts here
13+
</button>
14+
</div>
15+
</>,
16+
);
17+
cy.get('[data-testid="autumn-leaves"]')
18+
.should('have.css', 'pointer-events', 'none')
19+
.and('have.css', 'overflow', 'hidden')
20+
.then(($rail) => {
21+
const rail = $rail[0].getBoundingClientRect();
22+
cy.get('[data-testid="content-edge"]').then(($content) => {
23+
expect(rail.left).to.eq(0);
24+
expect(rail.right).to.be.at.most($content[0].getBoundingClientRect().left);
25+
});
26+
});
27+
cy.get('[data-testid="content-edge"]').click();
28+
cy.get('@clicked').should('have.been.calledOnce');
29+
});
30+
}
31+
});

‎packages/app/src/app/globals.css‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
/* October header treatment; keep chart and export branding unchanged. */
4545
.halloween-wordmark {
4646
color: #b54708;
47+
/* Hinted glyph advances at 18px left DM Sans visibly uneven ("Inf e r");
48+
unhinted advances plus explicit kerning keep the letters evenly set. */
49+
text-rendering: geometricPrecision;
50+
font-kerning: normal;
4751
}
4852

4953
.halloween-wordmark-x {
@@ -70,6 +74,89 @@
7074
}
7175
}
7276

77+
/* October decoration: leaves falling down the left edge (autumn-leaves.tsx).
78+
Sits under the sticky header (z-50) and never takes pointer input. */
79+
.autumn-leaves {
80+
position: fixed;
81+
inset: 0 auto 0 0;
82+
z-index: 40;
83+
/* At container breakpoints only the page padding remains available. */
84+
width: clamp(14px, calc((100vw - 1280px) / 2), 96px);
85+
--leaf-sway-scale: 1;
86+
overflow: hidden;
87+
pointer-events: none;
88+
}
89+
90+
.autumn-leaf {
91+
position: absolute;
92+
top: 0;
93+
left: var(--leaf-left);
94+
width: var(--leaf-size);
95+
height: var(--leaf-size);
96+
opacity: 0.85;
97+
animation: autumn-leaf-fall var(--leaf-duration) linear var(--leaf-delay) infinite;
98+
will-change: transform;
99+
}
100+
101+
.autumn-leaf-sway {
102+
display: block;
103+
width: 100%;
104+
height: 100%;
105+
animation: autumn-leaf-sway calc(var(--leaf-duration) / 4) ease-in-out var(--leaf-delay) infinite
106+
alternate;
107+
}
108+
109+
/* Tablet and laptop breakpoints need the same protection as phones. */
110+
@media (max-width: 1359px) {
111+
.autumn-leaves {
112+
--leaf-sway-scale: 0.1;
113+
}
114+
.autumn-leaf {
115+
left: calc(var(--leaf-left) * 0.4);
116+
width: calc(var(--leaf-size) * 0.25);
117+
height: calc(var(--leaf-size) * 0.25);
118+
}
119+
}
120+
121+
@keyframes autumn-leaf-fall {
122+
from {
123+
transform: translateY(-10vh);
124+
}
125+
to {
126+
transform: translateY(110vh);
127+
}
128+
}
129+
130+
@keyframes autumn-leaf-sway {
131+
from {
132+
transform: translateX(calc(var(--leaf-sway) * var(--leaf-sway-scale) * -1)) rotate(-35deg);
133+
}
134+
to {
135+
transform: translateX(calc(var(--leaf-sway) * var(--leaf-sway-scale))) rotate(40deg);
136+
}
137+
}
138+
139+
@media (prefers-reduced-motion: reduce) {
140+
.autumn-leaf,
141+
.autumn-leaf-sway {
142+
animation: none;
143+
}
144+
.autumn-leaf {
145+
top: var(--leaf-rest);
146+
transform: rotate(-20deg);
147+
}
148+
}
149+
150+
@media (forced-colors: active) {
151+
.autumn-leaves {
152+
display: none;
153+
}
154+
}
155+
156+
html[data-inferencex-embed] .autumn-leaves {
157+
display: none;
158+
}
159+
73160
/* Benchmark log viewer gutter. Line numbers come from a CSS counter rather than
74161
DOM text so they stay out of the element's text content and out of anything
75162
the reader copies from the pane. */

‎packages/app/src/app/layout.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Footer } from '@/components/footer/footer';
1111
import { Header } from '@/components/header/header';
1212
import { RouteTransition } from '@/components/motion/route-transition';
1313
import { JsonLd } from '@/components/json-ld';
14+
import { AutumnLeaves } from '@/components/autumn-leaves';
1415
import { CircuitBackground } from '@/components/circuit-background';
1516
import { MinecraftBackgroundLazy } from '@/components/minecraft/minecraft-background-lazy';
1617
import { MinecraftDecorations } from '@/components/minecraft/minecraft-decorations';
@@ -213,6 +214,7 @@ export default async function RootLayout({
213214
<CircuitBackground />
214215
<MinecraftBackgroundLazy />
215216
<MinecraftDecorations />
217+
<AutumnLeaves />
216218
<PostHogProvider>
217219
<JsonLd data={jsonLd} />
218220
<QueryProvider>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// @vitest-environment jsdom
2+
import { renderToStaticMarkup } from 'react-dom/server';
3+
import { describe, expect, it } from 'vitest';
4+
5+
import { AutumnLeaves } from './autumn-leaves';
6+
7+
describe('AutumnLeaves', () => {
8+
const html = renderToStaticMarkup(<AutumnLeaves />);
9+
const doc = new DOMParser().parseFromString(html, 'text/html');
10+
const strip = doc.querySelector('[data-testid="autumn-leaves"]')!;
11+
const leaves = [...doc.querySelectorAll<HTMLElement>('[data-testid="autumn-leaf"]')];
12+
13+
it('is hidden from assistive tech', () => {
14+
expect(strip.getAttribute('aria-hidden')).toBe('true');
15+
});
16+
17+
it('keeps phones to a few leaves and starts every leaf mid-fall', () => {
18+
expect(leaves.length).toBeGreaterThan(4);
19+
expect(leaves.filter((leaf) => !leaf.classList.contains('hidden')).length).toBe(4);
20+
for (const leaf of leaves) {
21+
expect(leaf.style.getPropertyValue('--leaf-delay')).toMatch(/^-\d+s$/);
22+
expect(leaf.style.getPropertyValue('--leaf-rest')).toMatch(/^\d+%$/);
23+
}
24+
});
25+
});
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import type { CSSProperties } from 'react';
2+
3+
/**
4+
* October decoration: a few leaves drifting down the left edge of the
5+
* viewport. Pure CSS (no client JS), aria-hidden and pointer-events: none so
6+
* it never intercepts clicks or reaches assistive tech. Negative delays start
7+
* each leaf mid-fall so the strip is populated on first paint. Under
8+
* `prefers-reduced-motion` the leaves rest in place instead of falling.
9+
*/
10+
type LeafShape = 'maple' | 'oval';
11+
12+
interface Leaf {
13+
shape: LeafShape;
14+
/** Horizontal position inside the strip, as a percentage. */
15+
left: number;
16+
/** Rendered size in px. */
17+
size: number;
18+
/** Fall duration in seconds. */
19+
duration: number;
20+
/** Negative start offset in seconds. */
21+
delay: number;
22+
/** Peak horizontal sway in px. */
23+
sway: number;
24+
/** Resting top position (%) used when motion is reduced. */
25+
rest: number;
26+
color: string;
27+
/** Rendered on wide viewports only, keeping phones uncluttered. */
28+
desktopOnly?: boolean;
29+
}
30+
31+
const LEAVES: readonly Leaf[] = [
32+
{
33+
shape: 'maple',
34+
left: 18,
35+
size: 32,
36+
duration: 14,
37+
delay: -2,
38+
sway: 18,
39+
rest: 14,
40+
color: '#c2410c',
41+
},
42+
{
43+
shape: 'oval',
44+
left: 62,
45+
size: 23,
46+
duration: 17,
47+
delay: -9,
48+
sway: 14,
49+
rest: 33,
50+
color: '#d97706',
51+
},
52+
{
53+
shape: 'maple',
54+
left: 40,
55+
size: 38,
56+
duration: 19,
57+
delay: -14,
58+
sway: 22,
59+
rest: 55,
60+
color: '#b91c1c',
61+
},
62+
{
63+
shape: 'oval',
64+
left: 10,
65+
size: 26,
66+
duration: 15,
67+
delay: -6,
68+
sway: 16,
69+
rest: 76,
70+
color: '#ca8a04',
71+
},
72+
{
73+
shape: 'maple',
74+
left: 74,
75+
size: 26,
76+
duration: 16,
77+
delay: -11,
78+
sway: 12,
79+
rest: 88,
80+
color: '#ea580c',
81+
desktopOnly: true,
82+
},
83+
{
84+
shape: 'oval',
85+
left: 34,
86+
size: 20,
87+
duration: 21,
88+
delay: -17,
89+
sway: 20,
90+
rest: 44,
91+
color: '#9a3412',
92+
desktopOnly: true,
93+
},
94+
{
95+
shape: 'maple',
96+
left: 55,
97+
size: 29,
98+
duration: 18,
99+
delay: -4,
100+
sway: 16,
101+
rest: 66,
102+
color: '#d97706',
103+
desktopOnly: true,
104+
},
105+
{
106+
shape: 'maple',
107+
left: 22,
108+
size: 28,
109+
duration: 20,
110+
delay: -12,
111+
sway: 18,
112+
rest: 6,
113+
color: '#c2410c',
114+
desktopOnly: true,
115+
},
116+
{
117+
shape: 'oval',
118+
left: 82,
119+
size: 22,
120+
duration: 13,
121+
delay: -8,
122+
sway: 10,
123+
rest: 22,
124+
color: '#b45309',
125+
desktopOnly: true,
126+
},
127+
];
128+
129+
const MAPLE_PATH =
130+
'M12 1l1.5 4.5 2.5-1-.5 4.5 4-2.5-.5 3 4 .5-3 3 1 1.5-5 1 .5 2.5-4-1.5V23h-1v-6.5l-4 1.5.5-2.5-5-1 1-1.5-3-3 4-.5-.5-3 4 2.5L8 4.5l2.5 1z';
131+
const OVAL_PATH =
132+
'M12 2C7 6 4.5 10 5 14.5c.5 4 3.5 6.5 7 7.5 3.5-1 6.5-3.5 7-7.5C19.5 10 17 6 12 2z';
133+
134+
function LeafGlyph({ shape, color }: { shape: LeafShape; color: string }) {
135+
return (
136+
<svg viewBox="0 0 24 24" width="100%" height="100%" focusable="false">
137+
<path d={shape === 'maple' ? MAPLE_PATH : OVAL_PATH} fill={color} />
138+
{shape === 'oval' && (
139+
<path d="M12 4.5V23" stroke="rgba(0,0,0,0.25)" strokeWidth="0.9" fill="none" />
140+
)}
141+
</svg>
142+
);
143+
}
144+
145+
export function AutumnLeaves() {
146+
return (
147+
<div aria-hidden="true" data-testid="autumn-leaves" className="autumn-leaves">
148+
{LEAVES.map((leaf, i) => (
149+
<span
150+
key={i}
151+
data-testid="autumn-leaf"
152+
className={leaf.desktopOnly ? 'autumn-leaf hidden sm:block' : 'autumn-leaf'}
153+
style={
154+
{
155+
'--leaf-left': `${leaf.left}%`,
156+
'--leaf-size': `${leaf.size}px`,
157+
'--leaf-duration': `${leaf.duration}s`,
158+
'--leaf-delay': `${leaf.delay}s`,
159+
'--leaf-sway': `${leaf.sway}px`,
160+
'--leaf-rest': `${leaf.rest}%`,
161+
} as CSSProperties
162+
}
163+
>
164+
<span className="autumn-leaf-sway">
165+
<LeafGlyph shape={leaf.shape} color={leaf.color} />
166+
</span>
167+
</span>
168+
))}
169+
</div>
170+
);
171+
}

‎packages/app/src/components/header/inferencex-wordmark.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export function InferenceXWordmark() {
22
return (
33
<span
44
data-testid="inferencex-wordmark"
5-
className="halloween-wordmark relative inline-block pr-2 text-lg font-bold tracking-tight"
5+
className="halloween-wordmark relative inline-block pr-1.5 text-lg font-bold tracking-tight"
66
>
77
Inference<span className="halloween-wordmark-x">X</span>
88
<span

0 commit comments

Comments
 (0)