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:
@@ -35,6 +35,8 @@
|
|||||||
| `Ticker` | L3 | ✅ | 无限滚动的信息条 | `children` `speed` `paused` `label` |
|
| `Ticker` | L3 | ✅ | 无限滚动的信息条 | `children` `speed` `paused` `label` |
|
||||||
| `Layout` | L3 | ✅ | 页面骨架:页头 / 侧栏 / 内容 / 页脚 | `header` `sider` `aside` `maxWidth` `gap` `height` |
|
| `Layout` | L3 | ✅ | 页面骨架:页头 / 侧栏 / 内容 / 页脚 | `header` `sider` `aside` `maxWidth` `gap` `height` |
|
||||||
| `Card` | L3 | ✅ | 白色容器(1px 极浅描边) | `as` `padding` `hoverable` |
|
| `Card` | L3 | ✅ | 白色容器(1px 极浅描边) | `as` `padding` `hoverable` |
|
||||||
|
| `EntryRow` | L2 | ✅ | 条目行:头像 + 标题 + 描述,整行可点 | `title` `description` `href` `avatar` `badge` `onOpen` |
|
||||||
|
| `EntryTile` | L3 | ✅ | 条目磁贴:上图下文白卡 | `title` `description` `cover` `ratio` `onOpen` |
|
||||||
| `Cover` | L3 | ✅ | 比例封面卡(2:3 / 4:3 / 16:9 + 标题遮罩) | `ratio` `src` `caption` `lift` `onClick` |
|
| `Cover` | L3 | ✅ | 比例封面卡(2:3 / 4:3 / 16:9 + 标题遮罩) | `ratio` `src` `caption` `lift` `onClick` |
|
||||||
| `Image` | L3 | ✅ | 带比例 / 圆角 / 失败降级的图片 | `ratio` `fit` `radius` `fallback` |
|
| `Image` | L3 | ✅ | 带比例 / 圆角 / 失败降级的图片 | `ratio` `fit` `radius` `fallback` |
|
||||||
| `FormField` | L3 | ✅ | 表单字段排版(标签 + 控件 + 提示/错误) | `label` `hint` `error` `required` |
|
| `FormField` | L3 | ✅ | 表单字段排版(标签 + 控件 + 提示/错误) | `label` `hint` `error` `required` |
|
||||||
@@ -767,3 +769,4 @@
|
|||||||
| 2026-10-30 | **修复**:`PasswordInput` 的眼睛按钮移入输入框内部(`Input` 新增 `suffix` 插槽);`Input` 隐藏 Edge 原生 `::-ms-reveal` / `::-ms-clear`,避免与自带显隐按钮叠成两个眼睛。兼容性:`Input` 新增可选 prop,无破坏性;`PasswordInput` API 不变、仅外观变化 |
|
| 2026-10-30 | **修复**:`PasswordInput` 的眼睛按钮移入输入框内部(`Input` 新增 `suffix` 插槽);`Input` 隐藏 Edge 原生 `::-ms-reveal` / `::-ms-clear`,避免与自带显隐按钮叠成两个眼睛。兼容性:`Input` 新增可选 prop,无破坏性;`PasswordInput` API 不变、仅外观变化 |
|
||||||
| 2026-10-31 | `Layout` 新增 `toolbar` 插槽:页头与页身之间的一条真实占位带(搜索 / 筛选 / 操作),宽度跟随容器(`contained` 时与侧栏 + 内容同宽),在 `height="screen"` 下不参与内部滚动。用它替代「搜索框 fixed + `contentOffset` 让位」的写法,页面不再需要用 JS 改 DOM 尺寸。兼容性:纯新增可选 prop,`contentOffset` 行为不变 |
|
| 2026-10-31 | `Layout` 新增 `toolbar` 插槽:页头与页身之间的一条真实占位带(搜索 / 筛选 / 操作),宽度跟随容器(`contained` 时与侧栏 + 内容同宽),在 `height="screen"` 下不参与内部滚动。用它替代「搜索框 fixed + `contentOffset` 让位」的写法,页面不再需要用 JS 改 DOM 尺寸。兼容性:纯新增可选 prop,`contentOffset` 行为不变 |
|
||||||
| 2026-11-01 | 新增三个「站点外壳」组件,把应用里各自手写的一套收进库:`Brand`(logo / 首字标记 + 名称,`href` 可选)、`Ticker`(无限滚动信息条,按实测宽度位移,循环不跳帧)、`SearchField`(语义版搜索框,`variant` 切普通 / 悬浮条)。同时新增展示级令牌 `--su-font-size-2xl` / `--su-line-height-2xl`(32 / 40)。兼容性:纯新增,无破坏 |
|
| 2026-11-01 | 新增三个「站点外壳」组件,把应用里各自手写的一套收进库:`Brand`(logo / 首字标记 + 名称,`href` 可选)、`Ticker`(无限滚动信息条,按实测宽度位移,循环不跳帧)、`SearchField`(语义版搜索框,`variant` 切普通 / 悬浮条)。同时新增展示级令牌 `--su-font-size-2xl` / `--su-line-height-2xl`(32 / 40)。兼容性:纯新增,无破坏 |
|
||||||
|
| 2026-11-01 | 新增 `EntryRow` / `EntryTile`:把应用里两套手写内容卡(`.app-card` / `.ces-card`)收进库。两者共用同一套点击语义(无修饰键接管、按住修饰键保留默认)与悬停反馈;`EntryTile` 由 `Card padding="none" hoverable` + `Cover` 拼成。兼容性:纯新增 |
|
||||||
|
|||||||
+39
-1
@@ -1,4 +1,4 @@
|
|||||||
import { Badge, Card, Icon, IconButton, Space } from '../../src'
|
import { Badge, Card, EntryRow, EntryTile, Icon, IconButton, ItemBadge, Space } from '../../src'
|
||||||
import { Demo } from '../components/Demo'
|
import { Demo } from '../components/Demo'
|
||||||
import { PropsTable } from '../components/PropsTable'
|
import { PropsTable } from '../components/PropsTable'
|
||||||
import { Section } from '../components/Section'
|
import { Section } from '../components/Section'
|
||||||
@@ -94,12 +94,50 @@ export function Cards() {
|
|||||||
</Card>
|
</Card>
|
||||||
</Demo>
|
</Demo>
|
||||||
|
|
||||||
|
<Demo
|
||||||
|
title="条目行与条目磁贴"
|
||||||
|
desc="同一份内容数据的两种版式:EntryRow 是横向一行(头像 + 标题 + 描述),EntryTile 是上图下文的白卡。点击语义一致——不带修饰键时接管点击,按住 Ctrl/Cmd 时保留浏览器默认行为。"
|
||||||
|
layout="column"
|
||||||
|
code={`<EntryRow title="GitHub" description="代码托管与开源社区" fallback="G" badge={<ItemBadge tone="hot">HOT</ItemBadge>} />
|
||||||
|
<EntryTile title="可塑性极强的玻璃" description="前沿科技" ratio={16 / 10} />`}
|
||||||
|
>
|
||||||
|
<EntryRow
|
||||||
|
title="GitHub"
|
||||||
|
description="代码托管与开源社区"
|
||||||
|
fallback="G"
|
||||||
|
badge={<ItemBadge tone="hot">HOT</ItemBadge>}
|
||||||
|
onOpen={() => undefined}
|
||||||
|
/>
|
||||||
|
<EntryRow title="知乎" description="深度内容问答社区" fallback="知" onOpen={() => undefined} />
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 'var(--su-space-3)' }}>
|
||||||
|
<EntryTile
|
||||||
|
title="可塑性极强的玻璃,在设计师手里能变成什么"
|
||||||
|
description="前沿科技"
|
||||||
|
ratio={16 / 10}
|
||||||
|
onOpen={() => undefined}
|
||||||
|
/>
|
||||||
|
<EntryTile title="废旧共享单车如何变成微型共享…" description="环保设计" ratio={16 / 10} />
|
||||||
|
</div>
|
||||||
|
</Demo>
|
||||||
|
|
||||||
<PropsTable
|
<PropsTable
|
||||||
rows={[
|
rows={[
|
||||||
{ name: 'as', type: "'div' | 'section' | 'article' | 'aside'", defaultValue: "'div'", desc: '渲染标签,按语义选' },
|
{ name: 'as', type: "'div' | 'section' | 'article' | 'aside'", defaultValue: "'div'", desc: '渲染标签,按语义选' },
|
||||||
{ name: 'padding', type: "'none' | 'sm' | 'md' | 'lg'", defaultValue: "'md'", desc: '内边距档位' },
|
{ name: 'padding', type: "'none' | 'sm' | 'md' | 'lg'", defaultValue: "'md'", desc: '内边距档位' },
|
||||||
{ name: 'hoverable', type: 'boolean', defaultValue: 'false', desc: '悬停加深阴影' },
|
{ name: 'hoverable', type: 'boolean', defaultValue: 'false', desc: '悬停加深阴影' },
|
||||||
{ name: 'className / style', type: 'string / CSSProperties', desc: '布局微调,例如绝对定位角标' },
|
{ name: 'className / style', type: 'string / CSSProperties', desc: '布局微调,例如绝对定位角标' },
|
||||||
|
{ name: 'EntryRow.title', type: 'string', required: true, desc: '标题' },
|
||||||
|
{ name: 'EntryRow.description', type: 'string', desc: '单行描述' },
|
||||||
|
{ name: 'EntryRow.href', type: 'string', desc: '整行链接地址' },
|
||||||
|
{ name: 'EntryRow.avatar / fallback', type: 'string', desc: '头像图片 / 首字占位' },
|
||||||
|
{ name: 'EntryRow.verified', type: 'boolean', desc: '头像右下角认证勾' },
|
||||||
|
{ name: 'EntryRow.badge', type: 'ReactNode', desc: '标题右侧角标(如 HOT)' },
|
||||||
|
{ name: 'EntryRow.onOpen', type: '() => void', desc: '接管点击;不传保持链接行为' },
|
||||||
|
{ name: 'EntryTile.title', type: 'string', required: true, desc: '标题' },
|
||||||
|
{ name: 'EntryTile.description', type: 'string', desc: '单行描述' },
|
||||||
|
{ name: 'EntryTile.cover', type: 'string', desc: '封面地址' },
|
||||||
|
{ name: 'EntryTile.ratio', type: 'number', defaultValue: '16 / 10', desc: '封面宽高比' },
|
||||||
|
{ name: 'EntryTile.onOpen', type: '() => void', desc: '点击整张卡' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@@ -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 { Ticker } from './components/Ticker'
|
||||||
export type { TickerProps } 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 { Cover } from './components/Cover'
|
||||||
export type { CoverProps, CoverRatio } from './components/Cover'
|
export type { CoverProps, CoverRatio } from './components/Cover'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user