Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions design-system/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,10 @@ pnpm run design-system:check

Run `pnpm run design-system:check` for cross-package or release-boundary changes. Token and theme changes must also run the repository-level `pnpm run theme:color-audit:all`.

For shared icon rendering or geometry changes, use the focused package checks:

```bash
pnpm --dir design-system run build:ui && node --test design-system/packages/ui/tests/icon.test.mjs design-system/packages/ui/tests/icon-slots.test.mjs
```

Do not use browser automation or mock screenshots as visual proof. Design Lab is the manual authoring surface; source, build, HTTP, and package checks do not establish final visual fidelity.
18 changes: 12 additions & 6 deletions design-system/apps/design-lab/src/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ export const enUSMessages = {
"component.GitToolCard.description": "A prominent Git command card with output, warning, failure, and execution metadata.",
"component.GlobSearchToolCard.description": "An ambient file-pattern search card with reusable result-list anatomy.",
"component.GrepSearchToolCard.description": "An ambient text-search card with query metadata and a scrollable result preview.",
"component.IconButton.description": "A labeled icon-only action with annotation, quiet, outlined, filled, and primary variants. An annotation provides a small, muted entry to supplemental help.",
"component.IconButton.description": "A labeled icon action with annotation, quiet, glass, outlined, filled, and primary variants. Glass actions float over intact content.",
"component.Capsule.description": "A capsule with a circular glass action floating over intact text and equal edge spacing.",
"component.Input.description": "A native text field with independent leading and trailing content slots.",
"component.KeyHint.description": "A semantic keyboard hint with optional decorative icon content.",
"component.LoadingState.description": "A centered loading composition with an activity indicator and optional support copy.",
Expand All @@ -186,7 +187,7 @@ export const enUSMessages = {
"component.SkillToolCard.description": "An ambient trace for skill loading progress and outcomes.",
"component.Spinner.description": "A compact activity indicator with matrix and bar presentations.",
"component.Switch.description": "Toggles a single setting between on and off with native checkbox semantics.",
"component.TabGroup.description": "A compact single-selection tab list with optional leading icons, end actions, and automatic keyboard activation.",
"component.TabGroup.description": "A compact single-selection tab list with leading icons, glass end actions over intact labels, and automatic keyboard activation.",
"component.RollingText.description": "Vertical text replacement with coordinated width, interruption handling, and reduced-motion support. Try repeated clicks in the replacement specimen.",
"component.ShimmerText.description": "A soft fade moves slowly across activity text, keeping every glyph visible. Reduced motion, high contrast and print keep the text static and readable.",
"components.preview.replaceText": "Replace title",
Expand Down Expand Up @@ -557,6 +558,7 @@ export const enUSMessages = {
"detail.option.primary": "Primary",
"detail.option.quiet": "Quiet",
"detail.option.annotation": "Annotation",
"detail.option.glass": "Glass",
"detail.option.text": "Text",
"detail.option.sm": "Small",
"detail.option.start": "Start",
Expand Down Expand Up @@ -969,7 +971,8 @@ export const zhCNMessages = {
"component.GitToolCard.description": "关注型 Git 命令卡片,统一呈现输出、警告、失败与执行元数据。",
"component.GlobSearchToolCard.description": "复用结果列表结构的低关注文件模式搜索卡片。",
"component.GrepSearchToolCard.description": "带查询元数据和可滚动结果预览的低关注文本搜索卡片。",
"component.IconButton.description": "带无障碍标签的纯图标组件,支持标注、安静、描边、填充和主要呈现方式。标注以小尺寸、弱化颜色提供补充说明入口。",
"component.IconButton.description": "带无障碍标签的纯图标组件,支持标注、安静、毛玻璃、描边、填充和主要呈现方式。毛玻璃操作悬浮在完整内容上方。",
"component.Capsule.description": "胶囊内容保持完整,圆形毛玻璃操作悬浮其上,边缘间距一致。",
"component.Input.description": "使用原生输入语义,并提供独立前置与后置内容槽的文本字段。",
"component.KeyHint.description": "使用键盘提示语义,并支持可选装饰图标的基础组件。",
"component.LoadingState.description": "由活动指示器与可选说明文字组成的居中加载状态。",
Expand All @@ -992,7 +995,7 @@ export const zhCNMessages = {
"component.SkillToolCard.description": "低关注 Skill 轨迹,用于呈现加载进度和执行结果。",
"component.Spinner.description": "提供点阵与条形样式的紧凑活动指示器。",
"component.Switch.description": "通过原生复选框语义在开启与关闭之间切换单项设置。",
"component.TabGroup.description": "紧凑的单选标签组,支持前置图标、尾部操作与方向键自动切换。",
"component.TabGroup.description": "紧凑的单选标签组,支持前置图标、悬浮于完整标签上的毛玻璃尾部操作与方向键自动切换。",
"component.RollingText.description": "垂直滚动替换文本,宽度同步过渡,支持连续切换与减少动态效果。可在替换示例中连续点击体验。",
"component.ShimmerText.description": "柔和的渐隐在运行文字间缓慢流动,始终保留完整字形;减少动态效果、高对比度和打印时保持静态可读。",
"components.preview.replaceText": "替换标题",
Expand Down Expand Up @@ -1363,6 +1366,7 @@ export const zhCNMessages = {
"detail.option.primary": "主要",
"detail.option.quiet": "安静",
"detail.option.annotation": "标注",
"detail.option.glass": "毛玻璃",
"detail.option.text": "文字",
"detail.option.sm": "小",
"detail.option.start": "起始",
Expand Down Expand Up @@ -1772,7 +1776,8 @@ export const zhTWMessages = {
"meta.category.primitive": "基礎元件",
"meta.category.other": "元件",
"component.ActionCard.description": "可獨立設定前置內容、標題、說明、選取狀態和同級尾端操作的可互動卡片。",
"component.IconButton.description": "帶無障礙標籤的純圖示元件,支援標註、安靜、描邊、填滿和主要呈現方式。標註以小尺寸、弱化顏色提供補充說明入口。",
"component.IconButton.description": "帶無障礙標籤的純圖示元件,支援標註、安靜、毛玻璃、描邊、填滿和主要呈現方式。毛玻璃操作懸浮在完整內容上方。",
"component.Capsule.description": "膠囊內容保持完整,圓形毛玻璃操作懸浮其上,邊緣間距一致。",
"component.MobileActionSheet.description": "用於統一呈現一般和危險操作清單的可關閉行動端彈層。",
"component.MobileBadge.description": "用於狀態、身分與中繼資料的緊湊行動端標籤。",
"component.MobileBanner.description": "支援語義色調和可選操作的行動端行內狀態橫幅。",
Expand Down Expand Up @@ -1839,7 +1844,7 @@ export const zhTWMessages = {
"component.SkillToolCard.description": "低關注 Skill 軌跡,用於呈現載入進度和執行結果。",
"component.Spinner.description": "提供點陣與條形樣式的緊湊活動指示器。",
"component.Switch.description": "以原生核取方塊語意在開啟與關閉之間切換單一設定。",
"component.TabGroup.description": "緊湊的單選標籤群組,支援前置圖示、尾端操作與方向鍵自動切換。",
"component.TabGroup.description": "緊湊的單選標籤群組,支援前置圖示、懸浮於完整標籤上的毛玻璃尾端操作與方向鍵自動切換。",
"component.RollingText.description": "垂直捲動替換文字,寬度同步過渡,支援連續切換與減少動態效果。可在替換範例中連續點擊體驗。",
"component.ShimmerText.description": "柔和的漸隱在執行文字間緩慢流動,始終保留完整字形;減少動態效果、高對比度和列印時保持靜態可讀。",
"components.preview.replaceText": "替換標題",
Expand Down Expand Up @@ -2113,6 +2118,7 @@ export const zhTWMessages = {
"detail.iconPosition": "圖示位置",
"detail.option.outline": "描邊",
"detail.option.annotation": "標註",
"detail.option.glass": "毛玻璃",
"detail.option.fill": "填滿",
"detail.option.sm": "小",
"detail.option.start": "起始",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CapsulePreview } from "../preview/CapsulePreview";
import { Fragment, useEffect, useMemo, useState, type CSSProperties } from "react";
import { ComponentOverview } from "../preview/ComponentOverview";
import { getComponentPresentation, getInitialPreviewState, hasComponentOverview } from "../preview/componentPresentation";
Expand Down Expand Up @@ -163,7 +164,7 @@ type PageHeaderAlign = "center" | "start";
type PageHeaderSize = "display" | "lg" | "md" | "sm";

const buttonVariants = ["outline", "fill", "secondary", "primary", "text"] as const;
const iconButtonVariants = ["annotation", "quiet", "outline", "fill", "primary"] as const;
const iconButtonVariants = ["annotation", "quiet", "glass", "outline", "fill", "primary"] as const;
const iconButtonSizes = ["xs", "standard", "sm", "md", "lg"] as const;
const buttonInspectorStates = ["default", "hover", "active"] as const;
const fieldOrientations = ["vertical", "horizontal"] as const;
Expand Down Expand Up @@ -231,6 +232,7 @@ const optionLabelKeys: Readonly<Record<string, MessageKey>> = {
open: "detail.option.open",
outline: "detail.option.outline",
annotation: "detail.option.annotation",
glass: "detail.option.glass",
primary: "detail.option.primary",
quiet: "detail.option.quiet",
raised: "detail.option.raised",
Expand Down Expand Up @@ -537,6 +539,9 @@ export function ComponentDetailPage({
return `import { Icon } from "@openbitfun/ui";\n\n<Icon name="${iconName}" size="${iconSize}" tone="${iconTone}" />`;
}

if (component.name === "Capsule") {
return `import { Button, Capsule, Icon, IconButton } from "@openbitfun/ui";\n\n<Capsule size="sm" action={\n <IconButton aria-label="${t("components.preview.close")}" icon={<Icon name="xmark" />} onClick={remove} />\n}>\n <Button size="sm" variant="outline">${t("components.preview.modalProviderName")}</Button>\n</Capsule>`;
}
if (component.name === "IconButton") {
const stateProps = `${inspectorDisabled ? " disabled" : ""}${inspectorLoading ? " loading" : ""}`;
const annotation = iconButtonVariant === "annotation";
Expand Down Expand Up @@ -904,6 +909,7 @@ export function ComponentDetailPage({
);
}

if (component.name === "Capsule") return <CapsulePreview key={state} state={state} />;
if (component.name === "Icon") {
return <Icon name={iconName} size={iconSize} tone={iconTone} />;
}
Expand Down
3 changes: 3 additions & 0 deletions design-system/apps/design-lab/src/pages/ComponentsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { ComponentDetailPage } from "./ComponentDetailPage";
import { BrandPreview } from "../preview/BrandPreview";
import { CapsulePreview } from "../preview/CapsulePreview";
import { AppWindow, Heading, Keyboard, List, Rows3, MousePointerClick, PanelTop, ToggleLeft } from "lucide-react";
import { VoiceCallPreview, VoiceParticlePreview } from "../components/VoiceCallPreview";
import {
Expand Down Expand Up @@ -607,6 +608,8 @@ export function ComponentCardPreview({ component }: { component: ComponentMeta }
shortcut={<KeyHint icon={<CatalogIcon name="command-mac" size="lg" aria-hidden="true" />}>K</KeyHint>}
/>
);
case "Capsule":
return <CapsulePreview state="hover" />;
case "StatusPill":
return (
<StatusPill leading={<CatalogIcon name="unselected" />}>
Expand Down
22 changes: 22 additions & 0 deletions design-system/apps/design-lab/src/preview/CapsulePreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useState } from "react";
import { Button, Capsule, Icon, IconButton, Stack, Tooltip } from "@openbitfun/ui";
import { useI18n } from "../i18n";

export function CapsulePreview({ state = "default" }: { state?: string }) {
const { t } = useI18n();
const [removed, setRemoved] = useState<string[]>([]);
const disabled = state === "disabled";
return <Stack direction="horizontal" align="center" gap="3">
{(["xs", "sm"] as const).filter(size => !removed.includes(size)).map(size => (
<Capsule key={size} size={size} data-openbitfun-preview-state={state} action={(
<Tooltip content={t("components.preview.close")} trigger="hover-focus">
<IconButton aria-label={t("components.preview.close")} icon={<Icon name="xmark" />}
disabled={disabled} onClick={() => setRemoved(current => [...current, size])} />
</Tooltip>
)}>
<Button size={size} variant="outline" disabled={disabled}>{t("components.preview.modalProviderName")}</Button>
</Capsule>
))}
{removed.length > 0 && <Button size="sm" onClick={() => setRemoved([])}>{t("components.preview.add")}</Button>}
</Stack>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ export type MockToolScope = 'all' | ToolGroup;

// Only grouping is Lab-owned. Names and component bindings come from production.
const groupForTool = {
get_goal: 'collaboration', create_goal: 'collaboration', update_goal: 'collaboration', AgentList: 'collaboration', AgentDelete: 'collaboration',
get_goal: 'collaboration', create_goal: 'collaboration', update_goal: 'collaboration', AgentList: 'collaboration',
SessionHistory: 'collaboration', analyze_image: 'files', GetTime: 'execution',
ListMCPResources: 'execution', ReadMCPResource: 'execution', ListMCPPrompts: 'execution', GetMCPPrompt: 'execution',
Worktree: 'files', PortForward: 'execution', ReviewPlatform: 'collaboration', FrontendWorkbench: 'web',
FinalizeMiniApp: 'web', PublishMiniApp: 'web', PublishAppearance: 'web', Playbook: 'execution',
Read: 'files', Write: 'files', Edit: 'files', Delete: 'files', Grep: 'files', Glob: 'files', LS: 'files',
GetFileDiff: 'files', view_image: 'files',
ListModels: 'execution', ControlHub: 'web', AgentInterrupt: 'collaboration',
ListModels: 'execution', ControlHub: 'web',
RunCode: 'execution', ExecCommand: 'execution', WriteStdin: 'execution', ExecControl: 'execution', Cron: 'execution', GetToolSpec: 'execution', Skill: 'execution',
WebSearch: 'web', WebFetch: 'web', PageDeploy: 'web', PagePublish: 'web',
AgentSpawn: 'collaboration', AgentSendInput: 'collaboration', AgentWait: 'collaboration', TodoWrite: 'collaboration',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LayersPlusIcon } from "@openbitfun/ui";
import { Icon, LayersPlusIcon } from "@openbitfun/ui";
import { toolsForComponent } from '@openbitfun/flow-chat-presentation/registry';
import { semanticToolCardPreviews } from './SemanticToolCardPreview';
import { createScenarioClock, execScenarios, execSnapshot, type ExecToolName } from '@openbitfun/flow-chat-presentation/scenarios';
Expand Down Expand Up @@ -35,7 +35,6 @@ import {
Monitor,
MousePointer,
Plus,
Pencil,
Rocket,
Search,
SearchCheck,
Expand Down Expand Up @@ -257,7 +256,7 @@ function ChatComposerPreview({ interactive, state }: PreviewProps) {
/>
<IconButton
aria-label="Edit"
icon={<Pencil />}
icon={<Icon name="edit" />}
size="xs"
/>
</ChatComposerQueueItemActions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type ComponentPresentation =
// These are website presentation choices. The public registry still owns the
// component inventory, properties and supported states.
const presentations: Record<string, ComponentPresentation> = {
Button: "matrix", IconButton: "matrix",
Button: "matrix", IconButton: "matrix", Capsule: "compact",
Avatar: "compact", Checkbox: "compact", Radio: "compact", Switch: "compact",
StatusPill: "compact", Spinner: "compact", LoadingState: "compact",
LauncherButton: "compact", NumberBadge: "compact", KeyHint: "compact",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ test("IconButton preview exposes its icon-only presentation contract", async ()
assert.ok(declaration);
assert.deepEqual(
[...declaration[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]),
["annotation", "quiet", "outline", "fill", "primary"],
["annotation", "quiet", "glass", "outline", "fill", "primary"],
);
assert.match(source, /data-component="icon-button"/);
assert.match(source, /aria-label=\{t\(annotation \? "components\.preview\.fieldHelp" : "components\.preview\.listView"\)\}/);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ test("color and pattern directories expose one labelled panel while retaining ot
}
});

test("Capsule previews publish the glass action composition in every supported state", () => {
const { html, preview } = render("Capsule");
assert.match(preview, /data-openbitfun-component="capsule"/);
assert.match(preview, /data-openbitfun-variant="glass"/);
assert.match(preview, /data-openbitfun-shape="circle"/);
assert.match(preview, /data-openbitfun-preview-state="disabled"/);
assert.match(html, /import \{ Button, Capsule, Icon, IconButton \}/);
assert.doesNotMatch(html, />component\.Capsule\.description</);
});

test("Textarea opens with its editable, validation and disabled examples visible", () => {
const { html, preview } = render("Textarea");
assert.equal((preview.match(/data-openbitfun-component="textarea"/g) ?? []).length, 5);
Expand Down
39 changes: 38 additions & 1 deletion design-system/packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ by the component that needs them.
## Icons

Use `Icon` for shared size, tone and accessible labels. General-purpose symbols
use Lucide. Automatic reasoning uses the authored `reasoning-auto` SVG mark with
use Lucide. The named `edit` icon uses the simple `Pen` outline without Pencil's
cap divider, keeping compact action groups visually light.
Automatic reasoning uses the authored `reasoning-auto` SVG mark with
a `currentColor` mask so it follows the active theme. Manual reasoning levels use
the product slider and text labels.
Product consumers own capability filtering, translated names and
Expand Down Expand Up @@ -392,6 +394,41 @@ focus ring. Use it for toolbar, dialog, and row utilities. `fill` and `primary`
keep an opaque backing surface for persistent emphasis. Disabled quiet actions
remain transparent and do not show hover or pressed feedback.

`IconButton variant="glass"` is a translucent, blurred action over existing
content. It uses a low-opacity tint of the shared raised surface, a softened
subtle border and subtle blur, without an outer shadow. Hover and pressed states
retain translucency. Unsupported blur, increased contrast and reduced transparency
use a solid surface.
The button itself owns the glass material and its reveal transition so an opaque
backing or fading parent cannot prevent it from sampling the content behind it.

### Capsule actions

Wrap a pill-shaped `Button`, inline `Input`, or tag label in `Capsule`, matching
its `size` to the child. Pass one `IconButton` (optionally inside `Tooltip`) to
`action`. The slot selects circular glass presentation and compact geometry;
the consumer keeps the action's accessible label, tone, disabled/loading state,
and callbacks. The action is a sibling of the primary control, never a nested
button. Pointer hover or focus within the capsule reveals it; touch keeps it
available. Its right inset equals the top and bottom clearance at every density.

```tsx
<Capsule size="sm" action={
<IconButton aria-label="Remove provider" icon={<Icon name="delete" />}
tone="danger" onClick={requestRemoval} />
}>
<Button size="sm" variant="outline" onClick={editProvider}>Provider name</Button>
</Capsule>
```

The action floats over the intact label: do not clip, mask, hide, resize, or add
action padding to its text when revealing the icon. Normal constrained-width
`OverflowText` behavior remains available. `TabGroup.endAction` and removable
`MultiSelect` tags share this contract, including existing native tab buttons.
Leading identity icons stay in their label layout; this contract governs the
independent trailing actions. Product CSS owns placement of capsules, not their
action offsets, backgrounds, or visibility.

For supplemental label annotations, compose `Tooltip trigger="hover-focus"
openOnClick` with `IconButton variant="annotation" size="xs"` and an `info`
icon. The annotation uses `color.content.muted` at rest and
Expand Down
Loading
Loading