Skip to content

Commit f7fc1d0

Browse files
authored
Merge pull request #2906 from kev1n77/fmy/ui-main
fix(ui): align field states and search affordances with the design system
2 parents 181b54b + 447a41a commit f7fc1d0

30 files changed

Lines changed: 357 additions & 60 deletions

‎design-system/apps/design-lab/src/i18n/messages.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,9 @@ export const enUSMessages = {
528528
"detail.option.selected-item": "Selected item",
529529
"detail.option.focus-visible": "Focus visible",
530530
"detail.option.disabled": "Disabled",
531+
"detail.option.filled": "Filled",
532+
"detail.option.read-only": "Read only",
533+
"components.preview.searchClear": "Clear search",
531534
"detail.option.display": "Display",
532535
"detail.option.completed": "Completed",
533536
"detail.option.expanded": "Expanded",
@@ -1268,6 +1271,9 @@ export const zhCNMessages = {
12681271
"detail.option.selected-item": "选中项目",
12691272
"detail.option.focus-visible": "焦点可见",
12701273
"detail.option.disabled": "禁用",
1274+
"detail.option.filled": "已输入",
1275+
"detail.option.read-only": "只读",
1276+
"components.preview.searchClear": "清除搜索",
12711277
"detail.option.display": "展示",
12721278
"detail.option.completed": "已完成",
12731279
"detail.option.expanded": "展开",
@@ -1947,6 +1953,9 @@ export const zhTWMessages = {
19471953
"detail.option.selected-item": "選取項目",
19481954
"detail.option.focus-visible": "焦點可見",
19491955
"detail.option.disabled": "停用",
1956+
"detail.option.filled": "已輸入",
1957+
"detail.option.read-only": "唯讀",
1958+
"components.preview.searchClear": "清除搜尋",
19501959
"detail.option.display": "展示",
19511960
"detail.option.completed": "已完成",
19521961
"detail.option.expanded": "展開",

‎design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx‎

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ const optionLabelKeys: Readonly<Record<string, MessageKey>> = {
178178
default: "detail.option.default",
179179
replacing: "detail.option.replacing",
180180
disabled: "detail.option.disabled",
181+
filled: "detail.option.filled",
182+
"read-only": "detail.option.read-only",
181183
display: "detail.option.display",
182184
error: "detail.option.error",
183185
expanded: "detail.option.expanded",
@@ -298,6 +300,27 @@ function NumberInputPreview({ state }: { state: string }) {
298300
);
299301
}
300302

303+
function SearchFieldStatePreview({ state }: { state: string }) {
304+
const { t } = useI18n();
305+
const [value, setValue] = useState(state === "default" ? "" : "OpenBitFun");
306+
return (
307+
<SearchField
308+
aria-label={t("components.preview.searchLabel")}
309+
className={state === "hover" ? "lab-force-hover" : state === "focus-visible" ? "lab-force-focus" : undefined}
310+
clearLabel={t("components.preview.searchClear")}
311+
disabled={state === "disabled"}
312+
invalid={state === "invalid"}
313+
leadingIcon={<Icon name="search" />}
314+
onClear={() => setValue("")}
315+
onValueChange={setValue}
316+
placeholder={t("components.preview.searchPlaceholder")}
317+
readOnly={state === "read-only"}
318+
shortcut={<KeyHint icon={<Icon name="command-mac" />}>K</KeyHint>}
319+
value={value}
320+
/>
321+
);
322+
}
323+
301324
export function ComponentDetailPage({
302325
colorScheme,
303326
component,
@@ -399,7 +422,7 @@ export function ComponentDetailPage({
399422
return ["raised", "subtle", "media"] as const;
400423
case "Input":
401424
case "SearchField":
402-
return ["default", "hover", "focus-visible", "invalid", "disabled"] as const;
425+
return ["default", "filled", "hover", "focus-visible", "read-only", "invalid", "disabled"] as const;
403426
case "Select":
404427
return ["default", "hover", "focus-visible", "open", "invalid", "disabled"] as const;
405428
case "Field":
@@ -541,7 +564,9 @@ export function ComponentDetailPage({
541564
? " disabled"
542565
: previewState === "invalid"
543566
? " invalid"
544-
: "";
567+
: previewState === "read-only"
568+
? ' readOnly defaultValue="OpenBitFun"'
569+
: previewState === "default" ? "" : ' defaultValue="OpenBitFun"';
545570
return `import { Icon, Input } from "@openbitfun/ui";\n\n<Input\n aria-label="${t("components.preview.inputLabel")}"\n placeholder="${t("components.preview.inputPlaceholder")}"\n trailing={<Icon name="eye" />}${stateProps}\n/>`;
546571
}
547572
if (component.name === "KeyHint") {
@@ -570,12 +595,8 @@ export function ComponentDetailPage({
570595
return `import { Icon, IconButton, PageHeader } from "@openbitfun/ui";\n\n<PageHeader\n action={<IconButton aria-label="${t("components.preview.close")}" icon={<Icon name="xmark" />} />}\n align="${pageHeaderAlign}"\n description="${t("components.preview.appearanceDescription")}"\n leading={<Icon name="gear" />}\n level={2}${requiredProp}\n size="${pageHeaderSize}"\n title="${t("components.preview.appearance")}"\n/>`;
571596
}
572597
if (component.name === "SearchField") {
573-
const stateProps = previewState === "disabled"
574-
? " disabled"
575-
: previewState === "invalid"
576-
? " invalid"
577-
: "";
578-
return `import { Icon, KeyHint, SearchField } from "@openbitfun/ui";\n\n<SearchField\n aria-label="${t("components.preview.searchLabel")}"\n leadingIcon={<Icon name="search" />}\n placeholder="${t("components.preview.searchPlaceholder")}"\n shortcut={<KeyHint icon={<Icon name="command-mac" />}>K</KeyHint>}${stateProps}\n/>`;
598+
const searchStateProps = previewState === "disabled" ? " disabled" : previewState === "invalid" ? " invalid" : previewState === "read-only" ? " readOnly" : "";
599+
return `import { useState } from "react";\nimport { Icon, KeyHint, SearchField } from "@openbitfun/ui";\n\nfunction Example() {\n const [query, setQuery] = useState(${JSON.stringify(previewState === "default" ? "" : "OpenBitFun")});\n return (\n <SearchField\n clearLabel="${t("components.preview.searchClear")}"\n onClear={() => setQuery("")}\n onValueChange={setQuery}\n value={query}\n aria-label="${t("components.preview.searchLabel")}"\n leadingIcon={<Icon name="search" />}\n placeholder="${t("components.preview.searchPlaceholder")}"\n shortcut={<KeyHint icon={<Icon name="command-mac" />}>K</KeyHint>}${searchStateProps}\n />\n );\n}`;
579600
}
580601
if (component.name === "Combobox") {
581602
return `import { Combobox } from "@openbitfun/ui";\n\n<Combobox\n aria-label="Mode"\n onValueChange={setMode}\n options={[\n { label: "Ask", value: "ask" },\n { label: "Plan", value: "plan" },\n { disabled: true, label: "Agent", value: "agent" },\n ]}\n value={mode}\n/>`;
@@ -1327,10 +1348,13 @@ export function ComponentDetailPage({
13271348
<Input
13281349
aria-label={t("components.preview.inputLabel")}
13291350
className={previewClassName}
1351+
defaultValue={state === "default" ? undefined : "OpenBitFun"}
1352+
key={state}
13301353
disabled={state === "disabled"}
13311354
invalid={state === "invalid"}
13321355
placeholder={t("components.preview.inputPlaceholder")}
1333-
trailing={<Icon name="eye" size="lg" aria-hidden="true" />}
1356+
readOnly={state === "read-only"}
1357+
trailing={<Icon name="eye" />}
13341358
/>
13351359
);
13361360
}
@@ -1775,22 +1799,7 @@ export function ComponentDetailPage({
17751799
}
17761800

17771801
if (component.name === "SearchField") {
1778-
const previewClassName = state === "hover"
1779-
? "lab-force-hover"
1780-
: state === "focus-visible"
1781-
? "lab-force-focus"
1782-
: undefined;
1783-
return (
1784-
<SearchField
1785-
aria-label={t("components.preview.searchLabel")}
1786-
className={previewClassName}
1787-
disabled={state === "disabled"}
1788-
invalid={state === "invalid"}
1789-
leadingIcon={<Icon name="search" size="lg" aria-hidden="true" />}
1790-
placeholder={t("components.preview.searchPlaceholder")}
1791-
shortcut={<KeyHint icon={<Icon name="command-mac" size="lg" aria-hidden="true" />}>K</KeyHint>}
1792-
/>
1793-
);
1802+
return <SearchFieldStatePreview key={state} state={state} />;
17941803
}
17951804

17961805
if (component.name === "NavigationPanel") {

‎design-system/apps/design-lab/src/styles.css‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,11 +1096,6 @@ body,
10961096
outline-offset: var(--openbitfun-focus-offset);
10971097
}
10981098

1099-
input.lab-force-focus {
1100-
border-color: var(--openbitfun-color-field-border-focus) !important;
1101-
box-shadow: 0 0 0 var(--openbitfun-focus-width) var(--openbitfun-color-focus-ring) !important;
1102-
}
1103-
11041099
[data-openbitfun-component="input"].lab-force-hover,
11051100
[data-openbitfun-component="search-field"].lab-force-hover [data-openbitfun-component="input"] {
11061101
border-color: var(--openbitfun-color-field-border-hover);
@@ -1109,8 +1104,8 @@ input.lab-force-focus {
11091104

11101105
[data-openbitfun-component="input"].lab-force-focus,
11111106
[data-openbitfun-component="search-field"].lab-force-focus [data-openbitfun-component="input"] {
1112-
border-color: var(--openbitfun-color-field-border-focus);
1113-
box-shadow: 0 0 0 var(--openbitfun-focus-width) var(--openbitfun-color-focus-ring);
1107+
border-color: var(--openbitfun-color-field-border-active);
1108+
box-shadow: none;
11141109
}
11151110

11161111
.component-code-panel {

‎design-system/apps/design-lab/vite/component-detail-contract.test.mjs‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -419,14 +419,24 @@ test("ConfirmDialog preview exposes semantic, destructive, preview, and pending
419419
test("Input, KeyHint, and SearchField previews expose composable slot and state contracts", async () => {
420420
const source = await readFile(detailSource, "utf8");
421421

422-
assert.match(source, /case "Input":\s*case "SearchField":\s*return \["default", "hover", "focus-visible", "invalid", "disabled"\] as const/);
422+
assert.match(source, /case "Input":\s*case "SearchField":\s*return \["default", "filled", "hover", "focus-visible", "read-only", "invalid", "disabled"\] as const/);
423423
assert.match(source, /case "Select":\s*return \["default", "hover", "focus-visible", "open", "invalid", "disabled"\] as const/);
424424
assert.match(source, /component\.name === "Input"/);
425425
assert.match(source, /component\.name === "KeyHint"/);
426426
assert.match(source, /component\.name === "SearchField"/);
427-
assert.match(source, /trailing=\{<Icon name="eye" size="lg" aria-hidden="true" \/>\}/);
428-
assert.match(source, /leadingIcon=\{<Icon name="search" size="lg" aria-hidden="true" \/>\}/);
429-
assert.match(source, /shortcut=\{<KeyHint icon=\{<Icon name="command-mac" size="lg" aria-hidden="true" \/>\}>K<\/KeyHint>\}/);
427+
assert.match(source, /trailing=\{<Icon name="eye" \/>\}/);
428+
assert.match(source, /leadingIcon=\{<Icon name="search" \/>\}/);
429+
assert.match(source, /shortcut=\{<KeyHint icon=\{<Icon name="command-mac" \/>\}>K<\/KeyHint>\}/);
430+
assert.match(source, /onClear=\{\(\) => setValue\(""\)\}/);
431+
assert.match(source, /readOnly=\{state === "read-only"\}/);
432+
433+
const styles = await readFile(stylesSource, "utf8");
434+
const fieldFocus = styles.match(/\[data-openbitfun-component="input"\]\.lab-force-focus,[^{]+\{([^}]+)\}/)?.[1];
435+
assert.ok(fieldFocus, "Input and SearchField must share their preview focus treatment");
436+
assert.match(fieldFocus, /border-color: var\(--openbitfun-color-field-border-active\)/);
437+
assert.match(fieldFocus, /box-shadow: none/);
438+
assert.doesNotMatch(fieldFocus, /border-width:|outline:|--openbitfun-focus-width/);
439+
assert.doesNotMatch(styles, /input\.lab-force-focus\s*\{/);
430440
});
431441

432442
test("ScrollArea preview exposes direction and native scrollbar visibility contracts", async () => {

‎design-system/packages/theme-openbitfun/README.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,24 @@ The same data is available as `@openbitfun/theme-openbitfun/reference-colors.jso
2828

2929
## Surface and state roles
3030

31+
`color.field.*` owns shared field surfaces and state borders. Light uses neutral
32+
8% borders, with 20% hover and active Input/SearchField borders. The dedicated
33+
`color.field.borderActive` owns the editing border because Input's design differs
34+
from the stronger focus treatment used by other controls. These text
35+
fields use one unchanged border for both pointer and keyboard focus; native
36+
`:focus-visible` must not substitute the stronger generic focus palette or add
37+
a second ring. Dark and high-contrast modes retain their own focus color, and forced
38+
colors use Highlight. `color.field.borderFocus` retains its 3:1 contract for
39+
other controls that consume that stronger focus treatment.
40+
`color.field.placeholder` separates
41+
40% empty hints and decorative adornments from general secondary prose; dark
42+
and high-contrast modes retain their readable muted content colors. The default
43+
light Web UI consumes these published values in root and chrome scopes. Named
44+
presets keep their own palette, and imported packages that only supply
45+
`color.content.muted` retain that field hint color unless explicitly overridden.
46+
Old packages that supply `color.field.borderFocus` retain that editing border
47+
unless they explicitly provide `color.field.borderActive`.
48+
3149
`component.button.*` owns Button's state palette. Its light fill stays at black
3250
8% while the shared neutral actions retain their 5/8/10% feedback; its primary
3351
background uses black 80/60/90% and disabled content 20%. Outline and text variants

‎design-system/packages/theme-openbitfun/src/dark.tokens.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@
175175
}
176176
},
177177
"field": {
178+
"borderActive": { "$type": "color", "$value": "{color.field.borderFocus}" },
179+
"placeholder": { "$type": "color", "$value": "{color.content.muted}" },
178180
"background": { "$type": "color", "$value": "{ref.color.neutral.900}" },
179181
"backgroundHover": { "$type": "color", "$value": "rgba(255, 255, 255, 0.06)" },
180182
"border": { "$type": "color", "$value": "rgba(255, 255, 255, 0.18)" },

‎design-system/packages/theme-openbitfun/src/high-contrast-dark.tokens.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
}
5252
},
5353
"field": {
54+
"borderActive": { "$type": "color", "$value": "{color.field.borderFocus}" },
55+
"placeholder": { "$type": "color", "$value": "{color.content.muted}" },
5456
"border": { "$type": "color", "$value": "{ref.color.gray.250}" },
5557
"borderHover": { "$type": "color", "$value": "{ref.color.gray.0}" },
5658
"borderFocus": { "$type": "color", "$value": "{ref.color.amber.400}" }

‎design-system/packages/theme-openbitfun/src/high-contrast-light.tokens.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
}
5252
},
5353
"field": {
54+
"borderActive": { "$type": "color", "$value": "{color.field.borderFocus}" },
55+
"placeholder": { "$type": "color", "$value": "{color.content.muted}" },
5456
"border": { "$type": "color", "$value": "{ref.color.gray.650}" },
5557
"borderHover": { "$type": "color", "$value": "{ref.color.gray.1000}" },
5658
"borderFocus": { "$type": "color", "$value": "{ref.color.blue.700}" }

‎design-system/packages/theme-openbitfun/src/light.tokens.json‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,12 @@
175175
}
176176
},
177177
"field": {
178+
"borderActive": { "$description": "Input and SearchField editing border, distinct from the stronger generic focus indicator used by other controls.", "$type": "color", "$value": "{color.field.borderHover}" },
179+
"placeholder": { "$description": "Empty field hints and decorative adornments; distinct from general secondary prose.", "$type": "color", "$value": "rgba(0, 0, 0, 0.40)" },
178180
"background": { "$type": "color", "$value": "{ref.color.neutral.0}" },
179181
"backgroundHover": { "$type": "color", "$value": "{color.field.background}" },
180-
"border": { "$type": "color", "$value": "rgba(16, 26, 39, 0.15)" },
181-
"borderHover": { "$type": "color", "$value": "rgba(16, 26, 39, 0.24)" },
182+
"border": { "$type": "color", "$value": "rgba(0, 0, 0, 0.08)" },
183+
"borderHover": { "$type": "color", "$value": "rgba(0, 0, 0, 0.20)" },
182184
"borderFocus": { "$type": "color", "$value": "{ref.color.neutral.550}" }
183185
},
184186
"control": {

‎design-system/packages/ui/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,10 @@ The Web UI's legacy Select implementation is retired. Like retired Button and
280280
Switch overrides, legacy `components.select` Appearance rules are ignored at
281281
the existing read-only migration boundary; original packages are not rewritten.
282282
Selection visuals now come from the public field/menu semantic tokens.
283+
SearchField sizes its decorative wrapper through Input's icon slot, so default
284+
catalog icons and native SVGs occupy the same region. Shortcut hints and clear
285+
actions can coexist; disabled and read-only fields disable the clear action.
286+
283287
Choose `size` explicitly when composing form rows: selectors default to `md`,
284288
while `Input` defaults to `sm`. The shared `control.height.sm/md/lg` tokens and
285289
active density own the actual heights; consumers must not replace them with

0 commit comments

Comments
 (0)