Raft 设计系统

基础

间距与尺寸

4px 间距网格、点击区域、控件和行的高度、头像尺寸、头部高度和布局宽度。

Raft 的所有间距都落在 4px 网格上,所有供人触摸的元素都至少 44px。同一屏上的头部高度一致,所以它们下方的接缝线对齐。

原则

  • 间距落在 4px 网格上。 用 Tailwind 的间距阶梯:4px、8px、12px、16px、20px、24px、32px、40px。
  • 触摸目标至少 44px。 看起来更小的控件,靠点击区域补足到 44px,而不是把绘制的部分放大。
  • 头部对齐。 一屏上的每个头部高度相同,所以各自下方的接缝线连成一条线。
  • 同一个 app 的各个标签页里,同类元素尺寸相同。 按元素对用户来说是什么来定尺寸,而不是按绘制它的组件(同类元素,同样尺寸)。

间距

  • 组件之间和组件内部的所有间距都用 4px 的倍数。
  • 在小元素内部(标签、像素图标与文字之间的间隔、tag)允许用 2px 的半步:6px 和 10px。
  • 在紧贴其中各项的轨道里,比如分段控件或切换按钮组,各项距轨道 3px,彼此间隔 3px:这是轨道自身的内缩,4px 网格无法整除。
  • 在一个框内,各自可能显示填充的同级元素(工具栏的按钮、分段控件的各段、字段里的标签和按钮)彼此间隔等于它们到框的距离:浮动工具栏里是 4px,分段控件里是 3px,字段里是 6px。如果彼此间隔不同,眼睛就要多比较一个距离(形状)。

时间线节奏

对话从上往下读,行与行之间的空白告诉读者一轮发言在哪里结束、对话的新一段从哪里开始。时间线只用三种距离,不用别的;每种都从文字量到文字(前一样东西的最后一行到下一样东西的第一行),因为眼睛比较的就是这段空白:

档距离用在
行8px同一轮发言里的两行:续行紧跟在它所接的那行下面(MessageItem)。
消息(timeline-gap-message)16px两轮发言之间:一条消息和下一个发送者行,一条消息和一条事件行(SystemMessage),两条事件行之间。
分段(timeline-gap-section)24px分界处:日期线两侧,量到它的线;列表顶部到第一行;最后一行到消息输入框的外框。
  • 一种断开用一档。 一轮发言内部比两轮之间紧,分界又比两者都宽一档,所以日期线读起来是新的一天,而不是又一条消息。三档都落在 4px 网格上,每档比上一档宽 8px。
  • 分界两侧一样宽。 日期线到上方文字 24px,到下方发送者行也是 24px。以前线下方的空白比上方大,线看起来像贴在上面那条消息上。
  • 最后一条消息不贴着消息输入框。 消息输入框的外框很重;按消息档的距离,它看起来碰到了最后一行。最后一行到输入框外框是一个分段档,和列表顶部到第一行一样。
  • 每行带着自己的内距,分界把它们扣回来。 每行在文字上下各留出行内距(timeline-row-inset,4px),也就是它悬停和选中时高亮伸出的范围。一轮发言的第一行在此之上再加 8px,是一条高亮不绘制的透明边框,所以每轮发言都从文字上方 12px 开始(spacing-timeline-lead)。日期线从自己的内边距里减去上方那行的内距和下方那行的起始距离,所以一行不需要知道下面跟着什么,两边是什么,距离都对。
  • 浮在列表末尾的那一行不挪动列表。 「谁正要发言」那一行出现时,会在列表末尾加上它自己的高度,让最后一条消息完整地留在它上方(正在工作的 agent);「回到底部」只在末尾不在视野里时出现,所以不会盖住最后一条消息。
  • 产品不写这些数字。 组件读时间线 token;产品自己搭列表的顶部或末尾时,只用两个:第一行上方用 pt-timeline-start(分段档减去每行自带的起始距离,历史开头的提示也算一行),最后一行下方用 pb-timeline-end(分段档减去最后一行的内距)。timeline-turn 和 timeline-lead 只给组件用;在产品里拿它们自己拼,正是以前间距走样的原因。
