创作者中心
概览
切换侧栏,观察标识、分组与页脚的位置。窄屏时打开抽屉。
'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 { AppShell } from '@hina-ui/react'
AppShell 占满视口,把界面分成三块:sidebarContent 属性是左侧栏,header 属性是顶部条,children 是主区域。三块都是可选的。此外还有 banner 属性,位于最顶部,横贯侧栏与主区域,用于放置 Banner。
它同时是侧栏状态的提供方。Sidebar、SidebarGroup、SidebarTrigger 以及侧栏内的 NavLink 都从这里取状态,脱离 AppShell 时它们退回展开形态,SidebarTrigger 不会渲染。
主区域的内容放在默认插槽里。
import { AppShell, NavLink, Sidebar, Text } from '@hina-ui/react'
export default function Demo() {
return (
<AppShell
className="border-line h-72 w-full rounded-lg border"
sidebarContent={
<Sidebar>
<NavLink href="#" active label="概览">
概览
</NavLink>
<NavLink href="#" label="我的书架">
我的书架
</NavLink>
</Sidebar>
}
>
<Text size="sm" tone="muted" className="block p-6">
主区域的内容放在默认插槽里。
</Text>
</AppShell>
)
}
组件默认高度为一屏。嵌在页面中的局部框架可以用 className 覆盖高度,本页示例用的就是这个办法。
示例
收起方式
collapsible 决定桌面端收起后的形态:rail 只留图标一条,hidden 让整条侧栏退场。默认为 rail。
侧栏状态可以用 sidebar / onSidebarChange 受控,取值为 expanded、rail、hidden。需要在别处读出或写入当前形态时绑定它,否则交给组件自己维护即可。
收起过程中标识、头像和已展开条目的位置保持不变。Sidebar 的 renderIcon 与 renderWordmark 自动处理图标与字标的显隐;自定义页眉与页脚内容可通过 SidebarLabel 使用相同过渡。
collapsible="rail" · expanded
创作者中心
概览
切换侧栏,观察标识、分组与页脚的位置。窄屏时打开抽屉。
collapsible="hidden" · expanded
创作者中心
概览
切换侧栏,观察标识、分组与页脚的位置。窄屏时打开抽屉。
'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,
type SidebarState,
} 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')
const [examples, setExamples] = useState<{ mode: 'rail' | 'hidden'; state: SidebarState }[]>([
{ mode: 'rail', state: 'expanded' },
{ mode: 'hidden', state: 'expanded' },
])
function update(mode: 'rail' | 'hidden', state: SidebarState) {
setExamples(list =>
list.map(example => (example.mode === mode ? { ...example, state } : example)),
)
}
return (
<Stack gap="lg" className="w-full">
{examples.map(example => (
<Stack key={example.mode} gap="sm">
<Text size="sm" tone="faint">
{`collapsible="${example.mode}" · ${example.state}`}
</Text>
<AppShell
sidebar={example.state}
onSidebarChange={state => update(example.mode, state)}
collapsible={example.mode}
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>
</Stack>
))}
</Stack>
)
}
桌面侧栏切换完成后调用一次 onSizeStable,无动画的切换也会调用。图表等测量成本较高的内容可以在此时调整尺寸;使用这个回调时,应关闭图表自身的持续尺寸监听,避免重复响应。内部的 ScrollArea 会在侧栏过渡期间暂缓测量,结束后统一更新,期间仍可正常滚动。
主区域滚动
主区域自带滚动容器,内容再长也只在其内部滚动,页眉与侧栏保持不动。
标题栏不随内容滚动
第 1 段正文。
第 2 段正文。
第 3 段正文。
第 4 段正文。
第 5 段正文。
第 6 段正文。
第 7 段正文。
第 8 段正文。
第 9 段正文。
第 10 段正文。
第 11 段正文。
第 12 段正文。
import { AppShell, Heading, NavLink, Sidebar, Stack, Text } from '@hina-ui/react'
const paragraphs = Array.from({ length: 12 }, (_, i) => `第 ${i + 1} 段正文。`)
export default function Demo() {
return (
<AppShell
className="border-line h-72 w-full rounded-lg border"
sidebarContent={
<Sidebar>
<NavLink href="#" active label="正文">
正文
</NavLink>
<NavLink href="#" label="注释">
注释
</NavLink>
</Sidebar>
}
header={
<Heading level={2} size="sm">
标题栏不随内容滚动
</Heading>
}
>
<Stack gap="sm" className="p-6">
{paragraphs.map(p => (
<Text key={p} size="sm" tone="muted">
{p}
</Text>
))}
</Stack>
</AppShell>
)
}
行为
- 视口宽度达到 1024 像素时为桌面布局,侧栏常驻左侧;低于此宽度时侧栏改由抽屉承载,
sidebarContent属性的内容原样搬进抽屉。 - 桌面端点击
SidebarTrigger在展开与collapsible指定的形态之间切换;窄屏则切换抽屉开合。 - 抽屉的开合状态可用
mobileOpen / onMobileOpenChange绑定。 - 移动端 Drawer 不额外显示标题栏,关闭按钮默认位于 Sidebar 页眉内,可在
Sidebar上设置closable={false}隐藏。mobileTitle仅设置抽屉的无障碍名,默认取自界面语言。 - 抽屉内侧栏占满可用高度,页眉和页脚固定在两端,导航区填满剩余空间并独立滚动。
autoClose为真时路由变化会关闭抽屉,避免跳转后抽屉仍挡在内容前面。默认开启。restoreKey写在主区域的滚动容器上,供滚动位置恢复使用。- 组件内置浮层提供方,侧栏在 rail 形态下的悬停提示不需要另行包裹。
无障碍
- 页眉渲染为
header,主区域渲染为main,侧栏的导航地标由Sidebar自行提供。 SidebarTrigger带有随界面语言给出的无障碍名(简体中文为「切换侧栏」)。- 抽屉形态下焦点被限制在抽屉内,关闭后归还给触发按钮,这由
Drawer保证。
API
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
collapsible | 'rail' | 'hidden' | 'rail' | 桌面端收起后的形态 |
autoClose | boolean | true | 路由变化时是否关闭移动端抽屉 |
mobileTitle | string | 取自界面语言 | 移动端抽屉的无障碍名,不显示标题栏 |
restoreKey | string | — | 主区域滚动容器的滚动位置恢复标识 |
className | string | — | 追加到根元素的类 |
受控状态
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
sidebar | SidebarState | 'expanded' | 桌面端侧栏形态 |
mobileOpen | boolean | false | 移动端抽屉是否打开 |
回调
| 回调 | 参数 | 说明 |
|---|---|---|
onSizeStable | — | 侧栏状态变化后的布局已稳定;中途反向切换只在最终布局稳定后通知 |
内容属性
| 属性 | 说明 |
|---|---|
banner | 最顶部的公告条,横贯整个壳 |
sidebarContent | 侧栏内容,窄屏时搬入抽屉 |
header | 顶部条内容 |
children | 主区域内容 |
Ref
| 名称 | 类型 | 说明 |
|---|---|---|
mainViewport | HTMLElement | undefined | 主区域滚动容器的视口元素 |
mainArea | ScrollAreaHandle | undefined | 主区域滚动容器的 ref 句柄 |