组件
每个可交互元素支持同一套封闭的状态集:默认、hover、按下、focus-visible、禁用、加载中、选中、错误。没有第九种;看起来需要第九种状态的控件,需要的是重新思考。
高度是骨架:输入族 28,按钮、分段控件、图标按钮 32,唯一的大号主操作 40,图标底板与色块 24。下面每个组件在模板的 <style> 块里都有同名的类——直接套用是最省的路径,用自己的框架实现出同样的尺寸与状态也一样成立。
按钮
上排是六种 32px 外壳(含禁用与加载中),下排是 40px 的大号主操作、纯图标按钮和 Tips 胶囊。
button-primary——近黑、32px,回答大多数问题。平台自己渲染的 Export 就是这个形状button-primary-large——40px 满宽,只给前置栏里唯一的主操作button-secondary——白底描边,hover 时边框加深button-ghost——透明,hover 才出底色
破坏性操作用次级外壳配 red-default 文字加二次确认,绝不用实心红。加载中保持按钮宽度、换成 spinner 并设 aria-busy。
文字是动词(Generate、Export、Reset),不是 OK 或 Submit。纯图标按钮是 8px 圆角的 32px 方块,在画布上为白色。画布顶部末端角的 Tips 按钮是系统里第二个、也是最后一个胶囊:32px、白底、stroke-default 边框、20px 灯泡加文字——它要读作「帮助」,而不是又一个操作。
输入族:灰底,无边框
input-field:28px 高、92px 宽、bg-control 底、无边框、6px 圆角,文字 text-secondary 加等宽数字。错误消息放在字段旁边,不只在 toast 里。
单位固定在字段末端,不跟在数值里:10px、text-tertiary、独立元素。这样数字在起始端成行、单位在末端成行,切换 mm 与 inch 时两者各自改写。单位写在值里会随数字变长而移动,在 RTL 双向重排里还会跑到数字前面。
数值字段的初值写在 value 里,并带 inputmode="decimal"。数值字段支持横向拖拽:按住拖动改值,Shift 十倍步进,Alt 微调,位移小于 3px 算点击。用 setPointerCapture 让拖出面板不丢手势,RTL 下方向反转,并保留 Shift + ↑/↓ 作为键盘等价。不要显示原生上下箭头:目标太小,语域也不对。
生成栏里的提示词框是长大了的同一族:8px 圆角、10/12 内边距,底部末端一枚 text-disabled 的字数计数。
下拉
触发器就是 110px 的输入框,末端一枚 text-tertiary 箭头,列表展开时箭头翻转。列表是浮起的表面,所以与停靠面板不同,它带描边、8px 圆角和 shadow-200。
两个互不干扰的信号,每个都能单独读出来。 底色说的是你在哪:指针和键盘共用它,所以永远只有一行有底色。brand-default 的对勾加 500 字重说的是选了哪个,没有底色时它照样成立。上图里圆形是已选项,方形是指针所在。
行高亮单独占一档 12%,因为面板上的 6% 与 8% 放到浮层白底上都会消失。
行高 32px、4px 圆角、左右 10px 内边距、12px 文字用 text-primary。列表内边距 6px,不窄于触发器,超过七行开始滚动并用细滚动条。超过三个选项,下拉胜过分段控件;不到三个,分段控件胜过下拉。
它欠着的那份契约
原生 <select> 把键盘、读屏和平台选择器白送给你。给弹出层定样式,意味着这些要全部自己写一遍。模板的脚本块里已经写好了,叫 upgradeSelects:移植它,不要自己重新推导。
它按渐进增强写,脚本没跑起来时控件同样可用:HTML 里是一个真实的 <select>,包在 <span class="select-field" data-select> 里。脚本把它隐藏、建出触发器与列表,并让原生元素继续作为取值的唯一真源——表单提交和你自己的 change 监听照旧生效。
aria-activedescendant 指向当前高亮项——把焦点移进列表,正是列表关闭时焦点被丢在 body 上的原因。role="combobox" 与 aria-expanded,列表 role="listbox",每行 role="option" 与 aria-selected。body 上 position: fixed——留在原地会被滚动的参数栏裁掉——并在滚动与窗口尺寸变化时重新定位。<select>:否则丢掉系统选择器,而 32px 的行在拇指下太小。以上每一条都实现在模板脚本块的 upgradeSelects 里。它是模板里唯一要留着的部分:其余都是演示接线,你要换成自己的状态。
开关
关闭 stroke-default,开启 brand-default。轨道低于 24px 目标下限,靠字段行的 <label for> 提供那个够大的目标——五份模板都是这么接的。
轨道和圆钮是画在真实 <input type="checkbox"> 上的装饰,每一层都要 pointer-events: none 和 aria-hidden。
滑块
绝不单独出现:当前值始终以数字显示在行末,需要精度时再配一个输入框。3px 轨道用 track-off,已填充段用 brand-default,12px 圆钮白心加 2px 品牌色环。步长与范围来自模型的真实约束,被悄悄截断的值是用户看不见的 bug。
分段控件
32px 的 bg-hover 轨道、2px 内边距、6px 圆角;选项 12-medium text-secondary,选中项白底、4px 圆角、shadow-100;相邻的未选中项之间有一条 1px stroke-divider 分隔线。容器 role="group" 带名字,每个选项是 <button aria-pressed>。用于两到三个互斥视图;超过三项改用下拉。它不是页签条,不承担页面导航。
缩略图与工具栏
模板缩略图是 140px 白卡片:8px 圆角、stroke-divider 边框、shadow-100。内联 SVG 内缩 14px,照片铺满到边。风格块是同一张卡片做到 90px、不带阴影(SVG 内缩 10px);圆形主体仍用方卡片,只把里面的图裁圆。选中是 2px brand-default 环,叠在卡片上画,内容不会因选中而缩放;hover 是 stroke-default。
工具栏 64px 宽,配 40px 底板。选中的工具是 brand-default 底、白图标。
由平台供数的两块
有两块界面的数据来自平台而不是你的生成器:预览所渲染的材质、用户的历史记录。模板把它们做成带占位数据的可用 UI,让它们从第一天就在正确的位置上;能力对你开放后再接数据。
Surface 是卡片堆底部的一张属性卡片:57px 方块一行四个,选中的那个在方块外 2px 处有一圈 2px 的 text-primary 描边。它只改变预览的渲染,绝不改变导出物。历史是前置栏里的二级页:一行返回、一行三个的 90px 方块、下面 Load more。方块沿用缩略图规则:内联 SVG 内缩 10px,照片铺满。
对话框
原生 <dialog> 配 showModal():480px 宽、12px 圆角、shadow-300、遮罩 bg-overlay(70% 黑)。视口不够高时正文内部滚动,头部与底部不动。头部 16/24:标题用 16-semibold,旁边一枚 20px text-tertiary 的 info 图标,末端 24px 关闭按钮,下有发丝线。往下是可选的媒体带(bg-control),再往下是底部:起始端分页点,末端按钮。放进对话框的大多数东西其实不该在里面:错误去控件旁,进度去预览区,设置去参数栏。加工 Tips 是对话框的标准用例。
生成对话框(布局 4)。深色 bg-inverse 头部复述这次选择:66px 缩略图、用 16-semibold 写的名字,以及每条已定死的事实一枚 tag。正文摞的是表单字段——这是全套规范里唯一让 form-label 站在控件上方而不是旁边的地方,因为 480px 的对话框放得下整宽的拖放区,320px 的侧栏放不下。点一个风格块就打开它,所以块本身绝不发起调用。
提示条、链接、说明文字
提示条把浅色放在消息后面、实色放在图标上,文字用 text-primary,紧挨它描述的对象(见色彩)。链接用 blue-default 并保留下划线——颜色本身不是链接的暗示。字段下的说明文字是 text-tertiary 的 12-regular,解释一个约束而不是复述标签。
要吝啬。 能放进单位后缀的东西不要写成一句话——20 % 胜过「单块边长的百分比」。结果面板里读得到的东西不要在参数栏重复。理想的终态是参数栏里只剩两三条真正起约束作用的说明。