跳转到内容

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 回复、文档、评审意见中展示代码,需要行号或需要指出某几行。
  • 代码是流式生成的,需要边接收边渲染,闭合之后再着色。
  • 代码较长,默认只显示前若干行。

何时不用

  • 只是一小段行内标识时,使用排印code 形态。
  • 展示运行日志时,使用日志
  • 展示改动前后时,使用差异视图

特性

  • 逐行切分在连接层完成。一个记号可以横跨多行(未闭合的字符串与块注释),因此行号与高亮行不能由皮肤反推。
  • 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

属性类型必填说明
codestring
langstring围栏语言标注,空白一律落为 plaintext。
filenamestring文件名,渲染在 header 中;渲染之后它即为 pre 的可访问名。
labelledboolean作者渲染了 filename 部件时置真,由适配器统计而不是判断 filename 是否有值。 为假时 pre 用 translations.code 兜底:指向未渲染的 id 会使读屏读空。
completeboolean代码是否已闭合,未闭合时按行数预撑高度且默认不着色。
wrapboolean长行自动换行,默认关闭(长行横向滚动)。
lineNumbersboolean渲染行号槽。
startLinenumber首行的行号,默认 1;摘录与 patch 片段需要使用。
highlightLinesstring | readonly number[]要高亮的行号,写为 '3,7-9' 或行号数组;非法片段丢弃不报错。
clampnumber超过该行数才视为可折叠。
clampedboolean折叠态,纯受控:没有 defaultClamped,需要非受控时套用 collapsible。
highlighterHighlighterPort着色实现。未提供时为纯文本,提供后也允许返回 null(语言未识别等),同样回退为纯文本。 未闭合的块默认不着色,见 {@link highlightWhileStreaming}。
highlightWhileStreamingboolean块尚未闭合时也着色,默认 false。 默认关闭是因为未闭合代码的词法本身不稳定:引号、括号随时会配对, 每到一个 token 整块变一次色,比不着色更差。
sizeSize尺寸:sm / md / lg。
translationsPartial<CodeViewTranslations>
onClampToggle(details: CodeViewClampToggleDetails) => void折叠态切换的意图回调;clamped 是纯受控的,是否落定由宿主决定。

事件

自定义元素将载荷放在 detail;Vue 使用同名 emit。

事件载荷说明
clamp-toggleCustomEvent折叠态切换的意图;detail 为 { clamped: boolean }

插槽

仅列出带载荷的插槽。

Vue 组件插槽载荷说明
XhCodeViewCodelineCodeViewLineSlotProps
XhCodeViewRootdefaultCodeViewRootSlotProps

状态

公开状态写入 data-state

部件取值
fold-trigger'closed' | 'open'

以下名称仅用于内部状态机。

状态idle

事件PRESS.START · PRESS.END

判据canPress

connect API

getXxxProps() 返回对应部件的宿主属性。

成员类型说明
langstring
lineCountnumber
linesreadonly CodeLine[]逐行切分后的文本与记号片段。
lineNumberAt(index: number) => number每行的行号,与 lines 同序。
lineNumbersboolean是否渲染行号槽;适配器据此决定是否创建该节点。
foldableboolean折叠可用:提供了正数 clamp 且行数确实超过它。
clampedboolean
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-labelaria-hidden'true'
prerole'group'
line-numberaria-hidden'true'
fold-triggeraria-controlspre 部件的 id
fold-triggeraria-expanded'false' | 'true'
fold-triggeraria-labeltranslations?.expand | translations?.collapse
  • pre 可聚焦并带可访问名称:渲染了文件名时指向它,否则使用 translations.code
  • 折叠按钮带 aria-expandedaria-controls,指向 pre
  • 语言角标与行号槽都对读屏隐藏,它们是装饰而非内容。

样式参考

皮肤

@xihan-ui/styles/code-view.css 使用 [data-scope="code-view"][data-part="root"] 部件选择器,位于 xihan.components 层。覆盖样式使用 xihan.overrides

数据属性

connect 生成;条件不成立时不输出无值属性。

