无头组件系统 · 新资源

Kobalte 设计系统.

Kobalte 为 Solid 提供无头组件,包括分别处理原始值和格式化值的数值输入框、复合复选框结构,以及带按下状态的按钮。本指南通过配方缩放器将这些基础组件与 Solid 信号结合起来,其中包含派生数量和可重置的食材勾选项。

OpenDesign 指南9 个章节29 个参考条目原始应用指南
01

Kobalte 设计系统界面预览

按比例调整食材用量、收集食材,并在本地固定原创食谱。

响应式食谱卡片打开完整示例
Kobalte 0.13.14 搭配 Solid 1.9.15;原生无头基础组件和完整的原创 CSS。· 实现参考
02

Kobalte 设计系统设计参考

Kobalte 0.13.14 提供无头 Solid 基础组件,不提供规范的视觉比例。下方所有 recipe-* 颜色和尺寸均为完整的原创应用配置。API 名称和状态属性均已依据已安装的软件包及官方组件文档核实。

原创语义色彩

来源
名称 / 规则值 / 配置用途
--recipe-canvas#f6f2ec 外层画布
--recipe-surface#fffefa 食谱和份量面板
--recipe-ink#403a30 主要文本
--recipe-muted#807465 辅助文本
--recipe-line#e6ded2 分隔线和边框
--recipe-action#95612f 选中复选框和操作文本
--recipe-on-action#fff填充式操作的前景色
--recipe-focus#b88955 可见的键盘焦点
--recipe-soft#f1e4d3 步进器和按下状态的固定按钮背景

原创字体排印和尺寸

来源
名称 / 规则值 / 配置用途
Bodysystem-ui, sans-serif不下载字体
Page heading34px / 1.15; 29px narrow原始层级
Recipe titleGeorgia, serif; 30px / 1.2原创编辑风格点缀
Ingredient copy13px / 1.5; 12px narrow易读且可换行的标签
--recipe-space-1 / 2 / 34px / 8px / 12px小间距
--recipe-space-4 / 6 / 816px / 24px / 32px面板和页面间距
Panel radius16px原创表面几何样式
Checkbox control21 × 21px; 5px radius原创可见指示器
Stepper trigger42px wide采用原创几何样式的原生触发器
Layout breakpoint760px两栏布局变为单栏
Focus ring3px; 3px offset原创可见键盘交互样式

原生组件状态约定

来源
名称 / 规则值 / 配置用途
NumberField.rawValuenumber使用算术状态,而非格式化文本
NumberField.onRawValueChange(value: number) => void更新数量并清除过期勾选
minValue / maxValue / step1 / 8 / 1传递给原生数字字段的应用层限制
formatOptions.maximumFractionDigits0整数份量显示
validationStatevalid | invalid将应用层有效性反映到原生字段
Checkbox.checked / onChangeboolean / callback受控食材选择
Checkbox data attributesdata-checked; data-focus-visible设置原生选中状态和键盘焦点样式
ToggleButton.pressed / onChangeboolean / callback受控的本地固定状态
ToggleButton data-pressedpresent when pressed本地固定项的填充样式
03

Kobalte 设计系统完整指南

实用指南,介绍如何为真实界面选择、应用和评审此系统。

DESIGN-kobalte.md

完整指导、实现起点和源代码链接。

下载指南

Kobalte 设计系统:将 Kobalte 组件连接到 Solid Signals

将 Kobalte 组件连接到 Solid Signals

该组件使用 Solid signals 管理份量、已收集食材和固定状态。createMemo 根据数值信号推导有效性,For 则渲染稳定的食材集合。Kobalte 的受控属性直接读取这些信号;更新无需单独的表单提供器或 React 风格的状态适配器。

摘自本 OpenDesign 指南

NumberField、Checkbox 和 ToggleButton 分别从各自的组件入口导入。它们各司其职:数字字段处理可编辑的数值输入,每个复选框代表一种已收集食材,切换按钮则管理食谱的固定状态。算术计算和失效规则由应用函数处理,而不嵌入视觉选择器中。

