@xiao-ying/miniapp-ui
v1.3.5
Published
Material 3 flavored UI kit for XiaoYing miniapps, built with React and Tailwind CSS plugin support.
Readme
@xiao-ying/miniapp-ui
面向 XiaoYing miniapp 的 React UI 组件库,遵循 Material 3 设计语言,并提供 Tailwind CSS 插件支持。组件与 @xiao-ying/miniapp-sdk、@xiao-ying/miniapp-hooks 打通,可自动获取宿主亮度、主题色 seed、震动等能力。
安装
pnpm add @xiao-ying/miniapp-uiTailwind 配置
请添加插件并扫描本包,以确保组件使用到的工具类被正确输出。
Tailwind v4(CSS-first):
/* e.g. src/index.css */ @import "tailwindcss"; @source "./src/**/*.{ts,tsx}", "./node_modules/@xiao-ying/miniapp-ui/**/*.{js,ts,tsx}"; @plugin "@xiao-ying/miniapp-ui/tailwind-plugin";Tailwind v3+(配置文件):
// tailwind.config.ts import { xyMaterial3Plugin } from '@xiao-ying/miniapp-ui/tailwind-plugin' export default { content: ['./index.html', './src/**/*.{ts,tsx}', './node_modules/@xiao-ying/miniapp-ui/**/*.{js,ts,tsx}'], plugins: [xyMaterial3Plugin()] }
插件默认会在 :root 注入浅色变量、在 [data-theme="dark"] 注入深色变量。插件还会提供 Material 3 的颜色语义、圆角、海拔阴影等 CSS 变量,并映射 Tailwind 的颜色/圆角/阴影别名(例如 text-error、bg-surface、rounded-md、shadow-lg)到对应的 M3 语义。
颜色别名与使用规范
请优先使用 text-* / bg-* / border-* 颜色别名,不要在 className 中直接写 var(...)。
- 推荐文本类:
text-on-surface、text-on-surface-variant、text-primary、text-error - 推荐背景类:
bg-surface、bg-surface-container、bg-surface-container-high、bg-primary-container、bg-error-container、bg-warning-container、bg-success-container - 推荐边框类:
border-outline、border-outline-variant、border-border、border-border-strong - 禁止写法:
text-[var(--xy-m3-on-surface-variant)]、bg-[var(--xy-m3-surface-container-high)]、border-[var(--xy-m3-outline-variant)]
业务语义色
业务小程序和 Codex 生成页面时,应优先使用本包 Tailwind 颜色别名,不要为常见状态重复定义项目级颜色。内置语义色会随宿主 themeSeedColor 与 light/dark 自动更新。
| 用途 | Tailwind 颜色 | CSS 变量 |
| --- | --- | --- |
| 主操作/品牌强调 | primary / on-primary / primary-container / on-primary-container | --xy-sem-accent / --xy-sem-on-accent / --xy-sem-accent-variant / --xy-sem-on-accent-variant |
| 错误、失败、删除、危险操作 | error / on-error / error-container / on-error-container | --xy-sem-error / --xy-sem-on-error / --xy-sem-error-container / --xy-sem-on-error-container |
| 警告、待处理、需要注意 | warning / on-warning / warning-container / on-warning-container | --xy-sem-warning / --xy-sem-on-warning / --xy-sem-warning-container / --xy-sem-on-warning-container |
| 成功、完成、通过 | success / on-success / success-container / on-success-container | --xy-sem-success / --xy-sem-on-success / --xy-sem-success-container / --xy-sem-on-success-container |
| 页面、卡片、列表背景 | surface / surface-subtle / surface-strong / surface-disabled | --xy-sem-surface-base / --xy-sem-surface-subtle / --xy-sem-surface-strong / --xy-sem-surface-disabled |
| 边框 | border / border-strong / border-disabled | --xy-sem-border / --xy-sem-border-strong / --xy-sem-border-disabled |
Tailwind 颜色不提供 danger / info 别名:危险或破坏性动作统一使用 error,普通信息提示优先使用 primary、surface-* 或组件默认样式。Button / IconButton 可使用 tone="danger" 作为删除等破坏性操作的语义别名,内部仍使用 error 语义色;variant="danger" 是 filled danger 的快捷样式。
Button / IconButton 的 tone 支持 primary、secondary、tertiary、error、danger、warning、success。Badge / NavBadge / LinearProgress / CircularProgress 的 tone 支持 primary、secondary、tertiary、error、warning、success。业务状态优先用这些 tone 或上表 Tailwind alias,不要再给常见状态手写一套项目内颜色。
主题与变量
你可以在自己的 index.css 中覆盖变量来自定义配色与圆角:
:root {
--xy-m3-primary: #0060a9;
--xy-m3-on-primary: #ffffff;
/* ...其他 Material 3 tokens... */
--xy-radius-md: 14px;
}
[data-theme="dark"] {
--xy-m3-primary: #a2c9ff;
--xy-m3-on-primary: #00315b;
/* ...dark tokens... */
}Tailwind 插件会生成 shadow-xy-*、rounded-xy-* 等工具类并读取这些变量。若需完全手动控制,可使用 xyMaterial3Plugin({ emitBase: false }) 关闭默认注入。
从 @xiao-ying/[email protected] 起,入口会读取 xy.appearance().themeSeedColor,并根据宿主 seed color 生成 Material 3 light/dark 配色;旧引擎或缺失 appearance 注入时保持默认小应蓝 #4da3ff。brightnessChange 只切换 light/dark scheme,不改变 seed color。
按钮、ButtonGroup、ToggleButton 与震动反馈
import { Button, ButtonGroup, IconButton, ToggleButton } from '@xiao-ying/miniapp-ui'
// onPressed 为 undefined/null 时视为禁用(Flutter 风格)
<Button variant="filled" tone="primary" onPressed={() => xy.showToast({ title: 'Clicked' })}>
触发操作
</Button>
<Button variant="danger" onPressed={() => xy.showToast({ title: '已删除' })}>
删除
</Button>
<Button variant="outlined" tone="danger" onPressed={() => xy.showToast({ title: '已删除' })}>
删除
</Button>
<ButtonGroup aria-label="审批操作">
<Button variant="outlined" onPressed={() => xy.showToast({ title: '已取消' })}>
取消
</Button>
<Button onPressed={() => xy.showToast({ title: '已确认' })}>
确认
</Button>
</ButtonGroup>
// IconButton 示例,支持自定义 tone 与 className
<IconButton
variant="tonal"
tone="secondary"
className="shadow-xy-2"
onPressed={() => xy.vibrate({ impact: 'medium' })}
>
<SomeIcon />
</IconButton>
<ToggleButton
pressed={advancedEnabled}
onPressedChange={setAdvancedEnabled}
leadingIcon={<IconSparkles aria-hidden />}
>
高级功能
</ToggleButton>
<ToggleButton
pressed={advancedEnabled}
onPressedChange={setAdvancedEnabled}
states={{
pressed: { icon: <IconSparkles aria-hidden />, label: '高级功能已开启' },
unpressed: { icon: <IconAdjustmentsHorizontal aria-hidden />, label: '开启高级功能' }
}}
/>Button、IconButton 与 ToggleButton 默认会在点击时调用 xy.vibrate(可通过 vibrateImpact 指定具体 XYVibrateImpact 或传入 false 禁用)。ButtonGroup 可将直接子级 Button / IconButton / ToggleButton 合并为接壤按钮组,并在组内移除单个按钮的按压缩放,只保留状态层反馈。ToggleButton 使用 aria-pressed 表达开启/关闭状态,支持 pressed / defaultPressed / onPressedChange。固定文本或固定图标用 children / leadingIcon;两种状态文案或图标不同用 states,组件会预留两态最大宽度,避免 wrap 布局跳动。这两种用法在类型层面互斥。
Toolbar
import { Button, ButtonGroup, Dock, Toolbar } from '@xiao-ying/miniapp-ui'
<Dock placement="bottom" offset={12} inset={16} contentClassName="flex justify-center">
<div className="w-full max-w-xl rounded-[1.75rem] border border-outline-variant/70 bg-surface/45 p-2 shadow-lg backdrop-blur-xl">
<Toolbar aria-label="详情操作" fullWidth gap={12}>
<Toolbar.Section>
<Button key="back" variant="tonal" tone="secondary" onPressed={() => window.history.back()}>
返回
</Button>
</Toolbar.Section>
<Toolbar.Spacer />
<Toolbar.Section align="end" gap={8}>
{canRenew ? (
<Button key="renew" variant="outlined" onPressed={renew}>
续期
</Button>
) : null}
<ButtonGroup key="manage" aria-label="管理操作">
<Button variant="tonal" onPressed={edit}>
编辑
</Button>
<Button variant="outlined" tone="danger" onPressed={hide}>
下架
</Button>
</ButtonGroup>
</Toolbar.Section>
</Toolbar>
</div>
</Dock>Toolbar 是操作布局容器,不提供背景、边框、模糊、阴影、padding 或最大宽度;这些外框视觉与宽度约束由外层 div / Card / Surface / 业务容器决定。Toolbar 默认按内容自适应,传 fullWidth 后占满父容器;使用 Toolbar.Spacer 做左右分隔时,通常应同时让 Toolbar 或外层容器拥有明确宽度。gap 可传 number(按 px 处理)或 CSS 长度字符串;Toolbar.Section 也可单独传 gap 覆盖组内间距。ButtonGroup 仍只接收直接的 Button-like 子级。Toolbar.Section 会自动对自己的直接子项做出现/消失收缩动画,ButtonGroup 会作为一个整体子项动画,不会深入组内按钮。条件渲染的直接子项请提供稳定 key,避免插入/删除时按 index 兜底导致动画识别不准。
Tooltip
import { Tooltip, IconButton } from '@xiao-ying/miniapp-ui'
import { IconInfoCircle } from '@tabler/icons-react'
<Tooltip content="这是一个提示">
<IconButton onPressed={() => {}}>
<IconInfoCircle />
</IconButton>
</Tooltip>Sheet
Sheet 支持普通受控组件式,也支持通过 OverlayProvider 进行命令式展示。dismissible={false} 会关闭内置关闭入口,命令式内容仍可通过渲染函数里的 close() 主动收起。
import { Button, OverlayProvider, Sheet, useSheet } from '@xiao-ying/miniapp-ui'
<Sheet
open={open}
title="选择课表"
dismissible={false}
onClose={() => setOpen(false)}
>
<Button onPressed={() => setOpen(false)}>完成</Button>
</Sheet>
const SheetButton = () => {
const sheet = useSheet()
return (
<Button
onPressed={() => {
sheet.show({
title: '选择课表',
dismissible: false,
content: ({ close }) => (
<Button onPressed={() => close()}>完成</Button>
)
})
}}
>
打开 Sheet
</Button>
)
}
<OverlayProvider>
<SheetButton />
</OverlayProvider>Dialog
Dialog 支持普通受控组件式,也支持通过 OverlayProvider 进行命令式展示。它只负责 HTML 浮层,不替代 xy.showModal 这类原生 SDK 能力。
import { Button, Dialog, DialogActions, OverlayProvider, useDialog } from '@xiao-ying/miniapp-ui'
<Dialog
open={open}
title="进入前确认"
dismissible={false}
actions={
<DialogActions>
<Button variant="outlined" onPressed={() => setOpen(false)}>稍后再玩</Button>
<Button onPressed={() => confirm()}>我已满 18 周岁</Button>
</DialogActions>
}
>
为了营造更健康的游戏环境,仅面向已满 18 周岁的同学开放。
</Dialog>
const ConfirmButton = () => {
const dialog = useDialog()
return (
<Button
onPressed={async () => {
const controller = dialog.show<'confirm' | 'cancel'>({
title: '进入前确认',
content: ({ close }) => (
<div className="space-y-4">
<div>确认已满 18 周岁后即可继续游玩。</div>
<DialogActions>
<Button variant="outlined" onPressed={() => close('cancel')}>取消</Button>
<Button onPressed={() => close('confirm')}>确认</Button>
</DialogActions>
</div>
)
})
const result = await controller.closed
if (result === 'confirm') {
// continue
}
}}
>
打开确认
</Button>
)
}
<OverlayProvider>
<ConfirmButton />
</OverlayProvider>Toast
Toast 支持受控组件式,也支持通过 OverlayProvider + useToast() 命令式展示。默认样式按 Material 3 snackbar 处理,使用 inverse-surface 容器、inverse-on-surface 文本与 elevation 3。
import { Button, OverlayProvider, useToast } from '@xiao-ying/miniapp-ui'
const SaveButton = () => {
const toast = useToast()
return (
<Button
onPressed={() => toast.show({
type: 'success',
title: '已保存',
subtitle: '草稿会在下次打开时恢复',
duration: 'short'
})}
>
保存
</Button>
)
}
<OverlayProvider toast={{ placement: 'bottom', maxVisible: 5 }}>
<SaveButton />
</OverlayProvider>Surface 与卡片
Card 使用建议
业务页面默认推荐使用 Card 的 filled 变体,也就是 <Card> 或 <Card variant="filled">。filled 是最稳定的页面分区样式,适合设置、表单、状态摘要、功能面板等大多数业务场景。
outlined 适合弱化背景、强调边界的列表项或嵌入式信息块。outlined 不传 tone 时默认使用 surface 背景,避免普通 outlined 卡片自带与页面背景不同的容器色;只有显式传入 tone 时,才展示对应背景。
尽量不要把 elevated 当作普通卡片默认样式。elevated 主要用于 Card 承载在另一个 Card / Surface 之上、浮层、局部预览,或确实需要用阴影表达层级分离的场景。页面上大量使用 elevated card 会增加视觉噪声。
import { Card, Surface } from '@xiao-ying/miniapp-ui'
<Card title="存储状态" subtitle="Miniapp storage powered by xy">
<p className="text-sm text-on-surface-variant">将内容放在这里。</p>
</Card>
<Card variant="outlined" title="列表项" subtitle="弱化背景,只强调边界">
<p className="text-sm text-on-surface-variant">适合嵌入式信息块。</p>
</Card>
<Card variant="elevated" title="浮层中的卡片" subtitle="仅在需要层级分离时使用">
<p className="text-sm text-on-surface-variant">例如卡片上再次承载卡片。</p>
</Card>
<Surface tone="container-high" elevation="level2" className="p-6">
<p>自定义外壳与内容区域</p>
</Surface>TextArea 自动高度
import { TextArea } from '@xiao-ying/miniapp-ui'
<TextArea
label="描述"
value={content}
onValueChange={setContent}
rows={2} // autoHeight=true 时表示最小行数
autoHeight // 默认 false
maxRows={8} // 可选:达到 8 行后内部滚动
/>autoHeight默认关闭,不影响现有固定高度行为。rows在autoHeight=false时仍是固定行数;在autoHeight=true时为最小行数。maxRows仅在autoHeight=true生效,不传表示不限制增长。
CounterInput
import { CounterInput } from '@xiao-ying/miniapp-ui'
<CounterInput
label="购买数量"
value={quantity}
onValueChange={setQuantity}
min={0}
max={12}
step={1}
helperText={`当前 ${quantity} 件`}
/>CounterInput 用于选择整数,支持受控/非受控、min / max / step、长按按钮快速增减、方向键增减、Home / End 跳到边界,以及 filled / outlined 两种视觉变体。输入过程中允许短暂为空,失焦后会归一化为合法整数。
Tabs、PageView 与选择控件
import { PageView, Picker, Tabs, SegmentedButton, Slider, usePageViewTabs } from '@xiao-ying/miniapp-ui'
const pageTabs = usePageViewTabs({
items: [
{ label: '概览', value: 'overview' },
{ label: '媒体', value: 'media' },
{ label: '存储', value: 'storage' }
],
defaultValue: 'overview'
})
<Tabs
{...pageTabs.tabsProps}
/>
<PageView
ref={pageTabs.pageViewRef}
{...pageTabs.pageViewProps}
renderPage={(index) => <section>第 {index + 1} 页</section>}
/>
<SegmentedButton
items={[
{ label: '全部', value: 'all' },
{ label: '进行中', value: 'active' },
{ label: '已完成', value: 'done' }
]}
value={filter}
onValueChange={setFilter}
fullWidth
/>
<Slider label="音量" value={volume} onValueChange={setVolume} />
<Picker
columns={[
{
key: 'hour',
label: '小时',
options: Array.from({ length: 24 }, (_, hour) => ({
label: `${String(hour).padStart(2, '0')} 时`,
value: hour
}))
},
{
key: 'minute',
label: '分钟',
options: [0, 15, 30, 45].map((minute) => ({
label: `${String(minute).padStart(2, '0')} 分`,
value: minute
}))
}
]}
value={time}
onValueChange={setTime}
/>Picker 是 inline 滚轮选择器,不内置弹层;需要底部弹出时可自行放进 Sheet。一个 Picker 可配置单列或多列,也可以在同一页面放置多个实例。
Scaffold 与悬浮底部导航
BottomNavigationBar 默认保持固定底栏样式;传入 variant="floating" 后会切换为底部悬浮的半透明毛玻璃样式。导航栏会按导航项数量自适应收缩并居中,避免宽屏下 item 被拉得过宽;如需恢复铺满父容器,可传 fullWidth,或用 maxWidth 指定最大宽度。配合 Scaffold 使用时,需要显式设置 bottomBarLayout="floating",由 Scaffold 负责定位、安全区和内容底部预留。
import { BottomNavigationBar, Scaffold } from '@xiao-ying/miniapp-ui'
import { IconCalendarWeek, IconHome, IconSchool, IconUserCircle } from '@tabler/icons-react'
<Scaffold
title="课程表"
bottomBarLayout="floating"
bottomBar={
<BottomNavigationBar
variant="floating"
items={[
{ label: '主页', icon: <IconHome />, path: '/' },
{ label: '课程表', icon: <IconCalendarWeek />, path: '/schedule' },
{ label: '校园', icon: <IconSchool />, path: '/campus' },
{ label: '我的', icon: <IconUserCircle />, path: '/me' }
]}
/>
}
>
{children}
</Scaffold>自定义底部栏也可以通过 Scaffold 的 bottomBarLayout、bottomBarHeight、bottomBarFloatingGap 调整同一套安全区与内容预留逻辑。standard 布局下若自定义底栏背景不是默认容器色,可传 bottomBarBackgroundColor 让 safe-area 背景保持一致。
Skeleton
Skeleton 是原子化骨架占位组件,推荐用 className 自由组合页面、卡片、列表和图片墙占位。
import { Skeleton } from '@xiao-ying/miniapp-ui'
<Skeleton className="h-4 w-32" />
<Skeleton variant="circle" className="h-12 w-12" />
<Skeleton variant="rect" className="aspect-video h-auto rounded-xl" />
<Skeleton animation="pulse" className="h-3 w-full" />animation 支持 wave / pulse / false,默认是 wave。visible={false} 时会直接渲染 children,便于在同一处切换加载态和真实内容。
Divider
import { Divider } from '@xiao-ying/miniapp-ui'
<Divider />
<Divider>OR</Divider>
<Divider inset />Host 托管图片
import { XyHostImage } from '@xiao-ying/miniapp-ui'
<XyHostImage
src="https://example.edu/avatar/123"
alt="avatar"
headers={{ authorization: 'Bearer token' }}
cacheKey="user-avatar-123"
fallbackSrc="/images/avatar-fallback.png"
className="h-12 w-12 rounded-full object-cover"
/>XyHostImage 会按运行时自动选择图片加载策略:
- App runtime 且
engineVersion >= 1.3.0:使用xyimg://(宿主托管加载)。 - App runtime 且低版本引擎:回退到
xy.downloadFile(组件内置去重缓存)。 - 浏览器 runtime:直接使用原始
src。
工具与组件清单
xyMaterial3Plugin(默认导出):Tailwind 插件,输出 Material 3 tokens、圆角、海拔阴影和颜色变量(默认注入,可emitBase: false关闭)。defaultMaterial3Theme/schemeToCssVars/shapeToCssVars/xyElevations/defaultStateOpacity:主题与变量工具。cn:基于clsx+tailwind-merge的 className 合并工具。Button/ButtonGroup/IconButton/Toolbar/FloatingActionButton:按钮、接壤按钮组、操作条与悬浮按钮。Surface/Card/Scaffold:容器与结构组件。TextField/TextArea/CounterInput/Select/Picker/Checkbox/Radio/Switch/Slider/SegmentedButton:表单与输入控件。Tabs/PageView/ListTile/BottomNavigationBar/NavBadge/Badge/Divider:导航与展示组件。LinearProgress/CircularProgress/Skeleton:进度与加载占位。Tooltip:悬浮提示,默认使用 portal 置于最顶层。XyHostImage:宿主托管图片组件,支持headers/cacheKey与老引擎回退。SafeArea:安全区内边距封装(CSS 变量 +env()回退)。useHapticFeedback(别名useHapticPress):对xy.vibrate的通用封装。
