展示
CodeBlock - 代码块
一段多行片段,摆在 plate 上,并配一条把它带走的路。
用法
什么时候用它
import { CodeBlock } from '@misoto22/folio'说明
一段多行片段,摆在 plate 上,并配一条把它带走的路。
有标题或语言标签时,顶部工具栏承载标签与复制按钮。没有标签时,复制按钮与代码放在同一行。它刻意不是一个悬停才出现的东西:悬停才出现的控件在触摸屏上等于不存在,而触摸屏恰恰是读者最想要这段代码、又最没法用手去选中它的地方。
行号住在它所标注的那一行自己的行框里,而不是一条平行的槽里。两列共用一个行高,在其中一列折行、换了字体或者自己滚起来之前都对得上;而一个作为它所标注那一行的子元素的数字,跟它拆不开。代价是行号会跟着它那一行一起滚走:它在滚动盒子里面,而不是在旁边一条不动的槽里。一条不动的槽就是第二列,还得跟它一直同步——而那正是这里要避开的故障。
主体是一个可聚焦、而且有名字的 role="group",这两半都是撑着事的。可聚焦,是因为一个内容本身不可聚焦的滚动盒子,键盘根本够不着——没有东西可以 Tab 过去,也就没法对着它按方向键,于是长行的右半边对任何不用鼠标的人来说是不存在的。有名字,是因为一个什么都不念的 Tab 停留点,会把读者丢进一个匿名的盒子里,让他自己去想刚才进的是什么。
用 group 而不是 region,这一半是刻意的。region 是地标——读者用来在整页里跳转的那几个主要区块之一——而一段代码片段不是其中之一。一篇文章里三个围栏代码块,会往那张地标表里塞进三个都叫“Code”的地标,而这正是 landmark-unique 这条规则要抓的噪声;顺带还会把页面上真正的地标挤进一张没人扫得动的列表里。group 在读者进来时念出同样的名字,也只念在这里。
句子里的函数名请使用 Code。
结构
| 部件 | 说明 |
|---|---|
| 底板必填 | --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 出现就等于永不出现。
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」,而两个共用一个名字的区域,是两个读者分不出彼此的地标。
pnpm add @misoto22/foliopnpm --filter @misoto22/folio buildhighlighted somewhere else
某个高亮器产出的标记,原样渲染出来。这个包不做高亮,也不会做:一份语法有几百 KB,而且那是构建期的活,所以本站在自己的生成器里跑 Shiki,再把结果交到这里。传了 html,lineNumbers 和 highlightLines 就会在类型层面被拿掉,而不是留成两个悄悄什么也不渲染的属性,因为 html 是一整条这个组件不会去解析的不透明字符串。code 仍然必填——它是复制按钮放进剪贴板的东西,也是读者真正带走的那一半。
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)。