阅读配套规范

Kobalte 设计系统:将数值状态与格式化文本分开

将数值状态与格式化文本分开

NumberField 接收 rawValue={portions()} 和 onRawValueChange={update}。数值信号直接驱动乘法运算,而输入框负责格式化和用户编辑。若将格式化后的显示文本用于算术运算,就必须假定分隔符或区域设置,而原生基础组件已经处理了这些问题。

示例允许输入 1 到 8 份整数,步进值为 1,并将 maximumFractionDigits 设为零。另有单独的计算属性检查数值是否有限、是否为整数以及是否在范围内,因为可编辑字段可能暂时包含不完整或无效的值。数值无效时,总量显示为长破折号,食材复选框也会禁用。界面不会把看似合理但实际错误的数量显示成用户已输入有效值的样子。

阅读配套规范

Kobalte 设计系统:使用原生标签和消息组合字段

使用原生标签和消息组合字段

数字字段包含 Label、DecrementTrigger、Input、IncrementTrigger、Description 和 ErrorMessage。可见说明同时解释了范围以及份量变化的影响。触发器带有明确标签“减少份量”和“增加份量”,因此无需仅靠减号和加号来传达完整的无障碍名称。

validationState 根据应用层面的判断条件在 valid 和 invalid 之间切换。ErrorMessage 属于该原生字段,而不是放在页面其他位置、与字段无关的段落。对于已提交的表单,Kobalte 还提供 HiddenInput,用于序列化原始值;此处的示例不包含它,因为没有提交端点或表单负载。

阅读配套规范

Kobalte 设计系统:使用不可变 Set 管理食材选择

使用不可变 Set 管理食材选择

每种食材都使用完整的 Checkbox 结构:Input、Control、Indicator 和 Label。真实输入控件保留语义和键盘交互,可见控件则显示勾选标记并应用原创状态样式。标签同时包含食材名称和用量,让读者知道所收集物品的具体数量。

mark 会根据先前的选择创建新的 Set,再添加或移除 ID。仅修改现有 Set 无法确保信号得到同样可靠的更新。渲染的计数读取 Set 的大小,每个 Checkbox 则根据稳定的食材 ID 检查是否已选中。数量变化会清除整个选择,因为收集了 320 克并不意味着满足新的 400 克需求。

  • 将每个可见的食材名称与 Checkbox.Label 关联。
  • 即使可见方框采用自定义样式,也要保留 Checkbox.Input。
  • 使用稳定 ID 管理选择,并在每次更新时创建新的 Set。

阅读配套规范

Kobalte 设计系统:使用 Toggle 保留本地选择状态

使用 Toggle 保留本地选择状态

固定按钮是一个 ToggleButton,具有 pressed 和 onChange,而不是仅更改标题的普通按钮。其 pressed 状态会反映在原生语义和 data-pressed 选择器中。标签会变为带对勾的“已固定”,旁边的状态说明会指出这只影响本地预览。

固定操作不会更改食材用量或选择状态。更改份数会清除食材勾选状态,但会保留固定状态,因为食谱身份没有改变。恢复默认食谱会一并重置三个状态:四份、空的食材 Set,以及未按下的固定按钮。这些不同的重置边界遵循状态本身的含义,而不是每次交互都清除所有内容。

阅读配套规范

Kobalte 设计系统:为无头控件提供可复现的视觉配置

为无头控件提供可复现的视觉配置

食谱样式表分别处理数值输入框的编辑界面表面、步进按钮,以及复选框的选择功能。只有勾选食材后,才会应用弱化的食材文本样式;用量仍保持清晰可读,方便用户将已备齐的数量与当前份数进行比较。

间距采用 4、8、12、16、24 和 32 像素。原有的字体层级将系统无衬线字体用于控件,并用 Georgia 呈现食谱标题。一个 21 像素的可视复选框与完整的关联标签并列;标签仍是便于点击的操作区域。完整的 CSS 中包含这些精确数值,读者无需对照截图猜测即可复现此示例。

