跳到正文
册页

展示

CodeBlock - 代码块

一段多行片段,摆在 plate 上,并配一条把它带走的路。

用法

什么时候用它

任何超过一个词的片段。构建期的高亮器已经跑过就传 html,没跑过就只传 code。
TSX
import { CodeBlock } from '@misoto22/folio'

说明

一段多行片段,摆在 plate 上,并配一条把它带走的路。

有标题或语言标签时,顶部工具栏承载标签与复制按钮。没有标签时,复制按钮与代码放在同一行。它刻意不是一个悬停才出现的东西:悬停才出现的控件在触摸屏上等于不存在,而触摸屏恰恰是读者最想要这段代码、又最没法用手去选中它的地方。

行号住在它所标注的那一行自己的行框里,而不是一条平行的槽里。两列共用一个行高,在其中一列折行、换了字体或者自己滚起来之前都对得上;而一个作为它所标注那一行的子元素的数字,跟它拆不开。代价是行号会跟着它那一行一起滚走:它在滚动盒子里面,而不是在旁边一条不动的槽里。一条不动的槽就是第二列,还得跟它一直同步——而那正是这里要避开的故障。

主体是一个可聚焦、而且有名字的 role="group",这两半都是撑着事的。可聚焦,是因为一个内容本身不可聚焦的滚动盒子,键盘根本够不着——没有东西可以 Tab 过去,也就没法对着它按方向键,于是长行的右半边对任何不用鼠标的人来说是不存在的。有名字,是因为一个什么都不念的 Tab 停留点,会把读者丢进一个匿名的盒子里,让他自己去想刚才进的是什么。

用 group 而不是 region,这一半是刻意的。region 是地标——读者用来在整页里跳转的那几个主要区块之一——而一段代码片段不是其中之一。一篇文章里三个围栏代码块,会往那张地标表里塞进三个都叫“Code”的地标,而这正是 landmark-unique 这条规则要抓的噪声;顺带还会把页面上真正的地标挤进一张没人扫得动的列表里。group 在读者进来时念出同样的名字,也只念在这里。

句子里的函数名请使用 Code。

结构

CodeBlock anatomy
部件说明
底板必填--radius-lg 圆角上的 --paper-2 盒子,配一条 --rule 细线。它是抬高的一档,不是第二种表面色。
顶栏顶上那条带子:起始端是 title,末端是语言标签,再往后是复制按钮。只要里面有东西可放它就在,而且它从来不是一个悬停才出现的东西。
复制按钮一个 ghost 的 iconOnly Button,把 code——那个字符串,绝不是渲染出来的标记——放进剪贴板,并把自己的可访问名称翻成“Copied”,持续 1.6 秒。
主体必填一个可聚焦、有名字的 role="group",两个方向都能滚。之所以可聚焦,是因为一个内部没有任何可聚焦元素的滚动盒子是键盘够不到的:没有东西可以 Tab 过去,于是长行的右半截在没有鼠标时就不存在。用 group 而不用 region,是因为 region 是地标,而一段代码片段不是——一篇文章里三个围栏代码块,就是三个都叫 Code 的地标。
行在不带高亮的那条路上,每行一个 <span data-line>,各自带着自己的行号和自己的高亮条。行号是它所标那一行的子节点,所以两者不可能分家。

实践建议

推荐

  • 永远要传 code,哪怕已经传了 html。复制按钮复制的就是它:一个去复制渲染后标记的代码块,交给读者的是一堵 span 墙;而一个反过来从 DOM 里刮 textContent 的,离粘贴出一段跑不起来的东西,只差一个不换行空格。
  • 在构建期做高亮,然后把 html 传进来。一个高亮器是几百 KB 的语法规则,运到浏览器只为重新算出一批从不改变的 span,那会是这个页面上最大的一件东西。
  • 很长的片段要给 maxHeight,别让它一路铺下去。主体会滚动,而且可聚焦,所以折叠线以外的内容用键盘照样够得到。

