Raft 设计系统

基础

对齐

落在边距线或中线上的是什么:形状、字母还是图标框;以及标题栏两端的图标按钮放在哪里。

对齐眼睛看到的东西,而不是盒子。每类元素都有一个落在线上的点,水平和垂直方向用的是同一个点。

原则

  • 每列一条边。 一列内容里的所有东西都从同一条边距开始:手机上 16px,桌面端 20px(间距)。
  • 对齐看得见的东西。 形状按边缘对齐,文字按字母对齐,无框图标按钮按图标框对齐。

用法

元素落在线上的是什么示例
有形状的东西:色块、头像、卡片、输入框、消息输入框、填充按钮形状的边缘;垂直方向上,形状居中。在一行中与纯文字并排时,改为对齐文字(与纯文字并排的形状)工作空间按钮的头像从 16px 开始,按下时的填充超出头像;顶栏里粉色的 Done 按钮在距右侧 16px 处结束
文字字母:水平方向是第一个字母的竖笔;垂直方向是大写字母高度的中心在 56px 的标题栏里,text-headline 的页面名称和工作空间按钮里的服务器名称共用一条中线;消息输入框里第一个工具图标与输入的文字对齐
无框图标按钮图标框。按钮本身看不见,它空白的点击区域延伸到线外(标题栏的两端)在面板中,返回的图标框从距边缘 20px 处开始、最后一个图标框在距边缘 20px 处结束;手机上是 16px

控件与它的标签

复选框、单选框或开关以其标签的第一行居中;无论后面有没有描述,都是同一行。

  • 让控件以标签的第一行居中。 把它偏移该行与控件高度之差的一半,20px 的方框就是 (1lh - 20px) / 2,这样在任何字号下都能落对位置,不需要手动微调。
  • 不要把控件对齐到文字块顶部。 20px 的方框贴着 18px 行的顶部,会比该行中心低 1px;在 16px 的行上低 2px。
  • 也不要以整个标签居中。 标签常常只有一行,这时两种做法看起来一样;可一旦换行,控件就会滑到文字块中间:窄宽度下偏离第一行 27px,一旦后面跟了描述就偏离 8px。控件属于第一行;和文字块对上只是巧合。
  • 控件和标签之间留 8px。
  • 不要缩小控件;移动它,而不是改变行高。 开关在任何位置都保持 50 × 30,复选框和单选按钮保持 20px。比行矮的控件占一个一行高的框;比行高的控件向上移到这一行上,而不是单独占一行。在 <label> 行里,方框占的空间永远不小于它的 20px,所以一行 text-xs 的行高 20px;会换行的小号标签要给 20px 的行高(leading-5),方框才能落在它的第一行上。
  • 一行的两端都以第一行居中。 在带描述的设置行里,开头的图标或头像,以及末尾的开关、值或箭头,都以标题居中,而不是以标题加描述整体居中。两端用同一条规则,它们彼此对齐,也和标题对齐。只有标题的行按它自己的高度整体居中:手机上带开关时是 54px。
  • 在两个选项之间点一下,落到较近的那个上。 <label> 行里的方框或圆圈向外延伸到行外 4px,也就是与下一个选项 8px 间隔的一半,其余部分由标签承担点击。不在标签里单独使用时,它在触屏上的点击区域是 44px。

raft-ui 在每个把控件放在标签旁的地方都这样处理,所以不要自己加 margin-top 或 items-center:

  • FieldItem 把放在其中的 Checkbox、RadioGroupItem 或 Switch 以它的 FieldLabel 的第一行居中,放在标签前 8px。
  • 直接放在 <label> 行里的 Checkbox 或 RadioGroupItem 占一个与标签一行等高的框,并以这一行居中,所以无论行的最小高度是多少,它都落在行放一行文字的位置:用 items-start 时落在第一行,用 items-center 时落在唯一的那一行。把标签的字体样式设在 <label> 上,因为方框从那里读取行高;文字可能换行时用 items-start。标签旁的开关放在 FieldItem 或 ListRow 里。
  • 带描述的 ListRow variant="plain" 把两端都以标题的第一行居中;只有标题时,整体居中。

