diff --git a/design-system/apps/design-lab/src/preview/FlowChatPreviewRegistry.tsx b/design-system/apps/design-lab/src/preview/FlowChatPreviewRegistry.tsx
index 216b3f8367..8aeca1a206 100644
--- a/design-system/apps/design-lab/src/preview/FlowChatPreviewRegistry.tsx
+++ b/design-system/apps/design-lab/src/preview/FlowChatPreviewRegistry.tsx
@@ -133,7 +133,7 @@ type FlowChatPreviewDefinitionMap = {
type PreviewProps = FlowChatPreviewRenderOptions;
function ChatComposerPreview({ interactive, state }: PreviewProps) {
- const [value, setValue] = useState("");
+ const [value, setValue] = useState(state === "expanded" ? "Review the attached project notes and explain how the implementation should behave across narrow and wide windows." : "");
const expanded = state === "expanded";
const queued = state === "queued";
@@ -141,7 +141,7 @@ function ChatComposerPreview({ interactive, state }: PreviewProps) {
This computer
OpenBitFun
diff --git a/design-system/packages/design-tokens/src/system.tokens.json b/design-system/packages/design-tokens/src/system.tokens.json
index 9857d65301..015906bf08 100644
--- a/design-system/packages/design-tokens/src/system.tokens.json
+++ b/design-system/packages/design-tokens/src/system.tokens.json
@@ -485,12 +485,12 @@
"chatComposer": {
"$type": "dimension",
"actionIconSize": { "$value": "14px" },
- "compactGap": { "$value": "9px" },
- "compactHeight": { "$value": "45px" },
- "compactPaddingBlock": { "$value": "9px" },
- "compactPaddingInline": { "$value": "9px" },
- "compactTrackHeight": { "$value": "25px" },
- "controlHeight": { "$value": "25px" }
+ "compactGap": { "$value": "{control.composer.surfaceGap}" },
+ "compactHeight": { "$value": "42px" },
+ "compactPaddingBlock": { "$value": "{control.composer.surfacePadding}" },
+ "compactPaddingInline": { "$value": "{control.composer.surfacePadding}" },
+ "compactTrackHeight": { "$value": "24px" },
+ "controlHeight": { "$value": "24px" }
},
"flowChat": {
"$description": "Shared FlowChat rhythm for product and package-owned conversation surfaces.",
@@ -585,6 +585,7 @@
"iconSizeMd": { "$value": "14px" }
},
"composer": {
+ "contextBlur": { "$type": "dimension", "$value": "10px" },
"$type": "dimension",
"minBlockSize": { "$value": "120px" },
"contextOffset": { "$value": "32px" },
diff --git a/design-system/packages/design-tokens/tests/contract.test.mjs b/design-system/packages/design-tokens/tests/contract.test.mjs
index 045a34bc9e..3b8bf2165c 100644
--- a/design-system/packages/design-tokens/tests/contract.test.mjs
+++ b/design-system/packages/design-tokens/tests/contract.test.mjs
@@ -175,12 +175,12 @@ test("AskUser geometry preserves the answered question reference contract", () =
});
test("ChatComposer geometry preserves the scaled compact capsule contract", () => {
- assert.equal(tokens["control.chatComposer.compactGap"], "9px");
- assert.equal(tokens["control.chatComposer.compactHeight"], "45px");
- assert.equal(tokens["control.chatComposer.compactPaddingBlock"], "9px");
- assert.equal(tokens["control.chatComposer.compactPaddingInline"], "9px");
- assert.equal(tokens["control.chatComposer.compactTrackHeight"], "25px");
- assert.equal(tokens["control.chatComposer.controlHeight"], "25px");
+ assert.equal(tokens["control.chatComposer.compactGap"], "12px");
+ assert.equal(tokens["control.chatComposer.compactHeight"], "42px");
+ assert.equal(tokens["control.chatComposer.compactPaddingBlock"], "8px");
+ assert.equal(tokens["control.chatComposer.compactPaddingInline"], "8px");
+ assert.equal(tokens["control.chatComposer.compactTrackHeight"], "24px");
+ assert.equal(tokens["control.chatComposer.controlHeight"], "24px");
});
test("FlowChat rhythm keeps compact rows line-like and Turn boundaries distinct", async () => {
diff --git a/design-system/packages/theme-openbitfun/README.md b/design-system/packages/theme-openbitfun/README.md
index 1c6173179d..b8ff315275 100644
--- a/design-system/packages/theme-openbitfun/README.md
+++ b/design-system/packages/theme-openbitfun/README.md
@@ -81,3 +81,5 @@ menu labels to secondary text (60%). Explicit imported action colors still win.
Action cards own `color.actionCard.background`: the light entry surface is black at 3% opacity. `surface.subtle` is a transient navy tint and `field.groupBackground` belongs to form groups, so neither represents this persistent action surface. Other modes retain their neutral action surface. Product Appearance preserves explicit legacy neutral-surface overrides in root and chrome.
Compact indicators own `color.numberBadge.background` and `color.keyHint.content` so reference light values (8% fill and 60% text) can coexist with existing dark/contrast values and explicit legacy Appearance overrides. The light Switch off track and resting scrollbar thumb use 10% black; KeyHint and Launcher resting fills use 8%. Long status labels keep content colors; StatusPill emphasis is opt-in.
+
+`color.composer.border` and `color.composer.contextBackground` are the shared editor-surface contract for Composer and ChatComposer. Reference light uses an 8% border and 3% context tint; persistent entry cards and form groups keep their separate owners. Legacy explicit field-border and subtle-surface overrides populate the new keys only when absent. Composer shadow uses a 12px CSS box-shadow blur (the Figma effect radius; its generated filter drop-shadow uses a 6px standard deviation).
diff --git a/design-system/packages/theme-openbitfun/src/dark.tokens.json b/design-system/packages/theme-openbitfun/src/dark.tokens.json
index 70f0bae823..c8103c1edb 100644
--- a/design-system/packages/theme-openbitfun/src/dark.tokens.json
+++ b/design-system/packages/theme-openbitfun/src/dark.tokens.json
@@ -19,6 +19,10 @@
}
},
"color": {
+ "composer": {
+ "border": { "$type": "color", "$value": "{color.field.border}" },
+ "contextBackground": { "$type": "color", "$value": "{color.surface.subtle}" }
+ },
"numberBadge": {
"background": { "$type": "color", "$value": "{color.action.neutral.surface}" }
},
diff --git a/design-system/packages/theme-openbitfun/src/high-contrast-dark.tokens.json b/design-system/packages/theme-openbitfun/src/high-contrast-dark.tokens.json
index 76117f9738..cf866f9a75 100644
--- a/design-system/packages/theme-openbitfun/src/high-contrast-dark.tokens.json
+++ b/design-system/packages/theme-openbitfun/src/high-contrast-dark.tokens.json
@@ -19,6 +19,10 @@
}
},
"color": {
+ "composer": {
+ "border": { "$type": "color", "$value": "{color.field.border}" },
+ "contextBackground": { "$type": "color", "$value": "{color.surface.subtle}" }
+ },
"keyHint": {
"content": { "$type": "color", "$value": "{color.content.muted}" }
},
diff --git a/design-system/packages/theme-openbitfun/src/high-contrast-light.tokens.json b/design-system/packages/theme-openbitfun/src/high-contrast-light.tokens.json
index d5ee484751..176263afd6 100644
--- a/design-system/packages/theme-openbitfun/src/high-contrast-light.tokens.json
+++ b/design-system/packages/theme-openbitfun/src/high-contrast-light.tokens.json
@@ -19,6 +19,10 @@
}
},
"color": {
+ "composer": {
+ "border": { "$type": "color", "$value": "{color.field.border}" },
+ "contextBackground": { "$type": "color", "$value": "{color.surface.subtle}" }
+ },
"keyHint": {
"content": { "$type": "color", "$value": "{color.content.muted}" }
},
diff --git a/design-system/packages/theme-openbitfun/src/light.tokens.json b/design-system/packages/theme-openbitfun/src/light.tokens.json
index cb6047f6d0..715e283296 100644
--- a/design-system/packages/theme-openbitfun/src/light.tokens.json
+++ b/design-system/packages/theme-openbitfun/src/light.tokens.json
@@ -19,6 +19,10 @@
}
},
"color": {
+ "composer": {
+ "border": { "$type": "color", "$value": "{color.field.border}" },
+ "contextBackground": { "$type": "color", "$value": "rgba(0, 0, 0, 0.03)" }
+ },
"numberBadge": {
"background": { "$type": "color", "$value": "{color.action.neutral.surfaceHover}" }
},
@@ -261,7 +265,7 @@
"lg": { "$value": "0 8px 16px rgba(16, 26, 39, 0.09)" },
"xl": { "$value": "0 12px 24px rgba(16, 26, 39, 0.11)" },
"raised": { "$value": "{shadow.sm}" },
- "composer": { "$value": "0 2px 6px rgba(0, 0, 0, 0.08)" },
+ "composer": { "$value": "0 2px 12px rgba(0, 0, 0, 0.08)" },
"menu": { "$value": "0 4px 20px rgba(0, 0, 0, 0.12)" },
"overlay": { "$value": "0 4px 20px rgba(0, 0, 0, 0.12)" },
"accentGlow": {
diff --git a/design-system/packages/theme-openbitfun/tests/theme-contract.test.mjs b/design-system/packages/theme-openbitfun/tests/theme-contract.test.mjs
index ee0647988e..0fb2c7cd23 100644
--- a/design-system/packages/theme-openbitfun/tests/theme-contract.test.mjs
+++ b/design-system/packages/theme-openbitfun/tests/theme-contract.test.mjs
@@ -328,7 +328,7 @@ test("default modes preserve the built-in Appearance anchor values", () => {
assert.equal(themes.light["color.identity.globalSearch.openFiles"], "#9e54ff");
assert.equal(themes.light["color.status.warning.surface"], "rgba(255, 140, 0, 0.1)");
assert.equal(themes.light["shadow.base"], "0 4px 8px rgba(16, 26, 39, 0.07)");
- assert.equal(themes.light["shadow.composer"], "0 2px 6px rgba(0, 0, 0, 0.08)");
+ assert.equal(themes.light["shadow.composer"], "0 2px 12px rgba(0, 0, 0, 0.08)");
assert.equal(themes.light["shadow.menu"], "0 4px 20px rgba(0, 0, 0, 0.12)");
assert.equal(themes.light["shadow.overlay"], "0 4px 20px rgba(0, 0, 0, 0.12)");
assert.equal(themes.light["opacity.disabled"], 0.55);
diff --git a/design-system/packages/ui/README.md b/design-system/packages/ui/README.md
index 5b3b420a09..943e5d687c 100644
--- a/design-system/packages/ui/README.md
+++ b/design-system/packages/ui/README.md
@@ -423,3 +423,5 @@ Extra-large (`xl`) dialogs have an 800px maximum width and continue shrinking wi
PageHeader `md` uses the settings title with a primary 15px description; `display` uses the welcome heading and medium 17px introduction with a 12px gap. ActionCard uses 12px padding, section-heading typography (15px semibold), and a primary 13px single-line action description. Its inset outline does not inflate the 62px medium minimum height; longer content keeps the independent sibling actions and OverflowText behavior.
KeyHint uses 10px text on a 10px line and 2px block padding. StatusPill uses a 14px line with 2px block padding; opt into `emphasis` for short mode labels such as Ask, while ordinary status descriptions retain readable content colors. LauncherButton owns a 72px minimum width, 40px height, 10px side padding, 4px gap, 12px icon and 11px monospace text. Product shells may retain a deliberate compact greeting that expands into this geometry.
+
+Composer and ChatComposer share the 16px surface radius, 8px padding, 12px content gap, composer border, context tint, and composer shadow. Compact ChatComposer uses a 24px action track plus 8px padding on each side and two 1px borders (42px border-box height); expanded height follows editor content. The generic context shell has no duplicate outer border or surface inset. Queue and editor state remain owned by their existing slots and product adapters.
diff --git a/design-system/packages/ui/src/components/Composer/Composer.meta.ts b/design-system/packages/ui/src/components/Composer/Composer.meta.ts
index 1e9e23cf72..98ce4534d8 100644
--- a/design-system/packages/ui/src/components/Composer/Composer.meta.ts
+++ b/design-system/packages/ui/src/components/Composer/Composer.meta.ts
@@ -15,12 +15,14 @@ export const composerMeta = {
states: ["default", "focus-within", "with-context", "invalid", "disabled"],
tokens: [
"color.surface.raised",
- "color.surface.subtle",
+ "color.composer.contextBackground",
+ "control.composer.contextBlur",
+ "color.surface.panel",
"color.border.subtle",
"color.content.primary",
"color.content.muted",
"color.content.disabled",
- "color.field.border",
+ "color.composer.border",
"color.field.borderFocus",
"color.focus.ring",
"color.status.danger.border",
diff --git a/design-system/packages/ui/src/components/Composer/Composer.module.css b/design-system/packages/ui/src/components/Composer/Composer.module.css
index f2d8925903..0c732b8ff4 100644
--- a/design-system/packages/ui/src/components/Composer/Composer.module.css
+++ b/design-system/packages/ui/src/components/Composer/Composer.module.css
@@ -26,9 +26,10 @@
.root[data-has-context="true"] {
padding-block-start: var(--openbitfun-control-composer-context-offset);
- border: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle);
border-radius: var(--openbitfun-control-composer-surface-radius);
- background: var(--openbitfun-color-surface-subtle);
+ background: var(--openbitfun-color-composer-context-background);
+ backdrop-filter: blur(var(--openbitfun-control-composer-context-blur));
+ -webkit-backdrop-filter: blur(var(--openbitfun-control-composer-context-blur));
}
.context {
@@ -53,7 +54,7 @@
flex-direction: column;
gap: var(--openbitfun-control-composer-surface-gap);
padding: var(--openbitfun-control-composer-surface-padding);
- border: var(--openbitfun-border-width-default) solid var(--openbitfun-color-field-border);
+ border: var(--openbitfun-border-width-default) solid var(--openbitfun-color-composer-border);
border-radius: var(--openbitfun-control-composer-surface-radius);
background: var(--openbitfun-color-surface-raised);
box-shadow: var(--openbitfun-shadow-composer);
@@ -62,12 +63,6 @@
box-shadow var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard);
}
- .root[data-has-context="true"] > .surface {
- inline-size: calc(100% - var(--openbitfun-border-width-default) - var(--openbitfun-border-width-default));
- margin-inline: var(--openbitfun-border-width-default);
- margin-block-end: var(--openbitfun-border-width-default);
- }
-
.surface:focus-within {
border-color: var(--openbitfun-color-field-border-focus);
box-shadow:
@@ -175,7 +170,20 @@
}
}
+ @media (prefers-reduced-transparency: reduce) {
+ .root[data-has-context="true"] {
+ background: var(--openbitfun-color-surface-panel);
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ }
+ }
+
@media (forced-colors: active) {
+ .root[data-has-context="true"] {
+ background: Canvas;
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ }
.root[data-has-context="true"],
.surface,
.root[data-invalid="true"] > .surface {
diff --git a/design-system/packages/ui/src/flow-chat/composer/ChatComposer.meta.ts b/design-system/packages/ui/src/flow-chat/composer/ChatComposer.meta.ts
index 77dc654828..3dc973e0d3 100644
--- a/design-system/packages/ui/src/flow-chat/composer/ChatComposer.meta.ts
+++ b/design-system/packages/ui/src/flow-chat/composer/ChatComposer.meta.ts
@@ -18,7 +18,12 @@ export const chatComposerMeta = {
states: ["default", "expanded", "queued", "focus-visible", "busy", "disabled"],
tokens: [
"border.width.default",
- "color.action.neutral.border",
+ "color.composer.border",
+ "color.composer.contextBackground",
+ "control.composer.contextBlur",
+ "control.composer.surfaceRadius",
+ "control.composer.surfacePadding",
+ "control.composer.surfaceGap",
"color.action.neutral.content",
"color.action.neutral.contentDisabled",
"color.action.neutral.surface",
@@ -51,7 +56,7 @@ export const chatComposerMeta = {
"radius.lg",
"radius.2xl",
"radius.pill",
- "shadow.base",
+ "shadow.composer",
"space.1",
"space.2",
"space.3",
diff --git a/design-system/packages/ui/src/flow-chat/composer/ChatComposer.module.css b/design-system/packages/ui/src/flow-chat/composer/ChatComposer.module.css
index d0364dd2e4..103fc794cc 100644
--- a/design-system/packages/ui/src/flow-chat/composer/ChatComposer.module.css
+++ b/design-system/packages/ui/src/flow-chat/composer/ChatComposer.module.css
@@ -4,20 +4,16 @@
min-inline-size: 0;
inline-size: 100%;
flex-direction: column;
- border-radius: var(--openbitfun-radius-2xl);
+ border-radius: var(--openbitfun-control-composer-surface-radius);
color: var(--openbitfun-color-content-primary);
background: transparent;
font-family:var(--openbitfun-type-body-md-font-family);
}
.root[data-has-context="true"] {
- /* Keep the band opaque while moving it toward the foreground: lighter in
- dark themes and darker in light themes. */
- background: color-mix(
- in srgb,
- var(--openbitfun-color-surface-panel) 94%,
- var(--openbitfun-color-content-primary) 6%
- );
+ background: var(--openbitfun-color-composer-context-background);
+ backdrop-filter: blur(var(--openbitfun-control-composer-context-blur));
+ -webkit-backdrop-filter: blur(var(--openbitfun-control-composer-context-blur));
}
.context {
@@ -50,13 +46,13 @@
min-block-size: var(--openbitfun-control-height-md);
box-sizing: border-box;
align-items: center;
- gap: var(--openbitfun-space-3);
- padding: var(--openbitfun-space-2);
- border: var(--openbitfun-border-width-default) solid var(--openbitfun-color-action-neutral-border);
- border-radius: var(--openbitfun-radius-2xl);
+ gap: var(--openbitfun-control-composer-surface-gap);
+ padding: var(--openbitfun-control-composer-surface-padding);
+ border: var(--openbitfun-border-width-default) solid var(--openbitfun-color-composer-border);
+ border-radius: var(--openbitfun-control-composer-surface-radius);
color: var(--openbitfun-color-content-primary);
- background: var(--openbitfun-color-surface-panel);
- box-shadow: var(--openbitfun-shadow-base);
+ background: var(--openbitfun-color-surface-raised);
+ box-shadow: var(--openbitfun-shadow-composer);
transition:
border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard),
box-shadow var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard);
@@ -73,7 +69,7 @@
align-items: center;
padding-block: var(--openbitfun-control-chat-composer-compact-padding-block);
padding-inline: var(--openbitfun-control-chat-composer-compact-padding-inline);
- border-radius: var(--openbitfun-radius-pill);
+ border-radius: var(--openbitfun-control-composer-surface-radius);
}
.surface[data-openbitfun-layout="expanded"] {
@@ -173,7 +169,20 @@
}
}
+ @media (prefers-reduced-transparency: reduce) {
+ .root[data-has-context="true"] {
+ background: var(--openbitfun-color-surface-panel);
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ }
+ }
+
@media (forced-colors: active) {
+ .root[data-has-context="true"] {
+ background: Canvas;
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ }
.surface,
.surface:focus-within {
border-color: CanvasText;
diff --git a/design-system/packages/ui/tests/chat-composer.test.mjs b/design-system/packages/ui/tests/chat-composer.test.mjs
index 15a699f0df..f9954d7afe 100644
--- a/design-system/packages/ui/tests/chat-composer.test.mjs
+++ b/design-system/packages/ui/tests/chat-composer.test.mjs
@@ -248,7 +248,7 @@ test("ChatComposer geometry is driven by public system and semantic tokens", asy
assert.match(styles, /--openbitfun-control-chat-composer-action-icon-size/);
assert.match(styles, /--openbitfun-control-chat-composer-control-height/);
assert.match(styles, /--openbitfun-space-8/);
- assert.match(styles, /--openbitfun-radius-2xl/);
+ assert.match(styles, /--openbitfun-control-composer-surface-radius/);
assert.match(styles, /--openbitfun-color-surface-panel/);
assert.match(styles, /--openbitfun-color-surface-subtle/);
assert.match(styles, /--openbitfun-color-surface-raised/);
@@ -256,16 +256,12 @@ test("ChatComposer geometry is driven by public system and semantic tokens", asy
/\.\w+\[data-has-context=(?:"true"|true)\][^{]*\{[^}]*\}/,
);
assert.ok(contextBackgroundRule);
- assert.match(
- contextBackgroundRule[0],
- /background:\s*color-mix\(in srgb,\s*var\(--openbitfun-color-surface-panel\)\s*94%,\s*var\(--openbitfun-color-content-primary\)\s*6%\)/,
- );
- assert.doesNotMatch(contextBackgroundRule[0], /\btransparent\b/);
- assert.match(styles, /--openbitfun-color-action-neutral-border/);
- assert.match(styles, /--openbitfun-shadow-base/);
+ assert.match(contextBackgroundRule[0], /background:\s*var\(--openbitfun-color-composer-context-background\)/);
+ assert.match(styles, /--openbitfun-color-composer-border/);
+ assert.match(styles, /--openbitfun-shadow-composer/);
assert.match(
styles,
- /border:\s*var\(--openbitfun-border-width-default\)\s+solid\s+var\(--openbitfun-color-action-neutral-border\)/,
+ /border:\s*var\(--openbitfun-border-width-default\)\s+solid\s+var\(--openbitfun-color-composer-border\)/,
);
assert.match(styles, /min-block-size:\s*var\(--openbitfun-control-height-md\)/);
assert.match(styles, /grid-template-areas:\s*"start content end"/);
@@ -276,7 +272,7 @@ test("ChatComposer geometry is driven by public system and semantic tokens", asy
assert.match(styles, /"content content"\s*"start end"/);
assert.match(
styles,
- /\[data-openbitfun-layout=(?:"compact"|compact)\][^{]*\{[^}]*block-size:\s*var\(--openbitfun-control-chat-composer-compact-height\)[^}]*border-radius:\s*var\(--openbitfun-radius-pill\)/,
+ /\[data-openbitfun-layout=(?:"compact"|compact)\][^{]*\{[^}]*block-size:\s*var\(--openbitfun-control-chat-composer-compact-height\)[^}]*border-radius:\s*var\(--openbitfun-control-composer-surface-radius\)/,
);
assert.match(
styles,
diff --git a/src/apps/data-migrator/ui/generated/design-system.css b/src/apps/data-migrator/ui/generated/design-system.css
index baafeb7b33..f3815e8ba6 100644
--- a/src/apps/data-migrator/ui/generated/design-system.css
+++ b/src/apps/data-migrator/ui/generated/design-system.css
@@ -51,12 +51,13 @@
--openbitfun-control-change-count-padding-inline: var(--openbitfun-space-1);
--openbitfun-control-change-count-radius: var(--openbitfun-radius-xs);
--openbitfun-control-chat-composer-action-icon-size: 14px;
- --openbitfun-control-chat-composer-compact-gap: 9px;
- --openbitfun-control-chat-composer-compact-height: 45px;
- --openbitfun-control-chat-composer-compact-padding-block: 9px;
- --openbitfun-control-chat-composer-compact-padding-inline: 9px;
- --openbitfun-control-chat-composer-compact-track-height: 25px;
- --openbitfun-control-chat-composer-control-height: 25px;
+ --openbitfun-control-chat-composer-compact-gap: var(--openbitfun-control-composer-surface-gap);
+ --openbitfun-control-chat-composer-compact-height: 42px;
+ --openbitfun-control-chat-composer-compact-padding-block: var(--openbitfun-control-composer-surface-padding);
+ --openbitfun-control-chat-composer-compact-padding-inline: var(--openbitfun-control-composer-surface-padding);
+ --openbitfun-control-chat-composer-compact-track-height: 24px;
+ --openbitfun-control-chat-composer-control-height: 24px;
+ --openbitfun-control-composer-context-blur: 10px;
--openbitfun-control-composer-context-gap: var(--openbitfun-space-2);
--openbitfun-control-composer-context-offset: 32px;
--openbitfun-control-composer-context-padding-block: 6px;
@@ -725,6 +726,8 @@
--openbitfun-color-border-subtle: rgba(16, 26, 39, 0.08);
--openbitfun-color-code-change-added: #1aa73e;
--openbitfun-color-code-change-removed: #ec221f;
+ --openbitfun-color-composer-border: rgba(0, 0, 0, 0.08);
+ --openbitfun-color-composer-context-background: rgba(0, 0, 0, 0.03);
--openbitfun-color-content-caption: rgba(0, 0, 0, 0.40);
--openbitfun-color-content-disabled: rgba(0, 0, 0, 0.30);
--openbitfun-color-content-inverse: #ffffff;
@@ -825,7 +828,7 @@
--openbitfun-opacity-muted: 0.75;
--openbitfun-shadow-accent-glow: 0 12px 32px color-mix(in srgb, #059cb0 25%, transparent), 0 6px 16px color-mix(in srgb, #059cb0 18%, transparent), 0 3px 8px rgba(0, 0, 0, 0.12);
--openbitfun-shadow-base: 0 4px 8px rgba(16, 26, 39, 0.07);
- --openbitfun-shadow-composer: 0 2px 6px rgba(0, 0, 0, 0.08);
+ --openbitfun-shadow-composer: 0 2px 12px rgba(0, 0, 0, 0.08);
--openbitfun-shadow-inner-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.08);
--openbitfun-shadow-inner-highlight-hover: inset 0 1px 0 rgba(255, 255, 255, 0.24);
--openbitfun-shadow-lg: 0 8px 16px rgba(16, 26, 39, 0.09);
@@ -877,6 +880,8 @@
--openbitfun-color-border-subtle: rgba(255, 255, 255, 0.12);
--openbitfun-color-code-change-added: #1aa73e;
--openbitfun-color-code-change-removed: #ec221f;
+ --openbitfun-color-composer-border: rgba(255, 255, 255, 0.18);
+ --openbitfun-color-composer-context-background: rgba(255, 255, 255, 0.06);
--openbitfun-color-content-caption: #858585;
--openbitfun-color-content-disabled: #555555;
--openbitfun-color-content-inverse: #0e0e10;
@@ -1007,6 +1012,8 @@
--openbitfun-color-border-default: #3b3b3b;
--openbitfun-color-border-strong: #000000;
--openbitfun-color-border-subtle: #707070;
+ --openbitfun-color-composer-border: #3b3b3b;
+ --openbitfun-color-composer-context-background: rgba(16, 26, 39, 0.03);
--openbitfun-color-content-caption: #343434;
--openbitfun-color-content-muted: #343434;
--openbitfun-color-content-primary: #000000;
@@ -1073,6 +1080,7 @@
--openbitfun-color-border-default: #b5b5b5;
--openbitfun-color-border-strong: #ffffff;
--openbitfun-color-border-subtle: #8a8a8a;
+ --openbitfun-color-composer-border: #b5b5b5;
--openbitfun-color-content-caption: #d0d0d0;
--openbitfun-color-content-muted: #d0d0d0;
--openbitfun-color-content-primary: #ffffff;
diff --git a/src/apps/desktop/src/generated/bootstrap_theme.css b/src/apps/desktop/src/generated/bootstrap_theme.css
index 2fba242395..408b17568d 100644
--- a/src/apps/desktop/src/generated/bootstrap_theme.css
+++ b/src/apps/desktop/src/generated/bootstrap_theme.css
@@ -38,6 +38,8 @@
--openbitfun-color-border-subtle: rgba(255, 255, 255, 0.12);
--openbitfun-color-code-change-added: #1aa73e;
--openbitfun-color-code-change-removed: #ec221f;
+ --openbitfun-color-composer-border: rgba(255, 255, 255, 0.18);
+ --openbitfun-color-composer-context-background: rgba(255, 255, 255, 0.06);
--openbitfun-color-content-caption: #858585;
--openbitfun-color-content-disabled: #555555;
--openbitfun-color-content-inverse: #0e0e10;
diff --git a/src/web-ui/src/flow_chat/components/ChatInput.scss b/src/web-ui/src/flow_chat/components/ChatInput.scss
index d52f794435..4637c49dbd 100644
--- a/src/web-ui/src/flow_chat/components/ChatInput.scss
+++ b/src/web-ui/src/flow_chat/components/ChatInput.scss
@@ -349,7 +349,7 @@
padding-top: 1px;
}
- // In capsule: vertically centre inside the 25px line height
+ // In capsule: vertically centre inside the 24px line height
&--capsule &__placeholder {
top: 50%;
transform: translateY(-50%);
@@ -495,7 +495,7 @@
}
// Product controls fill the stable public ChatComposer slots. The package
- // owns the 45px compact / 120px expanded surface geometry; Web UI owns concrete menus and
+ // owns the 42px compact / 120px expanded surface geometry; Web UI owns concrete menus and
// lifecycle states.
&__composer-surface {
.openbitfun-chat-input__actions-left,
diff --git a/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStripLayout.test.ts b/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStripLayout.test.ts
index dd7a9aed2c..144ff50a40 100644
--- a/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStripLayout.test.ts
+++ b/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStripLayout.test.ts
@@ -472,7 +472,7 @@ describe('composer context track layout', () => {
);
});
- it('uses the scaled 45px capsule and keeps 25px controls stable across layouts', () => {
+ it('uses the 42px compact surface and keeps 24px controls stable across layouts', () => {
const component = readLocalFile('ChatInput.tsx');
const stylesheet = readChatInputStylesheet();
const compactControls = stylesheet.slice(
diff --git a/src/web-ui/src/infrastructure/appearance/builtins/appearancePresetOutput.test.ts b/src/web-ui/src/infrastructure/appearance/builtins/appearancePresetOutput.test.ts
index 608a89ea50..555c9bc321 100644
--- a/src/web-ui/src/infrastructure/appearance/builtins/appearancePresetOutput.test.ts
+++ b/src/web-ui/src/infrastructure/appearance/builtins/appearancePresetOutput.test.ts
@@ -49,7 +49,7 @@ describe('builtin appearance preset output', () => {
const settings = getBuiltinAppearance('openbitfun-light')?.renderers?.['theme-tokens']?.settings;
for (const tokens of [settings?.tokens, settings?.scopes?.chrome].filter(Boolean)) {
for (const [name, value] of Object.entries(themes.light)) {
- if (name.startsWith('color.field.') || name === 'color.actionCard.background') expect(tokens?.[themeCssVariables[name as ThemeTokenName]]).toBe(value);
+ if (name.startsWith('color.field.') || name === 'color.actionCard.background' || name.startsWith('color.composer.')) expect(tokens?.[themeCssVariables[name as ThemeTokenName]]).toBe(value);
}
}
expect(settings?.tokens['--openbitfun-color-number-badge-background']).toBe(themes.light['color.numberBadge.background']);
diff --git a/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts b/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts
index 48d959986e..be84afff09 100644
--- a/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts
+++ b/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts
@@ -88,7 +88,7 @@ function themeValuesToCssTokens(
// Default light fields use the published neutral states in both root and
// chrome. Branded palettes and imported overrides retain their own colors.
for (const name of Object.keys(themes.light) as ThemeTokenName[]) {
- if (name.startsWith('color.field.')) {
+ if (name.startsWith('color.field.') || name.startsWith('color.composer.')) {
tokens[themeCssVariables[name] as AppearanceThemeTokenName] = String(themes.light[name]);
}
}
@@ -172,6 +172,8 @@ function createThemeTokenValues(palette: AppearancePalette): Record {
'--openbitfun-color-field-border-focus': '#654321',
'--openbitfun-color-content-muted': '#778899',
'--openbitfun-color-surface-tertiary': '#abcdef',
+ '--openbitfun-color-surface-subtle': '#abcdef',
},
scopes: { chrome: { '--openbitfun-color-content-muted': '#556677', '--openbitfun-color-field-border-focus': '#445566', '--openbitfun-color-surface-tertiary': '#aabbcc' } },
},
@@ -58,6 +59,8 @@ describe('composeAppearancePackage', () => {
const settings = resolved.renderers!['theme-tokens']!.settings;
expect(settings.tokens['--openbitfun-color-content-caption']).toBe('#778899');
expect(settings.scopes?.chrome?.['--openbitfun-color-content-caption']).toBe('#556677');
+ expect(settings.tokens['--openbitfun-color-composer-context-background']).toBe('#abcdef');
+ expect(settings.tokens['--openbitfun-color-composer-border']).toBe('#123456');
expect(settings.tokens).toMatchObject({
'--openbitfun-color-field-border': '#123456',
'--openbitfun-color-field-border-focus': '#654321',
@@ -71,6 +74,12 @@ describe('composeAppearancePackage', () => {
expect(composeAppearancePackage(JSON.parse(JSON.stringify(resolved))).renderers?.['theme-tokens']).toEqual(resolved.renderers?.['theme-tokens']);
expect(JSON.stringify(original)).toBe(payload);
+ original.renderers!['theme-tokens']!.settings.tokens['--openbitfun-color-composer-border'] = '#998877';
+ original.renderers!['theme-tokens']!.settings.tokens['--openbitfun-color-composer-context-background'] = '#887766';
+ expect(composeAppearancePackage(original).renderers!['theme-tokens']!.settings.tokens).toMatchObject({
+ '--openbitfun-color-composer-border': '#998877',
+ '--openbitfun-color-composer-context-background': '#887766',
+ });
original.renderers!['theme-tokens']!.settings.tokens['--openbitfun-color-field-placeholder'] = '#112233';
original.renderers!['theme-tokens']!.settings.tokens['--openbitfun-color-field-border-active'] = '#223344';
original.renderers!['theme-tokens']!.settings.scopes!.chrome!['--openbitfun-color-field-placeholder'] = '#334455';
diff --git a/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.ts b/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.ts
index 31f54cc66e..b00e3557ec 100644
--- a/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.ts
+++ b/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.ts
@@ -17,6 +17,7 @@ import { withLegacyFieldTokens } from './fieldThemeCompatibility';
import { withLegacyCaptionToken } from './captionThemeCompatibility';
import { withLegacyActionCardToken } from './actionCardThemeCompatibility';
import { withLegacyIndicatorTokens } from './indicatorThemeCompatibility';
+import { withLegacyComposerTokens } from './composerThemeCompatibility';
const COMPOSITION_LAYERS = Symbol('openbitfun.appearance.composition-layers');
@@ -125,12 +126,12 @@ function mergeRenderers(
...override['theme-tokens'].settings,
tokens: {
...(base?.['theme-tokens']?.settings.tokens ?? {}),
- ...withLegacyIndicatorTokens(withLegacyActionCardToken(withLegacyCaptionToken(withLegacyFieldTokens(withLegacyButtonTokens(override['theme-tokens'].settings.tokens))))),
+ ...withLegacyComposerTokens(withLegacyIndicatorTokens(withLegacyActionCardToken(withLegacyCaptionToken(withLegacyFieldTokens(withLegacyButtonTokens(override['theme-tokens'].settings.tokens)))))),
},
scopes: {
chrome: {
...(base?.['theme-tokens']?.settings.scopes?.chrome ?? {}),
- ...withLegacyIndicatorTokens(withLegacyActionCardToken(withLegacyCaptionToken(withLegacyFieldTokens(withLegacyButtonTokens(override['theme-tokens'].settings.scopes?.chrome))))),
+ ...withLegacyComposerTokens(withLegacyIndicatorTokens(withLegacyActionCardToken(withLegacyCaptionToken(withLegacyFieldTokens(withLegacyButtonTokens(override['theme-tokens'].settings.scopes?.chrome)))))),
},
},
},
diff --git a/src/web-ui/src/infrastructure/appearance/builtins/composerThemeCompatibility.ts b/src/web-ui/src/infrastructure/appearance/builtins/composerThemeCompatibility.ts
new file mode 100644
index 0000000000..6a01c5abd1
--- /dev/null
+++ b/src/web-ui/src/infrastructure/appearance/builtins/composerThemeCompatibility.ts
@@ -0,0 +1,14 @@
+import type { AppearanceThemeTokenName } from '../types';
+
+export function withLegacyComposerTokens(
+ tokens: Partial> | undefined,
+): Partial> {
+ const result = { ...tokens };
+ for (const [next, previous] of [
+ ['--openbitfun-color-composer-border', '--openbitfun-color-field-border'],
+ ['--openbitfun-color-composer-context-background', '--openbitfun-color-surface-subtle'],
+ ] as const) {
+ if (result[next] === undefined && tokens?.[previous] !== undefined) result[next] = tokens[previous];
+ }
+ return result;
+}
diff --git a/src/web-ui/src/tools/generative-widget/appearancePayload.test.ts b/src/web-ui/src/tools/generative-widget/appearancePayload.test.ts
index 45cafd9dd5..500a3b4cc5 100644
--- a/src/web-ui/src/tools/generative-widget/appearancePayload.test.ts
+++ b/src/web-ui/src/tools/generative-widget/appearancePayload.test.ts
@@ -55,6 +55,12 @@ const INDICATOR_THEME_VARIABLE_NAMES = [
'--openbitfun-color-key-hint-content',
'--openbitfun-color-number-badge-background',
] as const;
+// Composer and ChatComposer share editor borders and context tint independently
+// of form fields and transient surfaces; enumerate these additions exactly.
+const COMPOSER_THEME_VARIABLE_NAMES = [
+ '--openbitfun-color-composer-border',
+ '--openbitfun-color-composer-context-background',
+] as const;
const RETIRED_WIDGET_VARIABLE_NAMES = [
'--background-primary',
'--bg-primary',
@@ -100,6 +106,7 @@ describe('generated widget appearance payload contract', () => {
const buttonNames = WIDGET_APPEARANCE_VAR_NAMES.filter(name => name.startsWith('--openbitfun-component-button-'));
const fieldStateNames = WIDGET_APPEARANCE_VAR_NAMES.filter(name => FIELD_STATE_THEME_VARIABLE_NAMES.some(fieldName => fieldName === name));
const indicatorNames = WIDGET_APPEARANCE_VAR_NAMES.filter(name => INDICATOR_THEME_VARIABLE_NAMES.some(indicatorName => indicatorName === name));
+ const composerNames = WIDGET_APPEARANCE_VAR_NAMES.filter(name => COMPOSER_THEME_VARIABLE_NAMES.some(composerName => composerName === name));
const sharedNames = WIDGET_APPEARANCE_VAR_NAMES.filter(name => (
!name.startsWith('--openbitfun-component-button-')
&& !FIELD_STATE_THEME_VARIABLE_NAMES.some(fieldName => fieldName === name)
@@ -107,10 +114,12 @@ describe('generated widget appearance payload contract', () => {
&& name !== CAPTION_THEME_VARIABLE_NAME
&& name !== ACTION_CARD_THEME_VARIABLE_NAME
&& !INDICATOR_THEME_VARIABLE_NAMES.some(indicatorName => indicatorName === name)
+ && !COMPOSER_THEME_VARIABLE_NAMES.some(composerName => composerName === name)
));
expect(buttonNames).toEqual(BUTTON_THEME_VARIABLE_NAMES);
expect(fieldStateNames).toEqual(FIELD_STATE_THEME_VARIABLE_NAMES);
expect(indicatorNames).toEqual(INDICATOR_THEME_VARIABLE_NAMES);
+ expect(composerNames).toEqual(COMPOSER_THEME_VARIABLE_NAMES);
expect(WIDGET_APPEARANCE_VAR_NAMES).toContain(FIELD_GROUP_THEME_VARIABLE_NAME);
expect(WIDGET_APPEARANCE_VAR_NAMES).toContain(CAPTION_THEME_VARIABLE_NAME);
expect(WIDGET_APPEARANCE_VAR_NAMES).toContain(ACTION_CARD_THEME_VARIABLE_NAME);
@@ -171,6 +180,8 @@ describe('generated widget appearance payload contract', () => {
'--openbitfun-color-action-primary-hover': 'linear-gradient(test-hover)',
'--openbitfun-color-action-primary-pressed': '#202020',
'--openbitfun-color-action-card-background': 'rgba(0, 0, 0, 0.07)',
+ '--openbitfun-color-composer-border': 'rgba(0, 0, 0, 0.14)',
+ '--openbitfun-color-composer-context-background': 'rgba(0, 0, 0, 0.06)',
'--openbitfun-color-content-caption': 'rgba(0, 0, 0, 0.45)',
'--openbitfun-component-button-primary-background': '#303030',
'--openbitfun-component-button-fill-background': 'rgba(0, 0, 0, 0.08)',