避免

  • lineNumbers 和 highlightLines 在类型上就被排除在 html 那一支之外,两边都传是编译错误。它们是逐行的结构,而 html 是一整串组件不去解析的不透明字符串——所以这件事写在类型里,而不是让那个属性悄悄什么都不渲染。
  • html 就是 dangerouslySetInnerHTML。它是给你自己的高亮器处理你自己的源码之后的产物用的;读者写的 markdown 要作为字符串走 code,在那里它渲染成文字,不可能被误执行。
  • 别为了把顶栏收拾干净就关掉 copyable。正是这个按钮让读者不必再去手动框选一条折了行的命令,而在触屏上,手动框选就是这次交互的绝大部分。

示例

numbered and banded

朴素的那条路:不上高亮器,一个标题、一个语言标签、带行号,以及一行被标出来的行。行号住在它自己那一行的行内,而不是一条平行的边栏列里——两列共享一个 line-height,一直对得上,直到其中一列开始换行。复制按钮复制的是代码字符串,绝不是渲染出来的标记;那条工具条一直都在,而不是 hover 时才出现,而在触摸屏上 hover 出现就等于永不出现。

cn.tsTypeScript
import { clsx } from 'clsx'import { twMerge } from 'tailwind-merge'export function cn(...inputs) {  return twMerge(clsx(inputs))}

a command to copy

最小的一块有用的代码块:一行、一个语言,加复制按钮。那条工具条不是 hover 出来的东西,从来都不是——一个 hover 时才出现的控件,在触摸屏上等于不存在,而触摸屏恰恰是读者最难用手去框选一条折行命令的地方。它复制的是代码字符串,所以渲染层面的任何东西都进不了剪贴板。用 label 而不是 title,因为这个块保持素面:这个名字是给那个滚动区域用的,不是给顶上那条工具条的。两个块并排放着,没有它就都退回叫「Code」,而两个共用一个名字的区域,是两个读者分不出彼此的地标。

Shell
pnpm add @misoto22/folio
Shell
pnpm --filter @misoto22/folio build

highlighted somewhere else

某个高亮器产出的标记,原样渲染出来。这个包不做高亮,也不会做:一份语法有几百 KB,而且那是构建期的活,所以本站在自己的生成器里跑 Shiki,再把结果交到这里。传了 html,lineNumbers 和 highlightLines 就会在类型层面被拿掉,而不是留成两个悄悄什么也不渲染的属性,因为 html 是一整条这个组件不会去解析的不透明字符串。code 仍然必填——它是复制按钮放进剪贴板的东西,也是读者真正带走的那一半。

tone.tsTypeScript
export function tone(state: string) {
  return state === 'ok' ? 'success' : 'danger'
}

output nobody runs

一段日志,不是一条命令——copyable={false} 是给那种没人应该去运行的块用的,而这正是它存在的那个场景。别的地方按钮都要留着:为了让工具条清爽而把它去掉,等于拿走了读者不再用手去框选一条折行命令的那个理由。没有标题、没有语言、没有复制按钮,工具条里就没东西可放,于是也就没有工具条——而那个会滚动的正文仍然需要一个名字,label 就是干这个的。

> @misoto22/folio buildtokens      → dist/tokens.css     145 tokenscomponents  → dist/index.js        61 exportstypes       → dist/index.d.tssize check  → within budgetdone in 4.2s

无障碍

  • 会滚动的主体可以被 Tab 到,并且带一个有名字的 role="group",所以溢出的部分用键盘够得着,Tab 到的时候也会念出它是什么。刻意不用 role="region":那是地标,一个页面上放两段代码,就会往地标表里塞进两个同名的条目。
  • 复制按钮是一个 iconOnly 的 Button,aria-label 必填,成功后变成“Copied”——状态变化是被播报出来的,不只是被画出来的。
  • 在粗指针设备上,复制控件够到了 44px 的指针目标下限,而只靠那条紧凑的带子是够不到的(WCAG 2.5.5)。