CodeView 代码视图
一段代码的逐行呈现:行号、指定行高亮、超长折叠、文件名,可选语法着色,支持流式追加时的未闭合状态。
用法
代码原文由宿主提供,组件切出逐行结构并铺设记号;渲染文件名后它即成为代码块的可访问名
export function createTicker(intervalTime: number) { let handle = 0 return { start(onTick: () => void) { handle = setInterval(onTick, intervalTime) }, stop() { clearInterval(handle) }, }}组件结构
加粗的是必需部件。
data-scope="code-view":root · header · filename · lang-label · pre · code · line · line-number · line-content · token · fold-trigger
示例
行号与高亮行
行号由皮肤绘制,复制代码不会带上它;高亮行按行号写,与 startLine 对齐
function resolve(input: string) { const trimmed = input.trim() if (trimmed === '') { return null } return trimmed.toLowerCase()}折叠超长代码
clamped 是纯受控的:组件只发意图,是否落实由宿主决定,便于全部展开这类操作统一持有
const step1 = pipeline.at(0)const step2 = pipeline.at(1)const step3 = pipeline.at(2)const step4 = pipeline.at(3)const step5 = pipeline.at(4)const step6 = pipeline.at(5)const step7 = pipeline.at(6)const step8 = pipeline.at(7)const step9 = pipeline.at(8)const step10 = pipeline.at(9)const step11 = pipeline.at(10)const step12 = pipeline.at(11)const step13 = pipeline.at(12)const step14 = pipeline.at(13)const step15 = pipeline.at(14)const step16 = pipeline.at(15)const step17 = pipeline.at(16)const step18 = pipeline.at(17)const step19 = pipeline.at(18)const step20 = pipeline.at(19)const step21 = pipeline.at(20)const step22 = pipeline.at(21)const step23 = pipeline.at(22)const step24 = pipeline.at(23)流式追加
代码仍在写入时默认不着色:不完整代码的词法本就不稳定,每来一个字符整块变色比不着色更差
头部内建复制
复制交给剪贴板:把它放进头部条,用几个槽把描边按钮压为安静形态,1500 毫秒后自动回落
export function createStore(reduce: Reducer, initial: State) { let state = initial return { get: () => state, dispatch(action: Action) { state = reduce(state, action) }, }}着色端口
着色是可替换的端口:无法识别的语言退回纯文本,接入自己的实现时组件侧无需修改,传 null 则整个关闭
# 部署清单image: xihan/ui:latestreplicas: 2env: - name: MODE value: production# 部署清单image: xihan/ui:latestreplicas: 2env: - name: MODE value: production# 部署清单image: xihan/ui:latestreplicas: 2env: - name: MODE value: production流式期间也着色
未闭合默认不着色;确需着色时开启 highlight-while-streaming,同一段不完整代码的两种呈现并排对照
const stream = await client.chat({ model: 'demo', messages, onToken(token) { buffer +=const stream = await client.chat({ model: 'demo', messages, onToken(token) { buffer +=尺寸
size 切换字号、行高与内边距三档,行号槽与折叠按钮随之变化
export function clamp(n: number, min: number, max: number) { return Math.min(Math.max(n, min), max)}export function clamp(n: number, min: number, max: number) { return Math.min(Math.max(n, min), max)}export function clamp(n: number, min: number, max: number) { return Math.min(Math.max(n, min), max)}设计指引
何时使用
- 在 AI 回复、文档、评审意见中展示代码,需要行号或需要指出某几行。
- 代码是流式生成的,需要边接收边渲染,闭合之后再着色。
- 代码较长,默认只显示前若干行。
何时不用
特性
- 逐行切分在连接层完成。一个记号可以横跨多行(未闭合的字符串与块注释),因此行号与高亮行不能由皮肤反推。
complete标记这段代码是否已经写完。未闭合时默认不着色:半截代码的词法不稳定,逐字符变色比不着色更差。highlighter是着色端口,由宿主决定接入哪个着色器;返回null是合法结果,回到纯文本。适配器默认接@xihan-ui/code-highlight,它是可选 peer:已安装时自动着色,未安装时保持纯文本。适配器显式传null时不请求默认模块;只有模块缺席才回到纯文本,已安装模块的加载或初始化异常照常抛出。- 行号由皮肤用
attr()绘制,复制代码不会带上行号,读屏也不会逐行读出数字。 clamped是纯受控的:折叠状态通常由外部“全部展开 / 全部折叠”统一持有,内建状态会与之冲突。
组合
- 与剪贴板配合提供复制;需要非受控折叠时放入折叠区域。把剪贴板的三个部件放进
header,再用--xh-clipboard-copy-trigger-border: transparent、--xh-clipboard-copy-trigger-bg: transparent、--xh-clipboard-copy-trigger-h: var(--xh-control-h-sm)三个槽把按钮调整为头部内的低强调形态。 - 内建词法只区分注释、字符串、数字、关键字、标点五档。需要区分函数名、类型名、属性名时,自行实现
highlighter端口(同步纯函数,可接 Shiki 等)传入,皮肤按记号种类上色的规则不变。 - 放进 AI 回复正文时由流式正文交付代码块。
最佳实践
- 标出语言,读者与着色器都需要它。
- 高亮行用于指出重点,不一次点亮半屏。
- 折叠阈值取十几行:过少时读者每次都要展开,过多时折叠失去意义。
反模式
- 把代码放进普通段落,空白与换行会被折叠。
- 用行号作为跳转锚点,它是绘制上去的,DOM 中不可选中。
API 参考
产物
| 层 | 值 |
|---|---|
| 自定义元素 | <xh-code-view> |
| Vue 组件 | XhCodeViewCode XhCodeViewFilename XhCodeViewFoldTrigger XhCodeViewHeader XhCodeViewLangLabel XhCodeViewPre XhCodeViewRoot |
| 组合式函数 | useCodeView |
| 状态机 | codeViewMachine |
| 皮肤 | @xihan-ui/styles/code-view.css |
Props
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
code | string | 是 | |
lang | string | 围栏语言标注,空白一律落为 plaintext。 | |
filename | string | 文件名,渲染在 header 中;渲染之后它即为 pre 的可访问名。 | |
labelled | boolean | 作者渲染了 filename 部件时置真,由适配器统计而不是判断 filename 是否有值。 为假时 pre 用 translations.code 兜底:指向未渲染的 id 会使读屏读空。 | |
complete | boolean | 代码是否已闭合,未闭合时按行数预撑高度且默认不着色。 | |
wrap | boolean | 长行自动换行,默认关闭(长行横向滚动)。 | |
lineNumbers | boolean | 渲染行号槽。 | |
startLine | number | 首行的行号,默认 1;摘录与 patch 片段需要使用。 | |
highlightLines | string | readonly number[] | 要高亮的行号,写为 '3,7-9' 或行号数组;非法片段丢弃不报错。 | |
clamp | number | 超过该行数才视为可折叠。 | |
clamped | boolean | 折叠态,纯受控:没有 defaultClamped,需要非受控时套用 collapsible。 | |
highlighter | HighlighterPort | 着色实现。未提供时为纯文本,提供后也允许返回 null(语言未识别等),同样回退为纯文本。 未闭合的块默认不着色,见 {@link highlightWhileStreaming}。 | |
highlightWhileStreaming | boolean | 块尚未闭合时也着色,默认 false。 默认关闭是因为未闭合代码的词法本身不稳定:引号、括号随时会配对, 每到一个 token 整块变一次色,比不着色更差。 | |
size | Size | 尺寸:sm / md / lg。 | |
translations | Partial<CodeViewTranslations> | ||
onClampToggle | (details: CodeViewClampToggleDetails) => void | 折叠态切换的意图回调;clamped 是纯受控的,是否落定由宿主决定。 |
事件
自定义元素将载荷放在 detail;Vue 使用同名 emit。
| 事件 | 载荷 | 说明 |
|---|---|---|
clamp-toggle | CustomEvent | 折叠态切换的意图;detail 为 { clamped: boolean } |
插槽
仅列出带载荷的插槽。
| Vue 组件 | 插槽 | 载荷 | 说明 |
|---|---|---|---|
XhCodeViewCode | line | CodeViewLineSlotProps | |
XhCodeViewRoot | default | CodeViewRootSlotProps |
状态
公开状态写入 data-state。
| 部件 | 取值 |
|---|---|
fold-trigger | 'closed' | 'open' |
以下名称仅用于内部状态机。
状态:idle
事件:PRESS.START · PRESS.END
判据:canPress
connect API
getXxxProps() 返回对应部件的宿主属性。
| 成员 | 类型 | 说明 |
|---|---|---|
lang | string | |
lineCount | number | |
lines | readonly CodeLine[] | 逐行切分后的文本与记号片段。 |
lineNumberAt | (index: number) => number | 每行的行号,与 lines 同序。 |
lineNumbers | boolean | 是否渲染行号槽;适配器据此决定是否创建该节点。 |
foldable | boolean | 折叠可用:提供了正数 clamp 且行数确实超过它。 |
clamped | boolean | |
setClamped | (next: boolean) => void | 发出一次折叠意图;与当前态相同时不发。 |
getRootProps | () => T['element'] | |
getHeaderProps | () => T['element'] | |
getFilenameProps | () => T['element'] | |
getLangLabelProps | () => T['element'] | |
getPreProps | () => T['element'] | |
getCodeProps | () => T['element'] | |
getLineProps | (props: CodeViewLineProps) => T['element'] | |
getLineNumberProps | (props: CodeViewLineProps) => T['element'] | |
getLineContentProps | (props: CodeViewLineProps) => T['element'] | |
getTokenProps | (token: CodeToken) => T['element'] | |
getFoldTriggerProps | () => T['button'] |
无障碍
键盘
规格出处:W3C APG
| 按键 | 生效条件 | 行为 |
|---|---|---|
Tab | 代码块在 Tab 序列中 | <pre> 自身可聚焦,随后方向键的横向滚动交给浏览器,组件不接管 |
Enter / Space | 焦点在折叠按钮上 | 翻面折叠态并发出意图;组件只接 click,按键走原生 button 的默认行为 |
Enter / Space | 按住折叠按钮且代码可折叠 | 按住期间 fold-trigger 投影 data-pressed,与指针 :active 同一副按压面(disclosure trigger 只换面不缩放);抬起、失焦或折叠条收起撤下 |
ARIA
以下属性由 connect 生成。
| 部件 | 属性 | 值 |
|---|---|---|
lang-label | aria-hidden | 'true' |
pre | role | 'group' |
line-number | aria-hidden | 'true' |
fold-trigger | aria-controls | pre 部件的 id |
fold-trigger | aria-expanded | 'false' | 'true' |
fold-trigger | aria-label | translations?.expand | translations?.collapse |
pre可聚焦并带可访问名称:渲染了文件名时指向它,否则使用translations.code。- 折叠按钮带
aria-expanded与aria-controls,指向pre。 - 语言角标与行号槽都对读屏隐藏,它们是装饰而非内容。
样式参考
皮肤
@xihan-ui/styles/code-view.css 使用 [data-scope="code-view"][data-part="root"] 部件选择器,位于 xihan.components 层。覆盖样式使用 xihan.overrides。
数据属性
由 connect 生成;条件不成立时不输出无值属性。
| 部件 | 属性 | 值 |
|---|---|---|
root | data-clamped | ''(条件成立时才出现) |
root | data-complete | ''(条件成立时才出现) |
root | data-digits | String(Math.min( String(lineNumberAt(lineCount - 1)).… |
root | data-foldable | ''(条件成立时才出现) |
root | data-lang | prop('lang')?.trim() || CODE_VIEW_FALLBACK_LANG |
root | data-line-numbers | ''(条件成立时才出现) |
root | data-size | props.size |
pre | data-complete | ''(条件成立时才出现) |
pre | data-wrap | ''(条件成立时才出现) |
code | data-lang | prop('lang')?.trim() || CODE_VIEW_FALLBACK_LANG |
code | data-wrap | ''(条件成立时才出现) |
line | data-highlighted | ''(条件成立时才出现) |
line | data-line-number | String(lineNumberAt(index)) |
line-number | data-highlighted | ''(条件成立时才出现) |
line-number | data-line-number | String(lineNumberAt(index)) |
line-content | data-highlighted | ''(条件成立时才出现) |
line-content | data-line-number | String(lineNumberAt(index)) |
token | data-kind | token.kind |
fold-trigger | data-pressed | ''(条件成立时才出现) |
fold-trigger | data-state | 'closed' | 'open' |
fold-trigger | data-xh-action-control | '' |
fold-trigger | data-xh-action-display | 'always' |
fold-trigger | data-xh-action-profile | 'disclosure-trigger' |
fold-trigger | data-xh-action-size | props.size |
fold-trigger | data-xh-action-variant | 'ghost' |
CSS 变量
本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。
| 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 |
|---|---|---|---|---|---|
--xh-code-view-bg | root | background | default | --xh-bg-surface | code-view 的 root 部件 background 覆盖槽。 |
--xh-code-view-border | root | border | default | --xh-border-default | code-view 的 root 部件 border 覆盖槽。 |
--xh-code-view-comment-fg | token | color | kind=comment | --xh-fg-muted | code-view 的 token 部件 color 覆盖槽。 |
--xh-code-view-fg | root | color | default | --xh-fg-muted | code-view 的 root 部件 color 覆盖槽。 |
--xh-code-view-filename-fg | filename | color | default | --xh-fg-default | code-view 的 filename 部件 color 覆盖槽。 |
--xh-code-view-fold-bg-hover | fold-trigger | background-color | disabledhoverloadingnot([data-disabled])not([data-loading]) | --xh-_action-variant-bg-hover | code-view 的 fold-trigger 部件 background-color 覆盖槽。 |
--xh-code-view-fold-fg | fold-trigger | color | default | --xh-fg-muted | code-view 的 fold-trigger 部件 color 覆盖槽。 |
--xh-code-view-fold-py | fold-trigger | padding-block | xh-action-profile=disclosure-trigger | --xh-space-2 | code-view 的 fold-trigger 部件 padding-block 覆盖槽。 |
--xh-code-view-font | codefilename | font-family | default | --xh-font-family-mono | code-view 的 code、filename 部件 font-family 覆盖槽。 |
--xh-code-view-font-size | root | font-size | default | --xh-_code-view-font-size | code-view 的 root 部件 font-size 覆盖槽。 |
--xh-code-view-gutter-border | line-number | border-inline-end | default | --xh-border-default | code-view 的 line-number 部件 border-inline-end 覆盖槽。 |
--xh-code-view-gutter-gap | line-number | padding-inline-end | default | --xh-space-1 | code-view 的 line-number 部件 padding-inline-end 覆盖槽。 |
--xh-code-view-header-border | fold-triggerheader | borderborder-block-endborder-color | defaultdisabledfocus-visiblehoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressed | --xh-border-subtle | code-view 的 fold-trigger、header 部件 border、border-block-end、border-color 覆盖槽。 |
--xh-code-view-header-fg | header | color | default | --xh-fg-muted | code-view 的 header 部件 color 覆盖槽。 |
--xh-code-view-header-font-size | fold-triggerheader | font-size | default | --xh-text-secondary-size | code-view 的 fold-trigger、header 部件 font-size 覆盖槽。 |
--xh-code-view-header-gap | header | gap | default | --xh-space-2 | code-view 的 header 部件 gap 覆盖槽。 |
--xh-code-view-header-h | header | min-block-size | default | --xh-control-h-lg | code-view 的 header 部件 min-block-size 覆盖槽。 |
--xh-code-view-header-px | header | padding-inline | default | --xh-space-4 | code-view 的 header 部件 padding-inline 覆盖槽。 |
--xh-code-view-header-py | header | padding-block | default | --xh-space-2 | code-view 的 header 部件 padding-block 覆盖槽。 |
--xh-code-view-highlight-bar | line | box-shadowoutlineoutline-offset | @media printhighlighted | --xh-stroke-thick | code-view 的 line 部件 box-shadow、outline、outline-offset 覆盖槽。 |
--xh-code-view-highlight-bg | line | background | highlighted | --xh-bg-brand-subtle | code-view 的 line 部件 background 覆盖槽。 |
--xh-code-view-highlight-fg | line | box-shadow | highlighted | --xh-bg-brand | code-view 的 line 部件 box-shadow 覆盖槽。 |
--xh-code-view-keyword-fg | token | color | kind=keyword | --xh-syntax-keyword | code-view 的 token 部件 color 覆盖槽。 |
--xh-code-view-keyword-weight | token | font-weight | kind=keyword | --xh-font-weight-semibold | code-view 的 token 部件 font-weight 覆盖槽。 |
--xh-code-view-label-fg | lang-label | color | default | --xh-fg-subtle | code-view 的 lang-label 部件 color 覆盖槽。 |
--xh-code-view-label-font-size | lang-label | font-size | default | --xh-text-caption-size | code-view 的 lang-label 部件 font-size 覆盖槽。 |
--xh-code-view-line-height | linepre | line-heightmin-block-size | default | --xh-text-code-leading | code-view 的 line、pre 部件 line-height、min-block-size 覆盖槽。 |
--xh-code-view-number-fg | line-number | color | default | --xh-fg-subtle | code-view 的 line-number 部件 color 覆盖槽。 |
--xh-code-view-number-font-size | line-number | font-size | default | --xh-text-caption-size | code-view 的 line-number 部件 font-size 覆盖槽。 |
--xh-code-view-number-token-fg | token | color | kind=number | --xh-syntax-number | code-view 的 token 部件 color 覆盖槽。 |
--xh-code-view-punctuation-fg | token | color | kind=punctuation | --xh-fg-subtle | code-view 的 token 部件 color 覆盖槽。 |
--xh-code-view-px | fold-triggerlineline-contentline-numberroot | padding-inlinepadding-inline-endpadding-inline-start | defaultline-numbersnot([data-line-numbers]) | --xh-space-3 | code-view 的 fold-trigger、line、line-content、line-number、root 部件 padding-inline、padding-inline-end、padding-inline-start 覆盖槽。 |
--xh-code-view-py | pre | padding-block | default | --xh-space-3 | code-view 的 pre 部件 padding-block 覆盖槽。 |
--xh-code-view-radius | root | border-radius | default | --xh-shape-surface | code-view 的 root 部件 border-radius 覆盖槽。 |
--xh-code-view-shadow | root | box-shadow | default | none | code-view 的 root 部件 box-shadow 覆盖槽。 |
--xh-code-view-string-fg | token | color | kind=string | --xh-syntax-string | code-view 的 token 部件 color 覆盖槽。 |
动效
background · box-shadow 走 transition 过渡。时长与缓动读动效令牌,改令牌即改全局节奏。
系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。
RTL
皮肤用逻辑属性排布(inline-start 一族),dir="rtl" 下自动镜像。