阅读配套规范

Kobalte 设计系统:让布局在收缩时不隐藏信息

让布局在收缩时不隐藏信息

宽屏布局将灵活伸缩的食谱卡片与宽 280 像素的份数面板并排显示。在 760 像素处,布局会改为单列堆叠。卡片标题可以换行,食材标签可以折至下一行,而用量则保持紧凑且不换行。最小断点会略微缩小面板内边距和标签字号,而不是隐藏用量。

沙拉插图为原创 CSS,并标记为装饰性内容。食谱名称、份数和食材用量本身就能独立传达信息。更换插图或缩窄卡片时,应保留完整的食材标签和用量;这些才是备料任务的实际输入。

阅读配套规范

Kobalte 设计系统:将用量变化作为依赖状态更新处理

将数量变化作为依赖状态更新处理

每增加一份,五种食材的总重量就增加 250 克。从四份调整到五份时,显示的总重量会从 1,000 克变为 1,250 克,橙子的用量要求则从 320 克变为 400 克。先前勾选的已备齐状态会被清除,因为它们对应的是旧用量;食谱固定状态则保持不变,因为食谱身份没有改变。

更新函数会先使用 Object.is 将传入值与当前信号进行比较。若回调没有带来变化,就不应清除检查清单,也不应播报用量变化。无效值会使食材用量不再显示,并禁用备料操作;随后由错误消息说明可接受的范围。这些规则让中间编辑过程可见,同时避免无效数字产生误导性的总量。

  1. 在四份时勾选橙子瓣。
  2. 将份数增加到五份,并确认橙子用量为 400 g、总量为 1,250 g,已备齐项目数为零。
  3. 固定食谱,然后恢复默认值,并确认所有本地状态均已重置。

阅读配套规范

Kobalte 设计系统:使用完整构建配置并包含完整样式表

使用 Solid 构建,并引入完整样式表

此设置使用 Vite Solid 模板,固定经过验证的 Solid 和 Kobalte 版本,并挂载到明确指定的 app 根节点。上方展示的组件和样式表都是完整文件。此示例无需主题提供程序、图标包、外部字体或后端。

集成到其他应用时,请确保框架编译器与组件生成方式相匹配。React 转换器无法将 Solid JSX 转换为正确的 Kobalte 应用。同样,升级到未来的主要版本 Kobalte API 时,应先查阅该版本自己的文档,再替换已固定的依赖项。食谱用量和配色属于示例应用数据,因此调整这些内容不需要更改原生基础组件的约定。

阅读配套规范

Kobalte 设计系统组件示例

完整的响应式食谱组件

原生数字输入框、复选框结构,以及具有明确无效状态和重置行为的切换按钮。