Token浅色深色
timeline-gap-messageThe message step of a timeline: from the last line of one turn to the first line of the next (a sender line, an event line). Every row keeps the row inset above and below its text; the first row of a turn adds the rest above it.16px16px
timeline-gap-sectionThe section step of a timeline, one step wider than the message step: either side of a date line, from the top of the list to the first line, and from the last line to the composer, so a boundary reads as one and the last message never sits on the composer.24px24px
timeline-row-insetHow far a timeline row's hover and selected highlight reaches above and below its text. Two rows of one turn sit two insets apart.4px4px

点击区域

  • 每个触摸目标在两个方向上都至少 44px。
  • 看起来小于 44px 的控件和文字按钮,用透明的伪元素扩展到 44px。
  • 正文中的行内链接和引用标签不扩展:它们跟随文字行。
  • 只用鼠标指针操作的桌面端控件可以更小:侧边栏行和菜单项 36px,导航条项 40px,悬停工具栏高 36px、其中的按钮 28px(Button)。
  • 桌面端的默认控件是 32px,即飞书 PC 端的默认值,因为桌面端用鼠标指针操作,观看距离也更远。这里的桌面端指宽度 768px 起(桌面布局从这里开始)、主要指针是鼠标或触控板的屏幕,即 (width >= 48rem) and (pointer: fine):密度跟随输入方式,而不是宽度。用触屏操作的平板保持手机的 44px 控件和 16px 字段文字,这也能防止 iOS 和 iPadOS 在聚焦字段时放大页面;连接触控板的 iPad 报告精确指针,使用桌面端尺寸。32px 的按钮仍向外扩展出 44px 的点击区域(after:-inset-y-control-hit,再加上 1.5px 的边框,因为向外扩展的 ::after 是从边框内侧量起的),因为笔记本电脑也可能有触屏;字段不扩展,因为它很宽,它的高度本身就是目标。

同类元素,同样尺寸

在 app 的各个标签页之间切换的人,会比较看到的内容。两个看起来是同类的列表,尺寸却不一样,无论各自有什么理由,看起来都像是错误。所以在同一个 app 里,从标签栏或导航条能到达的各个屏幕上,同类元素尺寸相同:高度、字号和图标尺寸都相同。元素属于哪一类,取决于它代表什么,而不是出现在哪里或由哪个组件绘制(行)。

  • 在手机版 Raft Chat 中,主页的频道和私信、成员以及设置列表都是「地点列表」,都用同一种侧边栏行绘制:44px,16px、字重 600 的名称和 16px 的图标。改一个就要全部一起改。
  • 这条规则只在同一个 app 内成立,不跨 app。交互方式不同的另一个 app 可能会给同一个组件不同的尺寸:以设置为核心的 app 可以给设置行比 Raft Chat 更多的空间。尺寸属于 app 的上下文;组件文档给出的是它在 app 中实际使用的尺寸。
  • 因此下面的高度是 Raft Chat 目前使用的值。改动其中一项前,先找出 app 里显示同类元素的每一个屏幕,一起改。
  • 共享的尺寸用比较各屏幕的测试来固定,而不是用固定数值:Raft Chat 的测试会在主页的行和设置列表出现偏差时失败。

高度

元素高度
默认控件(control-height):Input、InputGroup、Select、Combobox、lg 和 icon-lg 尺寸的 Button、面板操作手机和触屏上 44px,桌面端 32px
小控件:md 和 icon-md 尺寸的 Button、筛选触发器、分段控件的一段32px
通栏行动按钮(control-height-cta,xl 尺寸的 Button)手机和触屏上 52px,桌面端 40px
手机上的地点列表(主页、成员、设置列表):侧边栏行44px
带头像和预览的行:对话、人、agent(ListRow)至少 64px
设置页面内的设置行(ListRow plain)单行时 48px
单选行至少 44px
选择控件:开关;复选框和单选按钮50 × 30px;20px。手机和桌面端相同。单独使用时,复选框或单选按钮在触屏上的点击区域高 44px;放在 <label> 行里时,它延伸到与下一个选项间隔的一半,其余部分由标签承担点击。以标签的第一行居中(对齐)
弹出列表中的选项、菜单项单行时 36px;行数更多时更高,内边距不变(表单)
手机顶栏行、面板头部、侧边栏头部、导航条头部、抽屉头部56px
网页顶栏、桌面应用标题栏48px
  • 默认控件的尺寸用控件 token 设定(h-control、size-control,图标用 size-control-icon),不要自己写断点:它们只在 token 层切换一次。跟随控件但没有 token 的度量值,比如内边距,或让字形贴边的外边距,用同一个查询条件 md:pointer-fine: 切换,不要只用 md::只用 md: 会让 768px 起的触屏也得到桌面端的 32px。
