Skip to content

Commit c213861

Browse files
committed
feat: interactive logo, dual-mode syntax theme, rainbow inline code
PlatonicLogo: - Drag-to-spin with quaternion rotation and momentum physics - Click faces for actions (next/prev shape, play/pause, spin, see-through) - Preferences persisted to localStorage, auto-resume after 5s inactivity - Morph scale shrink during transitions to prevent face overlap - Translucent see-through mode with chroma boost, hover-to-opaque per face - Light mode face darkening via color-mix to compensate for white bleed - Touch support, pre-hydration 3D tilt, nav logo hidden on first paint Syntax highlighting: - Dual-mode chromatic tokens: warm light (blue/purple/green/red) vs cool dark - Tagged template literal and import binding detection - Regenerated CVD-optimized light palette at L=0.64 via qlab Homepage & styling: - Mobile latest post card above logo, centered - Rainbow gradient inline code tags with 3x dark mode intensity - Darkened textMuted for better light mode emdash/byline contrast - Button > p margin reset for MDX paragraphs
1 parent d1871e9 commit c213861

19 files changed

Lines changed: 1005 additions & 280 deletions

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Before editing anything that touches styled-components APIs (`createTheme`, `The
4747
- Releases page uses `markdown-to-jsx`, not MDX.
4848
- `${ClientComponent} &` selector interpolation in a styled template calls the referenced component's `.toString()`, which trips RSC's client-reference guard from a server module. Any styled component using this pattern must stay `'use client'`. Currently applies to `CodeBlock.tsx` (uses `${Note} &`).
4949
- Import code mixins (`codeTextMixin`, `editorMixin`) from `components/codeMixins.ts`, not from `LiveEdit`. Importing from `LiveEdit` pulls `react-live-runner` + `sucrase` into the client bundle of every consumer.
50-
- `utils/logoPalette.ts` is the single source of `LOGO_LUM`, `LOGO_CHROMA`, `logoHueRing()`. `PlatonicLogo.tsx` and `CelebrationEffect.tsx` import from it; don't duplicate the constants at call sites.
50+
- `utils/logoPalette.ts` is the single source of the 20-step hue ring (offset 23°). Three L/C tiers: `logoPalette` (bright, for logo/celebration), `lightPalette` (CVD-optimized for light bg), `darkPalette` (CVD-optimized for dark bg). `theme.palette[N]` switches light/dark automatically. Syntax colors reference palette indices directly (e.g. `P[14]` for violet). Don't hand-pick oklch values — pick a palette index.
51+
- Blog posts are assembled dynamically from MDX files at build time by `utils/blog.server.ts`. No JSON index to maintain — just create the MDX file with `export const meta`.
5152
- `PlatonicLogo.tsx` faces must NOT use `backface-visibility: hidden`. Per-face axis-angle interpolation during morph transitions can briefly flip a face normal and cull mid-animation. `transform-style: preserve-3d` z-sorts back faces naturally.
5253
- `CelebrationEffect.tsx` particles are `React.memo`'d; `onAnimationEnd` is a stable `useCallback` that reads `fwId`/`particleId` from `data-*` attributes. Don't close over IDs in per-item arrow functions — it defeats memoization on the particle list.

app/homepage.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
font-weight: var(--sc-fontWeight-display);
1717
font-size: 2.5rem;
1818
line-height: 1.15;
19-
margin: 0.75em 0 0.5em;
19+
margin: 1.25em 0 0.5em;
2020
text-align: left;
2121
}
2222

app/layout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ const themeScript = `
8585
d.dataset.theme = 'dark';
8686
}
8787
} catch (e) {}
88+
d.setAttribute('data-hero-logo-visible', '');
8889
})();
8990
`;
9091

app/not-found.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,31 +114,31 @@ const Comment = styled(Line)`
114114
`;
115115

116116
const Keyword = styled.span`
117-
color: ${theme.color.codeKeyword};
117+
color: ${theme.color.codeComment};
118118
`;
119119

120120
const ComponentName = styled.span`
121-
color: ${theme.color.codeNumber};
121+
color: ${theme.color.codeText};
122122
`;
123123

124124
const FnName = styled.span`
125-
color: ${theme.color.codePropertyDeep};
125+
color: ${theme.color.codeFunction};
126126
`;
127127

128128
const Punct = styled.span`
129-
color: ${theme.color.codePunctuation};
129+
color: ${theme.color.codeComment};
130130
`;
131131

132132
const Tick = styled.span`
133133
color: ${theme.color.codeString};
134134
`;
135135

136136
const Prop = styled.span`
137-
color: ${theme.color.codeProperty};
137+
color: ${theme.color.codeDeclaration};
138138
`;
139139

140140
const Value = styled.span`
141-
color: ${theme.color.codeConstant};
141+
color: ${theme.color.codeText};
142142
`;
143143

144144
const Message = styled.p`