jsx
import {createSignal,createMemo,For} from 'solid-js';
import {NumberField} from '@kobalte/core/number-field';
import {Checkbox} from '@kobalte/core/checkbox';
import {ToggleButton} from '@kobalte/core/toggle-button';
export default function App(){
 const[portions,setPortions]=createSignal(4),[checked,setChecked]=createSignal(new Set()),[pinned,setPinned]=createSignal(false),[notice,setNotice]=createSignal('');
 const ingredients=[{id:'orange',name:'Orange segments',grams:80},{id:'beans',name:'Cooked white beans',grams:90},{id:'leaves',name:'Salad leaves',grams:25},{id:'fennel',name:'Thinly sliced fennel',grams:40},{id:'dressing',name:'Citrus dressing',grams:15}];
 const valid=createMemo(()=>Number.isFinite(portions())&&Number.isInteger(portions())&&portions()>=1&&portions()<=8);
 function update(value){if(Object.is(value,portions()))return;setPortions(value);setChecked(new Set());setNotice('Portions changed. Ingredient checks were cleared for the new quantities.')}
 function mark(id,value){setChecked(previous=>{const next=new Set(previous);value?next.add(id):next.delete(id);return next})}
 function reset(){setPortions(4);setChecked(new Set());setPinned(false);setNotice('Restored four portions and an unchecked ingredient list.')}
 return <main class="recipe-shell"><header><p class="eyebrow">KITCHEN NOTEBOOK · REACTIVE RECIPE CARD</p><h1>A brighter bowl, sized for your table.</h1><p>Change the portions, check the scaled ingredients and pin the recipe locally. The quantities are original sample data for this interface.</p></header><div class="recipe-layout"><section class="recipe-card"><div class="salad-art" aria-hidden="true"><div class="plate"><i/><b/><em/><span/><strong/></div></div><div class="recipe-heading"><div><p class="eyebrow">CITRUS & WHITE BEAN SALAD</p><h2>Fresh, crisp, generous.</h2></div><ToggleButton class="pin" pressed={pinned()} onChange={value=>{setPinned(value);setNotice(value?'Recipe pinned in this local preview.':'Recipe unpinned in this local preview.')}} aria-label="Pin recipe">{pinned()?'Pinned ✓':'Pin recipe'}</ToggleButton></div><p class="recipe-description">Sweet citrus, tender beans and crisp fennel. Use the checklist to gather the amounts for your chosen portion count.</p><div class="ingredient-title"><h3>Ingredients</h3><span>{checked().size} / 5 gathered</span></div><div class="ingredients"><For each={ingredients}>{ingredient=><Checkbox class="ingredient" checked={checked().has(ingredient.id)} onChange={value=>mark(ingredient.id,value)} disabled={!valid()}><Checkbox.Input/><Checkbox.Control class="check-control"><Checkbox.Indicator>✓</Checkbox.Indicator></Checkbox.Control><Checkbox.Label class="ingredient-label"><span>{ingredient.name}</span><strong>{valid()?ingredient.grams*portions()+' g':'—'}</strong></Checkbox.Label></Checkbox>}</For></div></section><aside class="portion-panel"><h2>Make it yours</h2><NumberField rawValue={portions()} onRawValueChange={update} minValue={1} maxValue={8} step={1} formatOptions={{maximumFractionDigits:0}} validationState={valid()?'valid':'invalid'} class="portion-field"><NumberField.Label class="field-label">Portions</NumberField.Label><div class="stepper"><NumberField.DecrementTrigger aria-label="Fewer portions">−</NumberField.DecrementTrigger><NumberField.Input/><NumberField.IncrementTrigger aria-label="More portions">+</NumberField.IncrementTrigger></div><NumberField.Description class="field-description">Choose 1–8 whole portions. Changing the quantity clears ingredient checks.</NumberField.Description><NumberField.ErrorMessage class="field-error">Enter a whole number from 1 to 8.</NumberField.ErrorMessage></NumberField><div class="portion-total"><span>Total ingredients</span><strong>{valid()?ingredients.reduce((s,x)=>s+x.grams,0)*portions()+' g':'—'}</strong></div><h3>A simple preparation order</h3><ol><li>Gather the scaled amounts and drain the cooked beans.</li><li>Combine the orange, fennel and leaves in a bowl.</li><li>Add the beans and dressing, then toss gently.</li></ol><button class="reset" onClick={reset}>Restore default recipe</button><p role="status" class="notice">{notice()||(pinned()?'Recipe pinned in this local preview.':'No shopping order or account update is created.')}</p></aside></div></main>
}

代码参考

  • 使用 Solid Vite 模板,而不是 React JSX 编译器。
  • rawValue 承载数值状态;所有食材用量都由该信号推导得出。
  • 更改份数会清除已勾选的 Set,因为之前的用量已不再适用。

完整的原创视觉配置

语义化自定义属性、字体层级、间距、状态样式和原创的 CSS 插图。

