React 组件包
assets/react/ArkUI.jsx 是一个单文件的 React 组件包(仅依赖 React 自身),与配套样式表 assets/react/ark-ui.css 一起,把「视觉家族(theme)× 信息密度(depth)」双维度设计系统封装为 7 个可组合的受控组件和 3 组导出常量。整个换肤与密度切换完全由 CSS 自定义属性 + data-* 属性选择器驱动,JS 层不做任何样式计算。
Purpose and Scope
本页覆盖 React 组件包的完整机制:
assets/react/ArkUI.jsx的模块结构、全部导出(常量与组件)、每个组件的内部实现与可访问性行为;assets/react/ark-ui.css的设计令牌体系:.arkR-shell基线变量、[data-ark-theme]家族变量、[data-ark-depth]密度变量,以及它们如何级联到子组件规则;- 主题切换、深度切换、移动端菜单、Tabs 键盘导航的真实控制流;
- 组件 props、CSS 变量、失败模式与扩展点。
不属于本页的内容(留给兄弟页面):
- 5 个视觉家族对应的纯 HTML 静态展示页(
assets/showcases/ark.html、endfield.html、exa.html、popucom.html、corporate.html)——它们是同一设计系统的非 React 呈现,本页仅在「家族对照」处引用其命名; - 宣传页资产
assets/promo/(promo.html / promo.css / promo.js)与 README 插图assets/readme/family-depth-map.zh-CN.svg。
Overview
组件包解决的问题是:用一套组件代码,同时表达 5 种截然不同的品牌视觉家族与 4 档信息密度,并且让两者可以正交组合(5 × 4 = 20 种外观,无需写 20 套样式)。
关键概念:
| 概念 | 载体 | 取值 |
|---|---|---|
| 视觉家族 theme | 根节点 data-ark-theme 属性 + CSS 家族变量 | ark / endfield / exa / popucom / corporate |
| 信息密度 depth | 根节点 data-ark-depth 属性 + CSS 密度变量 | minimal(L1) / moderate(L2) / complex(L3) / maximal(L4) |
| 主题档案 profile | JS 常量 arkThemeProfiles | 每个家族一份默认 brand / code / status 文案 |
设计意图:JS 只负责语义结构(class、ARIA、data 属性),CSS 只负责外观(变量 + 规则)。切换主题时 React 仅改写根节点的一个属性字符串,浏览器按属性选择器重新解析自定义属性,整棵子树随之换肤——没有内联样式、没有 CSS-in-JS 运行时、没有 JS 端颜色计算。
Architecture
分层说明:
- 消费方:页面把
ArkShell作为最外层外壳,其余组件作为children组合进<main>,ArkThemePicker/ArkDepthPicker通常放在内容区顶部的控制面板里。 - JS 模块层:
ArkUI.jsx第一行import './ark-ui.css'保证样式随组件包一起被引入;useState(菜单 / Tab 状态)、useEffect(导航后自动收起菜单)、useId(Tabs 的 SSR 安全 ID)是仅有的三个 React API。 - CSS 层:
.arkR-shell上的变量先被基线块赋值,再被[data-ark-theme](两段:颜色段 + 家族圆角/分隔线段)和[data-ark-depth](密度段)覆盖;所有组件规则(.arkR-panel、.arkR-button、.arkR-tabs…)只消费var(--ark-*),因此对家族与密度天然解耦。
模块结构
ArkUI.jsx 共 168 行,导出 3 个常量与 7 个函数组件,全部为命名导出(无默认导出):
| 导出 | 行号 | 类别 | 职责 |
|---|---|---|---|
arkThemes | L4 | 常量 | 家族名数组,驱动 ArkThemePicker 渲染 |
arkThemeProfiles | L5-L11 | 常量 | 每个家族的默认 brand / code / status 档案 |
arkDepths | L12-L17 | 常量 | 4 档密度的 value / level / label,驱动 ArkDepthPicker |
ArkShell | L19-L67 | 布局 | 顶栏 + 左侧导航栏 + 主内容区三段式外壳 |
ArkSectionTitle | L69-L77 | 展示 | 章节大标题(kicker + 序号 + 巨型 h2) |
ArkPanel | L79-L88 | 展示 | 内容卡片,支持 paper / ink 两种色调 |
ArkButton | L90-L92 | 展示 | 带 signal 色侧标的按钮,透传原生 props |
ArkThemePicker | L94-L107 | 交互 | 家族切换按钮组(aria-pressed 单选语义) |
ArkDepthPicker | L109-L122 | 交互 | 密度切换按钮组(aria-pressed 单选语义) |
ArkTabs | L124-L167 | 交互 | 完整 WAI-ARIA Tabs 模式(roving tabIndex + 方向键) |
模块级数据源如下——注意 arkDepths 的 level 字段被 ArkDepthPicker 直接渲染为按钮文案({depth.level} / {depth.label}),value 才是写入 data-ark-depth 的实际键:
1export const arkThemes = ['ark', 'endfield', 'exa', 'popucom', 'corporate'];
2export const arkThemeProfiles = {
3 ark: { brand: 'TERRA INDEX', code: 'OPERATION / 07', status: 'SHIFT ACTIVE' },
4 endfield: { brand: 'FIELD RELAY', code: 'LOGISTICS / 04', status: 'ROUTE VERIFIED' },
5 exa: { brand: 'WIND ATLAS', code: 'JOURNEY / 03', status: 'RECORD ALIGNED' },
6 popucom: { brand: 'PRISM PLAZA', code: 'PARTY ROOM / 204', status: '2 OF 4 READY' },
7 corporate: { brand: 'STUDIO INDEX', code: 'PROJECTS / 05', status: 'PORTFOLIO OPEN' },
8};
9export const arkDepths = [
10 { value: 'minimal', level: 1, label: 'Minimal' },
11 { value: 'moderate', level: 2, label: 'Moderate' },
12 { value: 'complex', level: 3, label: 'Complex' },
13 { value: 'maximal', level: 4, label: 'Maximal' },
14];Source: assets/react/ArkUI.jsx
设计意图:把「可选值」与「展示文案」收敛为模块级单一事实来源。ArkThemePicker / ArkDepthPicker 遍历这两个常量渲染按钮,因此新增一个家族只需同时改 JS 常量与 CSS 变量块(见「扩展点」),不需要触碰任何组件实现。
Core Flow
主题切换(theme)的端到端路径
要点:
ArkThemePicker是受控组件——自身不持有选中态,只读取value渲染aria-pressed,变更通过onChange?.(theme)回调抛给页面(L94-L107)。可选链调用意味着允许无回调的纯展示用法。- 换肤的唯一动作是
ArkShell根节点上的data-ark-theme={theme}(L36)。CSS 端用两段属性选择器完成覆盖:颜色段(L13-L16)+ 家族形制段(L17-L21,圆角--ark-family-radius与分隔线--ark-family-rule)。 ArkShell对未知家族做了降级:arkThemeProfiles[theme] || arkThemeProfiles.endfield(L31),无效值回退到 endfield 的品牌文案,避免顶栏出现undefined。
密度切换(depth)与移动端菜单流
ArkDepthPicker 与主题选择器同构(L109-L122),区别仅在于渲染 arkDepths 且按钮文案带档位号。CSS 端 [data-ark-depth] 段(L9-L12)一次性定义三个变量:
1.arkR-shell[data-ark-depth="minimal"]{--ark-depth-rule:12%;--ark-depth-panel-min:15rem;--ark-depth-shadow:0 0 0 transparent}
2.arkR-shell[data-ark-depth="moderate"]{--ark-depth-rule:18%;--ark-depth-panel-min:18rem;--ark-depth-shadow:2px 2px 0 color-mix(in srgb,var(--ark-ink),transparent 90%)}
3.arkR-shell[data-ark-depth="complex"]{--ark-depth-rule:24%;--ark-depth-panel-min:22rem;--ark-depth-shadow:5px 5px 0 color-mix(in srgb,var(--ark-ink),transparent 86%)}
4.arkR-shell[data-ark-depth="maximal"]{--ark-depth-rule:34%;--ark-depth-panel-min:26rem;--ark-depth-shadow:9px 9px 0 color-mix(in srgb,var(--ark-signal),transparent 42%)}Source: assets/react/ark-ui.css
三个变量在规则层各司其职(见 L35 的 .arkR-panel):--ark-depth-rule 控制面板边框与背景的混合透明度(密度越高边框越"实")、--ark-depth-panel-min 控制 min-height、--ark-depth-shadow 从 transparent(minimal 完全无投影)逐级放大到 9px 的 signal 色硬阴影(maximal)。此外 L38 还有少量点状覆写:minimal 档把品牌角标 border-width 收窄为 1px、去掉 clip-path;maximal 档给顶栏叠加 32px 重复的竖向栅格纹理。
ArkShell 内部唯一的自持状态是移动端菜单 menuOpen,配合一条副作用实现"导航即收起":
1export function ArkShell({
2 brand, code, status,
3 theme = 'endfield', depth = 'complex',
4 nav = [], activeId, onNavigate, children,
5}) {
6 const [menuOpen, setMenuOpen] = useState(false);
7 const profile = arkThemeProfiles[theme] || arkThemeProfiles.endfield;
8
9 useEffect(() => setMenuOpen(false), [activeId]);
10
11 return (
12 <div className="arkR-shell" data-ark-theme={theme} data-ark-depth={depth}>
13 <header className="arkR-topbar">
14 <div className="arkR-brand">
15 <span className="arkR-brandMark" aria-hidden="true" />
16 <span><strong>{brand || profile.brand}</strong><small>{code || profile.code}</small></span>
17 </div>
18 <span className="arkR-online"><i aria-hidden="true" /> {status || profile.status}</span>
19 <button
20 className="arkR-menu"
21 type="button"
22 aria-expanded={menuOpen}
23 aria-controls="arkR-rail"
24 onClick={() => setMenuOpen((value) => !value)}
25 >Menu</button>
26 </header>
27 <nav className="arkR-rail" id="arkR-rail" data-open={menuOpen} aria-label="Primary">
28 {nav.map((item, index) => (
29 <button
30 key={item.id}
31 type="button"
32 className={item.id === activeId ? 'is-active' : undefined}
33 aria-current={item.id === activeId ? 'page' : undefined}
34 onClick={() => onNavigate?.(item.id)}
35 >
36 <span>{String(index + 1).padStart(2, '0')}</span>{item.label}
37 </button>
38 ))}
39 </nav>
40 <main className="arkR-main">{children}</main>
41 </div>
42 );
43}Source: assets/react/ArkUI.jsx
逐点解读:
- 双维度属性写在一个根节点上(L36):
data-ark-theme与data-ark-depth同节点共存,CSS 端两条属性选择器串行覆盖同一批基础变量,天然正交。 - 属性优先(props > profile):
brand || profile.brand的三重回退链是"显式 props → 家族档案 → (被||保证的)endfield 默认",页面可不传任何文案字段直接拿到该家族的默认顶栏。 - 可访问性闭环:菜单按钮
aria-expanded+aria-controls="arkR-rail"与<nav id="arkR-rail" data-open>一一对应;导航按钮用aria-current="page"标记当前项。注意aria-controls直接写死了 idarkR-rail——因为整个ArkShell页面只应出现一次;若同页渲染两个ArkShell会产生 id 冲突(见「失败模式」)。 useEffect(() => setMenuOpen(false), [activeId]):依赖数组只含activeId,即"选中项一变就强制收起菜单"。这是刻意的收敛——移动端点击导航项后侧栏必须自动关闭,否则遮挡内容;同时该 effect 在首挂载时也会执行一次(无害,本来就是 false)。
ArkTabs:完整 ARIA Tabs 模式
ArkTabs 是包内最复杂的组件,实现了 WAI-ARIA Tabs 的 roving tabindex 键盘模式:
1export function ArkTabs({ items = [], label = 'Details' }) {
2 const baseId = useId();
3 const [selected, setSelected] = useState(items[0]?.id);
4
5 function onKeyDown(event, index) {
6 if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
7 event.preventDefault();
8 let next = index;
9 if (event.key === 'ArrowLeft') next = (index - 1 + items.length) % items.length;
10 if (event.key === 'ArrowRight') next = (index + 1) % items.length;
11 if (event.key === 'Home') next = 0;
12 if (event.key === 'End') next = items.length - 1;
13 setSelected(items[next].id);
14 document.getElementById(`${baseId}-tab-${items[next].id}`)?.focus();
15 }
16
17 return (
18 <div className="arkR-tabs">
19 <div className="arkR-tabList" role="tablist" aria-label={label}>
20 {items.map((item, index) => (
21 <button
22 key={item.id}
23 id={`${baseId}-tab-${item.id}`}
24 type="button"
25 role="tab"
26 aria-selected={selected === item.id}
27 aria-controls={`${baseId}-panel-${item.id}`}
28 tabIndex={selected === item.id ? 0 : -1}
29 onClick={() => setSelected(item.id)}
30 onKeyDown={(event) => onKeyDown(event, index)}
31 >{String(index + 1).padStart(2, '0')} / {item.label}</button>
32 ))}
33 </div>
34 {items.map((item) => (
35 <section
36 key={item.id}
37 id={`${baseId}-panel-${item.id}`}
38 role="tabpanel"
39 aria-labelledby={`${baseId}-tab-${item.id}`}
40 hidden={selected !== item.id}
41 >{item.content}</section>
42 ))}
43 </div>
44 );
45}Source: assets/react/ArkUI.jsx
控制流解读:
- 非受控选中态:
selected由组件内部useState(items[0]?.id)初始化,未暴露受控 props——与ArkThemePicker的受控设计形成对比:Tabs 的选中被视为纯展示关注点,主题/密度则是页面级状态。 - 键盘模式:
ArrowLeft/ArrowRight使用(index ± 1 + len) % len环形取模,Home/End跳首尾。切换后手动focus()目标 tab——这正是 roving tabindex 的正确实现:tabIndex={selected === item.id ? 0 : -1}保证 Tab 键只进入当前选中项,方向键在项间"游走"。 - 面板策略:所有
tabpanel始终渲染在 DOM 中,仅用 HTMLhidden属性切换显隐。相比条件渲染(卸载/重挂载),保留 DOM 意味着切换 Tab 不丢失面板内部状态与滚动位置,也便于打印/搜索到全部内容。 - ID 体系:
baseId来自useId()(React 18+,SSR 安全),再拼-tab-{id}/-panel-{id}两套 id,用aria-controls/aria-labelledby双向绑定。用?.调focus(),即使未来出现 id 漂移也只是不聚焦而不抛错。 - 联动样式:L39 的
.arkR-tabList button[aria-selected="true"]:before用 signal 色画选中态左竖条——样式选择器直接消费 ARIA 状态,状态与外观天然一致,无需额外 class。
CSS 令牌体系(数据模型)
组件包的"数据模型"就是三层 CSS 自定义属性的级联。基线块(默认 = endfield 家族配色)如下:
1.arkR-shell {
2 --ark-ink:#191919; --ark-paper:#f2f2f0; --ark-signal:#fffa00; --ark-state:#00ffa2;
3 ...
4}
5.arkR-shell[data-ark-theme="ark"]{--ark-ink:#080a0b;--ark-paper:#f4f6f6;--ark-signal:#18d1ff;--ark-state:#c8eb21}
6.arkR-shell[data-ark-theme="exa"]{--ark-ink:#080914;--ark-paper:#f3f2ef;--ark-signal:#46f6e6;--ark-state:#925dff;--ark-display:"Noto Serif SC","Source Han Serif SC",serif}
7.arkR-shell[data-ark-theme="popucom"]{--ark-ink:#141414;--ark-paper:#fffdf4;--ark-signal:#ffcc1a;--ark-state:#3994ff}
8.arkR-shell[data-ark-theme="corporate"]{--ark-ink:#050505;--ark-paper:#f3f3f3;--ark-signal:#f3ff00;--ark-state:#fff}Source: assets/react/ark-ui.css
家族 × 密度对照表
| 家族(theme) | --ark-ink / --ark-paper | --ark-signal / --ark-state | --ark-family-radius | 家族特化(L40-L41) |
|---|---|---|---|---|
ark | #080a0b / #f4f6f6 | #18d1ff / #c8eb21 | 0 | 顶栏硬底色;ink 面板叠加 3rem 网格底纹 |
endfield(默认) | #191919 / #f2f2f0 | #fffa00 / #00ffa2 | 2px | 仅基线,无覆写 |
exa | #080914 / #f3f2ef | #46f6e6 / #925dff | 2rem | 衬线显示字体 --ark-display;侧栏暗底、全套圆角化、按钮侧标变圆点 |
popucom | #141414 / #fffdf4 | #ffcc1a / #3994ff | 1.35rem | — |
corporate | #050505 / #f3f3f3 | #f3ff00 / #ffffff | 0 | — |
| 密度(depth) | --ark-depth-rule | --ark-depth-panel-min | --ark-depth-shadow | 附加覆写(L38) |
|---|---|---|---|---|
minimal(L1) | 12% | 15rem | 无(transparent) | 品牌角标 1px 边、无 clip-path;章节 kicker 前缀变窄 |
moderate(L2) | 18% | 18rem | 2px ink 混合阴影 | — |
complex(L3,默认) | 24% | 22rem | 5px ink 混合阴影 | — |
maximal(L4) | 34% | 26rem | 9px signal 色阴影 | 顶栏 32px 重复竖栅格底纹 |
设计意图:颜色变量(--ark-* 四色 + --ark-display 字体)与形制变量(--ark-family-radius / --ark-family-rule)刻意分成两个选择器块(L13-L16 与 L17-L21),因为二者语义不同:前者是"品牌色板",后者是"品牌形状语言"。同样,密度变量只描述"边框/尺寸/阴影强度"三件事,任何新规则只要继续消费这批 var(),就自动获得全部 20 种组合的外观,无需为每个家族写专门规则——exa 的全套圆角化(L41)是极少数需要家族特化规则的例外。
Usage Examples
基础用法:最小可运行页面骨架
1import { ArkShell, ArkSectionTitle, ArkPanel, ArkButton } from './assets/react/ArkUI.jsx';
2
3function App() {
4 return (
5 <ArkShell
6 theme="exa" // 值必须来自 arkThemes
7 depth="moderate" // 值必须来自 arkDepths
8 nav={[
9 { id: 'overview', label: 'Overview' },
10 { id: 'detail', label: 'Detail' },
11 ]}
12 activeId="overview"
13 >
14 <ArkSectionTitle index="02" kicker="Field Notes">Wind Atlas</ArkSectionTitle>
15 <ArkPanel code="JOURNEY / 03" title="Route Summary" tone="ink">
16 <p>面板正文……</p>
17 <ArkButton primary type="submit">Confirm</ArkButton>
18 </ArkPanel>
19 </ArkShell>
20 );
21}Source: assets/react/ArkUI.jsx
说明:省略 brand / code / status 时顶栏自动取 arkThemeProfiles.exa 的 WIND ATLAS / JOURNEY / 03 / RECORD ALIGNED;省略 theme / depth 时分别落到默认值 endfield / complex。ArkButton 透传全部原生 button props(...props),因此 type="submit"、disabled、onClick 等均可直接使用。
进阶用法:主题/密度实时切换(受控组合)
1import { useState } from 'react';
2import {
3 ArkShell, ArkThemePicker, ArkDepthPicker, ArkTabs,
4 arkThemes, arkThemeProfiles, arkDepths,
5} from './assets/react/ArkUI.jsx';
6
7function Lab() {
8 const [theme, setTheme] = useState(arkThemes[1]); // 'endfield'
9 const [depth, setDepth] = useState(arkDepths[2].value); // 'complex'
10
11 return (
12 <ArkShell theme={theme} depth={depth} nav={[]} >
13 <ArkThemePicker value={theme} onChange={setTheme} />
14 <ArkDepthPicker value={depth} onChange={setDepth} />
15 <ArkTabs
16 label="Details"
17 items={[
18 { id: 'spec', label: 'Spec', content: <p>……</p> },
19 { id: 'usage', label: 'Usage', content: <p>……</p> },
20 ]}
21 />
22 </ArkShell>
23 );
24}Source: assets/react/ArkUI.jsx
要点:value 初值直接从导出常量派生(arkThemes[1]、arkDepths[2].value),保证选择器按钮组、根节点属性与 CSS 选择器三处的键永远一致;两个 picker 的 onChange 直接用 setTheme / setDepth 函数引用,省去中间包装。
纯 CSS 侧:消费设计令牌的自定义规则
1/* 在项目样式表中追加,即可让自定义元素进入家族/密度体系 */
2.myCallout {
3 border: 1px solid color-mix(in srgb, var(--ark-ink), transparent calc(100% - var(--ark-depth-rule)));
4 background: var(--ark-paper);
5 box-shadow: var(--ark-depth-shadow);
6 border-radius: var(--ark-family-radius, 0);
7}Sources:
只要该元素位于 .arkR-shell 子树内,.myCallout 便会随 theme/depth 切换同步换装——这正是"规则只消费 var(--ark-*)"分层带来的免费扩展能力。
API Reference
所有组件均为命名导出的函数组件,无默认导出。以下签名均摘自源码。
arkThemes: string[]
家族名数组 ['ark', 'endfield', 'exa', 'popucom', 'corporate'],是 data-ark-theme 合法键的单一事实来源。
arkThemeProfiles: Record<string, {brand, code, status}>
每个家族的默认顶栏文案档案;未传 props 时由 ArkShell 回退使用,未知键回退 endfield。
arkDepths: Array<{value, level, label}>
密度档位数组(value 为 CSS 键、level 为档位号、label 为按钮文案),是 data-ark-depth 合法键的单一事实来源。
ArkShell(props): JSX.Element
Parameters(均来自 L19-L29):
| Prop | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
brand | string | 否 | 家族档案 profile.brand | 顶栏品牌名,优先级高于档案 |
code | string | 否 | profile.code | 品牌名下的小字代码 |
status | string | 否 | profile.status | 顶栏右侧在线状态文案 |
theme | string | 否 | 'endfield' | 写入 data-ark-theme;未知值仅回退文案,不回退属性 |
depth | string | 否 | 'complex' | 写入 data-ark-depth |
nav | Array<{id, label}> | 否 | [] | 左侧导航按钮组 |
activeId | string | 否 | — | 当前选中项 id,驱动 aria-current 并触发菜单收起副作用 |
onNavigate | (id) => void | 否 | — | 导航点击回调,可选链调用 |
children | ReactNode | 否 | — | 渲染进 <main className="arkR-main"> |
内部状态: menuOpen: boolean(移动端菜单)。
ArkSectionTitle({ index = '01', kicker, children })
章节标题。渲染 kicker / {index} 小字与巨型 h2(字号由 --ark-display + clamp() 控制)。
ArkPanel({ code, title, tone = 'paper', children, action })
内容卡片。
| Prop | 类型 | 默认 | 说明 |
|---|---|---|---|
code | string | — | 左上角等宽小字代码 |
title | string | — | 卡片标题(h3) |
tone | 'paper' | 'ink' | 'paper' | 写入 data-tone;CSS 侧 [data-tone="ink"] 反转为 ink 底白字 |
children | ReactNode | — | 卡片正文(.arkR-panelBody) |
action | ReactNode | — | 卡片底部操作区(不包 wrapper,直接插入 article 末尾) |
ArkButton({ primary = false, className = '', ...props })
按钮。拼装 class:`arkR-button ${primary ? 'is-primary' : ''} ${className}`.trim()(L91),其余 props 原样透传给 <button>。primary 追加 is-primary(ink 底白字),空白字符串经 .trim() 规避。
ArkThemePicker({ value, onChange }) / ArkDepthPicker({ value, onChange })
受控按钮组:role="group",遍历 arkThemes / arkDepths 渲染按钮,选中项标 aria-pressed={value === 键}(CSS 侧以 [aria-pressed="true"] 画选中态),点击触发 onChange?.(键)。ArkDepthPicker 按钮文案为 {level} / {label}。
ArkTabs({ items = [], label = 'Details' })
Parameters:
items(Array<{id, label, content}>,默认[]):标签页集合;content为 ReactNode。label(string,默认'Details'):role="tablist"的aria-label。
内部状态: selected(初始 items[0]?.id,非受控)。键盘: ArrowLeft / ArrowRight(环形)、Home / End,切换后聚焦目标 tab。
Configuration Options
组件包没有运行时配置文件,全部"配置"通过 props 与 CSS 变量表达:
| 配置项 | 载体 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| 视觉家族 | ArkShell prop theme → data-ark-theme | 'ark'|'endfield'|'exa'|'popucom'|'corporate' | 'endfield' | 选择 CSS 家族变量块 |
| 信息密度 | ArkShell prop depth → data-ark-depth | 'minimal'|'moderate'|'complex'|'maximal' | 'complex' | 选择 CSS 密度变量块 |
| 顶栏文案 | brand / code / status | string | 家族档案值 | 显式 props 覆盖档案 |
| 卡片色调 | ArkPanel prop tone | 'paper'|'ink' | 'paper' | 卡片反色开关 |
| 选中态外观 | CSS [aria-pressed="true"] 规则 | — | signal 底 | 修改该规则即改 picker 选中样式 |
| 焦点环 | CSS .arkR-shell :focus-visible | — | 2px solid var(--ark-signal) | 随家族自动换色 |
Failure Modes, Edge Cases & Concurrency
- 未知
theme值:arkThemeProfiles[theme] || arkThemeProfiles.endfield(L31)只回退顶栏文案;根节点仍会写入非法data-ark-theme,导致 CSS 端既不命中任何家族变量块,也就回落到.arkR-shell基线(恰为 endfield 配色)。净效果是外观降级安全,但theme应始终取自arkThemes。 - 未知
depth值:无 JS 校验;非法值使[data-ark-depth]全不命中,--ark-depth-*三个变量未定义。.arkR-panel的min-height: var(--ark-depth-panel-min)与box-shadow: var(--ark-depth-shadow)将退化为初始值(无最小高、无阴影),边框color-mix中未定义变量会使整条声明失效——面板视觉明显塌陷,属静默失败。 - 多个
ArkShell同页:aria-controls="arkR-rail"与id="arkR-rail"是硬编码 id(L47、L51),第二个实例会产生重复 id,破坏可访问性关联(对比ArkTabs已用useId())。组件包隐含"单 Shell 页面"约定。 ArkTabs空数组:items = []时useState(items[0]?.id)得undefined,items.map渲染空 tablist 与空面板——安全空态,无异常。ArkTabs的items运行时变更:selected只在挂载时初始化。若后续items换成不含当前selected的集合,所有 tab 的aria-selected均为 false、所有面板hidden,页面出现"全隐藏"状态;onKeyDown中的items[next]因next来自旧数组边界,理论上可越界(items.length取的是新数组长度)。调用方应保证 key 稳定或以key={...}强制重挂载。- 并发/重入:组件包全部状态为组件局部
useState,无共享可变模块状态、无外部 store、无网络/持久化副作用,天然无并发竞态;setMenuOpen((value) => !value)使用函数式更新,多次快速点击也安全。 onNavigate?.()/onChange?.()可选链:未传回调时静默跳过而非抛错,允许纯静态演示场景复用同一组件。
Performance & Operational Notes
- 换肤成本模型:theme/depth 切换只改根节点一个属性字符串,React 提交极小;浏览器随后对子树做自定义属性重解析。相比 CSS-in-JS 方案(逐元素写内联样式)或 JS 端主题对象映射,这是接近零 JS 成本的路径。
useId时机:ArkTabs的 id 在 SSR/Hydration 下稳定,组件可在服务端渲染环境直接使用(组件包无window/document顶层访问;document.getElementById仅出现在键盘事件处理器内)。- CSS 打包假设:
import './ark-ui.css'(L2)要求构建链支持从 JS 导入 CSS(Vite/Webpack/css-loader 等常见方案均满足);纯 CDN/无构建场景需手动以<link>引入样式表。 - 依赖面:仅
react一个运行时依赖(useEffect/useId/useState),无第三方库;useId要求 React 18+。 - 可维护性观察:样式表中大量单行多规则压缩(如 L35、L39),修改时注意这些行的级联顺序——家族特化规则(L40-L41)位于通用规则之后,靠源码顺序取胜,重排会改变 exa 圆角等特化是否生效。
Extension Points
- 新增视觉家族(4 步,互不触碰组件实现):
arkThemes数组追加键名(L4);arkThemeProfiles追加该家族的brand/code/status(L5-L11);- CSS 增加颜色段
.arkR-shell[data-ark-theme="<键>"]{--ark-ink:…;--ark-paper:…;--ark-signal:…;--ark-state:…}与形制段--ark-family-radius/--ark-family-rule(参照 L13-L21); - 需要家族特化时再追加
.arkR-shell[data-ark-theme="<键>"] .arkR-…覆写规则(参照 L40-L41 的 ark 网格底纹、exa 圆角化)。ArkThemePicker因遍历arkThemes自动多出一个按钮。
- 新增密度档:向
arkDepths追加{value, level, label},并在 CSS 增加[data-ark-depth="<value>"]变量块(参照 L9-L12);ArkDepthPicker自动渲染。 - 自定义组件接入体系:在
.arkR-shell子树内新增任意元素,规则只消费var(--ark-*)令牌即可免费获得 5×4 组合外观(见 Usage Examples 的纯 CSS 示例)。 - 导航内容扩展:
nav项当前仅使用id与label,且渲染时自动生成String(index + 1).padStart(2, '0')序号(L60)——可安全附加图标等额外字段而不影响现有行为。
Related Links
- 同一设计系统的静态 HTML 呈现(兄弟主题,本页不展开):assets/showcases/ark.html、assets/showcases/endfield.html、assets/showcases/exa.html、assets/showcases/popucom.html、assets/showcases/corporate.html
- 组件包样式表:assets/react/ark-ui.css
- 组件包源码:assets/react/ArkUI.jsx
- 家族 × 密度全景图(README 插图):assets/readme/family-depth-map.zh-CN.svg
注:仓库内未发现
ArkUI.jsx的自动化测试文件或其余直接 import 该模块的源文件(基于本次源码检索范围),故本页无 Tests 小节;上方案例均为按源码 props 契约编写的组合示例。