Skip to content

Commit 2025a07

Browse files
committed
feat(blog): align blog components with lynx style
1 parent 860c8f1 commit 2025a07

12 files changed

Lines changed: 1504 additions & 0 deletions

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@
4949
"./antd": {
5050
"types": "./dist/antd/index.d.ts",
5151
"import": "./dist/antd/index.js"
52+
},
53+
"./blog-avatar": {
54+
"types": "./dist/blog-avatar/index.d.ts",
55+
"import": "./dist/blog-avatar/index.js"
56+
},
57+
"./blog-list": {
58+
"types": "./dist/blog-list/index.d.ts",
59+
"import": "./dist/blog-list/index.js"
5260
}
5361
},
5462
"repository": {

rslib.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export default defineConfig({
2525
entry: {
2626
'nav-icon': './src/nav-icon/index.tsx',
2727
benchmark: './src/benchmark/index.tsx',
28+
'blog-avatar': './src/blog-avatar/index.tsx',
29+
'blog-list': './src/blog-list/index.tsx',
2830
'tool-stack': './src/tool-stack/index.tsx',
2931
hero: './src/hero/index.tsx',
3032
'section-style': './src/section-style/index.tsx',

src/blog-avatar/index.module.scss

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
:global {
2+
html:not(.dark) {
3+
--rs-blog-avatar-link-hover-bg: rgba(249, 57, 32, 0.08);
4+
--rs-blog-avatar-group-ring: rgba(255, 255, 255, 0.92);
5+
--rs-blog-avatar-group-overflow-bg: var(--rp-c-bg-soft);
6+
}
7+
8+
html.dark {
9+
--rs-blog-avatar-link-hover-bg: rgba(255, 112, 77, 0.14);
10+
--rs-blog-avatar-group-ring: #111827;
11+
--rs-blog-avatar-group-overflow-bg: rgba(255, 255, 255, 0.08);
12+
}
13+
}
14+
15+
.root {
16+
display: inline-flex;
17+
align-items: center;
18+
gap: 10px;
19+
max-width: 100%;
20+
padding: 4px 0;
21+
}
22+
23+
.avatar {
24+
width: 40px;
25+
height: 40px;
26+
border-radius: 50%;
27+
object-fit: cover;
28+
flex-shrink: 0;
29+
pointer-events: none;
30+
box-shadow: 0 0 0 1px rgba(143, 161, 185, 0.18);
31+
}
32+
33+
.content {
34+
display: flex;
35+
flex-direction: column;
36+
min-width: 0;
37+
gap: 2px;
38+
}
39+
40+
.nameRow {
41+
display: flex;
42+
align-items: center;
43+
gap: 8px;
44+
min-width: 0;
45+
}
46+
47+
.name {
48+
color: var(--rp-c-text-1);
49+
font-size: 0.875rem;
50+
font-weight: 600;
51+
line-height: 1.25rem;
52+
white-space: nowrap;
53+
overflow: hidden;
54+
text-overflow: ellipsis;
55+
}
56+
57+
.title {
58+
color: var(--rp-c-text-2);
59+
font-size: 0.75rem;
60+
line-height: 1.35;
61+
white-space: nowrap;
62+
overflow: hidden;
63+
text-overflow: ellipsis;
64+
}
65+
66+
.links {
67+
display: flex;
68+
align-items: center;
69+
gap: 4px;
70+
color: var(--rp-c-text-2);
71+
opacity: 0.55;
72+
transition:
73+
opacity 0.2s ease,
74+
color 0.2s ease;
75+
}
76+
77+
.root:hover .links,
78+
.root:focus-within .links {
79+
opacity: 1;
80+
}
81+
82+
.link {
83+
display: inline-flex;
84+
align-items: center;
85+
justify-content: center;
86+
width: 20px;
87+
height: 20px;
88+
border-radius: 999px;
89+
color: inherit;
90+
transition:
91+
color 0.2s ease,
92+
background-color 0.2s ease;
93+
94+
&:hover,
95+
&:focus-visible {
96+
color: var(--rp-c-brand);
97+
background-color: var(--rs-blog-avatar-link-hover-bg);
98+
}
99+
100+
&:focus-visible {
101+
outline: 2px solid var(--rp-c-brand);
102+
outline-offset: 2px;
103+
}
104+
105+
svg {
106+
width: 12px;
107+
height: 12px;
108+
}
109+
}
110+
111+
.group {
112+
display: flex;
113+
flex-wrap: wrap;
114+
gap: 10px 20px;
115+
max-width: 100%;
116+
}
117+
118+
.groupItem {
119+
max-width: 100%;
120+
}
121+
122+
.compactGroup {
123+
display: flex;
124+
align-items: center;
125+
gap: 10px;
126+
min-width: 0;
127+
max-width: 100%;
128+
}
129+
130+
.compactAvatars {
131+
display: flex;
132+
align-items: center;
133+
flex-shrink: 0;
134+
}
135+
136+
.compactAvatar,
137+
.compactOverflow {
138+
width: 28px;
139+
height: 28px;
140+
border-radius: 50%;
141+
box-shadow: 0 0 0 2px var(--rs-blog-avatar-group-ring);
142+
}
143+
144+
.compactAvatar {
145+
object-fit: cover;
146+
background-color: var(--rp-c-bg-soft);
147+
}
148+
149+
.compactAvatar + .compactAvatar,
150+
.compactOverflow {
151+
margin-left: -8px;
152+
}
153+
154+
.compactOverflow {
155+
display: inline-flex;
156+
align-items: center;
157+
justify-content: center;
158+
color: var(--rp-c-text-1);
159+
font-size: 0.75rem;
160+
font-weight: 600;
161+
line-height: 1;
162+
background-color: var(--rs-blog-avatar-group-overflow-bg);
163+
}
164+
165+
.compactNames {
166+
min-width: 0;
167+
color: var(--rp-c-text-2);
168+
font-size: 0.875rem;
169+
line-height: 1.35;
170+
white-space: nowrap;
171+
overflow: hidden;
172+
text-overflow: ellipsis;
173+
}
174+
175+
@media (max-width: 768px) {
176+
.group {
177+
gap: 10px 16px;
178+
}
179+
180+
.compactGroup {
181+
gap: 8px;
182+
}
183+
184+
.compactNames {
185+
font-size: 0.8125rem;
186+
}
187+
}

src/blog-avatar/index.tsx

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import type { ReactNode } from 'react';
2+
import styles from './index.module.scss';
3+
4+
const MAX_VISIBLE_COMPACT_AUTHORS = 3;
5+
6+
export type BlogAvatarLink = {
7+
href: string;
8+
label: string;
9+
icon: ReactNode;
10+
};
11+
12+
export type BlogAvatarAuthor = {
13+
name: string;
14+
avatar: string;
15+
title?: string;
16+
github?: string;
17+
x?: string;
18+
links?: BlogAvatarLink[];
19+
};
20+
21+
export type BlogAvatarProps = {
22+
author: BlogAvatarAuthor;
23+
className?: string;
24+
};
25+
26+
export type BlogAvatarGroupProps = {
27+
authors: BlogAvatarAuthor[];
28+
className?: string;
29+
compact?: boolean;
30+
};
31+
32+
const getClassName = (...classNames: Array<string | false | undefined>) => {
33+
return classNames.filter(Boolean).join(' ');
34+
};
35+
36+
const getAuthorKey = (author: BlogAvatarAuthor) => {
37+
return `${author.name}-${author.github ?? author.x ?? author.avatar}`;
38+
};
39+
40+
const GitHubIcon = () => (
41+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
42+
<path
43+
fill="currentColor"
44+
d="M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
45+
/>
46+
</svg>
47+
);
48+
49+
const XIcon = () => (
50+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
51+
<path
52+
fill="currentColor"
53+
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584l-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
54+
/>
55+
</svg>
56+
);
57+
58+
const getSocialLinks = ({
59+
github,
60+
links,
61+
name,
62+
x,
63+
}: BlogAvatarAuthor): BlogAvatarLink[] => {
64+
return [
65+
...(github
66+
? [
67+
{
68+
href: github,
69+
label: `${name}'s GitHub`,
70+
icon: <GitHubIcon />,
71+
},
72+
]
73+
: []),
74+
...(x
75+
? [
76+
{
77+
href: x,
78+
label: `${name}'s X profile`,
79+
icon: <XIcon />,
80+
},
81+
]
82+
: []),
83+
...(links ?? []),
84+
];
85+
};
86+
87+
export function BlogAvatar({ author, className }: BlogAvatarProps) {
88+
const { avatar, name, title } = author;
89+
const socialLinks = getSocialLinks(author);
90+
91+
return (
92+
<div className={getClassName(styles.root, className)}>
93+
<img
94+
src={avatar}
95+
alt={name}
96+
className={styles.avatar}
97+
loading="lazy"
98+
decoding="async"
99+
/>
100+
<div className={styles.content}>
101+
<div className={styles.nameRow}>
102+
<div className={styles.name}>{name}</div>
103+
{socialLinks.length > 0 ? (
104+
<div className={styles.links}>
105+
{socialLinks.map(link => (
106+
<a
107+
href={link.href}
108+
key={`${link.label}-${link.href}`}
109+
target="_blank"
110+
rel="noopener noreferrer"
111+
className={styles.link}
112+
aria-label={link.label}
113+
>
114+
{link.icon}
115+
</a>
116+
))}
117+
</div>
118+
) : null}
119+
</div>
120+
{title ? <div className={styles.title}>{title}</div> : null}
121+
</div>
122+
</div>
123+
);
124+
}
125+
126+
export function BlogAvatarGroup({
127+
authors,
128+
className,
129+
compact = false,
130+
}: BlogAvatarGroupProps) {
131+
if (authors.length === 0) {
132+
return null;
133+
}
134+
135+
if (!compact) {
136+
return (
137+
<div className={getClassName(styles.group, className)}>
138+
{authors.map(author => (
139+
<BlogAvatar
140+
author={author}
141+
className={styles.groupItem}
142+
key={getAuthorKey(author)}
143+
/>
144+
))}
145+
</div>
146+
);
147+
}
148+
149+
const visibleAuthors = authors.slice(0, MAX_VISIBLE_COMPACT_AUTHORS);
150+
const overflowCount = authors.length - visibleAuthors.length;
151+
const authorNames = authors.map(author => author.name).join(', ');
152+
153+
return (
154+
<div
155+
className={getClassName(styles.compactGroup, className)}
156+
title={authorNames}
157+
>
158+
<div className={styles.compactAvatars} aria-hidden="true">
159+
{visibleAuthors.map(author => (
160+
<img
161+
src={author.avatar}
162+
alt=""
163+
className={styles.compactAvatar}
164+
loading="lazy"
165+
decoding="async"
166+
key={getAuthorKey(author)}
167+
/>
168+
))}
169+
{overflowCount > 0 ? (
170+
<span className={styles.compactOverflow}>+{overflowCount}</span>
171+
) : null}
172+
</div>
173+
<div className={styles.compactNames}>{authorNames}</div>
174+
</div>
175+
);
176+
}

0 commit comments

Comments
 (0)