跳至正文
全部文章

一套高度表,拼出一个控制面板

从一个数值滑块出发,把边框、圆角、填充、手柄和高度统一成“方条”,再用同一张高度表补齐开关、分段选择、色调选择,最后拼成控制面板、文档里的调整面板和首页的“调一调”。

约 7 分钟#组件#设计#工程

本站首页首屏右边有一块“调一调”面板:一块二维板调视角,两组分段选择换材质和地面,一排色点换色调,一个开关管音效。组件文档里每个示例下面也能展开一块调整面板,改完可以直接复制成 JSX。

这几块面板里的控件是同一套,外观、高度和行为一致。这篇讲它们怎么从一个数值滑块长出来,中间哪些设计被我推翻了,以及面板怎么把“当前参数”变成一段代码。

从一个滑块开始

起点是 value-slider,一个基于 Base UI Slider 的数值滑块:标签和读数放在轨道里面,拖出两端时轨道有橡皮筋式的过冲,松手后弹簧回弹。

做它的时候定下了一套外观,后来整个系列都沿用:

  • 细边框 border-edge,圆角 rounded-md,底色 bg-muted/60;
  • 填充用色调变量 --flow-tone 铺一层淡色,不指定色调时用前景色的 10%;
  • 手柄是一道竖线,不是圆点。

这个造型在视觉上更像仪表上的一格,而不是通用的圆形滑块。我给它起名“方条”。

一张高度表

方条做出来之后,下一步是让它们能放在同一个面板里。控件各自的高度不一样,一排下来参差不齐,所以先定一张高度表:

档位smdefaultlg
bar28px32px36px
compact18px22px24px

bar 是默认高度,文字放在条里;compact 文字仍在条里,字号收到 xs。数值滑块另外有一档 slim(6 / 8 / 10px),轨道太矮放不下文字,标签挪到上方,竖线手柄比轨道高出一截。

实现上,每个控件用 cva 声明一组 compoundVariants,把“形态 × 尺寸”映射成一个 CSS 变量,样式只读这个变量:

toggle-switch.tsx
compoundVariants: [
  {
    variant: "bar",
    size: "default",
    class: "[--toggle-switch-height:2rem] [--toggle-switch-knob:0.875rem]",
  },
  {
    variant: "compact",
    size: "default",
    class: "[--toggle-switch-height:1.375rem] [--toggle-switch-knob:0.625rem]",
  },
  // …其余档位同理
],

根元素写成 h-(--toggle-switch-height),手柄大小跟着 --toggle-switch-knob 走。每个控件的变量名不同,数字是同一张表。value-slider、segmented-control、toggle-switch、tone-picker、angle-dial 用的都是这套值,所以并排放在一个面板里自然对齐。

下面这个示例把三档高度并排放在一起:

正在加载…

被我否决的设计

pill 和 line

高度表的第一版并不是现在这样。数值滑块的 size 三档只差 28 / 32 / 36px,看起来几乎一样高,我觉得不够有区分度,于是加了一个 variant:

  • pill:8 / 10 / 12px 的圆角粗轨,实色填充,圆形手柄;
  • line:2 / 4 / 4px 的细线,同样是圆形手柄。

做出来才发现,这两种和通用的滑块没有区别,方条的辨识度丢了,也和后面要补的开关、分段选择对不上。这两个变体在同一天里被我撤掉,换成了现在的 bar / compact / slim:三档都是方条,细边框、淡色分层填充、竖线手柄都不变,只是高度不同。

分段选择的标题

segmented-control 的第一版,标签是方条左边一个带竖线分隔的独立格子,后面才是选项。用起来发现标题容易被当成第一个选项。

现在的结构是:左侧是纯文字的字段名,和滑块、开关的标签同一个位置同一个样式;选项放在右侧一道下凹的槽里,选中的那一项是槽里凸起的一块。不写 label 时,整条都是选项。

后来我又把槽和选中块上的描边去掉了:外框、凹槽、选中块三层线叠在一起太杂。现在凹槽只用浅底色,选中块用背景色加一点轻阴影浮起来。

正在加载…

开关的手柄

开关最初沿用了竖线手柄,和滑块同形。但开关表达的是“开 / 关”两个状态,竖线停在左边还是右边,没有圆形手柄那么直观。我把它改成了胶囊轨道加圆形手柄,打开时轨道变成实色,整条也从左往右铺上淡色。滑块表达的是连续的值,用竖线;开关表达的是两个状态,用圆点。

正在加载…

读数可以直接点

滑块拖得再顺,想设一个精确的数还是要输入。所以读数可以点:点读数或者在手柄上按回车,读数变成一个输入块,进入编辑时整段选中,直接打字替换,回车确认。

