@slipstream-ui/ai 0.4.0 → 0.6.0
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 +51 -4
- package/dist/index.d.ts +51 -4
- package/dist/index.js +244 -113
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +246 -115
- package/dist/index.mjs.map +1 -1
- package/dist/manifests/attachment.manifest.json +37 -8
- package/dist/manifests/branch.manifest.json +3 -1
- package/dist/manifests/chain-thought.manifest.json +1 -1
- package/dist/manifests/code-block.manifest.json +3 -1
- package/dist/manifests/command-palette.manifest.json +59 -4
- package/dist/manifests/confirmation.manifest.json +3 -1
- package/dist/manifests/conversation.manifest.json +1 -1
- package/dist/manifests/inline-citation.manifest.json +3 -1
- package/dist/manifests/message-actions.manifest.json +61 -10
- package/dist/manifests/message.manifest.json +13 -1
- package/dist/manifests/prompt-input.manifest.json +2 -2
- package/dist/manifests/reasoning.manifest.json +19 -1
- package/dist/manifests/response.manifest.json +3 -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 +7 -1
- package/package.json +10 -10
- package/src/components/code-block/CodeBlock.tsx +91 -5
- package/src/components/command-palette/CommandPalette.test.tsx +42 -0
- package/src/components/command-palette/CommandPalette.tsx +82 -8
- package/src/components/confirmation/Confirmation.tsx +1 -1
- package/src/components/conversation/Conversation.tsx +6 -1
- 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 +20 -7
- package/src/components/tool-call/ToolCall.tsx +24 -8
- package/src/manifests/attachment.manifest.json +37 -8
- package/src/manifests/branch.manifest.json +3 -1
- package/src/manifests/chain-thought.manifest.json +1 -1
- package/src/manifests/code-block.manifest.json +3 -1
- package/src/manifests/command-palette.manifest.json +59 -4
- package/src/manifests/confirmation.manifest.json +3 -1
- package/src/manifests/conversation.manifest.json +1 -1
- package/src/manifests/inline-citation.manifest.json +3 -1
- package/src/manifests/message-actions.manifest.json +61 -10
- package/src/manifests/message.manifest.json +13 -1
- package/src/manifests/prompt-input.manifest.json +2 -2
- package/src/manifests/reasoning.manifest.json +19 -1
- package/src/manifests/response.manifest.json +3 -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 +7 -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,14 +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
|
],
|
|
57
|
+
"sourceDir": "attachment",
|
|
58
|
+
"exportName": "Attachment",
|
|
30
59
|
"package": "@slipstream-ui/ai",
|
|
31
60
|
"importPath": "@slipstream-ui/ai",
|
|
32
|
-
"version": "0.0
|
|
61
|
+
"version": "0.6.0"
|
|
33
62
|
}
|
|
@@ -44,7 +44,9 @@
|
|
|
44
44
|
"implementation": "Icon-labeled button with aria-label that flips to Copied."
|
|
45
45
|
}
|
|
46
46
|
],
|
|
47
|
+
"sourceDir": "code-block",
|
|
48
|
+
"exportName": "CodeBlock",
|
|
47
49
|
"package": "@slipstream-ui/ai",
|
|
48
50
|
"importPath": "@slipstream-ui/ai",
|
|
49
|
-
"version": "0.0
|
|
51
|
+
"version": "0.6.0"
|
|
50
52
|
}
|
|
@@ -22,7 +22,13 @@
|
|
|
22
22
|
"type": "Command[]",
|
|
23
23
|
"required": true,
|
|
24
24
|
"description": "Available commands: { id, label, section?, keywords?, shortcut? }",
|
|
25
|
-
"examples": [
|
|
25
|
+
"examples": [
|
|
26
|
+
{
|
|
27
|
+
"id": "go-docs",
|
|
28
|
+
"label": "Open documentation",
|
|
29
|
+
"section": "Navigation"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
26
32
|
},
|
|
27
33
|
{
|
|
28
34
|
"name": "onSelect",
|
|
@@ -33,9 +39,55 @@
|
|
|
33
39
|
{
|
|
34
40
|
"name": "placeholder",
|
|
35
41
|
"type": "string",
|
|
36
|
-
"defaultValue": "Type a command
|
|
42
|
+
"defaultValue": "Type a command.",
|
|
37
43
|
"required": false,
|
|
38
44
|
"description": "Input placeholder text"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "loadCommands",
|
|
48
|
+
"type": "(query: string) => Promise<Command[]>",
|
|
49
|
+
"required": false,
|
|
50
|
+
"description": "Async command source; debounced and stale-response guarded."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "debounceMs",
|
|
54
|
+
"type": "number",
|
|
55
|
+
"required": false,
|
|
56
|
+
"defaultValue": "200",
|
|
57
|
+
"description": "Debounce for loadCommands in ms."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "renderCommand",
|
|
61
|
+
"type": "(command, state: { active: boolean }) => ReactNode",
|
|
62
|
+
"required": false,
|
|
63
|
+
"description": "Custom row renderer (icons, type badges)."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "loadingText",
|
|
67
|
+
"type": "string",
|
|
68
|
+
"required": false,
|
|
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."
|
|
39
91
|
}
|
|
40
92
|
],
|
|
41
93
|
"examples": [
|
|
@@ -71,10 +123,13 @@
|
|
|
71
123
|
"relationship": "CommandPalette switches modes/actions; PromptInput captures free-form chat text."
|
|
72
124
|
}
|
|
73
125
|
],
|
|
74
|
-
"relatedComponents": [
|
|
126
|
+
"relatedComponents": [
|
|
127
|
+
"PromptInput",
|
|
128
|
+
"Dialog"
|
|
129
|
+
],
|
|
75
130
|
"package": "@slipstream-ui/ai",
|
|
76
131
|
"importPath": "@slipstream-ui/ai",
|
|
77
|
-
"version": "0.0
|
|
132
|
+
"version": "0.6.0",
|
|
78
133
|
"sourceDir": "command-palette",
|
|
79
134
|
"exportName": "CommandPalette"
|
|
80
135
|
}
|
|
@@ -137,7 +137,9 @@
|
|
|
137
137
|
"implementation": "Status chip text announces Approved/Rejected."
|
|
138
138
|
}
|
|
139
139
|
],
|
|
140
|
+
"sourceDir": "confirmation",
|
|
141
|
+
"exportName": "Confirmation",
|
|
140
142
|
"package": "@slipstream-ui/ai",
|
|
141
143
|
"importPath": "@slipstream-ui/ai",
|
|
142
|
-
"version": "0.0
|
|
144
|
+
"version": "0.6.0"
|
|
143
145
|
}
|
|
@@ -40,7 +40,9 @@
|
|
|
40
40
|
"implementation": "Card has role=tooltip linked via aria-describedby."
|
|
41
41
|
}
|
|
42
42
|
],
|
|
43
|
+
"sourceDir": "inline-citation",
|
|
44
|
+
"exportName": "InlineCitation",
|
|
43
45
|
"package": "@slipstream-ui/ai",
|
|
44
46
|
"importPath": "@slipstream-ui/ai",
|
|
45
|
-
"version": "0.0
|
|
47
|
+
"version": "0.6.0"
|
|
46
48
|
}
|
|
@@ -5,11 +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
|
-
|
|
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
|
+
}
|
|
13
50
|
],
|
|
14
51
|
"examples": [
|
|
15
52
|
{
|
|
@@ -22,14 +59,28 @@
|
|
|
22
59
|
}
|
|
23
60
|
],
|
|
24
61
|
"antiPatterns": [
|
|
25
|
-
{
|
|
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
|
+
}
|
|
26
66
|
],
|
|
27
67
|
"accessibility": [
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
}
|
|
31
80
|
],
|
|
81
|
+
"sourceDir": "message-actions",
|
|
82
|
+
"exportName": "MessageActions",
|
|
32
83
|
"package": "@slipstream-ui/ai",
|
|
33
84
|
"importPath": "@slipstream-ui/ai",
|
|
34
|
-
"version": "0.0
|
|
85
|
+
"version": "0.6.0"
|
|
35
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.0
|
|
244
|
+
"version": "0.6.0",
|
|
233
245
|
"sourceDir": "message",
|
|
234
246
|
"exportName": "Message"
|
|
235
247
|
}
|
|
@@ -53,6 +53,24 @@
|
|
|
53
53
|
"type": "boolean",
|
|
54
54
|
"required": false,
|
|
55
55
|
"description": "Shows a streaming indicator while reasoning is in progress"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "labels",
|
|
59
|
+
"type": "{ title?: string; count?: (count: number) => string; thinking?: string; step?: (index: number) => string }",
|
|
60
|
+
"required": false,
|
|
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."
|
|
56
74
|
}
|
|
57
75
|
],
|
|
58
76
|
"examples": [
|
|
@@ -171,7 +189,7 @@
|
|
|
171
189
|
],
|
|
172
190
|
"package": "@slipstream-ui/ai",
|
|
173
191
|
"importPath": "@slipstream-ui/ai",
|
|
174
|
-
"version": "0.0
|
|
192
|
+
"version": "0.6.0",
|
|
175
193
|
"sourceDir": "reasoning",
|
|
176
194
|
"exportName": "Reasoning"
|
|
177
195
|
}
|
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
"implementation": "Routed through CodeBlock with accessible copy button."
|
|
46
46
|
}
|
|
47
47
|
],
|
|
48
|
+
"sourceDir": "response",
|
|
49
|
+
"exportName": "Response",
|
|
48
50
|
"package": "@slipstream-ui/ai",
|
|
49
51
|
"importPath": "@slipstream-ui/ai",
|
|
50
|
-
"version": "0.0
|
|
52
|
+
"version": "0.6.0"
|
|
51
53
|
}
|
|
@@ -30,6 +30,12 @@
|
|
|
30
30
|
"type": "string",
|
|
31
31
|
"required": false,
|
|
32
32
|
"description": "Additional CSS classes to apply"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "labels",
|
|
36
|
+
"type": "{ pending?: string; running?: string; completed?: string; failed?: string; arguments?: string; result?: string; error?: string }",
|
|
37
|
+
"required": false,
|
|
38
|
+
"description": "Localized status and section labels."
|
|
33
39
|
}
|
|
34
40
|
],
|
|
35
41
|
"examples": [
|
|
@@ -152,7 +158,7 @@
|
|
|
152
158
|
],
|
|
153
159
|
"package": "@slipstream-ui/ai",
|
|
154
160
|
"importPath": "@slipstream-ui/ai",
|
|
155
|
-
"version": "0.0
|
|
161
|
+
"version": "0.6.0",
|
|
156
162
|
"sourceDir": "tool-call",
|
|
157
163
|
"exportName": "ToolCall"
|
|
158
164
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slipstream-ui/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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.0"
|
|
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": "^
|
|
44
|
-
"jsdom": "^
|
|
45
|
-
"tsup": "^
|
|
43
|
+
"jest-axe": "^11.0.0",
|
|
44
|
+
"jsdom": "^30.1.3",
|
|
45
|
+
"tsup": "^9.0.0",
|
|
46
46
|
"typescript": "^5.3.3",
|
|
47
47
|
"vitest": "^5.0.5",
|
|
48
48
|
"@slipstream-ui/eslint-config": "0.0.1",
|
|
49
49
|
"@slipstream-ui/tailwind-config": "0.0.1",
|
|
50
|
-
"@slipstream-ui/
|
|
51
|
-
"@slipstream-ui/
|
|
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",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import * as React from 'react';
|
|
4
|
-
import { Highlight,
|
|
4
|
+
import { Highlight, type PrismTheme } from 'prism-react-renderer';
|
|
5
5
|
import { cn } from '@slipstream-ui/core';
|
|
6
6
|
|
|
7
7
|
export type CodeBlockProps = {
|
|
@@ -29,12 +29,95 @@ function useIsDark(): boolean {
|
|
|
29
29
|
return dark;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* AA-safe syntax palettes.
|
|
34
|
+
*
|
|
35
|
+
* prism-react-renderer's stock themes put comments (#999988) and constants
|
|
36
|
+
* (#36acaa) at ~2.7-2.9:1 on the code surface. Every colour below clears
|
|
37
|
+
* 4.5:1 against its background (white light / near-black dark).
|
|
38
|
+
*/
|
|
39
|
+
const LIGHT_CODE_THEME: PrismTheme = {
|
|
40
|
+
plain: { color: '#1f2328', backgroundColor: 'transparent' },
|
|
41
|
+
styles: [
|
|
42
|
+
{ types: ['comment', 'prolog', 'doctype', 'cdata'], style: { color: '#57606a' } },
|
|
43
|
+
{ types: ['punctuation', 'operator'], style: { color: '#24292f' } },
|
|
44
|
+
{ types: ['namespace'], style: { color: '#6f42c1' } },
|
|
45
|
+
{ types: ['tag', 'deleted', 'keyword', 'atrule', 'important'], style: { color: '#b31d28' } },
|
|
46
|
+
{ types: ['attr-name', 'variable'], style: { color: '#953800' } },
|
|
47
|
+
{ types: ['string', 'char', 'attr-value', 'inserted', 'regex'], style: { color: '#0a5c2f' } },
|
|
48
|
+
{
|
|
49
|
+
types: [
|
|
50
|
+
'function',
|
|
51
|
+
'class-name',
|
|
52
|
+
'number',
|
|
53
|
+
'boolean',
|
|
54
|
+
'constant',
|
|
55
|
+
'symbol',
|
|
56
|
+
'property',
|
|
57
|
+
'selector',
|
|
58
|
+
'builtin',
|
|
59
|
+
'entity',
|
|
60
|
+
'url',
|
|
61
|
+
],
|
|
62
|
+
style: { color: '#0550ae' },
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const DARK_CODE_THEME: PrismTheme = {
|
|
68
|
+
plain: { color: '#e6edf3', backgroundColor: 'transparent' },
|
|
69
|
+
styles: [
|
|
70
|
+
{ types: ['comment', 'prolog', 'doctype', 'cdata'], style: { color: '#8b949e' } },
|
|
71
|
+
{ types: ['punctuation', 'operator'], style: { color: '#c9d1d9' } },
|
|
72
|
+
{ types: ['namespace'], style: { color: '#d2a8ff' } },
|
|
73
|
+
{ types: ['tag', 'deleted', 'keyword', 'atrule', 'important'], style: { color: '#ff7b72' } },
|
|
74
|
+
{ types: ['attr-name', 'variable'], style: { color: '#ffa657' } },
|
|
75
|
+
{ types: ['string', 'char', 'attr-value', 'inserted', 'regex'], style: { color: '#7ee787' } },
|
|
76
|
+
{
|
|
77
|
+
types: [
|
|
78
|
+
'function',
|
|
79
|
+
'class-name',
|
|
80
|
+
'number',
|
|
81
|
+
'boolean',
|
|
82
|
+
'constant',
|
|
83
|
+
'symbol',
|
|
84
|
+
'property',
|
|
85
|
+
'selector',
|
|
86
|
+
'builtin',
|
|
87
|
+
'entity',
|
|
88
|
+
'url',
|
|
89
|
+
],
|
|
90
|
+
style: { color: '#79c0ff' },
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
|
|
32
95
|
const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
|
33
96
|
({ code, language = 'tsx', showCopyButton = true, className, ...props }, ref) => {
|
|
34
97
|
const [copied, setCopied] = React.useState(false);
|
|
35
98
|
const isDark = useIsDark();
|
|
36
99
|
const trimmedCode = code.replace(/\n$/, '');
|
|
37
100
|
|
|
101
|
+
// Horizontally scrollable code must be reachable by keyboard (WCAG 2.1.1).
|
|
102
|
+
// Only make it a tab stop when it actually overflows, so short snippets
|
|
103
|
+
// do not add noise to the tab order.
|
|
104
|
+
const preRef = React.useRef<HTMLPreElement>(null);
|
|
105
|
+
const [scrollable, setScrollable] = React.useState(false);
|
|
106
|
+
React.useEffect(() => {
|
|
107
|
+
const element = preRef.current;
|
|
108
|
+
if (!element) return;
|
|
109
|
+
const update = () => setScrollable(element.scrollWidth > element.clientWidth + 1);
|
|
110
|
+
update();
|
|
111
|
+
// jsdom (tests) has no ResizeObserver; window resize is the fallback.
|
|
112
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
113
|
+
window.addEventListener('resize', update);
|
|
114
|
+
return () => window.removeEventListener('resize', update);
|
|
115
|
+
}
|
|
116
|
+
const observer = new ResizeObserver(update);
|
|
117
|
+
observer.observe(element);
|
|
118
|
+
return () => observer.disconnect();
|
|
119
|
+
}, [trimmedCode, language]);
|
|
120
|
+
|
|
38
121
|
const handleCopy = async () => {
|
|
39
122
|
try {
|
|
40
123
|
await navigator.clipboard.writeText(trimmedCode);
|
|
@@ -50,8 +133,8 @@ const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
|
|
50
133
|
<div
|
|
51
134
|
ref={ref}
|
|
52
135
|
className={cn(
|
|
53
|
-
// bg-background (not muted):
|
|
54
|
-
// surface
|
|
136
|
+
// bg-background (not muted): the light syntax palette is tuned for
|
|
137
|
+
// a white surface; on gray the token contrast drops below AA.
|
|
55
138
|
'relative group rounded-md border border-border bg-background overflow-hidden',
|
|
56
139
|
className
|
|
57
140
|
)}
|
|
@@ -70,10 +153,13 @@ const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
|
|
70
153
|
</button>
|
|
71
154
|
)}
|
|
72
155
|
</div>
|
|
73
|
-
<Highlight theme={isDark ?
|
|
156
|
+
<Highlight theme={isDark ? DARK_CODE_THEME : LIGHT_CODE_THEME} code={trimmedCode} language={language}>
|
|
74
157
|
{({ style, tokens, getLineProps, getTokenProps }) => (
|
|
75
158
|
<pre
|
|
76
|
-
|
|
159
|
+
ref={preRef}
|
|
160
|
+
tabIndex={scrollable ? 0 : undefined}
|
|
161
|
+
aria-label={scrollable ? `${language} code, scrollable` : undefined}
|
|
162
|
+
className="overflow-x-auto p-4 text-xs leading-relaxed m-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
77
163
|
style={{ ...style, background: 'transparent' }}
|
|
78
164
|
>
|
|
79
165
|
{tokens.map((line, i) => (
|