部件属性
rootdata-clamped''(条件成立时才出现)
rootdata-complete''(条件成立时才出现)
rootdata-digitsString(Math.min( String(lineNumberAt(lineCount - 1)).…
rootdata-foldable''(条件成立时才出现)
rootdata-langprop('lang')?.trim() || CODE_VIEW_FALLBACK_LANG
rootdata-line-numbers''(条件成立时才出现)
rootdata-sizeprops.size
predata-complete''(条件成立时才出现)
predata-wrap''(条件成立时才出现)
codedata-langprop('lang')?.trim() || CODE_VIEW_FALLBACK_LANG
codedata-wrap''(条件成立时才出现)
linedata-highlighted''(条件成立时才出现)
linedata-line-numberString(lineNumberAt(index))
line-numberdata-highlighted''(条件成立时才出现)
line-numberdata-line-numberString(lineNumberAt(index))
line-contentdata-highlighted''(条件成立时才出现)
line-contentdata-line-numberString(lineNumberAt(index))
tokendata-kindtoken.kind
fold-triggerdata-pressed''(条件成立时才出现)
fold-triggerdata-state'closed' | 'open'
fold-triggerdata-xh-action-control''
fold-triggerdata-xh-action-display'always'
fold-triggerdata-xh-action-profile'disclosure-trigger'
fold-triggerdata-xh-action-sizeprops.size
fold-triggerdata-xh-action-variant'ghost'

CSS 变量

本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。

变量部件CSS 属性状态默认来源说明
--xh-code-view-bgrootbackgrounddefault--xh-bg-surfacecode-view 的 root 部件 background 覆盖槽。
--xh-code-view-borderrootborderdefault--xh-border-defaultcode-view 的 root 部件 border 覆盖槽。
--xh-code-view-comment-fgtokencolorkind=comment--xh-fg-mutedcode-view 的 token 部件 color 覆盖槽。
--xh-code-view-fgrootcolordefault--xh-fg-mutedcode-view 的 root 部件 color 覆盖槽。
--xh-code-view-filename-fgfilenamecolordefault--xh-fg-defaultcode-view 的 filename 部件 color 覆盖槽。
--xh-code-view-fold-bg-hoverfold-triggerbackground-colordisabled
hover
loading
not([data-disabled])
not([data-loading])
--xh-_action-variant-bg-hovercode-view 的 fold-trigger 部件 background-color 覆盖槽。
--xh-code-view-fold-fgfold-triggercolordefault--xh-fg-mutedcode-view 的 fold-trigger 部件 color 覆盖槽。
--xh-code-view-fold-pyfold-triggerpadding-blockxh-action-profile=disclosure-trigger--xh-space-2code-view 的 fold-trigger 部件 padding-block 覆盖槽。
--xh-code-view-fontcode
filename
font-familydefault--xh-font-family-monocode-view 的 code、filename 部件 font-family 覆盖槽。
--xh-code-view-font-sizerootfont-sizedefault--xh-_code-view-font-sizecode-view 的 root 部件 font-size 覆盖槽。
--xh-code-view-gutter-borderline-numberborder-inline-enddefault--xh-border-defaultcode-view 的 line-number 部件 border-inline-end 覆盖槽。
--xh-code-view-gutter-gapline-numberpadding-inline-enddefault--xh-space-1code-view 的 line-number 部件 padding-inline-end 覆盖槽。
--xh-code-view-header-borderfold-trigger
header
border
border-block-end
border-color
default
disabled
focus-visible
hover
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
pressed
--xh-border-subtlecode-view 的 fold-trigger、header 部件 border、border-block-end、border-color 覆盖槽。
--xh-code-view-header-fgheadercolordefault--xh-fg-mutedcode-view 的 header 部件 color 覆盖槽。
--xh-code-view-header-font-sizefold-trigger
header
font-sizedefault--xh-text-secondary-sizecode-view 的 fold-trigger、header 部件 font-size 覆盖槽。
--xh-code-view-header-gapheadergapdefault--xh-space-2code-view 的 header 部件 gap 覆盖槽。
--xh-code-view-header-hheadermin-block-sizedefault--xh-control-h-lgcode-view 的 header 部件 min-block-size 覆盖槽。
--xh-code-view-header-pxheaderpadding-inlinedefault--xh-space-4code-view 的 header 部件 padding-inline 覆盖槽。
--xh-code-view-header-pyheaderpadding-blockdefault--xh-space-2code-view 的 header 部件 padding-block 覆盖槽。
--xh-code-view-highlight-barlinebox-shadow
outline
outline-offset
@media print
highlighted
--xh-stroke-thickcode-view 的 line 部件 box-shadow、outline、outline-offset 覆盖槽。
--xh-code-view-highlight-bglinebackgroundhighlighted--xh-bg-brand-subtlecode-view 的 line 部件 background 覆盖槽。
--xh-code-view-highlight-fglinebox-shadowhighlighted--xh-bg-brandcode-view 的 line 部件 box-shadow 覆盖槽。
--xh-code-view-keyword-fgtokencolorkind=keyword--xh-syntax-keywordcode-view 的 token 部件 color 覆盖槽。
--xh-code-view-keyword-weighttokenfont-weightkind=keyword--xh-font-weight-semiboldcode-view 的 token 部件 font-weight 覆盖槽。
--xh-code-view-label-fglang-labelcolordefault--xh-fg-subtlecode-view 的 lang-label 部件 color 覆盖槽。
--xh-code-view-label-font-sizelang-labelfont-sizedefault--xh-text-caption-sizecode-view 的 lang-label 部件 font-size 覆盖槽。
--xh-code-view-line-heightline
pre
line-height
min-block-size
default--xh-text-code-leadingcode-view 的 line、pre 部件 line-height、min-block-size 覆盖槽。
--xh-code-view-number-fgline-numbercolordefault--xh-fg-subtlecode-view 的 line-number 部件 color 覆盖槽。
--xh-code-view-number-font-sizeline-numberfont-sizedefault--xh-text-caption-sizecode-view 的 line-number 部件 font-size 覆盖槽。
--xh-code-view-number-token-fgtokencolorkind=number--xh-syntax-numbercode-view 的 token 部件 color 覆盖槽。
--xh-code-view-punctuation-fgtokencolorkind=punctuation--xh-fg-subtlecode-view 的 token 部件 color 覆盖槽。
--xh-code-view-pxfold-trigger
line
line-content
line-number
root
padding-inline
padding-inline-end
padding-inline-start
default
line-numbers
not([data-line-numbers])
--xh-space-3code-view 的 fold-trigger、line、line-content、line-number、root 部件 padding-inline、padding-inline-end、padding-inline-start 覆盖槽。
--xh-code-view-pyprepadding-blockdefault--xh-space-3code-view 的 pre 部件 padding-block 覆盖槽。
--xh-code-view-radiusrootborder-radiusdefault--xh-shape-surfacecode-view 的 root 部件 border-radius 覆盖槽。
--xh-code-view-shadowrootbox-shadowdefaultnonecode-view 的 root 部件 box-shadow 覆盖槽。
--xh-code-view-string-fgtokencolorkind=string--xh-syntax-stringcode-view 的 token 部件 color 覆盖槽。

动效

background · box-shadowtransition 过渡。时长与缓动读动效令牌,改令牌即改全局节奏。

系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。

RTL

皮肤用逻辑属性排布(inline-start 一族),dir="rtl" 下自动镜像。

Released under The MIT License