Token浅色深色桌面端
control-heightA default control: a field, a Button at lg, an icon button at icon-lg. A finger's 44px on a phone, Feishu's 32px on a desktop.44px44px32px
control-height-ctaThe full-width call to action across a sign-in, sign-up or setup form (Button xl).52px52px40px
control-icon-sizeThe icon in a control-height icon button.22px22px18px
control-hit-insetHow far a control-height button's hit area reaches past its top and bottom, to keep 44px.0px0px6px
control-embedded-insetHow far a chip or a button embedded in a field, the composer or a corner sits inside its frame's outer edge, and from its sibling.6px6px
control-embedded-sizeA chip or a button embedded in a field: the control height less the embedded inset above and below, so the field keeps its height.32px32px20px
  • 导航条头部是 56px 的正方形。它放工作空间图标,无论其他部分怎么变,左上角都保持正方形。所以导航条宽 56px 加接缝线,只显示图标:带文字的导航条需要 68px 才能放下 Computers 这样的名称,那样左上角就不再是正方形。
  • 桌面窗口高度小于 600px 时(@media (max-height: 600px)),所有 56px 的头部一起缩到 48px。导航条随之缩小,其头部保持正方形,导航条项从 40px 缩到 36px;见响应式布局。

头像尺寸

头像有 19px、27px、35px、51px 和 99px 五种:8 的倍数加上两侧各 1.5px 的边。见 Avatar。

布局宽度

区域宽度
手机内容边距每侧 16px
桌面端主区域、面板头部、侧边栏头部、抽屉边距每侧 20px
导航条56px,加 1px 接缝线;带文字的导航条 68px,加 1px 接缝线
侧边栏(所有带侧边栏的页面:Chat、成员、Computers、设置、服务器的公开页面)240px,可在 200px 到 400px 之间调整
动态和搜索结果列表,位于所选项目旁边380px,可在 320px 到 640px 之间调整
消息列面板400px,可在 360px 到 720px 之间调整
个人资料和任务侧面板400px,可在 320px 到 640px 之间调整
agent 的文件树256px,可在 180px 到 480px 之间调整
主区域,位于各栏之间至少 360px
文档旁的评论栏280px,距文档 24px(评论)

元素如何落在这些边距上见对齐。断点见响应式布局。

调整栏宽

在桌面端,上表中有范围的每一栏都可以拖动其接缝线,在该范围内调整宽度;评论栏保持 280px,因为评论卡片是依据它排布的(Resizable)。范围取决于每一栏放的内容:侧边栏的名称、计数和标记在 200px 以下开始拥挤,超过 400px 也没有好处;结果行在 320px 时能显示消息列标题的两三个词,超过 640px 后只是多显示一些单行预览。

  • 每个宽度按浏览器记住,而不是按服务器或账号。所有带侧边栏的页面上侧边栏宽度相同,所以在 Chat、成员、Computers 和设置之间切换时它不会跳动。个人资料面板和任务侧面板也共用一个宽度。
  • 窗口太窄、放不下记住的宽度时,从最右边的栏开始把各栏画窄,直到最小值,并保持主区域至少 360px;仍然放不下的侧面板覆盖在主区域上(响应式布局)。记住的宽度不变,所以窗口变宽后会恢复。主区域左侧的栏只在变窄后侧面板仍能放在主区域旁边时,才为侧面板变窄;否则该栏保持宽度,侧面板覆盖主区域。
  • 记住的宽度超出某栏的范围时,按范围中最近的一端绘制;不要重置它,这样修改范围时不会丢掉用户的选择。