创作者中心
概览
切换侧栏,观察标识、分组与页脚的位置。窄屏时打开抽屉。
'use client'
import { useState } from 'react'
import { FileText, House, Images, Settings } from 'lucide-react'
import {
AppShell,
Avatar,
Heading,
Inline,
NavLink,
Sidebar,
SidebarGroup,
SidebarLabel,
SidebarTrigger,
Stack,
Text,
} from '@hina-ui/react'
const groups = [
{
label: '内容',
items: [
{ id: 'overview', label: '概览', icon: House },
{ id: 'articles', label: '文章', icon: FileText },
{ id: 'library', label: '媒体库', icon: Images },
],
},
{ label: '管理', items: [{ id: 'settings', label: '设置', icon: Settings }] },
]
export default function Demo() {
const [selected, setSelected] = useState('overview')
return (
<AppShell
mobileTitle="Hina UI"
className="border-line h-112 w-full rounded-lg border"
sidebarContent={
<Sidebar
renderIcon={() => <Avatar src="/favicon.png" name="Hina UI" className="rounded-md" />}
renderWordmark={() => (
<Stack gap="none">
<Text weight="medium" className="truncate">
Hina UI
</Text>
<Text size="xs" tone="muted" className="truncate">
工作空间
</Text>
</Stack>
)}
renderFooter={() => (
<Inline gap="sm" align="center" wrap={false}>
<Avatar src="/avatars/paper.webp" name="星见书音" />
<SidebarLabel as="div" className="flex-1">
<Stack gap="none">
<Text size="sm" weight="medium" className="truncate">
星见书音
</Text>
<Text size="xs" tone="muted" className="truncate">
管理员
</Text>
</Stack>
</SidebarLabel>
</Inline>
)}
>
{groups.map(group => (
<SidebarGroup key={group.label} label={group.label}>
{group.items.map(item => (
<NavLink
key={item.id}
href={`#${item.id}`}
label={item.label}
active={selected === item.id}
icon={<item.icon />}
onClick={event => {
event.preventDefault()
setSelected(item.id)
}}
>
{item.label}
</NavLink>
))}
</SidebarGroup>
))}
</Sidebar>
}
header={
<>
<SidebarTrigger />
<Heading level={2} size="sm" className="truncate">
创作者中心
</Heading>
</>
}
>
<Stack gap="sm" className="p-6">
<Heading level={3} size="sm">
{groups.flatMap(group => group.items).find(item => item.id === selected)?.label}
</Heading>
<Text size="sm" tone="muted">
切换侧栏,观察标识、分组与页脚的位置。窄屏时打开抽屉。
</Text>
</Stack>
</AppShell>
)
}
用法
import { Sidebar, SidebarGroup, SidebarLabel, SidebarTrigger } from '@hina-ui/react'
Sidebar 放进 AppShell 的 sidebarContent 属性,条目写在 children 里,通常是一串 NavLink。它自带导航地标与滚动容器,条目再多也只在栏内滚动。
它的形态由 AppShell 提供,自身不持有状态。脱离 AppShell 时它始终是展开形态,也无法收起。
侧栏条目放在默认插槽里。
import { AppShell, NavLink, Sidebar, Text } from '@hina-ui/react'
export default function Demo() {
return (
<AppShell
className="border-line h-64 w-full rounded-lg border"
sidebarContent={
<Sidebar>
<NavLink href="#" active label="概览">
概览
</NavLink>
<NavLink href="#" label="我的书架">
我的书架
</NavLink>
<NavLink href="#" label="收藏">
收藏
</NavLink>
</Sidebar>
}
>
<Text size="sm" tone="muted" className="block p-6">
侧栏条目放在默认插槽里。
</Text>
</AppShell>
)
}
侧栏内的每个 NavLink 都应当写 label:收起为 rail 后文字淡出,label 会接手成为悬停提示与无障碍名。
示例
分组
SidebarGroup 把条目归到一个可折叠的标题下。label 是组名,defaultOpen 决定初始是否展开,默认展开。
「创作」一组默认收起。
import { AppShell, NavLink, Sidebar, SidebarGroup, Text } from '@hina-ui/react'
export default function Demo() {
return (
<AppShell
className="border-line h-96 w-full rounded-lg border"
sidebarContent={
<Sidebar>
<NavLink href="#" active label="概览">
概览
</NavLink>
<SidebarGroup label="作品库">
<NavLink href="#" label="Galgame">
Galgame
</NavLink>
<NavLink href="#" label="轻小说">
轻小说
</NavLink>
</SidebarGroup>
<SidebarGroup label="创作" defaultOpen={false}>
<NavLink href="#" label="我的条目">
我的条目
</NavLink>
<NavLink href="#" label="草稿箱">
草稿箱
</NavLink>
</SidebarGroup>
</Sidebar>
}
>
<Text size="sm" tone="muted" className="block p-6">
「创作」一组默认收起。
</Text>
</AppShell>
)
}
品牌图标与字标
renderIcon 与 renderWordmark 组成默认页眉。图标位固定为 32 × 32 像素,SVG 和图片按比例显示;字标可以是文字、SVG、Image 或组合内容。
| 传入的内容 | 展开 | 收起为 rail |
|---|---|---|
renderIcon + renderWordmark | 图标与字标并排显示 | 图标保持原位,字标淡出 |
只有 renderIcon | 显示图标 | 保持原位 |
只有 renderWordmark | 字标从页眉起始位置显示 | 品牌区整体收起 |
| 均未提供 | 不生成品牌区 | 不生成品牌区 |
有图标时,收起后品牌行保留高度;只有字标时,品牌区连同上下内边距一起收起,导航向上填补空位。重新展开或进入移动端抽屉时,品牌区完整显示。字标自动使用与导航文字相同的过渡,不需要再包 SidebarLabel。
两个函数均接收 { state }。传入 renderHeader 时由它完全接管页眉,renderIcon 和 renderWordmark 不再渲染。
图标与字标
品牌插槽
图标与字标
只有字标
品牌插槽
只有字标
只有图标
品牌插槽
只有图标
'use client'
import { House } from 'lucide-react'
import { AppShell, Image, NavLink, Sidebar, SidebarTrigger, Stack, Text } from '@hina-ui/react'
import { Wordmark } from '../../../components/Wordmark'
const examples = [
{ mode: 'both', label: '图标与字标' },
{ mode: 'wordmark', label: '只有字标' },
{ mode: 'icon', label: '只有图标' },
]
export default function Demo() {
return (
<Stack gap="lg" className="w-full">
{examples.map(example => (
<Stack key={example.mode} gap="sm">
<Text size="sm" tone="muted">
{example.label}
</Text>
<AppShell
mobileTitle="Hina UI"
className="border-line h-52 w-full rounded-lg border"
sidebarContent={
<Sidebar
renderIcon={
example.mode !== 'wordmark'
? () => <Image src="/favicon.png" alt="Hina UI" className="rounded-md" />
: undefined
}
renderWordmark={
example.mode !== 'icon' ? () => <Wordmark className="items-center" /> : undefined
}
>
<NavLink
href="#"
active
label="概览"
icon={<House />}
onClick={event => event.preventDefault()}
>
概览
</NavLink>
</Sidebar>
}
header={
<>
<SidebarTrigger />
<Text size="sm" weight="medium">
品牌插槽
</Text>
</>
}
>
<Text size="sm" tone="muted" className="block p-6">
{example.label}
</Text>
</AppShell>
</Stack>
))}
</Stack>
)
}
页眉与页脚
renderHeader 完整替换默认品牌页眉,renderFooter 位于条目区下方,两者都不随条目滚动。自定义页眉不会被自动视为 logo 隐藏。
页眉保持展开时的内容宽度。页脚随侧栏实际宽度收起,内部按钮与浮层锚点不会超出 rail。水平排布的头像和文字可使用 Inline 并设置 wrap={false};文字使用 truncate 或 whitespace-nowrap 避免收起时换行。把文字与附属操作放进 SidebarLabel,它会与 NavLink 的文字一起淡出,展开时延后淡入;标识和 Avatar 留在外面,位置与尺寸保持不变。
SidebarLabel 不改变内容的占位。rail 形态下其内容不可见、不可交互,也不进入朗读和键盘焦点序列。renderHeader 和 renderFooter 仍接收 { state },供自定义内容读取当前形态。
创作者中心
概览
切换侧栏,观察标识、分组与页脚的位置。窄屏时打开抽屉。
'use client'
import { useState } from 'react'
import { FileText, House, Images, Settings } from 'lucide-react'
import {
AppShell,
Avatar,
Heading,
Inline,
NavLink,
Sidebar,
SidebarGroup,
SidebarLabel,
SidebarTrigger,
Stack,
Text,
} from '@hina-ui/react'
const groups = [
{
label: '内容',
items: [
{ id: 'overview', label: '概览', icon: House },
{ id: 'articles', label: '文章', icon: FileText },
{ id: 'library', label: '媒体库', icon: Images },
],
},
{ label: '管理', items: [{ id: 'settings', label: '设置', icon: Settings }] },
]
export default function Demo() {
const [selected, setSelected] = useState('overview')
return (
<AppShell
mobileTitle="Hina UI"
className="border-line h-112 w-full rounded-lg border"
sidebarContent={
<Sidebar
renderHeader={() => (
<Inline gap="sm" align="center" wrap={false}>
<Avatar src="/favicon.png" name="Hina UI" className="rounded-md" />
<SidebarLabel as="div" className="flex-1">
<Stack gap="none">
<Text weight="medium" className="truncate">
Hina UI
</Text>
<Text size="xs" tone="muted" className="truncate">
工作空间
</Text>
</Stack>
</SidebarLabel>
</Inline>
)}
renderFooter={() => (
<Inline gap="sm" align="center" wrap={false}>
<Avatar src="/avatars/paper.webp" name="星见书音" />
<SidebarLabel as="div" className="flex-1">
<Stack gap="none">
<Text size="sm" weight="medium" className="truncate">
星见书音
</Text>
<Text size="xs" tone="muted" className="truncate">
管理员
</Text>
</Stack>
</SidebarLabel>
</Inline>
)}
>
{groups.map(group => (
<SidebarGroup key={group.label} label={group.label}>
{group.items.map(item => (
<NavLink
key={item.id}
href={`#${item.id}`}
label={item.label}
active={selected === item.id}
icon={<item.icon />}
onClick={event => {
event.preventDefault()
setSelected(item.id)
}}
>
{item.label}
</NavLink>
))}
</SidebarGroup>
))}
</Sidebar>
}
header={
<>
<SidebarTrigger />
<Heading level={2} size="sm" className="truncate">
创作者中心
</Heading>
</>
}
>
<Stack gap="sm" className="p-6">
<Heading level={3} size="sm">
{groups.flatMap(group => group.items).find(item => item.id === selected)?.label}
</Heading>
<Text size="sm" tone="muted">
切换侧栏,观察标识、分组与页脚的位置。窄屏时打开抽屉。
</Text>
</Stack>
</AppShell>
)
}
行为
- 三种形态的宽度分别是展开 256 像素、rail 56 像素、隐藏 0 像素,切换时宽度连续过渡。
- 收起为 rail 时,
SidebarGroup强制展开、组标题原位淡出并显示分隔线,标题占位保持不变,已展开的条目不会随收起动作上下移动。 - 条目区使用不带边缘阴影的 ScrollArea,页眉与页脚固定在两端。
- 完全隐藏时,侧栏整体退出交互与键盘焦点序列。
- 宽度和文字过渡使用 Hina 动画 token;系统开启减弱动态效果时直接切换。
- 搬入移动端 Drawer 时,侧栏撑满抽屉高度,页脚固定在底部,仅条目区滚动。页眉内默认提供关闭按钮,设置
closable={false}可隐藏;未提供renderHeader、renderIcon或renderWordmark时,也不保留按钮行的占位。不重复显示抽屉标题;水平内边距由抽屉提供,页眉、条目区和页脚保留各自的纵向内边距。
无障碍
- 条目区是
nav地标,默认无障碍名取自界面语言(简体中文为「侧边导航」),label可覆盖。 - rail 形态下条目的文字虽然不可见,但 NavLink 的
label会作为aria-label保留。 closable仅控制移动端关闭按钮,隐藏后仍可按 Escape、点击遮罩或通过受控状态关闭抽屉。- 隐藏的字标退出朗读与焦点序列。品牌图标使用图片时提供
alt,使用 SVG 时提供合适的无障碍名;若图标包含链接,链接本身也应有名称。 - 收起后被隐藏的组标题同时退出键盘序列,不会出现能聚焦却看不见的控件。
API
Sidebar
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
label | string | 取自界面语言 | 导航地标的无障碍名 |
closable | boolean | true | 是否显示移动端抽屉的关闭按钮 |
className | string | — | 追加到根元素的类 |
| 属性 | 参数 | 说明 |
|---|---|---|
children | — | 侧栏条目 |
renderHeader | { state } | 完整替换页眉,优先于 renderIcon 与 renderWordmark |
renderIcon | { state } | 固定方形区域内的品牌图标,rail 时保留 |
renderWordmark | { state } | 品牌字标,rail 时自动淡出 |
renderFooter | { state } | 条目区下方的内容 |
SidebarGroup
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
label | string | 必填 | 组名 |
defaultOpen | boolean | true | 初始是否展开 |
className | string | — | 追加到根元素的类 |
| 属性 | 说明 |
|---|---|
children | 组内的条目 |
SidebarLabel
控制自定义文字与附属内容在 rail 形态下的显隐;不在侧栏状态提供方内时始终显示。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
as | string | 'span' | 渲染的元素 |
className | string | — | 追加到根元素的类 |
| 属性 | 说明 |
|---|---|
children | 随侧栏收起而隐藏的内容 |
SidebarTrigger
切换侧栏形态的按钮,通常放在 AppShell 的 header 属性里。它没有可配置的行为,脱离 AppShell 时不渲染。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
className | string | — | 追加到根元素的类 |