css
:root{--recipe-canvas:#f6f2ec;--recipe-surface:#fffefa;--recipe-ink:#403a30;--recipe-muted:#807465;--recipe-line:#e6ded2;--recipe-action:#95612f;--recipe-on-action:#fff;--recipe-focus:#b88955;--recipe-soft:#f1e4d3;--recipe-space-1:4px;--recipe-space-2:8px;--recipe-space-3:12px;--recipe-space-4:16px;--recipe-space-6:24px;--recipe-space-8:32px}html,body,#app{height:auto;min-height:0;margin:0}*{box-sizing:border-box}body{font-family:system-ui,sans-serif;color:var(--recipe-ink);background:var(--recipe-canvas)}button,input{font:inherit}button{cursor:pointer}button:disabled{opacity:.4;cursor:default}button:focus-visible,input:focus-visible{outline:3px solid var(--recipe-focus);outline-offset:3px}.recipe-shell{max-width:1090px;margin:auto;padding:32px}.eyebrow{font-size:10px;font-weight:700;letter-spacing:.13em;color:var(--recipe-muted)}h1{font-size:34px;line-height:1.15;max-width:700px;margin:12px 0}header>p:last-child{font-size:14px;line-height:1.7;color:var(--recipe-muted);max-width:630px}.recipe-layout{display:grid;grid-template-columns:minmax(0,1fr) 280px;gap:24px;margin-top:28px}.recipe-card,.portion-panel{background:var(--recipe-surface);border:1px solid var(--recipe-line);border-radius:16px;padding:24px;min-width:0}.portion-panel{align-self:start}.salad-art{height:180px;background:#e8e4d5;border-radius:10px;position:relative;overflow:hidden;margin-bottom:24px}.plate{position:absolute;width:210px;height:210px;left:50%;top:18px;transform:translateX(-50%);border:18px solid #faf8f0;border-radius:50%;background:#dedcbf;box-shadow:0 10px 25px #5e584b1c}.plate i,.plate b,.plate em,.plate span,.plate strong{position:absolute;display:block}.plate i{width:85px;height:42px;border-radius:0 0 85px 85px;background:#dc9658;border:6px solid #f7c888;left:14px;top:18px;transform:rotate(-23deg)}.plate b{width:72px;height:36px;border-radius:0 0 72px 72px;background:#dc9658;border:6px solid #f7c888;left:75px;top:83px;transform:rotate(28deg)}.plate em{width:50px;height:77px;background:#83976a;border-radius:80% 0 80% 0;left:108px;top:4px;transform:rotate(20deg)}.plate span{width:30px;height:50px;background:#667f56;border-radius:80% 0 80% 0;left:25px;top:88px;transform:rotate(-23deg)}.plate strong{width:24px;height:34px;background:#fff3d7;border-radius:50%;left:80px;top:46px;box-shadow:-34px 14px 0 #fff3d7,25px 28px 0 #fff3d7}.recipe-heading{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}.recipe-heading h2{font:400 30px/1.2 Georgia,serif;margin:8px 0}.pin{border:1px solid var(--recipe-line);background:white;border-radius:999px;padding:9px 13px;font-size:11px;color:var(--recipe-action)}.pin[data-pressed]{background:var(--recipe-soft);border-color:var(--recipe-action)}.recipe-description{font-size:13px;line-height:1.7;color:var(--recipe-muted)}.ingredient-title{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-top:24px}.ingredient-title h3{font-size:16px;margin:0}.ingredient-title span{font-size:11px;color:var(--recipe-muted)}.ingredients{margin-top:14px}.ingredient{display:flex;gap:12px;align-items:center;border-top:1px solid var(--recipe-line);padding:16px 0;cursor:pointer}.check-control{height:21px;width:21px;border:1px solid #b9ab98;border-radius:5px;flex-shrink:0;display:flex;justify-content:center;align-items:center;font-size:13px}.check-control[data-checked]{background:var(--recipe-action);color:white;border-color:var(--recipe-action)}.check-control[data-focus-visible]{outline:3px solid var(--recipe-focus);outline-offset:3px}.ingredient-label{display:flex;justify-content:space-between;gap:12px;width:100%;font-size:13px;line-height:1.5;cursor:pointer}.ingredient-label strong{white-space:nowrap;font-size:12px}.ingredient[data-checked] .ingredient-label>span{text-decoration:line-through;color:var(--recipe-muted)}.portion-panel>h2{font-size:20px;margin:0 0 24px}.field-label{font-size:13px;font-weight:600}.stepper{display:flex;margin-top:10px;border:1px solid var(--recipe-line);border-radius:8px;overflow:hidden}.stepper button{width:42px;flex-shrink:0;background:var(--recipe-soft);border:0;font-size:21px}.stepper input{width:100%;min-width:0;text-align:center;border:0;padding:12px 0;background:white;color:var(--recipe-ink)}.field-description{font-size:11px;line-height:1.7;color:var(--recipe-muted);margin:10px 0 0}.field-error{font-size:12px;color:#a73324}.portion-total{display:flex;justify-content:space-between;gap:12px;margin:24px 0;padding:18px 0;border-top:1px solid var(--recipe-line);border-bottom:1px solid var(--recipe-line);font-size:12px}.portion-total strong{font-size:19px;white-space:nowrap}.portion-panel h3{font-size:13px;line-height:1.6}.portion-panel ol{padding-left:18px;font-size:12px;line-height:1.7;color:var(--recipe-muted)}.portion-panel li{margin-bottom:12px}.reset{width:100%;padding:12px;border:1px solid var(--recipe-line);border-radius:8px;background:white;color:var(--recipe-action);font-size:12px;margin-top:16px}.notice{font-size:11px;line-height:1.7;color:var(--recipe-muted)}@media(max-width:760px){.recipe-layout{grid-template-columns:minmax(0,1fr)}.recipe-shell{padding:20px}h1{font-size:29px}}@media(max-width:360px){.recipe-shell{padding:14px}.recipe-card,.portion-panel{padding:16px}.ingredient-label{gap:8px;font-size:12px}.ingredient{gap:8px}.ingredient-label strong{font-size:11px}.recipe-heading h2{font-size:27px}}

代码参考

  • 这些 recipe-* 值是应用自行设定的选项,并非官方 Kobalte 令牌。
  • 完整样式表包含响应式布局和清晰可见的焦点样式。
04

Kobalte 设计系统 Agent 设置

实现起点

Solid Vite 模板提供所需的 Solid JSX 编译器。上方完整的 App.jsx 和 style.css 示例包含所有应用代码和样式;无需提供程序或外部服务。

javascript
// npm create vite@latest kobalte-recipe -- --template solid
// cd kobalte-recipe && npm install
// npm install [email protected] @kobalte/[email protected]
// Save the complete examples as src/App.jsx and src/style.css.
// src/index.jsx:
import {render} from 'solid-js/web';
import App from './App.jsx';
import './style.css';
render(() => <App />, document.getElementById('app'));
// index.html: <div id="app"></div><script type="module" src="/src/index.jsx"></script>
// npm run dev

适用于你项目的提示词

Agent 提示词
使用 Kobalte 0.13.14 和 Solid 1.9.15 复现响应式食谱卡片。保留 NumberField 的 rawValue/onRawValueChange、1 到 8 的边界、整数份数校验,以及完整的 Label/Description/ErrorMessage 结构。根据份数信号推导食材克数和总量;份数确实发生变化时,清除不可变的已勾选 ID Set,但保留食谱固定状态。保留原生 Checkbox 部件和 ToggleButton 的 pressed/onChange;用量无效时禁用备料操作;重置时恢复为四份、未勾选任何食材且未固定。包含完整的原创食谱 CSS;所有状态均保留在本地。
05

Kobalte 设计系统:来源与范围

已通过 Kobalte 0.13.14 和 Solid 1.9.15 验证。所有 recipe-* 令牌、CSS 插图和食材用量均为此示例自行创作的数据。固定和备料状态都保留在本地;未连接账户、购物服务或持久化食谱存储。