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
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ export function ComponentDetailPage({
const [pageHeaderRequired, setPageHeaderRequired] = useState(false);
const [actionItemShowMetadata, setActionItemShowMetadata] = useState(false);
const [actionCardSize, setActionCardSize] = useState<ActionCardSize>("sm");
const [tabGroupSize, setTabGroupSize] = useState<TabGroupSize>("md");
const [tabGroupSize, setTabGroupSize] = useState<TabGroupSize>("sm");
const [toolbarSize, setToolbarSize] = useState<ToolbarSize>("sm");
const [previewState, setPreviewState] = useState(
component.name === "Card"
Expand Down Expand Up @@ -615,7 +615,7 @@ export function ComponentDetailPage({
}
if (component.name === "SegmentedControl") {
const defaultMode = previewState === "unselected" ? "agent" : "chat";
return `import { Icon, SegmentedControl } from "@openbitfun/ui";\n\n<SegmentedControl\n aria-label="${t("components.preview.segmentedLabel")}"\n defaultValue="${defaultMode}"\n onValueChange={setMode}\n options={[\n { icon: <Icon name="session" />, label: "${t("components.preview.segmentedChat")}", value: "chat" },\n { label: "${t("components.preview.segmentedAgent")}", value: "agent" },\n ]}\n/>`;
return `import { Icon, SegmentedControl } from "@openbitfun/ui";\n\n<SegmentedControl\n size="md"\n aria-label="${t("components.preview.segmentedLabel")}"\n defaultValue="${defaultMode}"\n onValueChange={setMode}\n options={[\n { icon: <Icon name="session" />, label: "${t("components.preview.segmentedChat")}", value: "chat" },\n { label: "${t("components.preview.segmentedAgent")}", value: "agent" },\n ]}\n/>`;
}
if (component.name === "StatusPill") {
return `import { Icon, StatusPill } from "@openbitfun/ui";\n\n<StatusPill leading={<Icon name="unselected" />} tone="${previewState}">\n Ask\n</StatusPill>`;
Expand Down Expand Up @@ -1886,8 +1886,9 @@ export function ComponentDetailPage({
const defaultMode = state === "unselected" ? "agent" : "chat";
return (
<SegmentedControl
size="md"
aria-label={t("components.preview.segmentedLabel")}
data-openbitfun-preview-state={state === "hover" ? "hover" : undefined}
data-openbitfun-preview-state={state === "hover" || state === "active" ? state : undefined}
defaultValue={defaultMode}
disabled={state === "disabled"}
key={state}
Expand Down Expand Up @@ -1915,7 +1916,7 @@ export function ComponentDetailPage({
return (
<TabGroup
aria-label={t("components.preview.tabGroupLabel")}
data-openbitfun-preview-state={state === "hover" ? "hover" : undefined}
data-openbitfun-preview-state={state === "hover" || state === "active" ? state : undefined}
defaultValue={defaultTab}
items={[
{
Expand Down
6 changes: 3 additions & 3 deletions design-system/apps/design-lab/src/pages/PatternsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export function PatternsPage({ colorScheme, contrast, density, tokenOverrides }:
</FieldRow>
<FieldRow>
<Field controlWidth="fill" description={t("patterns.settings.themeDescription")} label={t("patterns.settings.theme")} labelWidth="md" orientation="horizontal">
<SegmentedControl onValueChange={setAppearance} options={[{ label: t("patterns.settings.system"), value: "system" }, { label: t("settings.light"), value: "light" }, { label: t("settings.dark"), value: "dark" }]} value={appearance} />
<SegmentedControl size="md" onValueChange={setAppearance} options={[{ label: t("patterns.settings.system"), value: "system" }, { label: t("settings.light"), value: "light" }, { label: t("settings.dark"), value: "dark" }]} value={appearance} />
</Field>
</FieldRow>
<FieldRow>
Expand All @@ -107,7 +107,7 @@ export function PatternsPage({ colorScheme, contrast, density, tokenOverrides }:
<FieldGroup appearance="subtle">
<FieldRow>
<Field controlWidth="fill" description={t("patterns.settings.fontSizeDescription")} label={t("patterns.settings.fontSize")} labelWidth="md" orientation="horizontal">
<SegmentedControl onValueChange={setFontSize} options={[{ label: t("settings.compact"), value: "small" }, { label: t("settings.comfortable"), value: "medium" }, { label: t("settings.touch"), value: "large" }]} value={fontSize} />
<SegmentedControl size="md" onValueChange={setFontSize} options={[{ label: t("settings.compact"), value: "small" }, { label: t("settings.comfortable"), value: "medium" }, { label: t("settings.touch"), value: "large" }]} value={fontSize} />
</Field>
</FieldRow>
</FieldGroup>
Expand Down Expand Up @@ -155,7 +155,7 @@ export function PatternsPage({ colorScheme, contrast, density, tokenOverrides }:

<PatternSection description={t("patterns.search.description")} index="03" title={t("patterns.search.title")}>
<Card appearance="raised" className="pattern-command" data-openbitfun-pattern="search-command-surface" gap="md" padding="md" radius="md">
<CardHeader actions={<SegmentedControl onValueChange={setScope} options={[{ label: t("patterns.search.all"), value: "all" }, { label: t("patterns.search.files"), value: "files" }, { label: t("patterns.search.commands"), value: "commands" }]} value={scope} />} description={t("patterns.search.description")} title={t("patterns.search.title")} />
<CardHeader actions={<SegmentedControl size="md" onValueChange={setScope} options={[{ label: t("patterns.search.all"), value: "all" }, { label: t("patterns.search.files"), value: "files" }, { label: t("patterns.search.commands"), value: "commands" }]} value={scope} />} description={t("patterns.search.description")} title={t("patterns.search.title")} />
<SearchField aria-label={t("patterns.search.searchPlaceholder")} clearLabel={t("components.preview.close")} leadingIcon={<Icon name="search" />} onClear={() => setQuery("")} onValueChange={setQuery} placeholder={t("patterns.search.searchPlaceholder")} shortcut={<KeyHint>Ctrl K</KeyHint>} value={query} />
<CardBody>
<div className="pattern-action-grid">
Expand Down
4 changes: 2 additions & 2 deletions design-system/apps/design-lab/src/pages/ReferencePatterns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,14 @@ function ProviderFields() {

export function SceneToolbarPattern() {
const { t } = useI18n();
const [tabs, setTabs] = useState(["README.md", "models.ts", "design-system"]);
const [tabs, setTabs] = useState(["README.md", "models.ts", "design-system/packages/ui/src/components/TabGroup/TabGroup.tsx"]);
const [active, setActive] = useState("README.md");
const [search, setSearch] = useState(false);
const [details, setDetails] = useState(false);
const nextTab = useRef(1);
const close = (value: string) => { const next = tabs.filter(tab => tab !== value); setTabs(next); if (active === value) setActive(next[0] ?? ""); };
return <Card appearance="raised" data-openbitfun-pattern="scene-toolbar" className="pattern-scene-toolbar">
<Toolbar leadingOverflow="scroll" leading={<TabGroup aria-label={t("patterns.toolbar.tabs")} value={active} onValueChange={setActive} items={tabs.map(value => ({ value, label: value, id: `pattern-tab-${value}`, panelId: `pattern-panel-${value}`, icon: <Icon name="files" size="sm" />, endAction: <IconButton aria-label={`${t("components.preview.close")}: ${value}`} icon={<Icon name="xmark" />} variant="quiet" size="xs" onClick={() => close(value)} /> }))} />}
<Toolbar leadingOverflow="scroll" leading={<TabGroup size="sm" aria-label={t("patterns.toolbar.tabs")} value={active} onValueChange={setActive} items={tabs.map(value => ({ value, label: value, id: `pattern-tab-${value}`, panelId: `pattern-panel-${value}`, icon: value === "README.md" ? undefined : <Icon name="files" />, endAction: <IconButton aria-label={`${t("components.preview.close")}: ${value}`} icon={<Icon name="xmark" />} variant="quiet" size="xs" onClick={() => close(value)} /> }))} />}
trailing={<ToolbarGroup>
<ChangeCount additions={12} deletions={3} />
<ToolbarSeparator />
Expand Down
9 changes: 6 additions & 3 deletions design-system/packages/design-tokens/src/system.tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -530,9 +530,11 @@
"$type": "dimension",
"gap": { "$value": "{space.2}" },
"itemGap": { "$value": "6px" },
"itemGapSm": { "$value": "{space.1}" },
"itemHeight": { "$value": "{control.height.md}" },
"itemHeightSm": { "$value": "30px" },
"itemIconSize": { "$value": "16px" },
"itemIconSizeSm": { "$value": "14px" },
"itemPaddingInline": { "$value": "{space.4}" },
"itemPaddingBlockSm": { "$value": "7px" },
"itemPaddingInlineSm": { "$value": "{space.3}" },
Expand All @@ -542,14 +544,15 @@
},
"segmentedControl": {
"$type": "dimension",
"gap": { "$value": "2px" },
"gap": { "$value": "{space.1}" },
"padding": { "$value": "2px" },
"paddingMd": { "$value": "3px" },
"radius": { "$value": "{radius.pill}" },
"segmentHeight": { "$value": "22px" },
"segmentHeightMd": { "$value": "28px" },
"segmentHeight": { "$value": "24px" },
"segmentHeightMd": { "$value": "30px" },
"pillSegmentHeight": { "$value": "24px" },
"segmentPaddingInline": { "$value": "{space.2}" },
"segmentPaddingInlineMd": { "$value": "{space.3}" },
"segmentGap": { "$value": "{space.1}" },
"segmentRadius": { "$value": "{radius.pill}" },
"pillSegmentRadius": { "$value": "{radius.sm}" },
Expand Down
6 changes: 3 additions & 3 deletions design-system/packages/design-tokens/tests/contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ test("TabGroup geometry preserves the capsule selected and outline contract", as
test("SegmentedControl geometry preserves compact, filled, and filter compositions", async () => {
const systemDocument = await readSource("system.tokens.json");

assert.equal(tokens["control.segmentedControl.gap"], "2px");
assert.equal(tokens["control.segmentedControl.gap"], "4px");
assert.equal(tokens["control.segmentedControl.padding"], "2px");
assert.equal(tokens["control.segmentedControl.paddingMd"], "3px");
assert.equal(tokens["control.segmentedControl.segmentHeight"], "22px");
assert.equal(tokens["control.segmentedControl.segmentHeightMd"], "28px");
assert.equal(tokens["control.segmentedControl.segmentHeight"], "24px");
assert.equal(tokens["control.segmentedControl.segmentHeightMd"], "30px");
assert.equal(tokens["control.segmentedControl.pillSegmentHeight"], "24px");
assert.equal(tokens["control.segmentedControl.segmentPaddingInline"], "8px");
assert.equal(tokens["control.segmentedControl.segmentGap"], "4px");
Expand Down
2 changes: 2 additions & 0 deletions design-system/packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,3 +413,5 @@ ActionItem hover and pressed surfaces use the semantic neutral hover fill;
pressed text remains semibold. Menu and navigation captions consume the final
caption color directly, avoiding a second opacity multiplier. The nested-menu
Pattern includes a scrolling toggle for keyboard and submenu verification.

Compact tabs use `size="sm"` (30px, 14px icons, 4px icon gap); standard tabs retain 40px and 16px icons. Tabs share the outline-button surface contract and keep selection separate from pointer press. `SegmentedControl size="md"` uses a borderless 36px bar with 30px segments, 3px inset, 4px gaps and 12px segment padding. The default `sm` bar keeps its 28px outer height; separate pills retain their existing heights. Mobile controls own their touch geometry independently.
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ export const segmentedControlMeta = {
{ defaultValue: '"accent"', name: "tone", type: '"accent" | "neutral"' },
{ defaultValue: '"bar"', name: "variant", type: '"bar" | "pills"' },
],
states: ["selected", "unselected", "hover", "disabled"],
states: ["selected", "unselected", "hover", "active", "disabled"],
tokens: [
"color.action.neutral.content",
"color.action.neutral.contentDisabled",
"color.action.neutral.surfaceHover",
"color.action.neutral.surfacePressed",
"color.border.subtle",
"space.3",
"color.content.primary",
"color.focus.ring",
"color.surface.raised",
Expand All @@ -35,6 +35,7 @@ export const segmentedControlMeta = {
"control.segmentedControl.segmentHeightMd",
"control.segmentedControl.pillSegmentHeight",
"control.segmentedControl.segmentPaddingInline",
"control.segmentedControl.segmentPaddingInlineMd",
"control.segmentedControl.segmentGap",
"control.segmentedControl.segmentRadius",
"control.segmentedControl.pillSegmentRadius",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
align-items: center;
gap: var(--openbitfun-control-segmented-control-gap);
padding: var(--openbitfun-control-segmented-control-padding);
border: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle);
border: 0;
border-radius: var(--openbitfun-control-segmented-control-radius);
background: var(--openbitfun-color-action-neutral-surface);
background: var(--openbitfun-color-action-neutral-surface-hover);
}

.root[data-distribution="fill"] {
Expand Down Expand Up @@ -63,6 +63,7 @@

.root[data-size="md"] .segment {
block-size: var(--openbitfun-control-segmented-control-segment-height-md);
padding-inline: var(--openbitfun-control-segmented-control-segment-padding-inline-md);
}

.root[data-size="md"] .icon {
Expand Down Expand Up @@ -108,7 +109,8 @@
background: var(--openbitfun-color-action-neutral-surface-hover);
}

.segment:active:not(:disabled)[aria-checked="false"] {
.segment:active:not(:disabled)[aria-checked="false"],
.root[data-openbitfun-preview-state="active"] .segment[aria-checked="false"]:not(:disabled) {
background: var(--openbitfun-color-action-neutral-surface-pressed);
}

Expand Down Expand Up @@ -153,7 +155,7 @@

@media (forced-colors: active) {
.root {
border-color: ButtonText;
outline: var(--openbitfun-border-width-default) solid ButtonText;
}

.segment[aria-checked="true"] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ export const tabGroupMeta = {
{ name: "onValueChange", type: "(value: string) => void" },
{ defaultValue: "md", name: "size", type: "sm | md" },
],
states: ["selected", "unselected", "hover", "disabled"],
states: ["selected", "unselected", "hover", "active", "disabled"],
tokens: [
"opacity.iconArtwork",
"color.action.neutral.border",
"component.button.outlineBorder",
"component.button.outlineBorderInteractive",
"component.button.fillBackground",
"color.action.neutral.content",
"color.action.neutral.contentDisabled",
"color.action.neutral.surface",
Expand All @@ -26,9 +28,11 @@ export const tabGroupMeta = {
"color.focus.ring",
"control.tabGroup.gap",
"control.tabGroup.itemGap",
"control.tabGroup.itemGapSm",
"control.tabGroup.itemHeight",
"control.tabGroup.itemHeightSm",
"control.tabGroup.itemIconSize",
"control.tabGroup.itemIconSizeSm",
"control.tabGroup.itemPaddingInline",
"control.tabGroup.itemPaddingBlockSm",
"control.tabGroup.itemPaddingInlineSm",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
--_tab-group-item-height: var(--openbitfun-control-tab-group-item-height);
--_tab-group-item-padding-block: var(--openbitfun-space-component-block);
--_tab-group-item-padding-inline: var(--openbitfun-control-tab-group-item-padding-inline);
--_tab-group-item-gap: var(--openbitfun-control-tab-group-item-gap);
--_tab-group-item-icon-size: var(--openbitfun-control-tab-group-item-icon-size);
--_tab-group-item-action-reserve: calc(
var(--openbitfun-control-tab-group-item-action-size)
+ var(--openbitfun-control-tab-group-item-action-inset)
+ var(--openbitfun-control-tab-group-item-gap)
+ var(--_tab-group-item-gap)
);

display: inline-flex;
Expand All @@ -18,6 +20,8 @@
--_tab-group-item-height: var(--openbitfun-control-tab-group-item-height-sm);
--_tab-group-item-padding-block: var(--openbitfun-control-tab-group-item-padding-block-sm);
--_tab-group-item-padding-inline: var(--openbitfun-control-tab-group-item-padding-inline-sm);
--_tab-group-item-gap: var(--openbitfun-control-tab-group-item-gap-sm);
--_tab-group-item-icon-size: var(--openbitfun-control-tab-group-item-icon-size-sm);
}

.item {
Expand All @@ -30,9 +34,9 @@

.tab {
--_tab-background: transparent;
--_tab-background-hover: var(--openbitfun-color-action-neutral-surface);
--_tab-background-active: var(--openbitfun-color-action-neutral-surface-pressed);
--_tab-border: var(--openbitfun-color-action-neutral-border);
--_tab-background-hover: var(--openbitfun-component-button-fill-background);
--_tab-background-active: var(--openbitfun-component-button-fill-background);
--_tab-border: var(--openbitfun-component-button-outline-border);
--_tab-content: var(--openbitfun-color-content-primary);

display: inline-flex;
Expand Down Expand Up @@ -82,11 +86,13 @@
.item:hover .tab:not(:disabled),
.tabGroup[data-openbitfun-preview-state="hover"] .tab[aria-selected="false"]:not(:disabled) {
background: var(--_tab-background-hover);
border-color: var(--openbitfun-component-button-outline-border-interactive);
}

.tab:active:not(:disabled),
.tabGroup[data-openbitfun-preview-state="active"] .tab[aria-selected="false"]:not(:disabled) {
background: var(--_tab-background-active);
border-color: var(--openbitfun-component-button-outline-border-interactive);
font-weight: var(--openbitfun-type-label-selected-font-weight);
}

Expand All @@ -106,9 +112,9 @@
flex: 0 0 auto;
align-items: center;
justify-content: center;
inline-size: var(--openbitfun-control-tab-group-item-icon-size);
block-size: var(--openbitfun-control-tab-group-item-icon-size);
margin-inline-end: var(--openbitfun-control-tab-group-item-gap);
inline-size: var(--_tab-group-item-icon-size);
block-size: var(--_tab-group-item-icon-size);
margin-inline-end: var(--_tab-group-item-gap);
color: currentColor;
}

Expand Down
5 changes: 5 additions & 0 deletions design-system/packages/ui/tests/registry.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ test("every registered component declares states and owned tokens", () => {
token.startsWith("border.") ||
token.startsWith("color.") ||
(component.name === "Button" && token.startsWith("component.button.")) ||
(component.name === "TabGroup" && [
"component.button.outlineBorder",
"component.button.outlineBorderInteractive",
"component.button.fillBackground",
].includes(token)) ||
token.startsWith("control.") ||
token.startsWith("effect.") ||
token.startsWith("font.") ||
Expand Down
Loading
Loading