feat(ui): 新增 EntryRow / EntryTile,收掉应用里两套手写内容卡
- EntryRow:头像 + 标题(可带角标)+ 单行描述,整行可点; 点击语义内置(无修饰键接管,按住 Ctrl/Cmd/Shift/Alt 保留浏览器默认) - EntryTile:上图下文白卡,由 Card padding=none hoverable + Cover 拼成 - 展示页 #cards 补两个示例与 props,COMPONENTS.md 补契约与 changelog 兼容性:纯新增;应用侧的旧类名(.app-card / .ces-card / .ces-body)随之删除
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--su-space-4);
|
||||
padding: var(--su-space-2) 0;
|
||||
border-radius: var(--su-radius-sm);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.root:hover .title {
|
||||
color: var(--su-color-primary);
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--su-space-1);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--su-space-2);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.root .title {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Avatar } from './Avatar'
|
||||
import { Text } from './Typography'
|
||||
import styles from './EntryRow.module.css'
|
||||
|
||||
/**
|
||||
* 条目行:头像 + 标题(可带角标)+ 单行描述,整行可点。
|
||||
*
|
||||
* 点击语义跟站点里的其它卡片一致:不带修饰键时接管点击(走 SPA 路由),
|
||||
* 按住 Ctrl/Cmd/Shift/Alt 时保留浏览器默认行为(新标签打开原始链接)。
|
||||
*/
|
||||
export type EntryRowProps = {
|
||||
title: string
|
||||
description?: string
|
||||
/** 不带修饰键点击时跳转的地址 */
|
||||
href?: string
|
||||
/** 头像图片;不传时用 fallback 首字 */
|
||||
avatar?: string
|
||||
fallback?: string
|
||||
/** 头像右下角的认证勾 */
|
||||
verified?: boolean
|
||||
/** 标题右侧的角标,例如 `<ItemBadge tone="hot">HOT</ItemBadge>` */
|
||||
badge?: ReactNode
|
||||
/** 接管点击;不传则保持普通链接行为 */
|
||||
onOpen?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function EntryRow({
|
||||
title,
|
||||
description,
|
||||
href = '#',
|
||||
avatar,
|
||||
fallback,
|
||||
verified = false,
|
||||
badge,
|
||||
onOpen,
|
||||
className,
|
||||
}: EntryRowProps) {
|
||||
return (
|
||||
<a
|
||||
className={[styles.root, className].filter(Boolean).join(' ')}
|
||||
href={href}
|
||||
title={title}
|
||||
onClick={(event) => {
|
||||
if (onOpen === undefined) return
|
||||
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
|
||||
event.preventDefault()
|
||||
onOpen()
|
||||
}}
|
||||
>
|
||||
<Avatar size="lg" tone="primary" src={avatar} fallback={fallback} verified={verified} />
|
||||
|
||||
<span className={styles.info}>
|
||||
<span className={styles.titleRow}>
|
||||
<Text strong truncate className={styles.title}>
|
||||
{title}
|
||||
</Text>
|
||||
{badge}
|
||||
</span>
|
||||
{description !== undefined && description !== '' && (
|
||||
<Text size="xs" tone="muted" truncate>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
.root {
|
||||
overflow: hidden;
|
||||
transition: transform var(--su-duration-fast) var(--su-ease-in-out);
|
||||
}
|
||||
|
||||
/* 悬停时整张卡浮起 2px,与 Cover 的 lift 是同一套手感 */
|
||||
.root:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--su-space-1);
|
||||
padding: var(--su-space-3);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.bodyButton {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: none;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bodyButton:hover {
|
||||
background-color: var(--su-bg-subtle);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.root {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.root:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Card } from './Card'
|
||||
import { Cover } from './Cover'
|
||||
import { Text } from './Typography'
|
||||
import styles from './EntryTile.module.css'
|
||||
|
||||
/**
|
||||
* 条目磁贴:上图下文的白卡,整张卡可点。
|
||||
*
|
||||
* 与 `EntryRow` 的区别是版式(横向一行 / 纵向一卡),交互语义一致:
|
||||
* 不带修饰键时接管点击,按住修饰键保留浏览器默认行为。
|
||||
*/
|
||||
export type EntryTileProps = {
|
||||
title: string
|
||||
description?: string
|
||||
cover?: string
|
||||
/** 封面宽高比,默认 16 / 10 */
|
||||
ratio?: number
|
||||
/** 点击整张卡;不传则只是展示 */
|
||||
onOpen?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function EntryTile({
|
||||
title,
|
||||
description,
|
||||
cover,
|
||||
ratio = 16 / 10,
|
||||
onOpen,
|
||||
className,
|
||||
}: EntryTileProps) {
|
||||
const body = (
|
||||
<>
|
||||
<Text strong truncate>
|
||||
{title}
|
||||
</Text>
|
||||
{description !== undefined && description !== '' && (
|
||||
<Text size="xs" tone="muted" truncate>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Card padding="none" hoverable className={[styles.root, className].filter(Boolean).join(' ')}>
|
||||
<Cover src={cover} alt={title} ratio={ratio} radius="none" onClick={onOpen} />
|
||||
|
||||
{onOpen === undefined ? (
|
||||
<div className={styles.body}>{body}</div>
|
||||
) : (
|
||||
<button type="button" className={`${styles.body} ${styles.bodyButton}`} title={title} onClick={onOpen}>
|
||||
{body}
|
||||
</button>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -37,6 +37,12 @@ export type { SearchFieldProps } from './components/SearchField'
|
||||
export { Ticker } from './components/Ticker'
|
||||
export type { TickerProps } from './components/Ticker'
|
||||
|
||||
export { EntryRow } from './components/EntryRow'
|
||||
export type { EntryRowProps } from './components/EntryRow'
|
||||
|
||||
export { EntryTile } from './components/EntryTile'
|
||||
export type { EntryTileProps } from './components/EntryTile'
|
||||
|
||||
export { Cover } from './components/Cover'
|
||||
export type { CoverProps, CoverRatio } from './components/Cover'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user