@astryxdesign/core 0.6.3-canary.db4e378 → 0.6.3-canary.ddb63c3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Chat/ChatMessageList.d.ts +2 -1
- package/dist/Chat/ChatMessageList.d.ts.map +1 -1
- package/dist/Chat/ChatMessageList.js +4 -3
- package/dist/Markdown/plugins/index.d.ts +1 -0
- package/dist/Markdown/plugins/index.d.ts.map +1 -1
- package/dist/Markdown/plugins/index.js +2 -1
- package/dist/Markdown/plugins/softBreaks.d.ts +3 -0
- package/dist/Markdown/plugins/softBreaks.d.ts.map +1 -0
- package/dist/Markdown/plugins/softBreaks.js +172 -0
- package/package.json +3 -3
- package/src/Chat/ChatMessageList.spec.md +222 -0
- package/src/Chat/ChatMessageList.test.tsx +64 -2
- package/src/Chat/ChatMessageList.tsx +4 -3
- package/src/Chat/__tests__/ChatMessageList.a11y.chromium.spec.ts +456 -0
- package/src/Markdown/Markdown.doc.mjs +26 -0
- package/src/Markdown/Markdown.public.test.ts +4 -0
- package/src/Markdown/Markdown.spec.md +3 -2
- package/src/Markdown/modules/softBreaks.spec.md +137 -0
- package/src/Markdown/plugins/index.ts +1 -0
- package/src/Markdown/plugins/softBreaks.test.tsx +172 -0
- package/src/Markdown/plugins/softBreaks.ts +169 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file ChatMessageList.tsx
|
|
3
|
-
* @input Uses React, StyleX, ChatListContext, theme tokens, spacing step utilities
|
|
3
|
+
* @input Uses React, StyleX, ChatListContext, theme tokens, spacing step utilities,
|
|
4
|
+
* and isRenderable to preserve accepted empty-state content
|
|
4
5
|
* @output Exports ChatMessageList component and ChatMessageListProps
|
|
5
6
|
* @position Presentational message container — holds ChatMessage children
|
|
6
7
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatMessageList.d.ts","sourceRoot":"","sources":["../../src/Chat/ChatMessageList.tsx"],"names":[],"mappings":"AAIA
|
|
1
|
+
{"version":3,"file":"ChatMessageList.d.ts","sourceRoot":"","sources":["../../src/Chat/ChatMessageList.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAC,KAAK,SAAS,EAA4C,MAAM,OAAO,CAAC;AAGhF,OAAO,EAEL,KAAK,WAAW,EAEjB,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAGhD,MAAM,WAAW,oBAAqB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACrE,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAEhC;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;OAEG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IAEvB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,WAAW,CAAC;IAElB;;;;;;;;;;;;;;;OAeG;IACH,KAAK,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAEzB;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AA4FD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,UAAU,EACV,iBAAiB,EACjB,OAAoB,EACpB,GAAG,EACH,KAAgB,EAChB,WAAmB,EACnB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,EACH,GAAG,IAAI,EACR,EAAE,oBAAoB,+BAgGtB;yBA9Ge,eAAe"}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file ChatMessageList.tsx
|
|
7
|
-
* @input Uses React, StyleX, ChatListContext, theme tokens, spacing step utilities
|
|
7
|
+
* @input Uses React, StyleX, ChatListContext, theme tokens, spacing step utilities,
|
|
8
|
+
* and isRenderable to preserve accepted empty-state content
|
|
8
9
|
* @output Exports ChatMessageList component and ChatMessageListProps
|
|
9
10
|
* @position Presentational message container — holds ChatMessage children
|
|
10
11
|
*
|
|
@@ -27,7 +28,7 @@ import * as stylex from '@stylexjs/stylex';
|
|
|
27
28
|
import "../theme/tokens.stylex.js";
|
|
28
29
|
import { spacingVars } from "../theme/tokens.stylex.js";
|
|
29
30
|
import { ChatListContext, useChatLayoutContext } from "./ChatContext.js";
|
|
30
|
-
import { mergeProps } from "../utils/index.js";
|
|
31
|
+
import { isRenderable, mergeProps } from "../utils/index.js";
|
|
31
32
|
import { Spinner } from "../Spinner/index.js";
|
|
32
33
|
import { themeProps } from "../utils/themeProps.js";
|
|
33
34
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -225,7 +226,7 @@ export function ChatMessageList({
|
|
|
225
226
|
className: "x98rzlu x2lwn1j"
|
|
226
227
|
},
|
|
227
228
|
"aria-hidden": true
|
|
228
|
-
}), hasChildren ? children : emptyState ? /*#__PURE__*/_jsx("div", {
|
|
229
|
+
}), hasChildren ? children : isRenderable(emptyState) ? /*#__PURE__*/_jsx("div", {
|
|
229
230
|
...{
|
|
230
231
|
className: "x78zum5 x6s0dn4 xl56j7k x98rzlu x2lwn1j"
|
|
231
232
|
},
|
|
@@ -11,6 +11,7 @@ export type { MarkdownFenceContext, MarkdownFenceNode, MarkdownFenceTransformOpt
|
|
|
11
11
|
export { createMarkdownSourceDecoration, getMarkdownSourceDecorations, } from './sourceDecoration';
|
|
12
12
|
export type { MarkdownSourceDecoration, MarkdownSourceDecorationOptions, MarkdownSourceDecorationRange, } from './sourceDecoration';
|
|
13
13
|
export { createMarkdownFrontmatter } from './frontmatter';
|
|
14
|
+
export { markdownSoftBreaksPlugin } from './softBreaks';
|
|
14
15
|
export type { MarkdownFrontmatter, MarkdownFrontmatterOptions, MarkdownFrontmatterParseResult, } from './frontmatter';
|
|
15
16
|
export type { MarkdownPluginData, MarkdownExtensionNode, MarkdownTokenizerInput, MarkdownTokenizeResult, MarkdownSyntaxContribution, MarkdownSyntaxCapability, MarkdownTransformContext, MarkdownTransform, MarkdownExtensionRenderer, MarkdownExtensionRenderers, MarkdownSyntaxPluginDefinition, MarkdownTransformPluginDefinition, MarkdownPluginDefinition, MarkdownPluginEntry, MarkdownNodeOf, MarkdownExtensionsOf, } from './protocol';
|
|
16
17
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/Markdown/plugins/index.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,OAAO,EAAC,oBAAoB,EAAE,uBAAuB,EAAC,MAAM,YAAY,CAAC;AACzE,OAAO,EAAC,2BAA2B,EAAC,MAAM,iBAAiB,CAAC;AAC5D,YAAY,EACV,4BAA4B,EAC5B,4BAA4B,GAC7B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAC,4BAA4B,EAAC,MAAM,iBAAiB,CAAC;AAC7D,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,6BAA6B,GAC9B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,8BAA8B,EAC9B,4BAA4B,GAC7B,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,wBAAwB,EACxB,+BAA+B,EAC/B,6BAA6B,GAC9B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAC,yBAAyB,EAAC,MAAM,eAAe,CAAC;AACxD,YAAY,EACV,mBAAmB,EACnB,0BAA0B,EAC1B,8BAA8B,GAC/B,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,kBAAkB,EAClB,qBAAqB,EACrB,sBAAsB,EACtB,sBAAsB,EACtB,0BAA0B,EAC1B,wBAAwB,EACxB,wBAAwB,EACxB,iBAAiB,EACjB,yBAAyB,EACzB,0BAA0B,EAC1B,8BAA8B,EAC9B,iCAAiC,EACjC,wBAAwB,EACxB,mBAAmB,EACnB,cAAc,EACd,oBAAoB,GACrB,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/Markdown/plugins/index.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,OAAO,EAAC,oBAAoB,EAAE,uBAAuB,EAAC,MAAM,YAAY,CAAC;AACzE,OAAO,EAAC,2BAA2B,EAAC,MAAM,iBAAiB,CAAC;AAC5D,YAAY,EACV,4BAA4B,EAC5B,4BAA4B,GAC7B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAC,4BAA4B,EAAC,MAAM,iBAAiB,CAAC;AAC7D,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,6BAA6B,GAC9B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,8BAA8B,EAC9B,4BAA4B,GAC7B,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,wBAAwB,EACxB,+BAA+B,EAC/B,6BAA6B,GAC9B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAC,yBAAyB,EAAC,MAAM,eAAe,CAAC;AACxD,OAAO,EAAC,wBAAwB,EAAC,MAAM,cAAc,CAAC;AACtD,YAAY,EACV,mBAAmB,EACnB,0BAA0B,EAC1B,8BAA8B,GAC/B,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,kBAAkB,EAClB,qBAAqB,EACrB,sBAAsB,EACtB,sBAAsB,EACtB,0BAA0B,EAC1B,wBAAwB,EACxB,wBAAwB,EACxB,iBAAiB,EACjB,yBAAyB,EACzB,0BAA0B,EAC1B,8BAA8B,EAC9B,iCAAiC,EACjC,wBAAwB,EACxB,mBAAmB,EACnB,cAAc,EACd,oBAAoB,GACrB,MAAM,YAAY,CAAC"}
|
|
@@ -10,4 +10,5 @@ export { createMarkdownPlugin, isMarkdownExtensionNode } from "./protocol.js";
|
|
|
10
10
|
export { createMarkdownTextTransform } from "./textTransform.js";
|
|
11
11
|
export { createMarkdownFenceTransform } from "./semanticFence.js";
|
|
12
12
|
export { createMarkdownSourceDecoration, getMarkdownSourceDecorations } from "./sourceDecoration.js";
|
|
13
|
-
export { createMarkdownFrontmatter } from "./frontmatter.js";
|
|
13
|
+
export { createMarkdownFrontmatter } from "./frontmatter.js";
|
|
14
|
+
export { markdownSoftBreaksPlugin } from "./softBreaks.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"softBreaks.d.ts","sourceRoot":"","sources":["../../../src/Markdown/plugins/softBreaks.ts"],"names":[],"mappings":"AAmKA,8EAA8E;AAC9E,eAAO,MAAM,wBAAwB,iDAInC,CAAC"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file softBreaks.ts
|
|
5
|
+
* @input Soft line endings in Markdown phrasing content
|
|
6
|
+
* @output A first-party plugin that renders each soft line ending as a hard break
|
|
7
|
+
* @position Optional Markdown transform built only on the public plugin protocol
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createMarkdownPlugin } from "./protocol.js";
|
|
11
|
+
function replaceLineEndings(value) {
|
|
12
|
+
const pattern = /\r?\n|\r/g;
|
|
13
|
+
let match = pattern.exec(value);
|
|
14
|
+
if (match == null) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const output = [];
|
|
18
|
+
let cursor = 0;
|
|
19
|
+
do {
|
|
20
|
+
if (match.index > cursor) {
|
|
21
|
+
output.push({
|
|
22
|
+
type: 'text',
|
|
23
|
+
value: value.slice(cursor, match.index)
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
output.push({
|
|
27
|
+
type: 'break'
|
|
28
|
+
});
|
|
29
|
+
cursor = match.index + match[0].length;
|
|
30
|
+
match = pattern.exec(value);
|
|
31
|
+
} while (match != null);
|
|
32
|
+
if (cursor < value.length) {
|
|
33
|
+
output.push({
|
|
34
|
+
type: 'text',
|
|
35
|
+
value: value.slice(cursor)
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return output;
|
|
39
|
+
}
|
|
40
|
+
function transformPhrasing(children) {
|
|
41
|
+
let next;
|
|
42
|
+
for (let index = 0; index < children.length; index++) {
|
|
43
|
+
const node = children[index];
|
|
44
|
+
if (node.type === 'text') {
|
|
45
|
+
const replacement = replaceLineEndings(node.value);
|
|
46
|
+
if (replacement == null) {
|
|
47
|
+
next?.push(node);
|
|
48
|
+
} else {
|
|
49
|
+
next ??= children.slice(0, index);
|
|
50
|
+
next.push(...replacement);
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (node.type === 'strong' || node.type === 'emphasis' || node.type === 'delete' || node.type === 'link') {
|
|
55
|
+
const nested = transformPhrasing(node.children);
|
|
56
|
+
if (nested !== node.children) {
|
|
57
|
+
next ??= children.slice(0, index);
|
|
58
|
+
next.push({
|
|
59
|
+
...node,
|
|
60
|
+
children: nested
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
next?.push(node);
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
next?.push(node);
|
|
68
|
+
}
|
|
69
|
+
return next ?? children;
|
|
70
|
+
}
|
|
71
|
+
function transformTableCell(cell) {
|
|
72
|
+
const children = transformPhrasing(cell.children);
|
|
73
|
+
return children === cell.children ? cell : {
|
|
74
|
+
...cell,
|
|
75
|
+
children
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function transformTableRow(row) {
|
|
79
|
+
const children = row.children.map(transformTableCell);
|
|
80
|
+
return children.every((child, index) => child === row.children[index]) ? row : {
|
|
81
|
+
...row,
|
|
82
|
+
children
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function transformListItem(item) {
|
|
86
|
+
const children = transformBlocks(item.children);
|
|
87
|
+
return children === item.children ? item : {
|
|
88
|
+
...item,
|
|
89
|
+
children
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function transformBlocks(children) {
|
|
93
|
+
let next;
|
|
94
|
+
for (let index = 0; index < children.length; index++) {
|
|
95
|
+
const node = children[index];
|
|
96
|
+
let transformed = node;
|
|
97
|
+
switch (node.type) {
|
|
98
|
+
case 'heading':
|
|
99
|
+
case 'paragraph':
|
|
100
|
+
{
|
|
101
|
+
const nested = transformPhrasing(node.children);
|
|
102
|
+
if (nested !== node.children) {
|
|
103
|
+
transformed = {
|
|
104
|
+
...node,
|
|
105
|
+
children: nested
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case 'blockquote':
|
|
111
|
+
{
|
|
112
|
+
const nested = transformBlocks(node.children);
|
|
113
|
+
if (nested !== node.children) {
|
|
114
|
+
transformed = {
|
|
115
|
+
...node,
|
|
116
|
+
children: nested
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case 'list':
|
|
122
|
+
{
|
|
123
|
+
const nested = node.children.map(transformListItem);
|
|
124
|
+
if (nested.some((child, childIndex) => child !== node.children[childIndex])) {
|
|
125
|
+
transformed = {
|
|
126
|
+
...node,
|
|
127
|
+
children: nested
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case 'table':
|
|
133
|
+
{
|
|
134
|
+
const nested = node.children.map(transformTableRow);
|
|
135
|
+
if (nested.some((child, childIndex) => child !== node.children[childIndex])) {
|
|
136
|
+
transformed = {
|
|
137
|
+
...node,
|
|
138
|
+
children: nested
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 'code':
|
|
144
|
+
case 'math':
|
|
145
|
+
case 'thematicBreak':
|
|
146
|
+
case 'image':
|
|
147
|
+
case 'extension':
|
|
148
|
+
break;
|
|
149
|
+
default:
|
|
150
|
+
node;
|
|
151
|
+
}
|
|
152
|
+
if (transformed !== node) {
|
|
153
|
+
next ??= children.slice(0, index);
|
|
154
|
+
}
|
|
155
|
+
next?.push(transformed);
|
|
156
|
+
}
|
|
157
|
+
return next ?? children;
|
|
158
|
+
}
|
|
159
|
+
const transformSoftBreaks = document => {
|
|
160
|
+
const children = transformBlocks(document.children);
|
|
161
|
+
return children === document.children ? document : {
|
|
162
|
+
...document,
|
|
163
|
+
children
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/** Converts soft line endings in Markdown phrasing content to hard breaks. */
|
|
168
|
+
export const markdownSoftBreaksPlugin = createMarkdownPlugin({
|
|
169
|
+
name: 'soft-breaks',
|
|
170
|
+
apiVersion: 1,
|
|
171
|
+
transform: transformSoftBreaks
|
|
172
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.6.3-canary.
|
|
3
|
+
"version": "0.6.3-canary.ddb63c3",
|
|
4
4
|
"displayName": "Astryx Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -692,8 +692,8 @@
|
|
|
692
692
|
"react-dom": ">=19.0.0"
|
|
693
693
|
},
|
|
694
694
|
"devDependencies": {
|
|
695
|
-
"@astryxdesign/a11y-spec": "0.6.3-canary.
|
|
696
|
-
"@astryxdesign/cli": "0.6.3-canary.
|
|
695
|
+
"@astryxdesign/a11y-spec": "0.6.3-canary.ddb63c3",
|
|
696
|
+
"@astryxdesign/cli": "0.6.3-canary.ddb63c3",
|
|
697
697
|
"@babel/cli": "^7.29.7",
|
|
698
698
|
"@babel/core": "^7.29.7",
|
|
699
699
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 3
|
|
3
|
+
template_version: 6
|
|
4
|
+
kind: component
|
|
5
|
+
id: component:ChatMessageList
|
|
6
|
+
authority: draft
|
|
7
|
+
archive_reason: null
|
|
8
|
+
superseded_by: null
|
|
9
|
+
approved_by: null
|
|
10
|
+
approved_at: null
|
|
11
|
+
owners: [cixzhang]
|
|
12
|
+
review_triggers:
|
|
13
|
+
[public-api, behavior, layout, scrolling, accessibility, theming, testing]
|
|
14
|
+
verified_by:
|
|
15
|
+
[
|
|
16
|
+
packages/core/src/Chat/ChatMessageList.test.tsx,
|
|
17
|
+
packages/core/src/Chat/__tests__/ChatMessageList.a11y.chromium.spec.ts,
|
|
18
|
+
apps/storybook/stories/ChatMessageList.stories.tsx,
|
|
19
|
+
packages/core/src/theme/themingTargets.test.ts,
|
|
20
|
+
]
|
|
21
|
+
modules: []
|
|
22
|
+
families: []
|
|
23
|
+
design_specs: []
|
|
24
|
+
architecture:
|
|
25
|
+
[
|
|
26
|
+
architecture:public-component-api,
|
|
27
|
+
architecture:component-test-sufficiency,
|
|
28
|
+
architecture:component-style-authoring,
|
|
29
|
+
architecture:component-theming-surface,
|
|
30
|
+
architecture:react-component-runtime,
|
|
31
|
+
architecture:knowledge-contracts,
|
|
32
|
+
]
|
|
33
|
+
contributing: []
|
|
34
|
+
system_specs: [spec:AST-029]
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
# ChatMessageList component contract
|
|
38
|
+
|
|
39
|
+
This draft is an observational account of shipped `core/ChatMessageList`. Its
|
|
40
|
+
candidate invariants name their source and evidence; they do not decide any
|
|
41
|
+
new API, default, loading protocol, or visual direction. The current shared
|
|
42
|
+
records linked above continue to govern independently.
|
|
43
|
+
|
|
44
|
+
## Contract at a glance
|
|
45
|
+
|
|
46
|
+
| Area | Contract |
|
|
47
|
+
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
48
|
+
| Public contract | `children`, `emptyState`, `scrollToTopAction`, `density`, `gap`, `align`, `isStreaming`, the root's `BaseProps<HTMLDivElement>`, and `ref`. |
|
|
49
|
+
| Behavior | A polite log holds caller messages. Density selects row spacing and passes through context. A top sentinel can request older messages. A spacer moves short lists down by default. |
|
|
50
|
+
| End-user impact | A reader with a numeric empty-state value sees `0`; other transcript arrangements remain unchanged. |
|
|
51
|
+
| Builder impact | None. No new caller choice or migration is introduced. |
|
|
52
|
+
| Compatibility/readiness | The published `./Chat` subpath and its defaults remain unchanged. This is a draft pending verification and exact-head owner review. |
|
|
53
|
+
| Review checks | Reject a claim that the list owns ChatLayout auto-scroll, that a draft settles an unobserved loading policy, or that DOM `aria-busy` proves spoken output. |
|
|
54
|
+
| Governing rules | `architecture:public-component-api/INV1, INV5–INV8`; `architecture:react-component-runtime/INV3, INV5–INV6`; `spec:AST-029/FR2–FR5`. |
|
|
55
|
+
|
|
56
|
+
This table is a review projection; the body below describes the observed
|
|
57
|
+
surface rather than creating new authority.
|
|
58
|
+
|
|
59
|
+
## Intent
|
|
60
|
+
|
|
61
|
+
ChatMessageList is the presentational message container within the Chat
|
|
62
|
+
composition. It owns a flex-column transcript, spacing/density context, an
|
|
63
|
+
optional empty state and older-message sentinel, and the log's rendered ARIA
|
|
64
|
+
attributes. `ChatLayout` owns auto-follow scrolling and the dock; message and
|
|
65
|
+
bubble components own sender presentation and content.
|
|
66
|
+
|
|
67
|
+
Consumer prop syntax and examples remain in `ChatMessageList.doc.mjs`.
|
|
68
|
+
|
|
69
|
+
## Compatibility and migration
|
|
70
|
+
|
|
71
|
+
- Released default preserved: yes. The published Core package exposes `./Chat`
|
|
72
|
+
with `ChatMessageList` and `ChatMessageListProps`.
|
|
73
|
+
- Compatibility class: no new API, default, or migration. Visible numeric
|
|
74
|
+
empty-state content follows the existing `ReactNode` type.
|
|
75
|
+
- Controlled/uncontrolled behavior: not applicable; there is no controlled
|
|
76
|
+
value on this component.
|
|
77
|
+
- Migration decision: none.
|
|
78
|
+
|
|
79
|
+
## Ownership boundary
|
|
80
|
+
|
|
81
|
+
**Owns**
|
|
82
|
+
|
|
83
|
+
- The outer log element, its public DOM/ref/style passthrough, and the
|
|
84
|
+
`chat-message-list` target with its `density` selector.
|
|
85
|
+
- The inner flex-column message box and density-based padding and gap.
|
|
86
|
+
- The short-list block spacer when `align` is `bottom`.
|
|
87
|
+
- The optional `emptyState` substitution and older-message sentinel/loading
|
|
88
|
+
indicator.
|
|
89
|
+
- Passing the inner content element to `ChatLayoutContext.contentRef` while a
|
|
90
|
+
layout is present.
|
|
91
|
+
|
|
92
|
+
**Does not own / non-goals**
|
|
93
|
+
|
|
94
|
+
- Auto-scroll, scroll-to-bottom, dock, and scroll-owner selection, which belong
|
|
95
|
+
to `component:ChatLayout` and its scroll hooks.
|
|
96
|
+
- Sender identity, message alignment, bubbles, metadata, and text semantics,
|
|
97
|
+
which belong to their child components.
|
|
98
|
+
- The caller's region landmark or an accessible name for arbitrary transcript
|
|
99
|
+
context; no new naming policy is made by this draft.
|
|
100
|
+
|
|
101
|
+
## Public concepts
|
|
102
|
+
|
|
103
|
+
| Concept | Closed values or states | Meaning observed | Availability | Default | Owner | Stability | Invalid-value behavior |
|
|
104
|
+
| ------------------- | ----------------------------------------------- | --------------------------------------------------------------- | ------------- | ---------- | --------- | --------- | -------------------------------------------------- |
|
|
105
|
+
| `children` | `ReactNode` | Top-level transcript content | every render | required | caller | released | null, false, or `[]` counts empty at the top level |
|
|
106
|
+
| `emptyState` | `ReactNode`; omitted | Substitutes for top-level empty children | empty state | omitted | caller | released | absent renders no substitute |
|
|
107
|
+
| `scrollToTopAction` | async function; omitted | Invoked when the top sentinel intersects | when supplied | omitted | caller | released | absent creates no sentinel or observer |
|
|
108
|
+
| `density` | `compact`; `balanced`; `spacious` | Chooses message spacing and child density context | every render | `balanced` | component | released | closed type union |
|
|
109
|
+
| `gap` | published `SpacingStep`; omitted | Overrides the list's top-level row gap independently of density | every render | omitted | caller | released | closed type union |
|
|
110
|
+
| `align` | `top`; `bottom` | Positions short content at the start or block end | every render | `bottom` | component | released | closed type union |
|
|
111
|
+
| `isStreaming` | `true`; `false` | Exposes `aria-busy` on the polite log while streaming | every render | `false` | caller | released | boolean |
|
|
112
|
+
| root passthrough | `ref`, accepted DOM/data/ARIA and styling props | Extends the outer log element | every render | omitted | caller | released | owned role/live/busy attributes retain precedence |
|
|
113
|
+
|
|
114
|
+
The table records the implementation's top-level emptiness test; it does not
|
|
115
|
+
promise recursive child inspection or resolve the meaning of arbitrary
|
|
116
|
+
non-rendering React nodes.
|
|
117
|
+
|
|
118
|
+
## Behavioral and layout contract
|
|
119
|
+
|
|
120
|
+
These candidate invariants record observable shipped behavior. A `verify`
|
|
121
|
+
state means the audit has not upgraded implementation into policy.
|
|
122
|
+
|
|
123
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
124
|
+
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ | ------------------ |
|
|
125
|
+
| FR1 | The outer root renders `role="log"`, `aria-live="polite"`, and `tabIndex=0`; `isStreaming=true` adds `aria-busy="true"`, which is absent otherwise. | Shipped source, colocated DOM tests and consumer docs | verify |
|
|
126
|
+
| FR2 | The chosen density is reflected as `data-density` on the root and flows to child messages through `ChatListContext`; it selects three spacing groups. | Shipped source, colocated density test, family docs | verify |
|
|
127
|
+
| FR3 | The optional `gap` replaces only the top-level row gap; density still selects padding and context. | Shipped source and consumer docs | verify |
|
|
128
|
+
| FR4 | `emptyState` renders only when `children` is nullish, `false`, or an empty top-level array. Otherwise the passed children render unchanged. | Shipped source, empty-array and numeric-zero tests | verify |
|
|
129
|
+
| FR5 | `align="bottom"` includes a flex spacer before messages; `align="top"` omits it. Overflowing content is not given a different rendering path. | Shipped source, colocated tests, existing family story | verify |
|
|
130
|
+
| FR6 | Supplying `scrollToTopAction` creates an observed top sentinel. An intersection starts its async transition, and a top-positioned spinner renders while it is pending. | Shipped source, consumer docs, focused async test | verify |
|
|
131
|
+
| FR7 | Inside ChatLayout, the inner content element is passed to the layout's `contentRef` and removed from that registration on cleanup; standalone lists have no layout ref. | Shipped source and context definition | verify |
|
|
132
|
+
| FR8 | The root receives `ref`, accepted DOM/data/ARIA props, and composed `className`, `style`, and `xstyle`; component-owned log semantics are applied after forwarded props. | Shipped source and shared public API authority | verify |
|
|
133
|
+
|
|
134
|
+
For the `emptyState` slot, numeric `0` is visible content. Nullish values,
|
|
135
|
+
booleans, and an empty string do not create an empty wrapper. This describes
|
|
136
|
+
scalar output only; it does not decide how to inspect nested children.
|
|
137
|
+
|
|
138
|
+
### Allowed variation
|
|
139
|
+
|
|
140
|
+
- **AV1 - Content.** Any caller-owned ReactNode may be placed in the list; the
|
|
141
|
+
list does not infer sender metadata from children.
|
|
142
|
+
- **AV2 - Spacing.** Density selects token-based defaults; the optional `gap`
|
|
143
|
+
selects a published spacing step without changing the child density.
|
|
144
|
+
- **AV3 - Scroll ownership.** The list may be used alone or inside ChatLayout;
|
|
145
|
+
auto-follow behavior remains outside this component.
|
|
146
|
+
|
|
147
|
+
### Representative states
|
|
148
|
+
|
|
149
|
+
| State | Observed result | Allowed variation |
|
|
150
|
+
| ----------------------------- | ----------------------------------------------------- | ------------------------------------------ |
|
|
151
|
+
| Two messages and a system row | Polite log with messages in source order | Caller controls row content |
|
|
152
|
+
| Empty top-level children | Optional centered empty state | An omitted empty state leaves an empty log |
|
|
153
|
+
| Compact/balanced/spacious | Different token-derived row spacing and child density | `gap` may override only row spacing |
|
|
154
|
+
| Short top/bottom list | Bottom includes a spacer, top omits it | Container height is caller-owned |
|
|
155
|
+
| Streaming | Log has `aria-busy=true` | Message content remains caller-owned |
|
|
156
|
+
| Top action pending | Spinner above content while transition is pending | Async action result is caller-owned |
|
|
157
|
+
|
|
158
|
+
### Performance and resources
|
|
159
|
+
|
|
160
|
+
- An `IntersectionObserver` is installed only when `scrollToTopAction` exists,
|
|
161
|
+
and disconnected on cleanup. Its root is ChatLayout's scroll container when
|
|
162
|
+
available, otherwise the document viewport.
|
|
163
|
+
- An Effect registers the inner content element with ChatLayout, then clears
|
|
164
|
+
that registration on cleanup. This draft does not prescribe an internal hook
|
|
165
|
+
or listener implementation.
|
|
166
|
+
|
|
167
|
+
## Accessibility contract
|
|
168
|
+
|
|
169
|
+
- The shipped root is a focusable polite log with `aria-busy` reflecting
|
|
170
|
+
`isStreaming`; this describes browser-exposed semantics, not an observed
|
|
171
|
+
screen-reader announcement transcript.
|
|
172
|
+
- The loading spinner is a separate child of the log. The list adds no
|
|
173
|
+
interactive button or directional keyboard behavior of its own.
|
|
174
|
+
- Caller-supplied content remains responsible for names and semantics inside
|
|
175
|
+
messages. This record does not claim one browser/AT pairing proves another.
|
|
176
|
+
|
|
177
|
+
## Design relationships
|
|
178
|
+
|
|
179
|
+
No current component-local design decision is added by this observational
|
|
180
|
+
backfill. Spacing uses portable semantic token steps. The family consumer doc
|
|
181
|
+
owns the published `chat-message-list` theme target and its `density` axis.
|
|
182
|
+
This record does not choose a new visual representation or a new theming target.
|
|
183
|
+
|
|
184
|
+
## Family and system relationships
|
|
185
|
+
|
|
186
|
+
- No `family:chat` authority record currently exists. `Chat.doc.mjs` is the
|
|
187
|
+
consumer family document, not a product-authority family contract.
|
|
188
|
+
- `ChatLayout` owns the scroll container and provides `contentRef`; the list
|
|
189
|
+
registers its content element without taking over ChatLayout's scrolling.
|
|
190
|
+
- `spec:AST-029` governs observational audit backfills and keeps their
|
|
191
|
+
evidence inventory separate from component policy.
|
|
192
|
+
|
|
193
|
+
## Verification map
|
|
194
|
+
|
|
195
|
+
| Contract | Evidence | Representative states | Mutation or failure expectation | Audit section |
|
|
196
|
+
| -------- | ------------------------------------------------------------------- | ---------------------------------- | -------------------------------------------------------- | -------------------------- |
|
|
197
|
+
| FR1 | `ChatMessageList.test.tsx` ARIA assertions; Chromium audit receipts | rest, streaming | Dropping busy or the log role changes the exposed state | `audit:ChatMessageList/§1` |
|
|
198
|
+
| FR2–FR3 | density test, theming-target guard, rendered comparison | three densities and gap override | Losing density reflection or row spacing becomes visible | `audit:ChatMessageList/§2` |
|
|
199
|
+
| FR4–FR5 | empty/align tests, component-owned Storybook and Chromium receipts | empty, top and bottom | Empty substitute or short-list placement changes | `audit:ChatMessageList/§4` |
|
|
200
|
+
| FR6 | `ChatMessageList.test.tsx` async action/status test | sentinel absent, pending, settled | Action loss or stuck spinner fails the focused test | `audit:ChatMessageList/§6` |
|
|
201
|
+
| FR7 | ChatLayout integration and source review | standalone, inside layout, unmount | Content observation can lose registration | `audit:ChatMessageList/§7` |
|
|
202
|
+
| FR8 | `themingTargets.test.ts`, focused root passthrough inspection | ref, DOM/ARIA, styling | Consumer inputs fail to reach or compose on the root | `audit:ChatMessageList/§3` |
|
|
203
|
+
|
|
204
|
+
## Decision log
|
|
205
|
+
|
|
206
|
+
None. This draft settles no component-local product decision.
|
|
207
|
+
|
|
208
|
+
## Open questions
|
|
209
|
+
|
|
210
|
+
- **OQ1 - What is the intended overlap policy for repeated top-sentinel
|
|
211
|
+
intersections while an async load remains pending?** (`human-api`) The current
|
|
212
|
+
source invokes the action on intersection; this record does not add a
|
|
213
|
+
single-flight, initial-load, or exhaustion guarantee.
|
|
214
|
+
- **OQ2 - Should emptiness inspect nested non-rendering ReactNode values?**
|
|
215
|
+
(`human-api`) The shipped test is limited to the top-level `children` value.
|
|
216
|
+
Do not reinterpret that test as a policy about generators, fragments,
|
|
217
|
+
promised children, or arrays of non-rendering values.
|
|
218
|
+
|
|
219
|
+
## Content boundary
|
|
220
|
+
|
|
221
|
+
This file contains no audit score, screenshot matrix, run transcript, consumer
|
|
222
|
+
prop example, new public API, or cross-component visual policy.
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* @file ChatMessageList.test.tsx
|
|
5
|
+
* @input React Testing Library, Vitest, and the public ChatMessageList surface
|
|
6
|
+
* @output Observable log, empty-state, alignment, and async-loading regressions
|
|
7
|
+
* @position Colocated component behavior tests
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {describe, it, expect, vi} from 'vitest';
|
|
11
|
+
import {act, render, screen} from '@testing-library/react';
|
|
5
12
|
import {ChatMessageList} from './ChatMessageList';
|
|
6
13
|
import {ChatMessage} from './ChatMessage';
|
|
7
14
|
import {ChatMessageBubble} from './ChatMessageBubble';
|
|
@@ -55,6 +62,11 @@ describe('ChatMessageList', () => {
|
|
|
55
62
|
expect(screen.getByText('No messages yet')).toBeTruthy();
|
|
56
63
|
});
|
|
57
64
|
|
|
65
|
+
it('renders an accepted numeric empty state', () => {
|
|
66
|
+
render(<ChatMessageList emptyState={0}>{[]}</ChatMessageList>);
|
|
67
|
+
expect(screen.getByRole('log')).toHaveTextContent('0');
|
|
68
|
+
});
|
|
69
|
+
|
|
58
70
|
it('applies density class', () => {
|
|
59
71
|
render(
|
|
60
72
|
<ChatMessageList density="compact" data-testid="list">
|
|
@@ -129,4 +141,54 @@ describe('ChatMessageList', () => {
|
|
|
129
141
|
);
|
|
130
142
|
expect(screen.getByText('Hello')).toBeTruthy();
|
|
131
143
|
});
|
|
144
|
+
|
|
145
|
+
it('loads older messages through the top sentinel and exposes pending status', async () => {
|
|
146
|
+
let notify!: IntersectionObserverCallback;
|
|
147
|
+
let complete!: () => void;
|
|
148
|
+
const observeSentinel = vi.fn();
|
|
149
|
+
const disconnect = vi.fn();
|
|
150
|
+
const action = vi.fn(
|
|
151
|
+
async () =>
|
|
152
|
+
new Promise<void>(resolve => {
|
|
153
|
+
complete = resolve;
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
class Observer {
|
|
157
|
+
constructor(callback: IntersectionObserverCallback) {
|
|
158
|
+
notify = callback;
|
|
159
|
+
}
|
|
160
|
+
observe = observeSentinel;
|
|
161
|
+
disconnect = disconnect;
|
|
162
|
+
}
|
|
163
|
+
vi.stubGlobal('IntersectionObserver', Observer);
|
|
164
|
+
try {
|
|
165
|
+
const {unmount} = render(
|
|
166
|
+
<ChatMessageList scrollToTopAction={action}>
|
|
167
|
+
<div>Earlier message</div>
|
|
168
|
+
</ChatMessageList>,
|
|
169
|
+
);
|
|
170
|
+
expect(observeSentinel).toHaveBeenCalledTimes(1);
|
|
171
|
+
act(() => {
|
|
172
|
+
notify(
|
|
173
|
+
[{isIntersecting: false} as IntersectionObserverEntry],
|
|
174
|
+
{} as IntersectionObserver,
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
expect(action).not.toHaveBeenCalled();
|
|
178
|
+
act(() => {
|
|
179
|
+
notify(
|
|
180
|
+
[{isIntersecting: true} as IntersectionObserverEntry],
|
|
181
|
+
{} as IntersectionObserver,
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
expect(action).toHaveBeenCalledTimes(1);
|
|
185
|
+
expect(screen.getByRole('status', {name: 'Loading'})).toBeTruthy();
|
|
186
|
+
await act(async () => complete());
|
|
187
|
+
expect(screen.queryByRole('status', {name: 'Loading'})).toBeNull();
|
|
188
|
+
unmount();
|
|
189
|
+
expect(disconnect).toHaveBeenCalledTimes(1);
|
|
190
|
+
} finally {
|
|
191
|
+
vi.unstubAllGlobals();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
132
194
|
});
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file ChatMessageList.tsx
|
|
7
|
-
* @input Uses React, StyleX, ChatListContext, theme tokens, spacing step utilities
|
|
7
|
+
* @input Uses React, StyleX, ChatListContext, theme tokens, spacing step utilities,
|
|
8
|
+
* and isRenderable to preserve accepted empty-state content
|
|
8
9
|
* @output Exports ChatMessageList component and ChatMessageListProps
|
|
9
10
|
* @position Presentational message container — holds ChatMessage children
|
|
10
11
|
*
|
|
@@ -31,7 +32,7 @@ import {
|
|
|
31
32
|
type ChatDensity,
|
|
32
33
|
useChatLayoutContext,
|
|
33
34
|
} from './ChatContext';
|
|
34
|
-
import {mergeProps} from '../utils';
|
|
35
|
+
import {isRenderable, mergeProps} from '../utils';
|
|
35
36
|
import {Spinner} from '../Spinner';
|
|
36
37
|
import type {BaseProps} from '../BaseProps';
|
|
37
38
|
import type {SpacingStep} from '../utils/types';
|
|
@@ -321,7 +322,7 @@ export function ChatMessageList({
|
|
|
321
322
|
{/* Messages or empty state */}
|
|
322
323
|
{hasChildren ? (
|
|
323
324
|
children
|
|
324
|
-
) : emptyState ? (
|
|
325
|
+
) : isRenderable(emptyState) ? (
|
|
325
326
|
<div {...stylex.props(styles.emptyState)}>{emptyState}</div>
|
|
326
327
|
) : null}
|
|
327
328
|
</div>
|