diff --git a/design-system/AGENTS.md b/design-system/AGENTS.md index 5d6c846f1e..a65f055b98 100644 --- a/design-system/AGENTS.md +++ b/design-system/AGENTS.md @@ -48,6 +48,14 @@ For shared icon rendering or geometry changes, use the focused package checks: pnpm --dir design-system run build:ui && node --test design-system/packages/ui/tests/icon.test.mjs design-system/packages/ui/tests/icon-slots.test.mjs ``` +For composer geometry and queue anatomy, disclosure and status presentation: + +```bash +pnpm --dir design-system run build:ui && node --test design-system/packages/ui/tests/chat-composer.test.mjs +``` + +These package contracts do not establish native rendering or live remote behavior. + Do not use browser automation or mock screenshots as visual proof. Design Lab is the manual authoring surface; source, build, HTTP, and package checks do not establish final visual fidelity. For SplitView geometry, drag cancellation, and cached resize bounds: diff --git a/design-system/packages/ui/src/flow-chat/composer/ChatComposerQueue.module.css b/design-system/packages/ui/src/flow-chat/composer/ChatComposerQueue.module.css index bf80bc5d95..9126e6cddc 100644 --- a/design-system/packages/ui/src/flow-chat/composer/ChatComposerQueue.module.css +++ b/design-system/packages/ui/src/flow-chat/composer/ChatComposerQueue.module.css @@ -5,24 +5,38 @@ min-inline-size: 0; inline-size: 100%; flex-direction: column; - gap: var(--openbitfun-space-2); - padding-block: var(--openbitfun-space-3); - padding-inline: var(--openbitfun-space-4); - border-radius: var(--openbitfun-radius-lg); + gap: var(--openbitfun-space-1); + padding: var(--openbitfun-space-1); + border: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); + border-radius: var(--openbitfun-control-composer-surface-radius); + container: composer-queue / inline-size; color: var(--openbitfun-color-content-primary); background: var(--openbitfun-color-surface-subtle); font-family:var(--openbitfun-type-body-md-font-family); } + /* A composer with a context bar already tints the tray around its input. + The queue joins that tray instead of stacking a second translucent card. */ + :global([data-openbitfun-component="chat-composer"][data-has-context="true"]) .root { + margin-block-end: calc(-1 * var(--openbitfun-space-2)); + padding-block-end: var(--openbitfun-space-3); + border-block-end: 0; + border-end-start-radius: 0; + border-end-end-radius: 0; + background: transparent; + } + .header { display: flex; min-inline-size: 0; + min-block-size: var(--openbitfun-control-chat-composer-control-height); align-items: center; gap: var(--openbitfun-space-1); + padding-inline: var(--openbitfun-space-2); color: var(--openbitfun-color-content-secondary); font-size: var(--openbitfun-type-body-sm-font-size); font-weight: var(--openbitfun-type-body-md-font-weight); - line-height: var(--openbitfun-type-body-md-line-height); + line-height: var(--openbitfun-type-body-sm-line-height); } .header > :where(svg, img) { @@ -32,6 +46,12 @@ color: var(--openbitfun-color-content-muted); } + .toggle[data-openbitfun-component="button"][data-size] { + min-inline-size: 0; + padding-inline: 0; + color: inherit; + } + .title { display: inline-flex; min-inline-size: 0; @@ -53,18 +73,15 @@ .list { display: flex; min-inline-size: 0; - /* Four standard message rows plus the three gaps between them. */ + /* Four comfortable message rows before the tray scrolls. */ max-block-size: calc( - var(--openbitfun-control-chat-composer-control-height) + - var(--openbitfun-control-chat-composer-control-height) + - var(--openbitfun-control-chat-composer-control-height) + - var(--openbitfun-control-chat-composer-control-height) + - var(--openbitfun-space-1) + - var(--openbitfun-space-1) + - var(--openbitfun-space-1) + var(--openbitfun-control-height-sm) + + var(--openbitfun-control-height-sm) + + var(--openbitfun-control-height-sm) + + var(--openbitfun-control-height-sm) ); flex-direction: column; - gap: var(--openbitfun-space-1); + gap: 0; margin: 0; overflow-x: hidden; overflow-y: auto; @@ -73,86 +90,138 @@ list-style: none; } + .list[hidden] { + display: none; + } + .item { box-sizing: border-box; - display: grid; + display: flex; + flex: 0 0 auto; min-inline-size: 0; - min-block-size: var(--openbitfun-control-chat-composer-control-height); - grid-template-columns: minmax(0, 1fr) auto auto; + min-block-size: var(--openbitfun-control-height-sm); align-items: center; gap: var(--openbitfun-space-2); + padding-inline: var(--openbitfun-space-2) var(--openbitfun-space-1); + border-radius: var(--openbitfun-radius-md); color: var(--openbitfun-color-content-primary); } - .item[data-openbitfun-state="sending"] { - opacity: var(--openbitfun-opacity-disabled); + /* The row, not a floating button cluster, is the unit being acted on. */ + .item:focus-within { + background: var(--openbitfun-color-surface-subtle); + } + + .leadingIcon { + display: inline-flex; + flex: 0 0 var(--openbitfun-space-5); + align-items: center; + justify-content: center; + color: var(--openbitfun-color-content-muted); } .content { display: flex; + flex: 1 1 0; min-inline-size: 0; - flex-direction: column; - gap: var(--openbitfun-space-1); - color: var(--openbitfun-color-content-secondary); + flex-flow: row wrap; + align-items: baseline; + gap: 0 var(--openbitfun-space-2); + color: var(--openbitfun-color-content-primary); font-size: var(--openbitfun-type-body-md-font-size); font-weight: var(--openbitfun-type-body-md-font-weight); - line-height: var(--openbitfun-type-body-md-line-height); - transition: color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); + line-height: var(--openbitfun-type-body-sm-line-height); } - .item:hover .content, - .item:focus-within .content { - color: var(--openbitfun-color-content-primary); + /* Message text takes the line; status facts follow it and wrap below only + when the row is too narrow to hold both. */ + :where(.content) > * { + flex: 1 1 8em; + min-inline-size: 0; + } + + /* A truncated preview may be a tab stop so keyboard users can read its + full-text tooltip; it stays display-only. */ + :where(.content) > :focus-visible { + border-radius: var(--openbitfun-radius-xs); + outline: var(--openbitfun-focus-width) solid var(--openbitfun-color-focus-ring); + outline-offset: var(--openbitfun-focus-offset); + } + + /* Muted ink instead of row opacity keeps the status text legible. */ + .item[data-openbitfun-state="sending"] .content { + color: var(--openbitfun-color-content-muted); + } + + .status { + flex: 0 1 auto; + min-inline-size: 0; + color: var(--openbitfun-color-content-muted); + font-size: var(--openbitfun-type-meta-font-size); + line-height: var(--openbitfun-type-body-sm-line-height); + overflow-wrap: anywhere; + } + + .status[data-openbitfun-tone="warning"] { + color: var(--openbitfun-color-status-warning-content); + } + + .status[data-openbitfun-tone="danger"] { + color: var(--openbitfun-color-status-danger-content); } .attachmentBadge { - box-sizing: border-box; display: inline-flex; - min-inline-size: var(--openbitfun-control-chat-composer-control-height); - block-size: var(--openbitfun-control-chat-composer-control-height); + flex: 0 0 auto; align-items: center; - justify-content: center; - padding-inline: var(--openbitfun-space-2); - border-radius: var(--openbitfun-radius-pill); - color: var(--openbitfun-color-content-secondary); - background: var(--openbitfun-color-surface-raised); - font-size: var(--openbitfun-type-body-sm-font-size); - font-weight: var(--openbitfun-type-label-lg-font-weight); + gap: var(--openbitfun-space-1); + color: var(--openbitfun-color-content-muted); + font-size: var(--openbitfun-type-meta-font-size); font-variant-numeric: tabular-nums; line-height: var(--openbitfun-type-modifier-leading-none-line-height); } .actions { display: inline-flex; + flex: 0 0 auto; min-inline-size: 0; align-items: center; gap: var(--openbitfun-space-1); - transition: opacity var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); + } + + /* The tray stays quiet without making its controls disappear. */ + .actions [data-openbitfun-component="button"][data-openbitfun-variant="text"] { + color: var(--openbitfun-color-content-secondary); + padding-inline: var(--openbitfun-space-2); } @media (hover: hover) and (pointer: fine) { - .actions { - pointer-events: none; - opacity: 0; + .item:hover { + background: var(--openbitfun-color-surface-subtle); } + } - .item:hover .actions, - .item:focus-within .actions { - pointer-events: auto; - opacity: 1; + @container composer-queue (max-width: 22rem) { + .actions [data-openbitfun-component="button"] [data-openbitfun-part="label"] { + display: none; } } - @media (prefers-reduced-motion: reduce) { - .content, - .actions { - transition: none; + @media (pointer: coarse) { + .actions [data-openbitfun-component="button"] { + min-block-size: var(--openbitfun-control-height-sm); + } + + .actions [data-openbitfun-component="icon-button"] { + inline-size: var(--openbitfun-control-height-sm); + block-size: var(--openbitfun-control-height-sm); + min-inline-size: var(--openbitfun-control-height-sm); + min-block-size: var(--openbitfun-control-height-sm); } } @media (forced-colors: active) { - .root, - .attachmentBadge { + .root { border: var(--openbitfun-border-width-default) solid CanvasText; } } diff --git a/design-system/packages/ui/src/flow-chat/composer/ChatComposerQueue.tsx b/design-system/packages/ui/src/flow-chat/composer/ChatComposerQueue.tsx index 798b3022d7..1652f06ff3 100644 --- a/design-system/packages/ui/src/flow-chat/composer/ChatComposerQueue.tsx +++ b/design-system/packages/ui/src/flow-chat/composer/ChatComposerQueue.tsx @@ -1,3 +1,5 @@ +import { Button, type ButtonProps } from "../../components/Button"; +import { Icon } from "../../components/Icon"; import { OverflowText } from '../../primitives/OverflowText'; import { forwardRef, @@ -24,20 +26,35 @@ export interface ChatComposerQueueTitleProps count?: ReactNode; } +export interface ChatComposerQueueToggleProps + extends Omit { + expanded: boolean; +} + export interface ChatComposerQueueListProps extends HTMLAttributes {} export interface ChatComposerQueueItemProps extends LiHTMLAttributes { state?: ChatComposerQueueItemState; + leadingIcon?: ReactNode; } export interface ChatComposerQueueItemContentProps extends HTMLAttributes {} +export type ChatComposerQueueItemStatusTone = "neutral" | "warning" | "danger"; + +export interface ChatComposerQueueItemStatusProps + extends HTMLAttributes { + tone?: ChatComposerQueueItemStatusTone; +} + export interface ChatComposerQueueAttachmentBadgeProps extends Omit, "children"> { count: number; + /** Supply a real icon from the consumer's icon library. */ + icon?: ReactNode; label: string; } @@ -102,6 +119,25 @@ export const ChatComposerQueueTitle = forwardRef< ); }); +/** Disclosure for the message list; wrap a ChatComposerQueueTitle as its label. */ +export const ChatComposerQueueToggle = forwardRef< + HTMLButtonElement, + ChatComposerQueueToggleProps +>(function ChatComposerQueueToggle({ className, expanded, ...props }, ref) { + return ( + } -