Skip to content

Commit 75708f8

Browse files
committed
feat(docs): add theme preset marquee banner on homepage
Add a horizontal auto-scrolling marquee showcasing 21 theme presets inspired by tweakcn.com, mapped to Tiny Design's token system. Clicking a preset applies it site-wide via CSS custom properties. - Replace 12 existing presets with 21 tweakcn-inspired ones - Add ThemeShowcase component with two-row CSS marquee animation - Sync active preset ID between homepage and theme editor via localStorage - Add locale strings for the new section (EN/ZH)
1 parent cefd857 commit 75708f8

9 files changed

Lines changed: 580 additions & 118 deletions

File tree

apps/docs/src/containers/home/home.scss

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,133 @@
267267
}
268268
}
269269

270+
// Theme showcase marquee
271+
&__theme-showcase {
272+
overflow: hidden;
273+
}
274+
275+
&__theme-showcase-desc.ty-typography {
276+
text-align: center;
277+
color: var(--ty-color-text-secondary);
278+
font-size: 16px;
279+
margin: -24px 0 32px;
280+
}
281+
282+
&__marquee-container {
283+
display: flex;
284+
flex-direction: column;
285+
gap: 16px;
286+
padding: 4px 0;
287+
mask-image: linear-gradient(to right, transparent, black 5%, black 95%, transparent);
288+
-webkit-mask-image: linear-gradient(to right, transparent, black 5%, black 95%, transparent);
289+
}
290+
291+
&__marquee-row {
292+
display: flex;
293+
gap: 16px;
294+
width: max-content;
295+
animation: marquee-left 50s linear infinite;
296+
297+
&:hover {
298+
animation-play-state: paused;
299+
}
300+
301+
&_reverse {
302+
animation-name: marquee-right;
303+
304+
&:hover {
305+
animation-play-state: paused;
306+
}
307+
}
308+
}
309+
310+
&__marquee-card {
311+
flex-shrink: 0;
312+
display: flex;
313+
flex-direction: column;
314+
align-items: center;
315+
gap: 10px;
316+
padding: 16px 20px;
317+
border: 1px solid var(--ty-color-border-secondary);
318+
border-radius: 12px;
319+
background: var(--ty-color-bg-component);
320+
cursor: pointer;
321+
transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s;
322+
outline: none;
323+
font-family: inherit;
324+
325+
&:hover {
326+
transform: translateY(-3px);
327+
border-color: var(--ty-color-primary);
328+
}
329+
330+
&_active {
331+
transform: translateY(-2px);
332+
}
333+
}
334+
335+
&__marquee-swatches {
336+
display: flex;
337+
gap: 6px;
338+
}
339+
340+
&__marquee-swatch {
341+
width: 20px;
342+
height: 20px;
343+
border-radius: 50%;
344+
border: 1px solid rgba(0, 0, 0, 0.08);
345+
}
346+
347+
&__marquee-name {
348+
font-size: 13px;
349+
color: var(--ty-color-text-secondary);
350+
white-space: nowrap;
351+
line-height: 1;
352+
}
353+
354+
&__theme-showcase-cta {
355+
text-align: center;
356+
margin-top: 28px;
357+
}
358+
359+
@keyframes marquee-left {
360+
from {
361+
transform: translateX(0);
362+
}
363+
to {
364+
transform: translateX(-50%);
365+
}
366+
}
367+
368+
@keyframes marquee-right {
369+
from {
370+
transform: translateX(-50%);
371+
}
372+
to {
373+
transform: translateX(0);
374+
}
375+
}
376+
377+
@media (max-width: $size-sm) {
378+
&__marquee-card {
379+
padding: 12px 14px;
380+
}
381+
382+
&__marquee-swatch {
383+
width: 16px;
384+
height: 16px;
385+
}
386+
387+
&__marquee-name {
388+
font-size: 12px;
389+
}
390+
391+
&__theme-showcase-desc.ty-typography {
392+
padding: 0 20px;
393+
font-size: 14px;
394+
}
395+
}
396+
270397
@keyframes logo-spin {
271398
from {
272399
transform: rotate(0deg);

apps/docs/src/containers/home/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from '@tiny-design/icons';
1010
import type { IconProps } from '@tiny-design/icons';
1111
import { Footer } from './footer';
12+
import { ThemeShowcase } from './theme-showcase';
1213
import { useLocaleContext } from '../../context/locale-context';
1314
import { getComponentMenu } from '../../routers';
1415
import pkg from '../../../../../packages/react/package.json';
@@ -81,6 +82,8 @@ const HomePage = (): React.ReactElement => {
8182
</Flex>
8283
</div>
8384

85+
<ThemeShowcase />
86+
8487
<div className="home__section">
8588
<Typography.Heading level={1} className="home__feature-title">{s.home.designPrinciple}</Typography.Heading>
8689
<Row gutter={[24, 24]} justify="center" className="home__features">
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import React, { useState, useCallback, useMemo } from 'react';
2+
import { useNavigate } from 'react-router-dom';
3+
import { Typography, Button } from '@tiny-design/react';
4+
import { useTheme } from '@tiny-design/react';
5+
import { PRESETS, getPresetSeeds, ThemePreset } from '../theme-editor/constants/presets';
6+
import { applyThemeToDOM, saveSeeds } from '../../utils/theme-persistence';
7+
import { useLocaleContext } from '../../context/locale-context';
8+
9+
const PRESET_ID_KEY = 'ty-theme-preset-id';
10+
11+
function loadActivePresetId(): string {
12+
try {
13+
return localStorage.getItem(PRESET_ID_KEY) || 'default';
14+
} catch {
15+
return 'default';
16+
}
17+
}
18+
19+
interface PresetCardProps {
20+
preset: ThemePreset;
21+
isActive: boolean;
22+
isZh: boolean;
23+
onClick: () => void;
24+
}
25+
26+
const PresetCard = ({ preset, isActive, isZh, onClick }: PresetCardProps): React.ReactElement => (
27+
<button
28+
className={`home__marquee-card${isActive ? ' home__marquee-card_active' : ''}`}
29+
onClick={onClick}
30+
title={isZh ? preset.nameZh : preset.name}
31+
style={
32+
isActive
33+
? { borderColor: preset.swatches[0], boxShadow: `0 0 0 1px ${preset.swatches[0]}` }
34+
: undefined
35+
}
36+
>
37+
<div className="home__marquee-swatches">
38+
{preset.swatches.map((color, i) => (
39+
<span key={i} className="home__marquee-swatch" style={{ backgroundColor: color }} />
40+
))}
41+
</div>
42+
<span className="home__marquee-name">{isZh ? preset.nameZh : preset.name}</span>
43+
</button>
44+
);
45+
46+
export const ThemeShowcase = (): React.ReactElement => {
47+
const navigate = useNavigate();
48+
const { siteLocale: s } = useLocaleContext();
49+
const isZh = s.locale === 'zh_CN';
50+
const { resolvedTheme } = useTheme();
51+
const isDark = resolvedTheme === 'dark';
52+
const [activeId, setActiveId] = useState(loadActivePresetId);
53+
54+
const handleSelect = useCallback(
55+
(preset: ThemePreset) => {
56+
const lightSeeds = getPresetSeeds(preset, false);
57+
const darkSeeds = preset.darkSeeds ?? undefined;
58+
const currentSeeds = getPresetSeeds(preset, isDark);
59+
60+
applyThemeToDOM(currentSeeds, isDark);
61+
saveSeeds(lightSeeds, darkSeeds);
62+
63+
setActiveId(preset.id);
64+
try {
65+
localStorage.setItem(PRESET_ID_KEY, preset.id);
66+
} catch {
67+
// ignore
68+
}
69+
},
70+
[isDark]
71+
);
72+
73+
// Split presets into two rows
74+
const { row1, row2 } = useMemo(() => {
75+
const mid = Math.ceil(PRESETS.length / 2);
76+
return {
77+
row1: PRESETS.slice(0, mid),
78+
row2: PRESETS.slice(mid),
79+
};
80+
}, []);
81+
82+
// Duplicate items for seamless loop
83+
const row1Items = useMemo(() => [...row1, ...row1], [row1]);
84+
const row2Items = useMemo(() => [...row2, ...row2], [row2]);
85+
86+
return (
87+
<div className="home__section home__theme-showcase">
88+
<Typography.Heading level={1} className="home__feature-title">
89+
{s.home.themeShowcase}
90+
</Typography.Heading>
91+
<Typography.Paragraph className="home__theme-showcase-desc">
92+
{s.home.themeShowcaseDesc}
93+
</Typography.Paragraph>
94+
95+
<div className="home__marquee-container">
96+
<div className="home__marquee-row">
97+
{row1Items.map((preset, i) => (
98+
<PresetCard
99+
key={`${preset.id}-${i}`}
100+
preset={preset}
101+
isActive={preset.id === activeId}
102+
isZh={isZh}
103+
onClick={() => handleSelect(preset)}
104+
/>
105+
))}
106+
</div>
107+
<div className="home__marquee-row home__marquee-row_reverse">
108+
{row2Items.map((preset, i) => (
109+
<PresetCard
110+
key={`${preset.id}-${i}`}
111+
preset={preset}
112+
isActive={preset.id === activeId}
113+
isZh={isZh}
114+
onClick={() => handleSelect(preset)}
115+
/>
116+
))}
117+
</div>
118+
</div>
119+
120+
<div className="home__theme-showcase-cta">
121+
<Button btnType="link" onClick={() => navigate('/theme/theme-editor')}>
122+
{s.home.themeShowcaseCustomize} &rarr;
123+
</Button>
124+
</div>
125+
</div>
126+
);
127+
};

0 commit comments

Comments
 (0)