feat(ui): 新增站点外壳组件 Brand / SearchField / Ticker

把应用里各自手写的三块收进库,风格沿用前台、行为参数化:

- Brand:logo 图片或首字圆形标记 + 名称,href 决定是链接还是纯展示,size/tone 可选
- SearchField:语义版搜索框(onChange 直接给字符串),variant 切 plain / card,
  支持 shortcut 提示与 hotkey 聚焦
- Ticker:无限滚动信息条,内部三份内容按实测宽度位移,循环不跳帧;
  speed / paused / label 可配
- 令牌补展示级字号 --su-font-size-2xl / --su-line-height-2xl(32 / 40)
- 展示页新增 #site-chrome(Brand / Ticker / SearchField),COMPONENTS.md 补契约与 changelog
This commit is contained in:
2026-11-01 15:28:17 +08:00
parent 93bb5cbb28
commit 707cf98fe0
12 changed files with 524 additions and 0 deletions
+3
View File
@@ -13,6 +13,7 @@ import { Icons } from './sections/Icons'
import { Images } from './sections/Images'
import { Inputs } from './sections/Inputs'
import { Layouts } from './sections/Layouts'
import { SiteChrome } from './sections/SiteChrome'
import { Marks } from './sections/Marks'
import { MenuSection } from './sections/MenuSection'
import { Modals } from './sections/Modals'
@@ -103,6 +104,7 @@ const NAV = [
items: [
{ id: 'layout', label: '页面骨架 Layout' },
{ id: 'layouts', label: '网格与间距' },
{ id: 'site-chrome', label: '站点外壳' },
{ id: 'composition', label: '组合示例' },
],
},
@@ -229,6 +231,7 @@ export function App() {
<Tooltips />
<PageLayout />
<Layouts />
<SiteChrome />
<Composition />
</main>
+98
View File
@@ -0,0 +1,98 @@
import { useState } from 'react'
import { Brand, SearchField, Tag, Ticker } from '../../src'
import { Demo } from '../components/Demo'
import { PropsTable } from '../components/PropsTable'
import { Section } from '../components/Section'
const TICKER_ITEMS = ['共 3 个本地应用', 'git_helper', '实时同步已连接', '上次扫描 12 秒前']
export function SiteChrome() {
const [keyword, setKeyword] = useState('')
return (
<Section
id="site-chrome"
title="站点外壳"
api="Brand / Ticker / SearchField"
desc="页面上「每站都要有、但和业务无关」的几块:品牌标识、顶部跑马灯、搜索框。它们只吃 props,不读路由也不请求接口。"
>
<Demo
title="品牌标识 Brand"
desc="有 logo 图片就用图片,没有就用首字圆形标记;传 href 变成链接,不传就是纯展示。"
layout="column"
code={`<Brand name="STASH" mark="S" href="/" />
<Brand name="Docs" mark="D" size="sm" tone="soft" />`}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--su-space-6)' }}>
<Brand name="STASH" mark="S" href="/" />
<Brand name="Docs" mark="D" size="sm" tone="soft" />
<Brand name="只有文字" />
<Brand mark="N" />
</div>
</Demo>
<Demo
title="跑马灯 Ticker"
desc="内部渲染三份内容、按实测宽度位移,首尾相接不跳帧;speed 是一份内容滚完的秒数。"
layout="column"
code={`<Ticker speed={24} label="运行状态">
{items.map((text) => <span key={text}>{text}</span>)}
</Ticker>`}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--su-space-2)', width: '100%' }}>
<Brand name="STASH" mark="S" />
<Ticker speed={18} label="运行状态示例">
{TICKER_ITEMS.map((text) => (
<span
key={text}
style={{ color: 'var(--su-text-sub)', fontSize: 'var(--su-font-size-sm)', whiteSpace: 'nowrap' }}
>
{text}
</span>
))}
</Ticker>
</div>
</Demo>
<Demo
title="搜索框 SearchField"
desc="语义版 Input:对外只给字符串;variant=card 是「悬浮搜索条」的实底+阴影,plain 与普通输入框一致。"
layout="column"
code={`<SearchField value={keyword} onChange={setKeyword} placeholder="搜索项目…" />
<SearchField value={keyword} onChange={setKeyword} variant="card" shortcut="⌘K" />`}
>
<SearchField value={keyword} onChange={setKeyword} placeholder="搜索项目…" maxWidth={360} />
<SearchField
value={keyword}
onChange={setKeyword}
placeholder="悬浮搜索条(card)"
variant="card"
shortcut="⌘K"
maxWidth={420}
/>
<Tag tone="neutral">当前输入:{keyword === '' ? '(空)' : keyword}</Tag>
</Demo>
<PropsTable
rows={[
{ name: 'Brand.name', type: 'string', desc: '品牌文字,可留空只显示标记' },
{ name: 'Brand.mark', type: 'string', desc: '标记里的字,留空取 name 首字' },
{ name: 'Brand.logo', type: 'string', desc: 'logo 图片地址;留空用 mark' },
{ name: 'Brand.href', type: 'string', desc: '传了渲染成 <a>,不传是 <span>' },
{ name: 'Brand.size', type: "'sm' | 'md'", defaultValue: "'md'", desc: '标记 24 / 32' },
{ name: 'Brand.tone', type: "'solid' | 'soft'", defaultValue: "'solid'", desc: '深底白字 / 主色浅底' },
{ name: 'Ticker.children', type: 'ReactNode', required: true, desc: '一组内容,组件负责复制三份' },
{ name: 'Ticker.speed', type: 'number', defaultValue: '24', desc: '一份内容滚完的秒数' },
{ name: 'Ticker.paused', type: 'boolean', defaultValue: 'false', desc: '暂停但保留进度' },
{ name: 'Ticker.label', type: 'string', desc: '读屏用的整体描述' },
{ name: 'SearchField.value', type: 'string', required: true, desc: '受控值' },
{ name: 'SearchField.onChange', type: '(value: string) => void', required: true, desc: '直接给字符串,不用取 event' },
{ name: 'SearchField.variant', type: "'plain' | 'card'", defaultValue: "'plain'", desc: '普通输入框 / 悬浮搜索条' },
{ name: 'SearchField.size', type: "'md' | 'lg'", defaultValue: "'lg'", desc: '36 / 44 高' },
{ name: 'SearchField.maxWidth', type: 'number | string', defaultValue: '480', desc: '最大宽度' },
{ name: 'SearchField.hotkey', type: 'string', desc: 'Ctrl/Cmd + 该键聚焦并全选(需同时传 inputRef)' },
]}
/>
</Section>
)
}