app/page.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import Footer from '../components/Footer';
33
import HomepageBadges from '../components/HomepageBadges';
44
import HomepageHeroEditor, { HeroContent } from '../components/HomepageHeroEditor';
55
import LatestBlogPost from '../components/LatestBlogPost';
6+
import PlatonicLogo from '../components/LogoConcepts/PlatonicLogo';
7+
import HeroLogoObserver from '../components/HeroLogoObserver';
68
import { HomepageLogos, HomepageShowcase } from '../components/HomepageShowcase';
79
import HomepageGettingStartedContent from '../components/HomepageGettingStarted';
810
import { getPosts } from '@/utils/blog.server';
@@ -17,8 +19,10 @@ export default async function Index() {
1719

1820
<div className="hero-header">
1921
<HeroContent>
20-
<HomepageHeroEditor>
21-
<LatestBlogPost post={posts[0]} />
22+
<HomepageHeroEditor latestPost={<LatestBlogPost post={posts[0]} />}>
23+
<HeroLogoObserver>
24+
<PlatonicLogo size={200} hideLabel />
25+
</HeroLogoObserver>
2226

2327
<h1 className="hero-tagline">
2428
CSS for the <code>&lt;Component&gt;</code> Age

components/CelebrationEffect.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import React from 'react';
44
import styled, { css, createGlobalStyle, keyframes } from 'styled-components';
5-
import { logoHueRing } from '../utils/logoPalette';
5+
import { ringColor } from '../utils/logoPalette';
66

7-
const COLORS = logoHueRing(9);
7+
const COLORS = Array.from({ length: 9 }, (_, i) => ringColor(i * 2));
88

99
const CHARS = ['{', '}', '<', '>', '/', '*', '#', '.', ';', ':', '$', '&', '(', ')', '='];
1010
const TRAIL_CHARS = ['|', '.', '*', '·'];

components/CodeBlock.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const CodeBlock = styled(({ code, language = 'clike', className }: CodeBlockProp
3030
${codeTextMixin}
3131
3232
background-color: ${theme.color.codeBg};
33-
border-radius: ${theme.radius.md};
33+
border-radius: ${theme.radius.lg};
3434
border: 1px solid ${theme.color.border};
3535
box-shadow: 0 1px 3px ${theme.color.shadow};
3636
position: relative;

components/GlobalStyles.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ const GlobalStyles = createGlobalStyle`
8989
-moz-osx-font-smoothing: grayscale;
9090
}
9191
92+
button > p {
93+
margin: 0;
94+
}
95+
9296
#__next {
9397
overflow-x: hidden;
9498
}
@@ -167,15 +171,34 @@ const GlobalStyles = createGlobalStyle`
167171
}
168172
169173
code {
170-
background: ${theme.color.accentSubtle};
174+
--code-tint: 12%;
175+
background: linear-gradient(
176+
135deg,
177+
color-mix(in oklch, ${theme.palette[0]} var(--code-tint), transparent),
178+
color-mix(in oklch, ${theme.palette[5]} var(--code-tint), transparent),
179+
color-mix(in oklch, ${theme.palette[10]} var(--code-tint), transparent),
180+
color-mix(in oklch, ${theme.palette[15]} var(--code-tint), transparent)
181+
);
171182
border-radius: ${theme.radius.sm};
172183
font-family: ${font.mono};
173184
letter-spacing: -0.025em;
174-
padding: 0.15em 0.3em;
185+
padding: 0.15em 0.4em;
186+
}
187+
188+
html.dark code { --code-tint: 36%; }
189+
@media (prefers-color-scheme: dark) {
190+
html:not(.light) code { --code-tint: 36%; }
191+
}
192+
193+
pre code {
194+
background: none;
195+
padding: 0;
196+
border-radius: 0;
175197
}
176198
177199
h1 code {
178200
font-weight: ${theme.fontWeight.semibold};
201+
padding: 0 0.3em;
179202
}
180203
181204
/* ── Lists ────────────────────────────────────────────────── */

components/HeroLogoObserver.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use client';
2+
3+
import { useEffect, useRef } from 'react';
4+
5+
export default function HeroLogoObserver({ children }: { children: React.ReactNode }) {
6+
const ref = useRef<HTMLDivElement>(null);
7+
8+
useEffect(() => {
9+
const el = ref.current;
10+
if (!el) return;
11+
12+
const observer = new IntersectionObserver(
13+
([entry]) => {
14+
document.documentElement.toggleAttribute('data-hero-logo-visible', entry.isIntersecting);
15+
},
16+
{ threshold: 0 }
17+
);
18+
19+
observer.observe(el);
20+
return () => {
21+
observer.disconnect();
22+
document.documentElement.removeAttribute('data-hero-logo-visible');
23+
};
24+
}, []);
25+
26+
return <div ref={ref}>{children}</div>;
27+
}

components/HomepageHeroEditor.tsx

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,13 @@ const transformHeaderCode = (code: string) => `
9393
)
9494
`;
9595

96-
export default function HomepageHeroEditor({ children }: { children?: React.ReactNode }) {
96+
export default function HomepageHeroEditor({
97+
children,
98+
latestPost,
99+
}: {
100+
children?: React.ReactNode;
101+
latestPost?: React.ReactNode;
102+
}) {
97103
const id = React.useId();
98104
const liveScope = React.useMemo(() => createScope(id), [id]);
99105
const editorContainerRef = React.useRef<HTMLDivElement>(null);
@@ -108,6 +114,8 @@ export default function HomepageHeroEditor({ children }: { children?: React.Reac
108114
theme={prismTheme}
109115
>
110116
<HeroGrid>
117+
{latestPost && <MobileLatestPost>{latestPost}</MobileLatestPost>}
118+
111119
<HeroLeft>
112120
{children}
113121

@@ -116,10 +124,13 @@ export default function HomepageHeroEditor({ children }: { children?: React.Reac
116124
</Links>
117125
</HeroLeft>
118126

119-
<HeroRight ref={editorContainerRef}>
120-
<Editor theme={prismTheme} highlight={highlightTSXWithTagDepth} />
121-
<StyledError />
122-
</HeroRight>
127+
<EditorColumn>
128+
{latestPost && <DesktopLatestPost>{latestPost}</DesktopLatestPost>}
129+
<HeroRight ref={editorContainerRef}>
130+
<Editor theme={prismTheme} highlight={highlightTSXWithTagDepth} />
131+
<StyledError />
132+
</HeroRight>
133+
</EditorColumn>
123134
</HeroGrid>
124135
</LiveProvider>
125136
);
@@ -141,7 +152,7 @@ export const HeroContent = styled.div`
141152
const HeroGrid = styled.div`
142153
display: grid;
143154
grid-template-columns: auto 1fr;
144-
gap: ${theme.space[8]};
155+
gap: ${theme.space[12]};
145156
align-items: stretch;
146157
width: 100%;
147158
@@ -162,6 +173,13 @@ const HeroLeft = styled.div`
162173
`)}
163174
`;
164175

176+
const EditorColumn = styled.div`
177+
display: flex;
178+
flex-direction: column;
179+
align-items: flex-end;
180+
gap: ${theme.space[4]};
181+
`;
182+
165183
const HeroRight = styled.div`
166184
border: 1px solid ${theme.color.border};
167185
box-shadow: 0 1px 3px ${theme.color.shadow};
@@ -180,6 +198,25 @@ const Editor = styled(LiveEditor)`
180198
width: 100%;
181199
`;
182200

201+
const MobileLatestPost = styled.div`
202+
display: none;
203+
justify-content: center;
204+
order: -1;
205+
206+
${mobile(css`
207+
display: flex;
208+
`)}
209+
`;
210+
211+
const DesktopLatestPost = styled.div`
212+
display: flex;
213+
justify-content: flex-end;
214+
215+
${mobile(css`
216+
display: none;
217+
`)}
218+
`;
219+
183220
const Links = styled.div`
184221
margin: ${theme.space[8]} 0 0;
185222
`;

0 commit comments

Comments
 (0)