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
9 changes: 9 additions & 0 deletions design-system/apps/design-lab/src/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,9 @@ export const enUSMessages = {
"detail.option.selected-item": "Selected item",
"detail.option.focus-visible": "Focus visible",
"detail.option.disabled": "Disabled",
"detail.option.filled": "Filled",
"detail.option.read-only": "Read only",
"components.preview.searchClear": "Clear search",
"detail.option.display": "Display",
"detail.option.completed": "Completed",
"detail.option.expanded": "Expanded",
Expand Down Expand Up @@ -1268,6 +1271,9 @@ export const zhCNMessages = {
"detail.option.selected-item": "选中项目",
"detail.option.focus-visible": "焦点可见",
"detail.option.disabled": "禁用",
"detail.option.filled": "已输入",
"detail.option.read-only": "只读",
"components.preview.searchClear": "清除搜索",
"detail.option.display": "展示",
"detail.option.completed": "已完成",
"detail.option.expanded": "展开",
Expand Down Expand Up @@ -1947,6 +1953,9 @@ export const zhTWMessages = {
"detail.option.selected-item": "選取項目",
"detail.option.focus-visible": "焦點可見",
"detail.option.disabled": "停用",
"detail.option.filled": "已輸入",
"detail.option.read-only": "唯讀",
"components.preview.searchClear": "清除搜尋",
"detail.option.display": "展示",
"detail.option.completed": "已完成",
"detail.option.expanded": "展開",
Expand Down
59 changes: 34 additions & 25 deletions design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ const optionLabelKeys: Readonly<Record<string, MessageKey>> = {
default: "detail.option.default",
replacing: "detail.option.replacing",
disabled: "detail.option.disabled",
filled: "detail.option.filled",
"read-only": "detail.option.read-only",
display: "detail.option.display",
error: "detail.option.error",
expanded: "detail.option.expanded",
Expand Down Expand Up @@ -298,6 +300,27 @@ function NumberInputPreview({ state }: { state: string }) {
);
}

function SearchFieldStatePreview({ state }: { state: string }) {
const { t } = useI18n();
const [value, setValue] = useState(state === "default" ? "" : "OpenBitFun");
return (
<SearchField
aria-label={t("components.preview.searchLabel")}
className={state === "hover" ? "lab-force-hover" : state === "focus-visible" ? "lab-force-focus" : undefined}
clearLabel={t("components.preview.searchClear")}
disabled={state === "disabled"}
invalid={state === "invalid"}
leadingIcon={<Icon name="search" />}
onClear={() => setValue("")}
onValueChange={setValue}
placeholder={t("components.preview.searchPlaceholder")}
readOnly={state === "read-only"}
shortcut={<KeyHint icon={<Icon name="command-mac" />}>K</KeyHint>}
value={value}
/>
);
}

export function ComponentDetailPage({
colorScheme,
component,
Expand Down Expand Up @@ -399,7 +422,7 @@ export function ComponentDetailPage({
return ["raised", "subtle", "media"] as const;
case "Input":
case "SearchField":
return ["default", "hover", "focus-visible", "invalid", "disabled"] as const;
return ["default", "filled", "hover", "focus-visible", "read-only", "invalid", "disabled"] as const;
case "Select":
return ["default", "hover", "focus-visible", "open", "invalid", "disabled"] as const;
case "Field":
Expand Down Expand Up @@ -541,7 +564,9 @@ export function ComponentDetailPage({
? " disabled"
: previewState === "invalid"
? " invalid"
: "";
: previewState === "read-only"
? ' readOnly defaultValue="OpenBitFun"'
: previewState === "default" ? "" : ' defaultValue="OpenBitFun"';
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/>`;
}
if (component.name === "KeyHint") {
Expand Down Expand Up @@ -570,12 +595,8 @@ export function ComponentDetailPage({
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/>`;
}
if (component.name === "SearchField") {
const stateProps = previewState === "disabled"
? " disabled"
: previewState === "invalid"
? " invalid"
: "";
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/>`;
const searchStateProps = previewState === "disabled" ? " disabled" : previewState === "invalid" ? " invalid" : previewState === "read-only" ? " readOnly" : "";
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}`;
}
if (component.name === "Combobox") {
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/>`;
Expand Down Expand Up @@ -1327,10 +1348,13 @@ export function ComponentDetailPage({
<Input
aria-label={t("components.preview.inputLabel")}
className={previewClassName}
defaultValue={state === "default" ? undefined : "OpenBitFun"}
key={state}
disabled={state === "disabled"}
invalid={state === "invalid"}
placeholder={t("components.preview.inputPlaceholder")}
trailing={<Icon name="eye" size="lg" aria-hidden="true" />}
readOnly={state === "read-only"}
trailing={<Icon name="eye" />}
/>
);
}
Expand Down Expand Up @@ -1775,22 +1799,7 @@ export function ComponentDetailPage({
}

if (component.name === "SearchField") {
const previewClassName = state === "hover"
? "lab-force-hover"
: state === "focus-visible"
? "lab-force-focus"
: undefined;
return (
<SearchField
aria-label={t("components.preview.searchLabel")}
className={previewClassName}
disabled={state === "disabled"}
invalid={state === "invalid"}
leadingIcon={<Icon name="search" size="lg" aria-hidden="true" />}
placeholder={t("components.preview.searchPlaceholder")}
shortcut={<KeyHint icon={<Icon name="command-mac" size="lg" aria-hidden="true" />}>K</KeyHint>}
/>
);
return <SearchFieldStatePreview key={state} state={state} />;
}

if (component.name === "NavigationPanel") {
Expand Down
9 changes: 2 additions & 7 deletions design-system/apps/design-lab/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1096,11 +1096,6 @@ body,
outline-offset: var(--openbitfun-focus-offset);
}

input.lab-force-focus {
border-color: var(--openbitfun-color-field-border-focus) !important;
box-shadow: 0 0 0 var(--openbitfun-focus-width) var(--openbitfun-color-focus-ring) !important;
}

[data-openbitfun-component="input"].lab-force-hover,
[data-openbitfun-component="search-field"].lab-force-hover [data-openbitfun-component="input"] {
border-color: var(--openbitfun-color-field-border-hover);
Expand All @@ -1109,8 +1104,8 @@ input.lab-force-focus {

[data-openbitfun-component="input"].lab-force-focus,
[data-openbitfun-component="search-field"].lab-force-focus [data-openbitfun-component="input"] {
border-color: var(--openbitfun-color-field-border-focus);
box-shadow: 0 0 0 var(--openbitfun-focus-width) var(--openbitfun-color-focus-ring);
border-color: var(--openbitfun-color-field-border-active);
box-shadow: none;
}

.component-code-panel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,14 +419,24 @@ test("ConfirmDialog preview exposes semantic, destructive, preview, and pending
test("Input, KeyHint, and SearchField previews expose composable slot and state contracts", async () => {
const source = await readFile(detailSource, "utf8");

assert.match(source, /case "Input":\s*case "SearchField":\s*return \["default", "hover", "focus-visible", "invalid", "disabled"\] as const/);
assert.match(source, /case "Input":\s*case "SearchField":\s*return \["default", "filled", "hover", "focus-visible", "read-only", "invalid", "disabled"\] as const/);
assert.match(source, /case "Select":\s*return \["default", "hover", "focus-visible", "open", "invalid", "disabled"\] as const/);
assert.match(source, /component\.name === "Input"/);
assert.match(source, /component\.name === "KeyHint"/);
assert.match(source, /component\.name === "SearchField"/);
assert.match(source, /trailing=\{<Icon name="eye" size="lg" aria-hidden="true" \/>\}/);
assert.match(source, /leadingIcon=\{<Icon name="search" size="lg" aria-hidden="true" \/>\}/);
assert.match(source, /shortcut=\{<KeyHint icon=\{<Icon name="command-mac" size="lg" aria-hidden="true" \/>\}>K<\/KeyHint>\}/);
assert.match(source, /trailing=\{<Icon name="eye" \/>\}/);
assert.match(source, /leadingIcon=\{<Icon name="search" \/>\}/);
assert.match(source, /shortcut=\{<KeyHint icon=\{<Icon name="command-mac" \/>\}>K<\/KeyHint>\}/);
assert.match(source, /onClear=\{\(\) => setValue\(""\)\}/);
assert.match(source, /readOnly=\{state === "read-only"\}/);

const styles = await readFile(stylesSource, "utf8");
const fieldFocus = styles.match(/\[data-openbitfun-component="input"\]\.lab-force-focus,[^{]+\{([^}]+)\}/)?.[1];
assert.ok(fieldFocus, "Input and SearchField must share their preview focus treatment");
assert.match(fieldFocus, /border-color: var\(--openbitfun-color-field-border-active\)/);
assert.match(fieldFocus, /box-shadow: none/);
assert.doesNotMatch(fieldFocus, /border-width:|outline:|--openbitfun-focus-width/);
assert.doesNotMatch(styles, /input\.lab-force-focus\s*\{/);
});

test("ScrollArea preview exposes direction and native scrollbar visibility contracts", async () => {
Expand Down
18 changes: 18 additions & 0 deletions design-system/packages/theme-openbitfun/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ The same data is available as `@openbitfun/theme-openbitfun/reference-colors.jso

## Surface and state roles

`color.field.*` owns shared field surfaces and state borders. Light uses neutral
8% borders, with 20% hover and active Input/SearchField borders. The dedicated
`color.field.borderActive` owns the editing border because Input's design differs
from the stronger focus treatment used by other controls. These text
fields use one unchanged border for both pointer and keyboard focus; native
`:focus-visible` must not substitute the stronger generic focus palette or add
a second ring. Dark and high-contrast modes retain their own focus color, and forced
colors use Highlight. `color.field.borderFocus` retains its 3:1 contract for
other controls that consume that stronger focus treatment.
`color.field.placeholder` separates
40% empty hints and decorative adornments from general secondary prose; dark
and high-contrast modes retain their readable muted content colors. The default
light Web UI consumes these published values in root and chrome scopes. Named
presets keep their own palette, and imported packages that only supply
`color.content.muted` retain that field hint color unless explicitly overridden.
Old packages that supply `color.field.borderFocus` retain that editing border
unless they explicitly provide `color.field.borderActive`.

`component.button.*` owns Button's state palette. Its light fill stays at black
8% while the shared neutral actions retain their 5/8/10% feedback; its primary
background uses black 80/60/90% and disabled content 20%. Outline and text variants
Expand Down
2 changes: 2 additions & 0 deletions design-system/packages/theme-openbitfun/src/dark.tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@
}
},
"field": {
"borderActive": { "$type": "color", "$value": "{color.field.borderFocus}" },
"placeholder": { "$type": "color", "$value": "{color.content.muted}" },
"background": { "$type": "color", "$value": "{ref.color.neutral.900}" },
"backgroundHover": { "$type": "color", "$value": "rgba(255, 255, 255, 0.06)" },
"border": { "$type": "color", "$value": "rgba(255, 255, 255, 0.18)" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
}
},
"field": {
"borderActive": { "$type": "color", "$value": "{color.field.borderFocus}" },
"placeholder": { "$type": "color", "$value": "{color.content.muted}" },
"border": { "$type": "color", "$value": "{ref.color.gray.250}" },
"borderHover": { "$type": "color", "$value": "{ref.color.gray.0}" },
"borderFocus": { "$type": "color", "$value": "{ref.color.amber.400}" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
}
},
"field": {
"borderActive": { "$type": "color", "$value": "{color.field.borderFocus}" },
"placeholder": { "$type": "color", "$value": "{color.content.muted}" },
"border": { "$type": "color", "$value": "{ref.color.gray.650}" },
"borderHover": { "$type": "color", "$value": "{ref.color.gray.1000}" },
"borderFocus": { "$type": "color", "$value": "{ref.color.blue.700}" }
Expand Down
6 changes: 4 additions & 2 deletions design-system/packages/theme-openbitfun/src/light.tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,12 @@
}
},
"field": {
"borderActive": { "$description": "Input and SearchField editing border, distinct from the stronger generic focus indicator used by other controls.", "$type": "color", "$value": "{color.field.borderHover}" },
"placeholder": { "$description": "Empty field hints and decorative adornments; distinct from general secondary prose.", "$type": "color", "$value": "rgba(0, 0, 0, 0.40)" },
"background": { "$type": "color", "$value": "{ref.color.neutral.0}" },
"backgroundHover": { "$type": "color", "$value": "{color.field.background}" },
"border": { "$type": "color", "$value": "rgba(16, 26, 39, 0.15)" },
"borderHover": { "$type": "color", "$value": "rgba(16, 26, 39, 0.24)" },
"border": { "$type": "color", "$value": "rgba(0, 0, 0, 0.08)" },
"borderHover": { "$type": "color", "$value": "rgba(0, 0, 0, 0.20)" },
"borderFocus": { "$type": "color", "$value": "{ref.color.neutral.550}" }
},
"control": {
Expand Down
4 changes: 4 additions & 0 deletions design-system/packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ The Web UI's legacy Select implementation is retired. Like retired Button and
Switch overrides, legacy `components.select` Appearance rules are ignored at
the existing read-only migration boundary; original packages are not rewritten.
Selection visuals now come from the public field/menu semantic tokens.
SearchField sizes its decorative wrapper through Input's icon slot, so default
catalog icons and native SVGs occupy the same region. Shortcut hints and clear
actions can coexist; disabled and read-only fields disable the clear action.

Choose `size` explicitly when composing form rows: selectors default to `md`,
while `Input` defaults to `sm`. The shared `control.height.sm/md/lg` tokens and
active density own the actual heights; consumers must not replace them with
Expand Down
7 changes: 5 additions & 2 deletions design-system/packages/ui/src/components/Input/Input.meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,23 @@ export const inputMeta = {
{ name: "leading", type: "ReactNode" },
{ name: "trailing", type: "ReactNode" },
{ defaultValue: "sm", name: "size", type: "sm | md | lg" },
{ defaultValue: "false", name: "readOnly", type: "boolean" },
{ defaultValue: "false", name: "invalid", type: "boolean" },
{ defaultValue: "false", name: "disabled", type: "boolean" },
{ name: "onValueChange", type: "(value: string) => void" },
],
states: ["default", "hover", "focus-visible", "invalid", "disabled"],
states: ["default", "filled", "hover", "focus-visible", "read-only", "invalid", "disabled"],
tokens: [
"color.content.primary",
"color.content.muted",
"color.content.disabled",
"color.field.background",
"color.field.placeholder",
"opacity.iconArtwork",
"color.field.backgroundHover",
"color.field.border",
"color.field.borderHover",
"color.field.borderFocus",
"color.field.borderActive",
"color.accent.default",
"color.status.danger.border",
"control.height.sm",
Expand Down
12 changes: 9 additions & 3 deletions design-system/packages/ui/src/components/Input/Input.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
}

.field:focus-within {
border-color: var(--openbitfun-color-field-border-focus);
border-color: var(--openbitfun-color-field-border-active);
box-shadow: none;
}

Expand All @@ -47,6 +47,10 @@
--_field-icon-size: 16px;
}

.field[data-disabled="true"] :is(.leading, .trailing) {
color: var(--openbitfun-color-content-disabled);
}

.field[data-size="lg"] {
--_field-height: var(--openbitfun-control-height-lg);
--_field-icon-size: 20px;
Expand All @@ -71,7 +75,7 @@
}

.input::placeholder {
color: var(--openbitfun-color-content-muted);
color: var(--openbitfun-color-field-placeholder);
opacity: 1;
}

Expand All @@ -85,16 +89,18 @@

.leading,
.trailing {
--openbitfun-opacity-icon-artwork: 1;
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
color: var(--openbitfun-color-content-muted);
color: var(--openbitfun-color-field-placeholder);
}

.leading > :where(svg, img),
.trailing > :where(svg, img),
.leading > [data-openbitfun-component="icon"],
.leading > [data-openbitfun-part="icon"],
.trailing > [data-openbitfun-component="icon"] {
display: block;
inline-size: var(--_field-icon-size);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ export const searchFieldMeta = {
{ name: "footer", type: "ReactNode (panel only)" },
{ name: "clearLabel", type: "string" },
{ defaultValue: "sm", name: "size", type: "sm | md | lg" },
{ defaultValue: "false", name: "readOnly", type: "boolean" },
{ defaultValue: "false", name: "invalid", type: "boolean" },
{ defaultValue: "false", name: "disabled", type: "boolean" },
{ name: "onValueChange", type: "(value: string) => void" },
{ name: "onClear", type: "MouseEventHandler<HTMLButtonElement>" },
{ name: "onSearch", type: "(value: string) => void" },
],
states: ["default", "hover", "focus-visible", "invalid", "disabled"],
states: ["default", "filled", "hover", "focus-visible", "read-only", "invalid", "disabled"],
tokens: [
"color.content.primary",
"color.content.muted",
Expand All @@ -31,9 +32,11 @@ export const searchFieldMeta = {
"color.border.subtle",
"color.border.default",
"color.field.background",
"color.field.placeholder",
"color.field.backgroundHover",
"color.field.border",
"color.field.borderHover",
"color.field.borderActive",
"color.field.borderFocus",
"color.status.danger.border",
"control.height.sm",
Expand Down
Loading
Loading