在 Space Grotesk 中,大写字母高度的中心和行框的中心是同一个点:字体在 em 框内居中,误差在 0.006em 以内,在我们用到的任何字号下都不到 0.1px。所以让控件以行框居中就是视觉居中,不需要按字号修正。手动调出的 margin-top 说明控件对齐错了对象:它对一个字号下的一个标签是对的,换一个就悄悄错了。把偏移量写成算式,而不是写成它当前算出来的数字:Banner 曾经对 16px 图标用 mt-px、对 9px 状态点用 mt-[4.5px],也就是在 18px 行上的 (18 - 16) / 2 和 (18 - 9) / 2,桌面端行高一变,两个都错了。现在,引领一行文字的图标或状态灯在每个组件里都用同样的方式以第一行居中(引领文字的标记)。

引领文字的标记

横幅、轻提示、面板提示、操作卡片的提示和状态标签都以一个标记开头:一个 Lucide 图标或一个状态灯。标记属于第一行,就像控件属于它的标签。它们共用的只有一条规则:标记放在哪里。每个组件保留自己的图标尺寸和间隔,由组件决定:横幅的图标是 20px(sm 横幅里是 16px),轻提示的是 16px,操作卡片提示的是 14px。

  • 让标记以文字的第一行居中。 把偏移量写成算式 (1lh - size) / 2,或者先把标记下移半行、再上移它自身高度的一半,这样无论哪个字号、在手机字号阶梯还是桌面端字号阶梯上,它都落在这一行上;文字换行或后面跟着描述时,标记也留在第一行。
  • 以行框居中,也就是以大写字母高度的中心居中。 在 Space Grotesk 中实测,两者相差 0.006em:12px 到 14px 的文字下不到 0.1px,远低于能看出修正效果的 0.5px。一行中文沿用 Space Grotesk 的行框(后备字体不会把它撑高),所以其中拉丁字母的大写字母高度中心不变;汉字自身的墨迹中心围绕它分散,14px 时最多偏 0.4px,13px 时最多偏 0.7px,字与字之间的差异也同样大,所以没有稳定的偏移可以修正,同一个中心对中英混排和纯中文都适用。不要以整个文字块居中标记(它会滑到换行段落的中间,items-center 就是这样),也不要用固定的 margin-top 来放置它。
  • 状态灯在任何文字旁都保持唯一的尺寸:9px,带 1.5px 墨色边。 它在轻提示、横幅、标签、面板头部和侧边栏行里都是同一盏灯,所以在这些地方都是同样大小(Status):9px 是区分绿、黄、橙所需的 6px 颜色,加上两条墨色边。
  • 把字号设在行上,而不只是设在文字上。 标记从继承的字体样式读取 1lh,所以容纳标记和文字的那一行要带上文字的字体样式,Banner 和 PanelNotice 就是这样做的。

在 raft-ui 中,Banner、PanelNotice、ToastIcon 和 MessageActionCardNotice 会把放入其中的图标以第一行居中;Banner、PanelNotice、ToastIcon、Badge 和 PanelActivity 对 Status 也这样做,并且不管给了什么尺寸都把它固定在 9px。你自己放置的标记用 lead-icon 和 lead-dot 工具类(styles.css),放在带有文字字体样式的行上;用 lead-dot 时,不要给 Status 传尺寸。

与纯文字并排的形状

当一个自带内边距的形状(标签、无框按钮的悬停填充)在一行里与纯文字并排时,比如一行任务属性里的值,把它的内边距算作间隔的一部分:把形状向左拉出内边距的距离,让它的文字从纯文字开始的位置开始,填充向外伸出。

这一行是当作文字来读的,所以眼睛比较的是文字从哪里开始,而不是填充从哪里开始。如果按边缘对齐这些形状,它们的文字会比旁边的纯文字值多缩进 6px,整行看起来参差不齐。这是「形状按边缘对齐」的例外:那条规则适用于形状在一列中上下彼此对齐的情况。

一行中不同大小的文字

