模式 · PATTERN
侧栏导航 Sidebar
纸色侧栏,宽 266。导航行是扁平的,不是键——当前行只加一层 .07 灰底和 600 字重;真正的操作(收起、退出)才用微拟物键。
import { SidebarNav } from './extracted/SidebarNav.jsx'
import { ClockIcon, HomeIcon, MapIcon, PlayRectIcon, StarIcon } from './extracted/icons.jsx'
export function Hub() {
return (
<SidebarNav
active="home"
groups={[
{ items: [{ id: 'home', label: '首页', icon: <HomeIcon /> }] },
{ items: [
{ id: 'fav', label: '我的收藏', icon: <StarIcon />, count: 12 },
{ id: 'hist', label: '历史记录', icon: <ClockIcon />, count: 38 },
] },
]}
user={{ name: '学员示例', email: '[email protected]' }}
/>
)
}<aside
style="
width: 266px;
height: 560px;
display: flex;
flex-direction: column;
background: #f6f6f4;
border-right: 1px solid rgba(0, 0, 0, 0.07);
"
>
<div style="display: flex; align-items: center; gap: 10px; padding: 12px 14px">
<button
type="button"
title="收起侧边栏"
class="zzm-btn zzm-btn--ghost zzm-btn--sm zzm-btn--icon"
style="flex: none"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<rect
x="1.5"
y="2.5"
width="13"
height="11"
rx="2.5"
stroke="#6E6E6A"
stroke-width="1.3"
></rect>
<line x1="6" y1="2.5" x2="6" y2="13.5" stroke="#6E6E6A" stroke-width="1.3"></line>
</svg>
</button>
<svg width="22" height="22" viewBox="0 0 24 24" aria-hidden="true">
<path
d="M4 16 L10 7 L15 12 L20 5"
stroke="#161616"
stroke-width="1.5"
fill="none"
stroke-linecap="round"
></path>
<circle cx="4" cy="16" r="2" fill="#161616"></circle>
<circle cx="10" cy="7" r="2" fill="#161616"></circle>
<circle cx="15" cy="12" r="2.4" fill="var(--zzm-accent,#A6402F)"></circle>
<circle cx="20" cy="5" r="2" fill="#161616"></circle>
</svg>
<div style="flex: 1; font-size: 15px; font-weight: 650; letter-spacing: 0.3px">赵子民</div>
</div>
<nav style="padding: 2px 12px; display: flex; flex-direction: column; gap: 2px">
<button
type="button"
class="zzm-navbtn"
style="
display: flex;
align-items: center;
gap: 10px;
padding: 9px 10px;
border: 0;
border-radius: 8px;
cursor: pointer;
text-align: left;
font-size: 13.5px;
font-weight: 600;
width: 100%;
color: #26261f;
background: rgba(0, 0, 0, 0.07);
font-family: inherit;
"
aria-current="page"
>
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M2.5 7.5 L8 2.5 L13.5 7.5"
stroke="#3A3A36"
stroke-width="1.4"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M4 7 L4 13.5 L12 13.5 L12 7"
stroke="#3A3A36"
stroke-width="1.4"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
></path>
</svg>
首页
</button>
</nav>
<nav style="padding: 2px 12px; display: flex; flex-direction: column; gap: 2px">
<button
type="button"
class="zzm-navbtn"
style="
display: flex;
align-items: center;
gap: 10px;
padding: 9px 10px;
border: 0;
border-radius: 8px;
cursor: pointer;
text-align: left;
font-size: 13.5px;
font-weight: 450;
width: 100%;
color: #26261f;
background: transparent;
font-family: inherit;
"
>
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M8 2.2 L9.7 5.8 L13.6 6.3 L10.8 9 L11.5 12.9 L8 11 L4.5 12.9 L5.2 9 L2.4 6.3 L6.3 5.8 Z"
stroke="#3A3A36"
stroke-width="1.3"
fill="none"
stroke-linejoin="round"
></path>
</svg>
我的收藏
<span style="flex: 1"></span>
<span
style="font-family:ui-monospace,'SF Mono',Menlo,monospace;font-size:9.5px;color:#B0B0AA"
>
12
</span>
</button>
<button
type="button"
class="zzm-navbtn"
style="
display: flex;
align-items: center;
gap: 10px;
padding: 9px 10px;
border: 0;
border-radius: 8px;
cursor: pointer;
text-align: left;
font-size: 13.5px;
font-weight: 450;
width: 100%;
color: #26261f;
background: transparent;
font-family: inherit;
"
>
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<circle cx="8" cy="8" r="5.6" stroke="#3A3A36" stroke-width="1.3"></circle>
<path
d="M8 5.2 L8 8.2 L10.3 9.5"
stroke="#3A3A36"
stroke-width="1.3"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
></path>
</svg>
历史记录
<span style="flex: 1"></span>
<span
style="font-family:ui-monospace,'SF Mono',Menlo,monospace;font-size:9.5px;color:#B0B0AA"
>
38
</span>
</button>
</nav>
<div style="flex: 1"></div>
<div style="padding: 10px 12px 14px; border-top: 1px solid rgba(0, 0, 0, 0.07)">
<div
class="zzm-userfoot"
style="
padding: 10px 10px 11px;
border-radius: 12px;
cursor: pointer;
border: 1px solid transparent;
transition: background 0.15s ease;
"
>
<div style="display: flex; align-items: center; gap: 10px">
<span
style="
width: 36px;
height: 36px;
border-radius: 50%;
background: #1d1d1b;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 650;
flex: none;
"
>
学
</span>
<span style="flex: 1; min-width: 0">
<span style="display: block; font-size: 13px; font-weight: 600; color: #26261f">
学员示例
</span>
<span
style="
display: block;
margin-top: 1px;
font-size: 10.5px;
color: #a2a29c;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
"
>
[email protected]
</span>
</span>
<svg
width="11"
height="11"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
style="opacity: 0.4"
>
<path
d="M6 3.5 L10.5 8 L6 12.5"
stroke="#3A3A36"
stroke-width="1.5"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
></path>
</svg>
</div>
</div>
<button type="button" class="zzm-btn zzm-btn--block" style="margin-top: 6px">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M6 2.5 L3 2.5 L3 13.5 L6 13.5"
stroke="#8B8B85"
stroke-width="1.4"
fill="none"
stroke-linecap="round"
></path>
<path
d="M7 8 L13.5 8 M11 5.5 L13.5 8 L11 10.5"
stroke="#8B8B85"
stroke-width="1.4"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
></path>
</svg>
退出登录
</button>
</div>
</aside>/* tokens.css · 用到的令牌 */
:root {
--zzm-accent: #A6402F;
}
/* base.css */
.zzm-v4 .zzm-navbtn:hover { background: rgba(0, 0, 0, .07) !important; }
.zzm-v4 .zzm-userfoot:hover { background: rgba(0, 0, 0, .045); border-color: rgba(0, 0, 0, .07) !important; }
/* controls.css */
.zzm-v4 .zzm-btn {
-webkit-appearance: none;
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 36px;
padding: 0 16px;
font-family: inherit;
font-size: 13px;
font-weight: 600;
letter-spacing: .2px;
white-space: nowrap;
cursor: pointer;
user-select: none;
border-radius: 8px; /* 圆角矩形——按钮禁胶囊 */
border: 1px solid rgba(0, 0, 0, .14);
color: #3F3F3B;
background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%);
box-shadow:
0 0 0 3px rgba(255, 255, 255, .55), /* ④ 反光光晕环 */
0 1px 2px rgba(0, 0, 0, .09), /* ③ 近影 */
0 2px 6px rgba(0, 0, 0, .06), /* ③ 远影 */
inset 0 1px 0 rgba(255, 255, 255, .92), /* ② 顶部反光 */
inset 0 -1px 0 rgba(0, 0, 0, .05); /* ① 底缘收沉 */
transition: box-shadow .16s ease, transform .16s ease, background .16s ease,
border-color .16s ease, filter .16s ease, opacity .16s ease;
}
.zzm-v4 .zzm-btn:hover {
transform: translateY(-1px);
border-color: rgba(0, 0, 0, .18);
box-shadow:
0 0 0 4px rgba(255, 255, 255, .8),
0 2px 4px rgba(0, 0, 0, .10),
0 4px 12px rgba(0, 0, 0, .09),
inset 0 1px 0 rgba(255, 255, 255, .95),
inset 0 -1px 0 rgba(0, 0, 0, .05);
}
.zzm-v4 .zzm-btn:active {
transform: translateY(0);
background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); /* ⑤ 凹面翻转 */
box-shadow:
0 0 0 3px rgba(255, 255, 255, .45),
0 1px 0 rgba(255, 255, 255, .65),
inset 0 2px 4px rgba(0, 0, 0, .13),
inset 0 1px 2px rgba(0, 0, 0, .09);
}
.zzm-v4 .zzm-btn:focus-visible {
outline: none;
box-shadow:
0 0 0 3px rgba(166, 64, 47, .20), /* 键盘焦点=朱色光晕 */
0 1px 2px rgba(0, 0, 0, .09),
0 2px 6px rgba(0, 0, 0, .06),
inset 0 1px 0 rgba(255, 255, 255, .92);
}
.zzm-v4 .zzm-btn:disabled {
cursor: not-allowed;
transform: none;
}
.zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) {
color: #A2A29C;
background: #F2F2EF;
border-color: rgba(0, 0, 0, .08);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04);
}
.zzm-v4 .zzm-btn--ghost {
border-color: transparent;
background: transparent;
box-shadow: none;
color: #4A4A46;
}
.zzm-v4 .zzm-btn--ghost:hover {
transform: translateY(-1px);
border-color: rgba(0, 0, 0, .14);
background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%);
box-shadow:
0 0 0 3px rgba(255, 255, 255, .55),
0 1px 2px rgba(0, 0, 0, .09),
0 2px 6px rgba(0, 0, 0, .06),
inset 0 1px 0 rgba(255, 255, 255, .92);
}
.zzm-v4 .zzm-btn--ghost:active {
transform: translateY(0);
border-color: rgba(0, 0, 0, .14);
background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, .12);
}
.zzm-v4 .zzm-btn--sm { height: 28px; padding: 0 12px; font-size: 12px; gap: 4px; }
.zzm-v4 .zzm-btn--icon { width: 36px; padding: 0; }
.zzm-v4 .zzm-btn--icon.zzm-btn--sm { width: 28px; }
.zzm-v4 .zzm-btn--icon.zzm-btn--lg { width: 44px; }
.zzm-v4 .zzm-btn--block { width: 100%; }import { SidebarNav } from './extracted/SidebarNav.jsx'
import { ClockIcon, HomeIcon, MapIcon, PlayRectIcon, StarIcon } from './extracted/icons.jsx'
export function Course() {
return (
<SidebarNav
active="map"
groups={[
{ items: [
{ id: 'map', label: '学习地图', icon: <MapIcon /> },
{ id: 'ovw', label: '课程总览', icon: <PlayRectIcon /> },
] },
{ label: '主线课程', items: [
{ id: 's1', label: '软件基础', count: 5 },
{ id: 's2', label: '插件教学', count: 4 },
{ id: 's3', label: '笔记系统', count: 4 },
] },
{ label: '增值课程', items: [{ id: 's4', label: '人脉管理', count: 3 }] },
]}
/>
)
}<aside
style="
width: 266px;
height: 560px;
display: flex;
flex-direction: column;
background: #f6f6f4;
border-right: 1px solid rgba(0, 0, 0, 0.07);
"
>
<div style="display: flex; align-items: center; gap: 10px; padding: 12px 14px">
<button
type="button"
title="收起侧边栏"
class="zzm-btn zzm-btn--ghost zzm-btn--sm zzm-btn--icon"
style="flex: none"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<rect
x="1.5"
y="2.5"
width="13"
height="11"
rx="2.5"
stroke="#6E6E6A"
stroke-width="1.3"
></rect>
<line x1="6" y1="2.5" x2="6" y2="13.5" stroke="#6E6E6A" stroke-width="1.3"></line>
</svg>
</button>
<svg width="22" height="22" viewBox="0 0 24 24" aria-hidden="true">
<path
d="M4 16 L10 7 L15 12 L20 5"
stroke="#161616"
stroke-width="1.5"
fill="none"
stroke-linecap="round"
></path>
<circle cx="4" cy="16" r="2" fill="#161616"></circle>
<circle cx="10" cy="7" r="2" fill="#161616"></circle>
<circle cx="15" cy="12" r="2.4" fill="var(--zzm-accent,#A6402F)"></circle>
<circle cx="20" cy="5" r="2" fill="#161616"></circle>
</svg>
<div style="flex: 1; font-size: 15px; font-weight: 650; letter-spacing: 0.3px">赵子民</div>
</div>
<nav style="padding: 2px 12px; display: flex; flex-direction: column; gap: 2px">
<button
type="button"
class="zzm-navbtn"
style="
display: flex;
align-items: center;
gap: 10px;
padding: 9px 10px;
border: 0;
border-radius: 8px;
cursor: pointer;
text-align: left;
font-size: 13.5px;
font-weight: 600;
width: 100%;
color: #26261f;
background: rgba(0, 0, 0, 0.07);
font-family: inherit;
"
aria-current="page"
>
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M2.5 12.5 L6.5 4.5 L9.5 8.5 L13.5 3"
stroke="#3A3A36"
stroke-width="1.4"
stroke-linecap="round"
fill="none"
></path>
<circle cx="9.5" cy="8.5" r="1.6" fill="var(--zzm-accent,#A6402F)"></circle>
</svg>
学习地图
</button>
<button
type="button"
class="zzm-navbtn"
style="
display: flex;
align-items: center;
gap: 10px;
padding: 9px 10px;
border: 0;
border-radius: 8px;
cursor: pointer;
text-align: left;
font-size: 13.5px;
font-weight: 450;
width: 100%;
color: #26261f;
background: transparent;
font-family: inherit;
"
>
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<rect
x="2"
y="2.5"
width="12"
height="8"
rx="1.5"
stroke="#3A3A36"
stroke-width="1.4"
></rect>
<path d="M7 5.4 L9.6 6.9 L7 8.4 Z" fill="#3A3A36"></path>
</svg>
课程总览
</button>
</nav>
<nav style="padding: 2px 12px; display: flex; flex-direction: column; gap: 2px">
<div
style="padding:12px 10px 4px;font-family:ui-monospace,'SF Mono',Menlo,monospace;font-size:10px;letter-spacing:1.5px;color:#A2A29C"
>
主线课程
</div>
<button
type="button"
class="zzm-navbtn"
style="
display: flex;
align-items: center;
gap: 10px;
padding: 9px 10px;
border: 0;
border-radius: 8px;
cursor: pointer;
text-align: left;
font-size: 13.5px;
font-weight: 450;
width: 100%;
color: #26261f;
background: transparent;
font-family: inherit;
"
>
软件基础
<span style="flex: 1"></span>
<span
style="font-family:ui-monospace,'SF Mono',Menlo,monospace;font-size:9.5px;color:#B0B0AA"
>
05
</span>
</button>
<button
type="button"
class="zzm-navbtn"
style="
display: flex;
align-items: center;
gap: 10px;
padding: 9px 10px;
border: 0;
border-radius: 8px;
cursor: pointer;
text-align: left;
font-size: 13.5px;
font-weight: 450;
width: 100%;
color: #26261f;
background: transparent;
font-family: inherit;
"
>
插件教学
<span style="flex: 1"></span>
<span
style="font-family:ui-monospace,'SF Mono',Menlo,monospace;font-size:9.5px;color:#B0B0AA"
>
04
</span>
</button>
<button
type="button"
class="zzm-navbtn"
style="
display: flex;
align-items: center;
gap: 10px;
padding: 9px 10px;
border: 0;
border-radius: 8px;
cursor: pointer;
text-align: left;
font-size: 13.5px;
font-weight: 450;
width: 100%;
color: #26261f;
background: transparent;
font-family: inherit;
"
>
笔记系统
<span style="flex: 1"></span>
<span
style="font-family:ui-monospace,'SF Mono',Menlo,monospace;font-size:9.5px;color:#B0B0AA"
>
04
</span>
</button>
</nav>
<nav style="padding: 2px 12px; display: flex; flex-direction: column; gap: 2px">
<div
style="padding:12px 10px 4px;font-family:ui-monospace,'SF Mono',Menlo,monospace;font-size:10px;letter-spacing:1.5px;color:#A2A29C"
>
增值课程
</div>
<button
type="button"
class="zzm-navbtn"
style="
display: flex;
align-items: center;
gap: 10px;
padding: 9px 10px;
border: 0;
border-radius: 8px;
cursor: pointer;
text-align: left;
font-size: 13.5px;
font-weight: 450;
width: 100%;
color: #26261f;
background: transparent;
font-family: inherit;
"
>
人脉管理
<span style="flex: 1"></span>
<span
style="font-family:ui-monospace,'SF Mono',Menlo,monospace;font-size:9.5px;color:#B0B0AA"
>
03
</span>
</button>
</nav>
<div style="flex: 1"></div>
</aside>/* tokens.css · 用到的令牌 */
:root {
--zzm-accent: #A6402F;
}
/* base.css */
.zzm-v4 .zzm-navbtn:hover { background: rgba(0, 0, 0, .07) !important; }
/* controls.css */
.zzm-v4 .zzm-btn {
-webkit-appearance: none;
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 36px;
padding: 0 16px;
font-family: inherit;
font-size: 13px;
font-weight: 600;
letter-spacing: .2px;
white-space: nowrap;
cursor: pointer;
user-select: none;
border-radius: 8px; /* 圆角矩形——按钮禁胶囊 */
border: 1px solid rgba(0, 0, 0, .14);
color: #3F3F3B;
background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%);
box-shadow:
0 0 0 3px rgba(255, 255, 255, .55), /* ④ 反光光晕环 */
0 1px 2px rgba(0, 0, 0, .09), /* ③ 近影 */
0 2px 6px rgba(0, 0, 0, .06), /* ③ 远影 */
inset 0 1px 0 rgba(255, 255, 255, .92), /* ② 顶部反光 */
inset 0 -1px 0 rgba(0, 0, 0, .05); /* ① 底缘收沉 */
transition: box-shadow .16s ease, transform .16s ease, background .16s ease,
border-color .16s ease, filter .16s ease, opacity .16s ease;
}
.zzm-v4 .zzm-btn:hover {
transform: translateY(-1px);
border-color: rgba(0, 0, 0, .18);
box-shadow:
0 0 0 4px rgba(255, 255, 255, .8),
0 2px 4px rgba(0, 0, 0, .10),
0 4px 12px rgba(0, 0, 0, .09),
inset 0 1px 0 rgba(255, 255, 255, .95),
inset 0 -1px 0 rgba(0, 0, 0, .05);
}
.zzm-v4 .zzm-btn:active {
transform: translateY(0);
background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); /* ⑤ 凹面翻转 */
box-shadow:
0 0 0 3px rgba(255, 255, 255, .45),
0 1px 0 rgba(255, 255, 255, .65),
inset 0 2px 4px rgba(0, 0, 0, .13),
inset 0 1px 2px rgba(0, 0, 0, .09);
}
.zzm-v4 .zzm-btn:focus-visible {
outline: none;
box-shadow:
0 0 0 3px rgba(166, 64, 47, .20), /* 键盘焦点=朱色光晕 */
0 1px 2px rgba(0, 0, 0, .09),
0 2px 6px rgba(0, 0, 0, .06),
inset 0 1px 0 rgba(255, 255, 255, .92);
}
.zzm-v4 .zzm-btn:disabled {
cursor: not-allowed;
transform: none;
}
.zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) {
color: #A2A29C;
background: #F2F2EF;
border-color: rgba(0, 0, 0, .08);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04);
}
.zzm-v4 .zzm-btn--ghost {
border-color: transparent;
background: transparent;
box-shadow: none;
color: #4A4A46;
}
.zzm-v4 .zzm-btn--ghost:hover {
transform: translateY(-1px);
border-color: rgba(0, 0, 0, .14);
background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%);
box-shadow:
0 0 0 3px rgba(255, 255, 255, .55),
0 1px 2px rgba(0, 0, 0, .09),
0 2px 6px rgba(0, 0, 0, .06),
inset 0 1px 0 rgba(255, 255, 255, .92);
}
.zzm-v4 .zzm-btn--ghost:active {
transform: translateY(0);
border-color: rgba(0, 0, 0, .14);
background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, .12);
}
.zzm-v4 .zzm-btn--sm { height: 28px; padding: 0 12px; font-size: 12px; gap: 4px; }
.zzm-v4 .zzm-btn--icon { width: 36px; padding: 0; }
.zzm-v4 .zzm-btn--icon.zzm-btn--sm { width: 28px; }
.zzm-v4 .zzm-btn--icon.zzm-btn--lg { width: 44px; }规格
| 部位 | 规格 |
|---|---|
| 容器 | 宽 266 · 底 #F6F6F4 · 右侧 .07 细线 · 进场 zzmSlide .28s |
| 头部 | 收起键(28 幽灵图标键,恒位左 14 / 上 12)+ 星座标 22 + 品牌名 15 / 650 |
| 导航行 | 内距 9 × 10 · 圆角 8 · 13.5px · 当前行 .07 灰底 + 600 · 悬停 .zzm-navbtn |
| 计数 | mono 9.5 · #B0B0AA · 两位补零 |
| 名片 | 36 圆形头像 · 名字 13 / 600 · 邮箱 10.5 灰 · 悬停 .zzm-userfoot |
| 退出 | .zzm-btn --block 纸键 |
铁律
✓ 要导航行扁平,当前行灰底
✗ 不要把导航行做成一排凸键
✓ 要收起键展开态与收起态同一坐标,零位移
✗ 不要用朱色标当前导航项——朱色留给内容里的「下一步」