@slipstream-ui/ai 0.5.0 → 0.6.1
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/index.d.mts +19 -4
- package/dist/index.d.ts +19 -4
- package/dist/index.js +12 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +12 -7
- package/dist/index.mjs.map +1 -1
- package/dist/manifests/attachment.manifest.json +35 -8
- package/dist/manifests/branch.manifest.json +1 -1
- package/dist/manifests/chain-thought.manifest.json +1 -1
- package/dist/manifests/code-block.manifest.json +1 -1
- package/dist/manifests/command-palette.manifest.json +22 -1
- package/dist/manifests/confirmation.manifest.json +1 -1
- package/dist/manifests/conversation.manifest.json +1 -1
- package/dist/manifests/inline-citation.manifest.json +1 -1
- package/dist/manifests/message-actions.manifest.json +59 -11
- package/dist/manifests/message.manifest.json +13 -1
- package/dist/manifests/prompt-input.manifest.json +2 -2
- package/dist/manifests/reasoning.manifest.json +13 -1
- package/dist/manifests/response.manifest.json +1 -1
- package/dist/manifests/source.manifest.json +2 -2
- package/dist/manifests/streaming-indicator.manifest.json +1 -1
- package/dist/manifests/suggestions.manifest.json +2 -2
- package/dist/manifests/tool-call.manifest.json +1 -1
- package/package.json +10 -10
- package/src/components/command-palette/CommandPalette.test.tsx +21 -0
- package/src/components/command-palette/CommandPalette.tsx +12 -3
- package/src/components/message/Message.test.tsx +15 -0
- package/src/components/message/Message.tsx +9 -1
- package/src/components/reasoning/Reasoning.test.tsx +15 -0
- package/src/components/reasoning/Reasoning.tsx +8 -2
- package/src/manifests/attachment.manifest.json +35 -8
- package/src/manifests/branch.manifest.json +1 -1
- package/src/manifests/chain-thought.manifest.json +1 -1
- package/src/manifests/code-block.manifest.json +1 -1
- package/src/manifests/command-palette.manifest.json +22 -1
- package/src/manifests/confirmation.manifest.json +1 -1
- package/src/manifests/conversation.manifest.json +1 -1
- package/src/manifests/inline-citation.manifest.json +1 -1
- package/src/manifests/message-actions.manifest.json +59 -11
- package/src/manifests/message.manifest.json +13 -1
- package/src/manifests/prompt-input.manifest.json +2 -2
- package/src/manifests/reasoning.manifest.json +13 -1
- package/src/manifests/response.manifest.json +1 -1
- package/src/manifests/source.manifest.json +2 -2
- package/src/manifests/streaming-indicator.manifest.json +1 -1
- package/src/manifests/suggestions.manifest.json +2 -2
- package/src/manifests/tool-call.manifest.json +1 -1
|
@@ -5,9 +5,24 @@
|
|
|
5
5
|
"description": "File chips for prompts and messages. `Attachment` renders one file; `AttachmentList` renders a wrapping row. Display-only: upload and file-system logic stay in the consumer.",
|
|
6
6
|
"designIntent": "Show what files are part of a conversation turn (name, size, type, thumbnail, upload status) without coupling the library to any upload implementation.",
|
|
7
7
|
"props": [
|
|
8
|
-
{
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
{
|
|
9
|
+
"name": "attachment",
|
|
10
|
+
"type": "AttachmentItem",
|
|
11
|
+
"required": true,
|
|
12
|
+
"description": "File descriptor: id, name, size, type, previewUrl, status."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "attachments",
|
|
16
|
+
"type": "AttachmentItem[]",
|
|
17
|
+
"required": true,
|
|
18
|
+
"description": "Files to display (AttachmentList)."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "onRemove",
|
|
22
|
+
"type": "(attachment) => void",
|
|
23
|
+
"required": false,
|
|
24
|
+
"description": "Remove callback; omit to hide remove controls."
|
|
25
|
+
}
|
|
11
26
|
],
|
|
12
27
|
"examples": [
|
|
13
28
|
{
|
|
@@ -20,16 +35,28 @@
|
|
|
20
35
|
}
|
|
21
36
|
],
|
|
22
37
|
"antiPatterns": [
|
|
23
|
-
{
|
|
38
|
+
{
|
|
39
|
+
"title": "Do not use Attachment for drag-and-drop targets",
|
|
40
|
+
"explanation": "The chip displays an existing item; build dropzones in the consumer and render AttachmentList once items exist."
|
|
41
|
+
}
|
|
24
42
|
],
|
|
25
43
|
"accessibility": [
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
{
|
|
45
|
+
"feature": "Remove control",
|
|
46
|
+
"implementation": "Icon-only remove button has an aria-label including the file name."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"feature": "Upload status",
|
|
50
|
+
"implementation": "Uploading state is announced via role=status."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"feature": "Decorative thumbnails",
|
|
54
|
+
"implementation": "Preview images are aria-hidden; the file name carries the meaning."
|
|
55
|
+
}
|
|
29
56
|
],
|
|
30
57
|
"sourceDir": "attachment",
|
|
31
58
|
"exportName": "Attachment",
|
|
32
59
|
"package": "@slipstream-ui/ai",
|
|
33
60
|
"importPath": "@slipstream-ui/ai",
|
|
34
|
-
"version": "0.
|
|
61
|
+
"version": "0.6.1"
|
|
35
62
|
}
|
|
@@ -67,6 +67,27 @@
|
|
|
67
67
|
"type": "string",
|
|
68
68
|
"required": false,
|
|
69
69
|
"description": "Shown while async commands load."
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "dialogLabel",
|
|
73
|
+
"type": "string",
|
|
74
|
+
"required": false,
|
|
75
|
+
"defaultValue": "Command palette",
|
|
76
|
+
"description": "Accessible label for the combobox."
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "listLabel",
|
|
80
|
+
"type": "string",
|
|
81
|
+
"required": false,
|
|
82
|
+
"defaultValue": "Commands",
|
|
83
|
+
"description": "Accessible label for the command listbox."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "emptyText",
|
|
87
|
+
"type": "string",
|
|
88
|
+
"required": false,
|
|
89
|
+
"defaultValue": "No matching commands.",
|
|
90
|
+
"description": "Shown when no command matches the query."
|
|
70
91
|
}
|
|
71
92
|
],
|
|
72
93
|
"examples": [
|
|
@@ -108,7 +129,7 @@
|
|
|
108
129
|
],
|
|
109
130
|
"package": "@slipstream-ui/ai",
|
|
110
131
|
"importPath": "@slipstream-ui/ai",
|
|
111
|
-
"version": "0.
|
|
132
|
+
"version": "0.6.1",
|
|
112
133
|
"sourceDir": "command-palette",
|
|
113
134
|
"exportName": "CommandPalette"
|
|
114
135
|
}
|
|
@@ -5,12 +5,48 @@
|
|
|
5
5
|
"description": "Feedback toolbar for an AI response: copy to clipboard, thumbs up/down rating, and regenerate. Every action is a callback; the component holds no conversation state.",
|
|
6
6
|
"designIntent": "Collect user trust signals (copy, rating, regeneration) with minimal API surface; consumers own what happens after each action.",
|
|
7
7
|
"props": [
|
|
8
|
-
{
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
{
|
|
9
|
+
"name": "content",
|
|
10
|
+
"type": "string",
|
|
11
|
+
"required": false,
|
|
12
|
+
"description": "Text to copy; omit to hide the copy action."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "onCopy",
|
|
16
|
+
"type": "(content: string) => void",
|
|
17
|
+
"required": false,
|
|
18
|
+
"description": "Called after copy with the copied text."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "onFeedback",
|
|
22
|
+
"type": "(rating: 'up' | 'down' | undefined) => void",
|
|
23
|
+
"required": false,
|
|
24
|
+
"description": "Rating callback; selecting the active rating again passes undefined."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "onRegenerate",
|
|
28
|
+
"type": "() => void",
|
|
29
|
+
"required": false,
|
|
30
|
+
"description": "Regeneration request; omit to hide the action."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "autohide",
|
|
34
|
+
"type": "'always' | 'on-hover' | 'not-last'",
|
|
35
|
+
"required": false,
|
|
36
|
+
"defaultValue": "always",
|
|
37
|
+
"enum": [
|
|
38
|
+
"always",
|
|
39
|
+
"on-hover",
|
|
40
|
+
"not-last"
|
|
41
|
+
],
|
|
42
|
+
"description": "Controls visibility: always visible, visible on hover/focus, or hidden until hover for non-latest messages."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "labels",
|
|
46
|
+
"type": "{ copy?, copied?, thumbUp?, thumbDown?, regenerate? }",
|
|
47
|
+
"required": false,
|
|
48
|
+
"description": "Overrides for all visible labels; enables localization."
|
|
49
|
+
}
|
|
14
50
|
],
|
|
15
51
|
"examples": [
|
|
16
52
|
{
|
|
@@ -23,16 +59,28 @@
|
|
|
23
59
|
}
|
|
24
60
|
],
|
|
25
61
|
"antiPatterns": [
|
|
26
|
-
{
|
|
62
|
+
{
|
|
63
|
+
"title": "Do not place MessageActions inside the message text flow",
|
|
64
|
+
"explanation": "Render it below the Message bubble so actions remain visually and semantically separate from content."
|
|
65
|
+
}
|
|
27
66
|
],
|
|
28
67
|
"accessibility": [
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
68
|
+
{
|
|
69
|
+
"feature": "Toolbar semantics",
|
|
70
|
+
"implementation": "Root has role=toolbar with an aria-label."
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"feature": "Toggle state",
|
|
74
|
+
"implementation": "Rating buttons expose aria-pressed so screen readers announce selection."
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"feature": "Icon buttons",
|
|
78
|
+
"implementation": "All icon-only controls have accessible labels; labels are localizable."
|
|
79
|
+
}
|
|
32
80
|
],
|
|
33
81
|
"sourceDir": "message-actions",
|
|
34
82
|
"exportName": "MessageActions",
|
|
35
83
|
"package": "@slipstream-ui/ai",
|
|
36
84
|
"importPath": "@slipstream-ui/ai",
|
|
37
|
-
"version": "0.
|
|
85
|
+
"version": "0.6.1"
|
|
38
86
|
}
|
|
@@ -102,6 +102,18 @@
|
|
|
102
102
|
"type": "(content: string) => void",
|
|
103
103
|
"required": false,
|
|
104
104
|
"description": "Enables edit-and-resubmit; called with the edited content"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "formatTime",
|
|
108
|
+
"type": "(date: Date) => string",
|
|
109
|
+
"required": false,
|
|
110
|
+
"description": "Formats message or step timestamps. Defaults to the runtime toLocaleTimeString()."
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "locale",
|
|
114
|
+
"type": "string",
|
|
115
|
+
"required": false,
|
|
116
|
+
"description": "BCP-47 locale used by the default timestamp formatter."
|
|
105
117
|
}
|
|
106
118
|
],
|
|
107
119
|
"examples": [
|
|
@@ -229,7 +241,7 @@
|
|
|
229
241
|
],
|
|
230
242
|
"package": "@slipstream-ui/ai",
|
|
231
243
|
"importPath": "@slipstream-ui/ai",
|
|
232
|
-
"version": "0.
|
|
244
|
+
"version": "0.6.1",
|
|
233
245
|
"sourceDir": "message",
|
|
234
246
|
"exportName": "Message"
|
|
235
247
|
}
|
|
@@ -59,6 +59,18 @@
|
|
|
59
59
|
"type": "{ title?: string; count?: (count: number) => string; thinking?: string; step?: (index: number) => string }",
|
|
60
60
|
"required": false,
|
|
61
61
|
"description": "Localized header, count and step labels."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "formatTime",
|
|
65
|
+
"type": "(date: Date) => string",
|
|
66
|
+
"required": false,
|
|
67
|
+
"description": "Formats message or step timestamps. Defaults to the runtime toLocaleTimeString()."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "locale",
|
|
71
|
+
"type": "string",
|
|
72
|
+
"required": false,
|
|
73
|
+
"description": "BCP-47 locale used by the default timestamp formatter."
|
|
62
74
|
}
|
|
63
75
|
],
|
|
64
76
|
"examples": [
|
|
@@ -177,7 +189,7 @@
|
|
|
177
189
|
],
|
|
178
190
|
"package": "@slipstream-ui/ai",
|
|
179
191
|
"importPath": "@slipstream-ui/ai",
|
|
180
|
-
"version": "0.
|
|
192
|
+
"version": "0.6.1",
|
|
181
193
|
"sourceDir": "reasoning",
|
|
182
194
|
"exportName": "Reasoning"
|
|
183
195
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slipstream-ui/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/michaelmcshinsky/slipstream-ui.git",
|
|
@@ -31,24 +31,24 @@
|
|
|
31
31
|
"prism-react-renderer": "^2.4.1",
|
|
32
32
|
"react-markdown": "^10.1.0",
|
|
33
33
|
"remark-gfm": "^4.0.1",
|
|
34
|
-
"tailwind-merge": "^
|
|
35
|
-
"@slipstream-ui/core": "0.
|
|
34
|
+
"tailwind-merge": "^3.7.0",
|
|
35
|
+
"@slipstream-ui/core": "0.6.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@testing-library/jest-dom": "^7.0.1",
|
|
39
39
|
"@testing-library/react": "^16.0.0",
|
|
40
|
-
"@testing-library/user-event": "^14.
|
|
41
|
-
"@types/react": "^
|
|
40
|
+
"@testing-library/user-event": "^14.6.8",
|
|
41
|
+
"@types/react": "^19.0.0",
|
|
42
42
|
"eslint": "^8.56.0",
|
|
43
|
-
"jest-axe": "^
|
|
43
|
+
"jest-axe": "^11.0.0",
|
|
44
44
|
"jsdom": "^30.1.3",
|
|
45
|
-
"tsup": "^
|
|
45
|
+
"tsup": "^9.0.0",
|
|
46
46
|
"typescript": "^5.3.3",
|
|
47
47
|
"vitest": "^5.0.5",
|
|
48
|
-
"@slipstream-ui/eslint-config": "0.0.1",
|
|
49
48
|
"@slipstream-ui/tailwind-config": "0.0.1",
|
|
50
|
-
"@slipstream-ui/
|
|
51
|
-
"@slipstream-ui/vitest-config": "0.0.1"
|
|
49
|
+
"@slipstream-ui/eslint-config": "0.0.1",
|
|
50
|
+
"@slipstream-ui/vitest-config": "0.0.1",
|
|
51
|
+
"@slipstream-ui/typescript-config": "0.0.1"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -93,3 +93,24 @@ describe('CommandPalette async commands', () => {
|
|
|
93
93
|
expect(user).toBeTruthy();
|
|
94
94
|
});
|
|
95
95
|
});
|
|
96
|
+
|
|
97
|
+
describe('localized labels', () => {
|
|
98
|
+
it('uses the dialog, list and empty label overrides', async () => {
|
|
99
|
+
const user = userEvent.setup();
|
|
100
|
+
render(
|
|
101
|
+
<CommandPalette
|
|
102
|
+
open
|
|
103
|
+
onOpenChange={() => undefined}
|
|
104
|
+
commands={commands}
|
|
105
|
+
onSelect={() => undefined}
|
|
106
|
+
dialogLabel="Palette de commandes"
|
|
107
|
+
listLabel="Commandes FR"
|
|
108
|
+
emptyText="Aucune commande"
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
expect(screen.getByRole('combobox', { name: 'Palette de commandes' })).toBeInTheDocument();
|
|
112
|
+
expect(screen.getByRole('listbox', { name: 'Commandes FR' })).toBeInTheDocument();
|
|
113
|
+
await user.type(screen.getByRole('combobox'), 'zzz');
|
|
114
|
+
expect(screen.getByText('Aucune commande')).toBeInTheDocument();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -32,6 +32,12 @@ export type CommandPaletteProps = {
|
|
|
32
32
|
/** Shown while async commands load. */
|
|
33
33
|
loadingText?: string;
|
|
34
34
|
placeholder?: string;
|
|
35
|
+
/** Accessible label for the combobox. Default 'Command palette'. */
|
|
36
|
+
dialogLabel?: string;
|
|
37
|
+
/** Accessible label for the listbox. Default 'Commands'. */
|
|
38
|
+
listLabel?: string;
|
|
39
|
+
/** Shown when no command matches. Default 'No matching commands.'. */
|
|
40
|
+
emptyText?: string;
|
|
35
41
|
};
|
|
36
42
|
|
|
37
43
|
/**
|
|
@@ -49,6 +55,9 @@ export function CommandPalette({
|
|
|
49
55
|
renderCommand,
|
|
50
56
|
loadingText = 'Searching…',
|
|
51
57
|
placeholder = 'Type a command.',
|
|
58
|
+
dialogLabel = 'Command palette',
|
|
59
|
+
listLabel = 'Commands',
|
|
60
|
+
emptyText = 'No matching commands.',
|
|
52
61
|
}: CommandPaletteProps) {
|
|
53
62
|
const [query, setQuery] = React.useState('');
|
|
54
63
|
const [activeIndex, setActiveIndex] = React.useState(0);
|
|
@@ -143,7 +152,7 @@ export function CommandPalette({
|
|
|
143
152
|
aria-controls={listboxId}
|
|
144
153
|
aria-activedescendant={filtered.length > 0 ? optionId(activeIndex) : undefined}
|
|
145
154
|
aria-autocomplete="list"
|
|
146
|
-
aria-label=
|
|
155
|
+
aria-label={dialogLabel}
|
|
147
156
|
placeholder={placeholder}
|
|
148
157
|
value={query}
|
|
149
158
|
onChange={(event) => {
|
|
@@ -156,7 +165,7 @@ export function CommandPalette({
|
|
|
156
165
|
<ul
|
|
157
166
|
id={listboxId}
|
|
158
167
|
role="listbox"
|
|
159
|
-
aria-label=
|
|
168
|
+
aria-label={listLabel}
|
|
160
169
|
aria-busy={loading || undefined}
|
|
161
170
|
className="max-h-72 overflow-y-auto py-1"
|
|
162
171
|
>
|
|
@@ -167,7 +176,7 @@ export function CommandPalette({
|
|
|
167
176
|
)}
|
|
168
177
|
{!loading && filtered.length === 0 && (
|
|
169
178
|
<li role="presentation" className="px-4 py-3 text-sm text-muted-foreground">
|
|
170
|
-
|
|
179
|
+
{emptyText}
|
|
171
180
|
</li>
|
|
172
181
|
)}
|
|
173
182
|
{filtered.map((command, index) => {
|
|
@@ -78,3 +78,18 @@ describe('Message', () => {
|
|
|
78
78
|
expect(await axe(container)).toHaveNoViolations();
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
|
+
|
|
82
|
+
describe('timestamp formatting', () => {
|
|
83
|
+
const timestamp = new Date('2026-11-07T15:04:05.000Z');
|
|
84
|
+
const timedMessage: MessageType = { ...assistantMessage, timestamp };
|
|
85
|
+
|
|
86
|
+
it('uses a custom formatTime', () => {
|
|
87
|
+
render(<Message message={timedMessage} formatTime={(date) => `T:${date.toISOString()}`} />);
|
|
88
|
+
expect(screen.getByText('T:2026-11-07T15:04:05.000Z')).toBeInTheDocument();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('defaults to the runtime locale formatter', () => {
|
|
92
|
+
render(<Message message={timedMessage} />);
|
|
93
|
+
expect(screen.getByText(timestamp.toLocaleTimeString())).toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -15,6 +15,10 @@ export type MessageProps = {
|
|
|
15
15
|
userLabel?: string;
|
|
16
16
|
/** Display name for the assistant role; override for localization. */
|
|
17
17
|
assistantLabel?: string;
|
|
18
|
+
/** Formats the message timestamp; defaults to the runtime's toLocaleTimeString(). */
|
|
19
|
+
formatTime?: (date: Date) => string;
|
|
20
|
+
/** BCP-47 locale used by the default timestamp formatter. */
|
|
21
|
+
locale?: string;
|
|
18
22
|
/**
|
|
19
23
|
* Custom content renderer, e.g. for markdown/code blocks.
|
|
20
24
|
* When provided, it fully replaces the default plain-text rendering.
|
|
@@ -52,6 +56,8 @@ const Message = React.forwardRef<HTMLDivElement, MessageProps>(
|
|
|
52
56
|
avatarFallback,
|
|
53
57
|
userLabel = 'You',
|
|
54
58
|
assistantLabel = 'Assistant',
|
|
59
|
+
formatTime,
|
|
60
|
+
locale,
|
|
55
61
|
renderContent,
|
|
56
62
|
isStreaming = false,
|
|
57
63
|
error,
|
|
@@ -121,7 +127,9 @@ const Message = React.forwardRef<HTMLDivElement, MessageProps>(
|
|
|
121
127
|
<span className="text-sm font-medium">{displayLabel}</span>
|
|
122
128
|
{message.timestamp && (
|
|
123
129
|
<span className="text-xs text-muted-foreground">
|
|
124
|
-
{
|
|
130
|
+
{formatTime
|
|
131
|
+
? formatTime(new Date(message.timestamp))
|
|
132
|
+
: new Date(message.timestamp).toLocaleTimeString(locale)}
|
|
125
133
|
</span>
|
|
126
134
|
)}
|
|
127
135
|
</div>
|
|
@@ -56,3 +56,18 @@ describe('Reasoning', () => {
|
|
|
56
56
|
expect(await axe(container)).toHaveNoViolations();
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
|
+
|
|
60
|
+
describe('timestamp formatting', () => {
|
|
61
|
+
const timestamp = new Date('2026-11-07T15:04:05.000Z');
|
|
62
|
+
const timedSteps = [{ id: 's1', content: 'Parse the question', timestamp }];
|
|
63
|
+
|
|
64
|
+
it('uses a custom formatTime', () => {
|
|
65
|
+
render(<Reasoning steps={timedSteps} defaultExpanded formatTime={(date) => `T:${date.toISOString()}`} />);
|
|
66
|
+
expect(screen.getByText('T:2026-11-07T15:04:05.000Z')).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('defaults to the runtime locale formatter', () => {
|
|
70
|
+
render(<Reasoning steps={timedSteps} defaultExpanded />);
|
|
71
|
+
expect(screen.getByText(timestamp.toLocaleTimeString())).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -13,6 +13,10 @@ export type ReasoningProps = {
|
|
|
13
13
|
duration?: number | string;
|
|
14
14
|
/** Marks reasoning as still arriving: animated header state. */
|
|
15
15
|
isStreaming?: boolean;
|
|
16
|
+
/** Formats step timestamps; defaults to the runtime's toLocaleTimeString(). */
|
|
17
|
+
formatTime?: (date: Date) => string;
|
|
18
|
+
/** BCP-47 locale used by the default timestamp formatter. */
|
|
19
|
+
locale?: string;
|
|
16
20
|
/** Localized header, count and step labels. */
|
|
17
21
|
labels?: {
|
|
18
22
|
title?: string;
|
|
@@ -23,7 +27,7 @@ export type ReasoningProps = {
|
|
|
23
27
|
};
|
|
24
28
|
|
|
25
29
|
const Reasoning = React.forwardRef<HTMLDivElement, ReasoningProps>(
|
|
26
|
-
({ steps, className, collapsible = true, defaultExpanded = false, duration, isStreaming = false, labels, ...props }, ref) => {
|
|
30
|
+
({ steps, className, collapsible = true, defaultExpanded = false, duration, isStreaming = false, labels, formatTime, locale, ...props }, ref) => {
|
|
27
31
|
const [isExpanded, setIsExpanded] = React.useState(defaultExpanded);
|
|
28
32
|
const contentId = React.useId();
|
|
29
33
|
|
|
@@ -75,7 +79,9 @@ const Reasoning = React.forwardRef<HTMLDivElement, ReasoningProps>(
|
|
|
75
79
|
<div className="text-muted-foreground">{step.content}</div>
|
|
76
80
|
{step.timestamp && (
|
|
77
81
|
<div className="text-xs text-muted-foreground mt-1">
|
|
78
|
-
{
|
|
82
|
+
{formatTime
|
|
83
|
+
? formatTime(new Date(step.timestamp))
|
|
84
|
+
: new Date(step.timestamp).toLocaleTimeString(locale)}
|
|
79
85
|
</div>
|
|
80
86
|
)}
|
|
81
87
|
</div>
|
|
@@ -5,9 +5,24 @@
|
|
|
5
5
|
"description": "File chips for prompts and messages. `Attachment` renders one file; `AttachmentList` renders a wrapping row. Display-only: upload and file-system logic stay in the consumer.",
|
|
6
6
|
"designIntent": "Show what files are part of a conversation turn (name, size, type, thumbnail, upload status) without coupling the library to any upload implementation.",
|
|
7
7
|
"props": [
|
|
8
|
-
{
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
{
|
|
9
|
+
"name": "attachment",
|
|
10
|
+
"type": "AttachmentItem",
|
|
11
|
+
"required": true,
|
|
12
|
+
"description": "File descriptor: id, name, size, type, previewUrl, status."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "attachments",
|
|
16
|
+
"type": "AttachmentItem[]",
|
|
17
|
+
"required": true,
|
|
18
|
+
"description": "Files to display (AttachmentList)."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "onRemove",
|
|
22
|
+
"type": "(attachment) => void",
|
|
23
|
+
"required": false,
|
|
24
|
+
"description": "Remove callback; omit to hide remove controls."
|
|
25
|
+
}
|
|
11
26
|
],
|
|
12
27
|
"examples": [
|
|
13
28
|
{
|
|
@@ -20,16 +35,28 @@
|
|
|
20
35
|
}
|
|
21
36
|
],
|
|
22
37
|
"antiPatterns": [
|
|
23
|
-
{
|
|
38
|
+
{
|
|
39
|
+
"title": "Do not use Attachment for drag-and-drop targets",
|
|
40
|
+
"explanation": "The chip displays an existing item; build dropzones in the consumer and render AttachmentList once items exist."
|
|
41
|
+
}
|
|
24
42
|
],
|
|
25
43
|
"accessibility": [
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
{
|
|
45
|
+
"feature": "Remove control",
|
|
46
|
+
"implementation": "Icon-only remove button has an aria-label including the file name."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"feature": "Upload status",
|
|
50
|
+
"implementation": "Uploading state is announced via role=status."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"feature": "Decorative thumbnails",
|
|
54
|
+
"implementation": "Preview images are aria-hidden; the file name carries the meaning."
|
|
55
|
+
}
|
|
29
56
|
],
|
|
30
57
|
"sourceDir": "attachment",
|
|
31
58
|
"exportName": "Attachment",
|
|
32
59
|
"package": "@slipstream-ui/ai",
|
|
33
60
|
"importPath": "@slipstream-ui/ai",
|
|
34
|
-
"version": "0.
|
|
61
|
+
"version": "0.6.1"
|
|
35
62
|
}
|