第一版的输入框是带边框的,聚焦时和滑块整体的焦点环叠在一起,一圈套一圈。后来我把滑块整条的焦点环限制成只在手柄被键盘聚焦时出现,输入框改成背景色小块加一圈色调细描边。

正在加载…

控制面板和“复制为 JSX”

控件有了,需要一个放它们的容器。control-panel 由三部分组成:标题栏(标题加右侧的操作,比如复制和恢复),若干个分组,每个分组里是等宽网格。网格可以设成 1 到 3 列,窄屏(小于 640px)一律单列,相邻的分组之间自动画一条分隔线。

面板上最常用的是两个按钮:

  • 恢复:回到默认值;
  • 复制:把当前参数写成一段 JSX 放进剪贴板。

复制靠一个很小的函数 formatJsx(组件名, 属性)。它的规则很少:值为 undefined 的属性省略;true 写成裸属性名;字符串优先用双引号,含引号或换行时改成 {"..."};数字去掉浮点尾差,最多保留 4 位小数;一行放得下(默认 72 个字符)就写一行,放不下每个属性一行。

“省略等于默认值的属性”这一步不在 formatJsx 里,而是在调用的地方。拿文档站的做法来说,是把面板当前的所有值先换成组件属性,再把默认值也换一遍,两者用 JSON.stringify 对比,一样的就丢掉:

const props = config.toTuning(all)          // 面板当前值 → 组件属性
const defaults = config.toTuning(config.defaults)
const kept: Record<string, unknown> = {}
for (const [key, value] of Object.entries(props)) {
  if (value === undefined) continue
  if (JSON.stringify(value) === JSON.stringify(defaults[key])) continue
  kept[key] = value
}
return formatJsx(config.component ?? "Component", kept)

所以复制出来的永远是你相对默认值改过的那几项,比如调了速度和倾斜,得到的是 <Marquee speed={80} rotate={-6} />(示意),而不是把二十个属性全贴一遍。一个都没改时,得到的是一句自闭合标签。

门面示例 control-panel-demo 就是这么做的:左边是一张实时的名片卡,右边是控制面板,圆角、内边距、阴影、色调、朝向、边框都由面板控制,面板里的控件跟着所选的色调变色,点复制得到这张名片的 JSX。

正在加载…

文档里的调整面板

组件文档的每个示例下面,有一个滑块图标的按钮,点开是一块调整面板。它也是用这套控件拼的,位于 src/components/docs/tuner/。

每个组件声明一份配置,说明有哪些控件、面板的值怎么变成组件的覆盖项、示例自己的取值怎么回报给面板。组件那边只需要导出一个覆盖用的 Context,面板打开时由框架把覆盖项挂进去,示例本身不用改。面板的控件类型也是这个系列:滑块、分段、开关、色调、角度(angle-dial)、二维板(xy-pad)。

最早所有示例共用同一份面板。后来发现这样不行:面板的覆盖作用于示例里的所有实例,而很多示例里的几个实例是刻意取了不同值的。比如走马灯的一个示例里有两条彩带,一条正倾斜、一条负倾斜,方向也相反;如果面板里有“旋转”和“反向”两个控件,一拖就把这个差异抹平了,示例的意思也就没了。

所以配置里加了按示例收窄的能力:

tuner/marquee.tsx
examples: {
  // 两条彩带一正一负倾斜、方向相反
  "marquee-demo": { omit: ["rotate", "reverse"] },
  // 竖向滚动不适合倾斜;示例自带暂停按钮
  "marquee-vertical-demo": { omit: ["rotate", "paused"] },
},

键是示例名,值是 false(这个示例不出面板)或者 { omit: [...] }(去掉指定的控件)。复制代码时被去掉的项也同步排除。已经登记的有走马灯、数值滑块、文字动效、轨道、流程图、字符矩阵等。验收时要逐个示例检查面板有没有不合适的控件,这一条也写进了验收技能。

后来又补了两个控件。走马灯的旋转改用角度刻度尺 angle-dial:方条中间是一段可以左右拖动的刻度,正中的竖线指针读数。等轴测标志的朝向和俯仰合并成了一块二维板 xy-pad,拖一下同时调两个值。

首页的“调一调”

首页首屏的面板就是把这些拼起来:

  • XyPad:视角,朝向和俯仰在一块板上;
  • SegmentedControl:材质、地面,各一条;
  • TonePicker:色调;
  • ToggleSwitch:音效;
  • 标题栏的 ControlPanelReset:恢复默认。

面板里的控件全是系列里的组件。窄屏放不下时,它收成一个“调一调”按钮。

同一套控件还拼出了一个设置页区块 settings-01:外观、通知、声音与触感、编辑器、隐私五个分区,滑块、分段选择、开关、色调选择和控制面板全部来自这个系列,分区还会显示“已修改”的计数。