diff --git a/docs/specs/components/Chat/ChatMessageMetadata.spec.md b/docs/specs/components/Chat/ChatMessageMetadata.spec.md index 497ee42a40fc7..fda4157c86bf0 100644 --- a/docs/specs/components/Chat/ChatMessageMetadata.spec.md +++ b/docs/specs/components/Chat/ChatMessageMetadata.spec.md @@ -1,17 +1,18 @@ --- schema_version: 3 -template_version: 6 +template_version: 7 kind: component id: component:ChatMessageMetadata -authority: draft +authority: current archive_reason: null superseded_by: null -approved_by: null -approved_at: null +approved_by: cixzhang +approved_at: 2026-11-01 owners: [cixzhang] review_triggers: [public-api, behavior, layout, accessibility, theming, testing] verified_by: [ + packages/core/src/Chat/ChatMessageBubble.test.tsx, packages/core/src/Chat/ChatMessageMetadata.test.tsx, packages/core/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts, apps/storybook/stories/ChatMessageMetadata.stories.tsx, @@ -35,25 +36,20 @@ system_specs: [spec:AST-029] # ChatMessageMetadata component contract -This is an observational draft of the existing Core chat metadata surface and -its tested empty-scalar correction. It creates no new prop, status meaning, -announcement guarantee, or visual policy. Current shared records govern their -own scopes independently of this draft. - ## Contract at a glance | Area | Contract | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Public contract | Optional `timestamp` and `footer` ReactNode slots, optional closed `status` union, root `BaseProps` and `ref`, exported through `./Chat`. | | Behavior | The row renders its present slots in timestamp, footer, status order, with a separator only between present slots; user sender reverses the flex row. Five delivery statuses have localized icon-and-text displays. | -| End-user impact | Non-rendering scalar slot values no longer leave a standalone dot, a blank metadata row, or a separator without adjacent content. Visible numeric zero remains. | -| Builder impact | No new inputs, defaults, or migration. Caller-supplied footer controls remain caller-owned. | -| Compatibility/readiness | Patch-compatible scalar correction to a released `./Chat` subpath; draft pending exact-head observational verification and owner review. | -| Review checks | Reject a lost visible zero, stranded separator, omitted translated status label, dropped root passthrough, or claim that a static DOM attribute proves an AT announcement. | +| End-user impact | Readers see a compact line of time, extra details, and delivery status in reading order, with a dot only between items that show something and a caption beside each status icon. | +| Builder impact | None. | +| Compatibility/readiness | Records shipped behavior; no public change. One implementation gap, listed under Current-state impact; two owner questions stay open. | +| Review checks | Reject a lost visible zero, stranded separator, omitted translated status caption or title, dropped root passthrough, or claim that a static DOM attribute proves an AT announcement. | | Governing rules | `architecture:public-component-api/INV1, INV3, INV5–INV9`; `architecture:component-test-sufficiency/INV1–INV7`; `spec:AST-029/FR2–FR5`. | -The table is a review projection. Consumer syntax belongs in -`ChatMessageMetadata.doc.mjs` and the Chat family consumer documentation. +This table is a review projection; the body below is authoritative. Consumer +syntax belongs in `ChatMessageMetadata.doc.mjs`. ## Intent @@ -64,16 +60,20 @@ bubble surface, the transcript, or the footer control owner. ## Compatibility and migration -- Released default preserved: yes. Core `0.6.3` exports `./Chat` with this - component, props, and status type. -- Compatibility class: patch correction for non-rendering scalar ReactNode - values, preserving numeric `0` and the published props. +- Released default preserved: `yes` +- Compatibility class: records shipped behavior; no public change - Controlled/uncontrolled behavior: not applicable; the component stores no delivery state. -- Migration decision: none. Existing callers need no new option. +- Migration decision: none + +Consumer migration instructions belong in consumer docs and release notes. + +## Current-state impact -Consumer migration instructions, when needed, belong in release notes rather -than this record. +- **The status label may not be read.** The status span carries a translated + `aria-label` but no role. WAI-ARIA 1.2 prohibits naming an element with the + generic role, so assistive technology may ignore that label and read only + the visible caption (FR6). ## Ownership boundary @@ -101,27 +101,24 @@ than this record. ## Public concepts -| Concept | Closed values or states | Observed meaning | Availability | Default | Owner | Stability | Invalid-value behavior | -| -------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------- | ----------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------- | -| `timestamp` | Omitted or caller ReactNode | Optional first item, normally a time | Any composition | Omitted | Caller | Released | Null, booleans, and empty string do not render a scalar slot; `0` renders; non-scalars remain opaque | -| `footer` | Omitted or caller ReactNode | Optional middle item such as model information or caller controls | Any composition | Omitted | Caller | Released | Same scalar treatment as timestamp | -| `status` | `sending`, `sent`, `delivered`, `read`, `error`, omitted | Optional delivery caption and glyph | Any composition | Omitted | Component/caller input | Released | Closed type union; omitted contributes no item | -| Sender context | `user`, `assistant`, `system`, absent | Selects user-reversed or other forward row order | Within `ChatMessage` or standalone | Assistant when absent | `ChatMessage` / local fallback | Observational | Non-user values share the forward path | -| Root surface | `BaseProps`, `ref`, `xstyle`, `className`, `style` | Extends the metadata row when it exists | Slot passes scalar-presence check or status present | Absent for empty scalar slots and no status | `architecture:public-component-api` | Released | Non-scalar values remain opaque; target and consumer styles combine | +| Concept | Closed values or states | Meaning | Availability | Default | Owner | Stability | Invalid-value behavior | +| -------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------- | ----------------------------------- | --------- | ---------------------------------------------------------------------------------------------------- | +| `timestamp` | Omitted or caller ReactNode | Optional first item, normally a time | Any composition | Omitted | Caller | Stable | Null, booleans, and empty string do not render a scalar slot; `0` renders; non-scalars remain opaque | +| `footer` | Omitted or caller ReactNode | Optional middle item such as model information or caller controls | Any composition | Omitted | Caller | Stable | Same scalar treatment as timestamp | +| `status` | `sending`, `sent`, `delivered`, `read`, `error`, omitted | Optional delivery caption and glyph | Any composition | Omitted | Component/caller input | Stable | Closed type union; omitted contributes no item | +| Sender context | `user`, `assistant`, `system`, absent | Selects user-reversed or other forward row order | Within `ChatMessage` or standalone | Assistant when absent | `ChatMessage` / local fallback | Stable | Non-user values share the forward path | +| Root surface | `BaseProps`, `ref`, `xstyle`, `className`, `style` | Extends the metadata row when it exists | Slot passes scalar-presence check or status present | Absent for empty scalar slots and no status | `architecture:public-component-api` | Stable | Non-scalar values remain opaque; target and consumer styles combine | ## Behavioral and layout contract -The following candidate invariants name their evidence. They are observations -for review, not current product policy created by this draft. - -| ID | Candidate invariant | Basis | Draft review state | -| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ------------------ | -| FR1 | The root appears when at least one slot passes the shared scalar-presence predicate or `status` is present. Non-scalar values are treated as present without inspecting their inner content. | Public ReactNode seam, shared `isRenderable` scalar utility, focused red/green and browser evidence | Verify correction | -| FR2 | Accepted slot values render in timestamp, footer, status order with dots between values considered present; numeric `0` is visible in either slot, and non-scalar nodes remain opaque. | Shipped ordering, shared scalar utility, focused tests, light/dark browser cases | Verify correction | -| FR3 | A `user` message reverses row direction; `assistant`, `system`, and absent context use the forward path. | Source, Chat context, sender and RTL browser fixtures | Verify observation | -| FR4 | Status selects one of five registry icons and localized labels; failure uses the error text token, and sending has a CSS pulse suppressed under reduced motion. | Source, locale catalog, owned browser status matrix | Verify observation | -| FR5 | The root merges the target, default StyleX output, and consumer `xstyle`, `className`, and `style`; accepted neutral DOM/ARIA inputs and `ref` reach that root. | Source, public API authority, existing passthrough test | Verify observation | -| FR6 | A status item has a translated title and author-supplied `aria-label`, an icon and visible localized text. The row declares no live-region role or imperative announcement. | Source, DOM tests and axe fixtures; no real-AT timing claim | Verify observation | +| ID | Invariant | Basis | +| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| FR1 | The root appears when at least one slot passes the shared scalar-presence predicate or `status` is present. Non-scalar values are treated as present without inspecting their inner content. | Current source and tests | +| FR2 | Accepted slot values render in timestamp, footer, status order with dots between values considered present; numeric `0` is visible in either slot, and non-scalar nodes remain opaque. | Current source and tests | +| FR3 | A `user` message reverses row direction; `assistant`, `system`, and absent context use the forward path. | Current source | +| FR4 | Status selects one of four registry icons (`delivered` and `read` share `checkDouble`) and localized labels; failure uses the error text token, and sending has a CSS pulse suppressed under reduced motion. | Current source and locale catalog | +| FR5 | The root merges the target, default StyleX output, and consumer `xstyle`, `className`, and `style`; accepted neutral DOM/ARIA inputs and `ref` reach that root. | Current source and tests | +| FR6 | A status item has a translated title, an icon, and visible localized text. The row declares no live-region role or imperative announcement. Implementation gap: the status span also carries a translated `aria-label` without a role (Current-state impact). | Current source and tests | ### Allowed variation @@ -133,7 +130,7 @@ for review, not current product policy created by this draft. ### Representative states -| State | Observed result | Allowed variation | +| State | Required invariant | Allowed variation | | --------------------------- | --------------------------------------------------------------------------- | -------------------------------------- | | Standalone timestamp | One root and one visible item | Caller content | | User or assistant message | Sender-relative row order | Text direction inherited from the page | @@ -176,9 +173,7 @@ for review, not current product policy created by this draft. | Metadata row | Semantic supporting typography and sender-relative reading order | Local representation unsettled | Supporting | FR2–FR3 | | Delivery status | Registry glyph, caption, and semantic error token | Local prominence and motion choice unsettled | Supporting | FR4, AR2 | -No current component-local ChatMessageMetadata design decision is introduced. -The draft `design:motion` and `design:system-states` records are not adopted as -current authority by this record. +No component-local design decision is recorded. ## Family and system relationships @@ -187,34 +182,42 @@ current authority by this record. - `architecture:public-component-api` owns root export and passthrough; `architecture:component-theming-surface` owns the public painting target; `architecture:component-test-sufficiency` owns bounded evidence coverage. -- `spec:AST-029` keeps the closed audit inventory and score outside this - component draft. +- `spec:AST-029` owns the audit evidence inventory this record's verification + map follows. ## Verification map -| Contract | Evidence | Representative states | Mutation or failure expectation | Audit section | -| ------------ | ------------------------------------------------------ | ------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------- | -| FR1–FR2 | `ChatMessageMetadata.test.tsx`; Chromium receipts | empty scalar, zero, timestamp/footer/status | A nullish-only guard reintroduces a stray separator or blank row | `audit:ChatMessageMetadata/behavior` | -| FR3 | Storybook sender fixtures and Chromium RTL frames | user, assistant, standalone in LTR/RTL | Wrong relative ordering becomes visible | `audit:ChatMessageMetadata/rtl` | -| FR4, AR2 | Status fixtures, locale catalog, Chromium/axe receipts | five statuses, light/dark, reduced motion | A dropped status label or failed theme paint is visible | `audit:ChatMessageMetadata/accessibility` | -| FR5 | Existing passthrough test and theming-target guard | DOM/data/ARIA and styling | Root drops accepted caller inputs or target | `audit:ChatMessageMetadata/public-api` | -| FR6, AR1–AR3 | Axe and browser semantic receipts | static status and caller button | Missing text/icon or falsely claimed AT timing remains detectable | `audit:ChatMessageMetadata/testing` | +| Contract | Verification | Representative states | Mutation or failure expectation | Audit section | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | +| FR1–FR2 | `ChatMessageMetadata.test.tsx` separator, empty-row, and numeric-zero tests; `ChatMessageMetadata.a11y.chromium.spec.ts` `narrow-overflow` and `status-*` cases | empty scalar; zero; timestamp, footer, status | A stray separator, a blank row, or a hidden zero fails. | `audit:ChatMessageMetadata/behavior` | +| FR5 | `ChatMessageMetadata.test.tsx` `forwards rest props (data-*, aria-*, id) to the root element` | data and id props | A dropped prop fails. | `audit:ChatMessageMetadata/public-api` | +| FR3, FR4, FR6 | `ChatMessageMetadata.a11y.chromium.spec.ts` status and RTL sender-ordering tests; `ChatMessageBubble.test.tsx` "renders status" and "renders timestamp and status on one row" | each status; user and assistant in LTR and RTL | A wrong sender order, or a missing caption or title, fails; the bubble tests also pin the status `aria-label` behind the FR6 gap. | `audit:ChatMessageMetadata/behavior` | + +Read from source, with no focused test yet: + +- FR2: the separator before `status` after an empty scalar slot. +- FR3, AV3: the forward row for `system` and standalone use. +- FR4: each status's icon, the error color, and the sending pulse and its + reduced-motion suppression. +- FR5: ARIA passthrough, `ref`, `className`, `style`, and `xstyle` + composition, and the target. +- FR6, AR1: no live region of the row's own. +- PR1: no state, Effect, timer, observer, or listener. ## Decision log -None. This draft makes no new component-local product decision. +No component-local decision is recorded. ## Open questions - **OQ1 - Should non-scalar ReactNode slots ever be normalized for emptiness?** (`human-api`) Arrays, Fragments, lazy content, and promises remain opaque under - the existing scalar predicate; this audit does not consume or inspect them. + the existing scalar predicate; the component does not consume or inspect them. - **OQ2 - Should standalone delivery-status changes be announced, and if so by which owner?** (`human-api`) The current row is display-only. No spoken-timing claim is made without a current owner and applicable AT evidence. ## Content boundary -This record does not store an audit score, screenshot manifest, CI transcript, -consumer prop example, new public API, or shared visual policy. Those remain in -the audit PR evidence, consumer docs, and their canonical owners. +This file does not duplicate consumer prop tables or examples, or shared visual +policy. It links to their owners.