混合了不同字号或字体的一行,比如消息的发送者行(粗体 text-subhead 的名字,text-caption 的角色、模型和徽标文字,Space Mono 的时间),要让每个字母都在同一条基线上:用 items-baseline 布局,而不是 items-center。

  • 对齐基线,而不是盒子。 每个字号有自己的行框,让盒子居中,居中的是每种字体的上伸和下伸,而不是它的字母。这样较小的注释在每个字号下都会比名字的基线高出最多 1px,角色和时间高出的量还各不相同,整行一眼看上去就是断的。
  • 共用基线就是这两种字体的视觉对齐。 Space Grotesk 和 Space Mono 的大写字母高度相同(0.700em),数字高度相同(含过冲 0.714em),x 高度只差 0.01em。所以同字号的 Space Mono 时间和 Space Grotesk 角色在同一条基线上高度完全一样(以 3 倍渲染时,04:11 的数字和角色的大写字母墨迹框精确到设备像素都相同),不需要按字体偏移。Space Mono 更高的上伸和下伸只会让它的盒子位置不同,这就是为什么让盒子居中会让它移位。
  • 里面有文字的形状也按文字对齐。 发送者行里的徽标把它的字母放在基线上,就像句子里的标签一样(正文中的标记)。这样它 22px 的框比居中时低一个像素,所以在桌面端,带徽标的行比名字 22px 的行高一个像素(手机上徽标本来就把 20px 的行撑到了 22px)。不带徽标的行保持名字那一行的高度。
  • 换行时,对齐第一行。 换成多行的标签把第一行放在名字的基线上,时间留在那一行。

正文中的标记

引用标签、对当前读者的提及和行内代码都位于句子之中,所以它们按文字对齐。

  • 把它们的字母放在句子的基线上。 标签的文字与周围的文字对齐,不管有没有前置图标。以前有个标签从图标取基线,结果浮在句子上方 3px;用 1px 微调后,频道标签也跑到了句子上方。
  • 按大写字母高度让字母在填充中居中。 在 Space Grotesk 中,行框已经做到了这一点。Space Mono 的字母比其内容区中心低 0.0275em,所以行内代码把填充下移这么多(上内边距 1px - 0.0275em,下内边距 1px + 0.0275em)。不这样做,字母会低大约 0.6px。
  • 让前置图标以字母居中,而不是以盒子居中。

标题栏的两端

每个标题栏都用同样的方式放置两端的图标按钮:面板标题栏,消息列、个人资料或任务面板的标题栏,手机的顶栏,抽屉或底部面板的标题栏,对话框的 ×,侧边栏标题栏,以及网页顶栏的右端。

  • 把图标框放在内容边距上。 第一个图标框从标题栏所在那一列的边距开始,最后一个在这条边距处结束,也就是下方内容开始的那条线:手机上 16px,桌面端和对话框里 20px(间距)。
  • 让点击区域向外伸出。 按钮的其余部分是空的,所以它越过这条线伸向边缘,从不向内:44px 按钮配 22px 图标伸出 11px,32px 按钮配 18px 图标伸出 7px。它止于屏幕或面板的边缘;每个标题栏的边距都比最大的伸出量宽,所以目前没有一个被截断。
  • 在每种按钮尺寸下都保持这条线。 用鼠标时按钮是 32px,在手机或触屏上是 44px,在桌面端布局中也一样(间距);变的只有伸出量。不要按按钮自己的边缘放置边上的按钮:把 44px 的返回按钮按边缘放在边距上,它的图标会缩进到下方内容里 11px,而另一端按图标放置的 ⋮ 仍在线上,两端就不再对称。
  • 两端对称。 左右遵循同一条规则,所以返回按钮和最后一个按钮到各自边缘的距离相同。
  • 形状和文字保持原位。 标题栏末端的填充按钮、贴纸或标题,按它的边缘或字母落在同一条边距上(用法)。
  • 不要为了迎合图标的墨迹去挪按钮。 每个图标在框内留的空白都不同(箭头、齿轮和竖向省略号最多相差 9px),这样按钮会随图标移动,两端就不再对称。竖向省略号这类窄图标也一样。
  • 返回用 arrow-left,不用 chevron。 chevron 的墨迹只占框的中间,需要单独挪动;箭头像其他图标一样填满框。

手机顶栏曾经把图标框放在边距外 2px,距屏幕边缘 14px,好让常见图标的墨迹(大约在框内 2px)落在 16px 附近。这让手机成了唯一按墨迹而不是按图标框对齐的标题栏,而桌面端布局下的触屏则完全没有规则:它按按钮的边缘放置返回按钮,缩进线内 11px。按图标框对齐这一条规则,在每种屏幕、每种按钮尺寸下都成立。

在 raft-ui 中,标题栏掌管边距,每个无框图标按钮声明它的图标框在按钮内缩进多少,即 (button − icon) / 2;标题栏把第一个和最后一个子元素向外拉出这么多(Panel)。

  • 间距:边距和标题栏高度。
  • 字体排印:每种行高来自的字号阶梯。
  • 表单:控件、它的标签和描述如何组合在一起。
  • 图标:图标尺寸。
  • 页面结构:在桌面端屏幕上每列一条边。