banhaten 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.
Files changed (214) hide show
  1. package/CHANGELOG.md +78 -1
  2. package/CONTRIBUTING.md +6 -4
  3. package/README.md +4 -2
  4. package/docs/design-spacing.md +13 -0
  5. package/docs/design-system/README.md +2 -0
  6. package/docs/design-system/appearances/default.md +1 -1
  7. package/docs/design-system/appearances/rounded.md +1 -1
  8. package/docs/design-system/appearances/sharp.md +1 -1
  9. package/docs/design-system/component-showcase-consistency-report.md +5 -5
  10. package/docs/design-system/components/README.md +15 -0
  11. package/docs/design-system/components/accordion.md +1 -1
  12. package/docs/design-system/components/activity-feed.md +1 -1
  13. package/docs/design-system/components/activity-heatmap.md +1 -1
  14. package/docs/design-system/components/agent-input.md +85 -0
  15. package/docs/design-system/components/alert-dialog.md +1 -1
  16. package/docs/design-system/components/alert.md +10 -4
  17. package/docs/design-system/components/app-header.md +97 -0
  18. package/docs/design-system/components/approval-card.md +134 -0
  19. package/docs/design-system/components/attribute.md +14 -2
  20. package/docs/design-system/components/avatar.md +1 -1
  21. package/docs/design-system/components/badge.md +1 -1
  22. package/docs/design-system/components/banner.md +1 -1
  23. package/docs/design-system/components/breadcrumbs.md +8 -3
  24. package/docs/design-system/components/button-group.md +1 -1
  25. package/docs/design-system/components/button.md +8 -5
  26. package/docs/design-system/components/card.md +15 -1
  27. package/docs/design-system/components/catalog-components.md +1 -1
  28. package/docs/design-system/components/chart.md +95 -24
  29. package/docs/design-system/components/checkbox.md +1 -1
  30. package/docs/design-system/components/collapsible.md +20 -6
  31. package/docs/design-system/components/command.md +1 -1
  32. package/docs/design-system/components/context-menu.md +1 -1
  33. package/docs/design-system/components/date-picker.md +1 -1
  34. package/docs/design-system/components/file-upload.md +1 -1
  35. package/docs/design-system/components/hover-card.md +1 -1
  36. package/docs/design-system/components/inline-citations.md +93 -0
  37. package/docs/design-system/components/input-group.md +1 -1
  38. package/docs/design-system/components/input.md +59 -3
  39. package/docs/design-system/components/item.md +1 -1
  40. package/docs/design-system/components/kbd.md +6 -4
  41. package/docs/design-system/components/menu.md +35 -4
  42. package/docs/design-system/components/menubar.md +1 -1
  43. package/docs/design-system/components/modal.md +2 -2
  44. package/docs/design-system/components/native-select.md +1 -1
  45. package/docs/design-system/components/navigation-menu.md +1 -1
  46. package/docs/design-system/components/onboarding-step-list-item.md +1 -1
  47. package/docs/design-system/components/page-header.md +58 -17
  48. package/docs/design-system/components/pagination.md +2 -2
  49. package/docs/design-system/components/popover.md +1 -1
  50. package/docs/design-system/components/progress-slider.md +1 -1
  51. package/docs/design-system/components/radio-card.md +1 -1
  52. package/docs/design-system/components/radio-group.md +1 -1
  53. package/docs/design-system/components/resizable.md +1 -1
  54. package/docs/design-system/components/rolling-count.md +61 -0
  55. package/docs/design-system/components/scroll-area.md +1 -1
  56. package/docs/design-system/components/select.md +12 -2
  57. package/docs/design-system/components/sheet.md +1 -1
  58. package/docs/design-system/components/shimmer-text.md +67 -0
  59. package/docs/design-system/components/sidebar.md +75 -8
  60. package/docs/design-system/components/skeleton.md +2 -2
  61. package/docs/design-system/components/slideout.md +1 -1
  62. package/docs/design-system/components/slider.md +2 -2
  63. package/docs/design-system/components/social-button.md +2 -2
  64. package/docs/design-system/components/streaming-text.md +82 -0
  65. package/docs/design-system/components/table.md +103 -21
  66. package/docs/design-system/components/tabs.md +25 -6
  67. package/docs/design-system/components/tag.md +1 -1
  68. package/docs/design-system/components/task-list.md +95 -0
  69. package/docs/design-system/components/textarea.md +1 -1
  70. package/docs/design-system/components/thinking-reasoning.md +72 -0
  71. package/docs/design-system/components/thinking-state.md +67 -0
  72. package/docs/design-system/components/timeline.md +1 -1
  73. package/docs/design-system/components/toast.md +1 -1
  74. package/docs/design-system/components/toggle.md +2 -2
  75. package/docs/design-system/components/toolbar.md +11 -6
  76. package/docs/design-system/components/tooltip.md +6 -4
  77. package/docs/design-system/density-and-elevation.md +16 -16
  78. package/docs/design-system/design-system-test-missing-items.md +8 -8
  79. package/docs/design-system/installation.md +2 -0
  80. package/docs/design-system/tabs.md +2 -1
  81. package/docs/design-system/template-gap-audit.md +32 -0
  82. package/docs/design-system/templates.md +59 -0
  83. package/docs/design-system/token-governance.md +15 -2
  84. package/package.json +2 -2
  85. package/registry/components/accordion.tsx +2 -2
  86. package/registry/components/agent-input-parts.tsx +552 -0
  87. package/registry/components/agent-input-state.ts +156 -0
  88. package/registry/components/agent-input.css +307 -0
  89. package/registry/components/agent-input.tsx +382 -0
  90. package/registry/components/alert-dialog.tsx +2 -2
  91. package/registry/components/alert.tsx +23 -7
  92. package/registry/components/app-header.tsx +151 -0
  93. package/registry/components/approval-card.css +409 -0
  94. package/registry/components/approval-card.tsx +401 -0
  95. package/registry/components/approval-frame.tsx +108 -0
  96. package/registry/components/approval-questions.tsx +559 -0
  97. package/registry/components/attribute.tsx +48 -13
  98. package/registry/components/autocomplete.tsx +15 -68
  99. package/registry/components/avatar.tsx +2 -2
  100. package/registry/components/badge.tsx +8 -7
  101. package/registry/components/button-group.tsx +2 -2
  102. package/registry/components/button.tsx +19 -14
  103. package/registry/components/card.tsx +18 -5
  104. package/registry/components/chart-block-data.ts +177 -0
  105. package/registry/components/chart-block.css +171 -0
  106. package/registry/components/chart-block.tsx +260 -0
  107. package/registry/components/chart-interactions.ts +123 -0
  108. package/registry/components/chart-options.cartesian.ts +162 -0
  109. package/registry/components/chart-options.paint.ts +99 -0
  110. package/registry/components/chart-options.parts.ts +72 -0
  111. package/registry/components/chart-options.radial.ts +109 -0
  112. package/registry/components/chart-options.ts +44 -0
  113. package/registry/components/chart-options.types.ts +67 -0
  114. package/registry/components/chart.tsx +1 -1
  115. package/registry/components/checkbox.tsx +6 -6
  116. package/registry/components/collapsible.tsx +43 -3
  117. package/registry/components/command.tsx +1 -1
  118. package/registry/components/context-menu.tsx +1 -1
  119. package/registry/components/data-chart.tsx +40 -0
  120. package/registry/components/date-picker.tsx +9 -9
  121. package/registry/components/echart-theme.ts +246 -0
  122. package/registry/components/echart.css +49 -0
  123. package/registry/components/echart.tsx +178 -0
  124. package/registry/components/expanded/Breadcrumbs.tsx +5 -1
  125. package/registry/components/expanded/PageHeader.tsx +67 -24
  126. package/registry/components/expanded/Table.tsx +60 -22
  127. package/registry/components/expanded/activityFeed.css +2 -2
  128. package/registry/components/expanded/activityHeatmap.css +1 -1
  129. package/registry/components/expanded/banner.css +2 -2
  130. package/registry/components/expanded/breadcrumbs.css +19 -2
  131. package/registry/components/expanded/catalogComponentsShowcase.css +1 -1
  132. package/registry/components/expanded/fileUpload.css +7 -7
  133. package/registry/components/expanded/onboardingStepListItem.css +2 -2
  134. package/registry/components/expanded/pageHeader.css +38 -44
  135. package/registry/components/expanded/slideout.css +4 -4
  136. package/registry/components/expanded/table.css +58 -9
  137. package/registry/components/expanded/timeline.css +1 -1
  138. package/registry/components/hover-card.tsx +1 -1
  139. package/registry/components/inline-citations.css +126 -0
  140. package/registry/components/inline-citations.tsx +292 -0
  141. package/registry/components/input-group.tsx +4 -4
  142. package/registry/components/input.tsx +150 -137
  143. package/registry/components/item.tsx +1 -1
  144. package/registry/components/kbd.tsx +3 -2
  145. package/registry/components/menu.tsx +117 -12
  146. package/registry/components/menubar.tsx +2 -2
  147. package/registry/components/modal.tsx +3 -3
  148. package/registry/components/native-select.tsx +4 -4
  149. package/registry/components/navigation-menu.tsx +2 -2
  150. package/registry/components/pagination.tsx +4 -4
  151. package/registry/components/popover.tsx +3 -3
  152. package/registry/components/progress-slider.tsx +1 -1
  153. package/registry/components/radio-card.tsx +7 -7
  154. package/registry/components/radio-group.tsx +2 -2
  155. package/registry/components/resizable.tsx +2 -2
  156. package/registry/components/rolling-count.css +37 -0
  157. package/registry/components/rolling-count.tsx +110 -0
  158. package/registry/components/scroll-area.tsx +1 -1
  159. package/registry/components/select.tsx +15 -10
  160. package/registry/components/sheet.tsx +2 -2
  161. package/registry/components/shimmer-text.css +69 -0
  162. package/registry/components/shimmer-text.tsx +43 -0
  163. package/registry/components/sidebar.tsx +528 -223
  164. package/registry/components/skeleton.tsx +1 -1
  165. package/registry/components/slider.tsx +2 -2
  166. package/registry/components/social-button.tsx +1 -1
  167. package/registry/components/streaming-text.css +45 -0
  168. package/registry/components/streaming-text.tsx +165 -0
  169. package/registry/components/table-elements.tsx +164 -5
  170. package/registry/components/tabs.css +32 -4
  171. package/registry/components/tabs.tsx +5 -1
  172. package/registry/components/tag.tsx +1 -1
  173. package/registry/components/task-list.css +328 -0
  174. package/registry/components/task-list.tsx +429 -0
  175. package/registry/components/textarea.tsx +4 -4
  176. package/registry/components/thinking-reasoning.css +178 -0
  177. package/registry/components/thinking-reasoning.tsx +253 -0
  178. package/registry/components/thinking-state.css +8 -0
  179. package/registry/components/thinking-state.tsx +46 -0
  180. package/registry/components/toast.tsx +1 -1
  181. package/registry/components/toggle.tsx +2 -2
  182. package/registry/components/toolbar.tsx +6 -6
  183. package/registry/components/tooltip.tsx +6 -4
  184. package/registry/examples/agent-input-demo.tsx +90 -0
  185. package/registry/examples/app-header-demo.tsx +40 -0
  186. package/registry/examples/approval-card-demo.tsx +83 -0
  187. package/registry/examples/button-demo.tsx +3 -0
  188. package/registry/examples/chart-block-demo.tsx +5 -0
  189. package/registry/examples/chart-demo.tsx +6 -21
  190. package/registry/examples/collection-filter-bar-demo.tsx +1 -1
  191. package/registry/examples/control-density-contract-demo.tsx +5 -1
  192. package/registry/examples/inline-citations-demo.tsx +31 -0
  193. package/registry/examples/input-actions-demo.tsx +36 -0
  194. package/registry/examples/menu-demo.tsx +1 -1
  195. package/registry/examples/popover-demo.tsx +1 -0
  196. package/registry/examples/rolling-count-demo.tsx +39 -0
  197. package/registry/examples/shimmer-text-demo.tsx +20 -0
  198. package/registry/examples/streaming-text-demo.tsx +28 -0
  199. package/registry/examples/task-list-demo.tsx +74 -0
  200. package/registry/examples/thinking-reasoning-demo.tsx +74 -0
  201. package/registry/examples/thinking-state-demo.tsx +13 -0
  202. package/registry/guidance/charts.LICENSE.txt +24 -0
  203. package/registry/hooks/use-controllable-state.ts +33 -0
  204. package/registry/hooks/use-listbox-navigation.ts +73 -0
  205. package/registry/hooks/use-motion-end.ts +55 -0
  206. package/registry/hooks/use-reduced-motion.ts +28 -0
  207. package/registry/index.json +1808 -190
  208. package/registry/styles/globals.css +992 -1088
  209. package/registry.json +385 -10
  210. package/schema/registry.schema.json +5 -1
  211. package/schema/tokens.schema.json +8 -4
  212. package/src/cli/file-transaction.js +15 -8
  213. package/src/cli/index.js +22 -29
  214. package/tokens/banhaten.tokens.json +490 -192
@@ -0,0 +1,382 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ArrowUpIcon } from "lucide-react"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { Button } from "@/components/ui/button"
8
+ import { ShimmerText } from "@/components/ui/shimmer-text"
9
+ import { Spinner } from "@/components/ui/spinner"
10
+ import { useControllableState } from "@/hooks/use-controllable-state"
11
+
12
+ import {
13
+ AddMenu,
14
+ ChipRow,
15
+ SlashPalette,
16
+ useChipPresence,
17
+ useEnhancePill,
18
+ useFilePicker,
19
+ usePromptEnhancement,
20
+ useSlashPalette,
21
+ } from "./agent-input-parts"
22
+ import {
23
+ attachmentKind,
24
+ chipsOf,
25
+ withoutSlashToken,
26
+ type AgentInputAttachment,
27
+ type AgentInputLabels,
28
+ type AgentInputModel,
29
+ type AgentInputPayload,
30
+ type AgentInputSkill,
31
+ } from "./agent-input-state"
32
+
33
+ import "./agent-input.css"
34
+
35
+ type AgentInputProps = Omit<
36
+ React.ComponentProps<"div">,
37
+ "defaultValue" | "onSubmit"
38
+ > & {
39
+ /** Controlled attachment chips. Omit to let the field own picked files. */
40
+ attachments?: readonly AgentInputAttachment[]
41
+ defaultModel?: string
42
+ defaultSelectedSkills?: readonly string[]
43
+ defaultValue?: string
44
+ disabled?: boolean
45
+ labels?: Partial<AgentInputLabels>
46
+ model?: string
47
+ models?: readonly AgentInputModel[]
48
+ onAttachmentsChange?: (attachments: AgentInputAttachment[]) => void
49
+ /** Enables the Enhance Prompt action. Resolve to the rewritten prompt. */
50
+ onEnhance?: (prompt: string, signal: AbortSignal) => Promise<string>
51
+ onModelChange?: (model: string) => void
52
+ onSelectedSkillsChange?: (skills: string[]) => void
53
+ onSend?: (payload: AgentInputPayload) => void
54
+ onValueChange?: (value: string) => void
55
+ selectedSkills?: readonly string[]
56
+ skills?: readonly AgentInputSkill[]
57
+ value?: string
58
+ }
59
+
60
+ const defaultLabels: AgentInputLabels = {
61
+ addFiles: "Attach files",
62
+ addMenu: "Add attachment or switch model",
63
+ addPhotos: "Add photos",
64
+ enhance: "Enhance prompt",
65
+ enhancing: "Enhancing prompt",
66
+ model: "Model",
67
+ noSkills: "No matching skills",
68
+ placeholder: "Ask AI Agent",
69
+ removeAttachment: (name) => `Remove ${name}`,
70
+ removeSkill: (name) => `Remove ${name}`,
71
+ revert: "Revert",
72
+ send: "Send",
73
+ skills: "Skills",
74
+ }
75
+
76
+ /**
77
+ * Prompt composer for agent conversations: attachments, skills, model choice,
78
+ * a slash-command palette, and prompt enhancement inside one field.
79
+ */
80
+ const AgentInput = React.forwardRef<HTMLDivElement, AgentInputProps>(
81
+ function AgentInput(
82
+ {
83
+ attachments: controlledAttachments,
84
+ className,
85
+ defaultModel,
86
+ defaultSelectedSkills = [],
87
+ defaultValue = "",
88
+ disabled = false,
89
+ labels: labelOverrides,
90
+ model: controlledModel,
91
+ models = [],
92
+ onAttachmentsChange,
93
+ onEnhance,
94
+ onModelChange,
95
+ onSelectedSkillsChange,
96
+ onSend,
97
+ onValueChange,
98
+ selectedSkills: controlledSkills,
99
+ skills = [],
100
+ value: controlledValue,
101
+ ...props
102
+ },
103
+ ref
104
+ ) {
105
+ const labels = { ...defaultLabels, ...labelOverrides }
106
+ const [value, setValue] = useControllableState({
107
+ defaultValue,
108
+ onChange: onValueChange,
109
+ value: controlledValue,
110
+ })
111
+ const [selectedModel, setModel] = useControllableState({
112
+ defaultValue: defaultModel ?? "",
113
+ onChange: onModelChange,
114
+ value: controlledModel,
115
+ })
116
+ // An empty id means nothing is picked yet. Fall back to the first model at
117
+ // read time so a list that arrives after mount still yields a selection
118
+ // in the payload and the menu.
119
+ const model = selectedModel || models[0]?.id
120
+ // State stays readonly; the change callbacks receive fresh arrays.
121
+ const [attachments, setAttachments] = useControllableState<
122
+ readonly AgentInputAttachment[]
123
+ >({
124
+ defaultValue: [],
125
+ onChange: (next) => onAttachmentsChange?.([...next]),
126
+ value: controlledAttachments,
127
+ })
128
+ const [selectedSkills, setSelectedSkills] = useControllableState<readonly string[]>({
129
+ defaultValue: defaultSelectedSkills,
130
+ onChange: (next) => onSelectedSkillsChange?.([...next]),
131
+ value: controlledSkills,
132
+ })
133
+
134
+ const fieldRef = React.useRef<HTMLTextAreaElement>(null)
135
+ const frameRef = React.useRef<HTMLDivElement>(null)
136
+ const nextId = React.useRef(1)
137
+ // Differs per mount, so picked ids stay unique when the field remounts
138
+ // while the host still holds earlier attachments.
139
+ const attachmentIdPrefix = React.useId()
140
+
141
+ const focusField = () => window.requestAnimationFrame(() => fieldRef.current?.focus())
142
+ // The editor unmounts while enhancing, so focus returns to it afterwards
143
+ // unless the user has meanwhile moved to a control outside the field.
144
+ const refocusField = () => {
145
+ const active = document.activeElement
146
+ if (active && active !== document.body && !frameRef.current?.contains(active)) return
147
+ focusField()
148
+ }
149
+
150
+ const slash = useSlashPalette(skills)
151
+ const enhancement = usePromptEnhancement({
152
+ frameRef,
153
+ onEnhance,
154
+ onFinish: refocusField,
155
+ onStart: slash.close,
156
+ setValue,
157
+ value,
158
+ })
159
+ const hasText = value.trim().length > 0
160
+ const sendActive = hasText && !enhancement.enhancing && !disabled
161
+ const pill = useEnhancePill(
162
+ Boolean(onEnhance) && hasText && !enhancement.enhancing && !disabled,
163
+ enhancement.enhancing
164
+ )
165
+ const presence = useChipPresence(chipsOf(selectedSkills, attachments))
166
+ const picker = useFilePicker((files, pickerKind) => {
167
+ setAttachments([
168
+ ...attachments,
169
+ ...files.map((file) => ({
170
+ file,
171
+ id: `${attachmentIdPrefix}-${nextId.current++}`,
172
+ kind: attachmentKind(file, pickerKind),
173
+ name: file.name,
174
+ })),
175
+ ])
176
+ focusField()
177
+ })
178
+ const skillName = (id: string) =>
179
+ skills.find((skill) => skill.id === id)?.name ?? id
180
+
181
+ // Auto-grow the editor with its content up to the token max height.
182
+ React.useLayoutEffect(() => {
183
+ const field = fieldRef.current
184
+ if (!field) return
185
+ field.style.height = "auto"
186
+ field.style.height = `${field.scrollHeight}px`
187
+ }, [value, enhancement.enhancing])
188
+
189
+ // Picking from the palette swaps the "/query" token for a skill chip.
190
+ const applySlash = (skill: AgentInputSkill) => {
191
+ const field = fieldRef.current
192
+ const token = slash.token.current
193
+ if (field && token) {
194
+ setValue(withoutSlashToken(value, token))
195
+ window.requestAnimationFrame(() => {
196
+ field.focus()
197
+ field.setSelectionRange(token.start, token.start)
198
+ })
199
+ }
200
+ if (!selectedSkills.includes(skill.id)) {
201
+ setSelectedSkills([...selectedSkills, skill.id])
202
+ }
203
+ slash.close()
204
+ }
205
+
206
+ const removeChip = (key: string) => {
207
+ const chip = presence.leave(key)
208
+ if (chip?.kind === "skill") {
209
+ setSelectedSkills(selectedSkills.filter((id) => id !== chip.skillId))
210
+ } else if (chip?.kind === "attachment") {
211
+ setAttachments(attachments.filter((item) => item.id !== chip.attachment.id))
212
+ }
213
+ }
214
+
215
+ const toggleSkill = (skillId: string) => {
216
+ if (selectedSkills.includes(skillId)) {
217
+ removeChip(`skill-${skillId}`)
218
+ return
219
+ }
220
+ setSelectedSkills([...selectedSkills, skillId])
221
+ focusField()
222
+ }
223
+
224
+ const send = () => {
225
+ if (!sendActive) return
226
+ onSend?.({ attachments, model, skills: selectedSkills, text: value.trim() })
227
+ setValue("")
228
+ if (attachments.length > 0) setAttachments([])
229
+ if (selectedSkills.length > 0) setSelectedSkills([])
230
+ enhancement.reset()
231
+ slash.close()
232
+ focusField()
233
+ }
234
+
235
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
236
+ // Keys that pick or confirm an IME candidate (Japanese, Chinese, Korean)
237
+ // belong to the composition, so they never send or move the palette.
238
+ // Safari reports the confirming Enter with keyCode 229 instead.
239
+ if (event.nativeEvent.isComposing || event.keyCode === 229) return
240
+ if (slash.handleKey(event, applySlash)) return
241
+ if (event.key === "Enter" && !event.shiftKey) {
242
+ event.preventDefault()
243
+ send()
244
+ }
245
+ }
246
+
247
+ return (
248
+ <div
249
+ data-disabled={disabled ? "true" : "false"}
250
+ data-phase={enhancement.phase}
251
+ data-slot="agent-input"
252
+ ref={ref}
253
+ className={cn("min-w-0 max-w-full", className)}
254
+ {...props}
255
+ >
256
+ <input {...picker.inputProps} />
257
+ {/* Stays mounted so the text it gains is announced: a live region
258
+ inserted together with its text is often skipped. */}
259
+ <span className="sr-only" data-slot="agent-input-status" role="status">
260
+ {enhancement.enhancing ? labels.enhancing : null}
261
+ </span>
262
+
263
+ <div
264
+ data-enhancing={enhancement.enhancing ? "true" : "false"}
265
+ data-slot="agent-input-frame"
266
+ ref={frameRef}
267
+ >
268
+ <ChipRow
269
+ chips={presence.chips}
270
+ disabled={disabled}
271
+ labels={labels}
272
+ onExited={presence.settle}
273
+ onRemove={removeChip}
274
+ skillName={skillName}
275
+ />
276
+
277
+ <div data-slot="agent-input-editor">
278
+ {enhancement.enhancing ? (
279
+ <div data-slot="agent-input-enhancing">
280
+ <ShimmerText tone="default">{value}</ShimmerText>
281
+ </div>
282
+ ) : (
283
+ <textarea
284
+ aria-activedescendant={slash.nav.activeOptionId}
285
+ aria-autocomplete="list"
286
+ aria-controls={slash.open ? slash.id : undefined}
287
+ aria-haspopup="listbox"
288
+ aria-label={labels.placeholder}
289
+ data-slot="agent-input-field"
290
+ disabled={disabled}
291
+ onBlur={slash.close}
292
+ onChange={(event) => {
293
+ setValue(event.target.value)
294
+ if (enhancement.phase === "enhanced") enhancement.reset()
295
+ slash.detect(event.target.value, event.target.selectionStart)
296
+ }}
297
+ onClick={(event) =>
298
+ slash.detect(event.currentTarget.value, event.currentTarget.selectionStart)
299
+ }
300
+ onKeyDown={handleKeyDown}
301
+ onKeyUp={(event) => {
302
+ if (["ArrowUp", "ArrowDown", "Enter", "Tab", "Escape"].includes(event.key))
303
+ return
304
+ slash.detect(event.currentTarget.value, event.currentTarget.selectionStart)
305
+ }}
306
+ placeholder={labels.placeholder}
307
+ ref={fieldRef}
308
+ rows={1}
309
+ value={value}
310
+ />
311
+ )}
312
+
313
+ {slash.open && !enhancement.enhancing ? (
314
+ <SlashPalette labels={labels} onPick={applySlash} slash={slash} />
315
+ ) : null}
316
+ </div>
317
+
318
+ <div data-slot="agent-input-row">
319
+ <AddMenu
320
+ disabled={disabled || enhancement.enhancing}
321
+ labels={labels}
322
+ model={model}
323
+ models={models}
324
+ onModelChange={setModel}
325
+ onPickFiles={picker.open}
326
+ onToggleSkill={toggleSkill}
327
+ selectedSkills={selectedSkills}
328
+ skills={skills}
329
+ />
330
+
331
+ <div data-slot="agent-input-actions">
332
+ {enhancement.enhancing ? (
333
+ <span data-slot="agent-input-spinner">
334
+ <Spinner />
335
+ </span>
336
+ ) : pill.mounted ? (
337
+ <Button
338
+ data-exit={pill.exiting ? "true" : "false"}
339
+ data-slot="agent-input-enhance"
340
+ onClick={() => {
341
+ if (enhancement.phase === "enhanced") {
342
+ enhancement.revert()
343
+ focusField()
344
+ return
345
+ }
346
+ void enhancement.run()
347
+ }}
348
+ ref={pill.ref}
349
+ size="xs"
350
+ variant="soft"
351
+ >
352
+ {enhancement.phase === "enhanced" ? labels.revert : labels.enhance}
353
+ </Button>
354
+ ) : null}
355
+ <Button
356
+ aria-label={labels.send}
357
+ data-slot="agent-input-send"
358
+ disabled={!sendActive}
359
+ onClick={send}
360
+ size="icon-xs"
361
+ variant={sendActive ? "default" : "soft"}
362
+ >
363
+ <ArrowUpIcon />
364
+ </Button>
365
+ </div>
366
+ </div>
367
+ </div>
368
+ </div>
369
+ )
370
+ }
371
+ )
372
+
373
+ export { AgentInput }
374
+ export type {
375
+ AgentInputAttachment,
376
+ AgentInputLabels,
377
+ AgentInputModel,
378
+ AgentInputPayload,
379
+ AgentInputPhase,
380
+ AgentInputSkill,
381
+ } from "./agent-input-state"
382
+ export type { AgentInputProps }
@@ -15,8 +15,8 @@ const alertDialogContentVariants = cva(
15
15
  [
16
16
  "fixed left-1/2 top-1/2 z-[var(--bh-z-overlay)] flex max-h-[calc(100vh-(var(--bh-space-8xl-48)*2))] w-[var(--bh-alert-dialog-width)]",
17
17
  "max-w-[calc(100vw-(var(--bh-space-4xl-20)*2))] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden",
18
- "rounded-[var(--bh-radius-2xl-12)] bg-[var(--bh-bg-raised)] text-[var(--bh-content-default)] shadow-[var(--shadow-modal)] outline-none",
19
- "[--bh-alert-dialog-border:var(--bh-border-subtle)] [--shadow-modal:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-alert-dialog-border),var(--shadow-2xl)]",
18
+ "rounded-[var(--bh-radius-2xl-12)] bg-[var(--bh-bg-raised)] text-[var(--bh-content-default)] shadow-[var(--elevation-modal)] outline-none",
19
+ "[--bh-alert-dialog-border:var(--bh-border-subtle)] [--elevation-modal:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-alert-dialog-border),var(--elevation-06)]",
20
20
  ],
21
21
  {
22
22
  variants: {
@@ -23,13 +23,13 @@ const alertVariants = cva(
23
23
  variants: {
24
24
  status: {
25
25
  danger:
26
- "[--alert-bg:var(--bh-bg-danger-subtle)] [--alert-border:var(--bh-border-danger-subtle)] [--alert-title:var(--bh-content-danger-strong)] [--alert-description:var(--bh-content-danger-strong)] [--alert-icon:var(--bh-content-danger-default)]",
26
+ "[--alert-bg:var(--bh-bg-danger-subtle)] [--alert-border:var(--bh-border-danger-subtle)] [--alert-title:var(--bh-content-danger-strong)] [--alert-description:var(--bh-content-danger-default)] [--alert-icon:var(--bh-content-danger-default)]",
27
27
  success:
28
- "[--alert-bg:var(--bh-bg-success-subtle)] [--alert-border:var(--bh-border-success-subtle)] [--alert-title:var(--bh-content-success-strong)] [--alert-description:var(--bh-content-success-strong)] [--alert-icon:var(--bh-content-success-default)]",
28
+ "[--alert-bg:var(--bh-bg-success-subtle)] [--alert-border:var(--bh-border-success-subtle)] [--alert-title:var(--bh-content-success-strong)] [--alert-description:var(--bh-content-success-default)] [--alert-icon:var(--bh-content-success-default)]",
29
29
  warning:
30
- "[--alert-bg:var(--bh-bg-warning-subtle)] [--alert-border:var(--bh-border-warning-subtle)] [--alert-title:var(--bh-content-warning-strong)] [--alert-description:var(--bh-content-warning-strong)] [--alert-icon:var(--bh-content-warning-default)]",
30
+ "[--alert-bg:var(--bh-bg-warning-subtle)] [--alert-border:var(--bh-border-warning-subtle)] [--alert-title:var(--bh-content-warning-strong)] [--alert-description:var(--bh-content-warning-default)] [--alert-icon:var(--bh-content-warning-default)]",
31
31
  info:
32
- "[--alert-bg:var(--bh-bg-info-subtle)] [--alert-border:var(--bh-border-info-subtle)] [--alert-title:var(--bh-content-brand-strong)] [--alert-description:var(--bh-content-brand-strong)] [--alert-icon:var(--bh-content-brand-default)]",
32
+ "[--alert-bg:var(--bh-bg-info-subtle)] [--alert-border:var(--bh-border-info-subtle)] [--alert-title:var(--bh-content-brand-strong)] [--alert-description:var(--bh-content-brand-default)] [--alert-icon:var(--bh-content-brand-default)]",
33
33
  neutral:
34
34
  "[--alert-bg:var(--bh-bg-neutral-subtle)] [--alert-border:var(--bh-border-subtle)] [--alert-title:var(--bh-content-default)] [--alert-description:var(--bh-content-default)] [--alert-icon:var(--bh-content-default)]",
35
35
  },
@@ -48,6 +48,7 @@ const alertVariants = cva(
48
48
  )
49
49
 
50
50
  type AlertStatus = "danger" | "success" | "warning" | "info" | "neutral"
51
+ type AlertDescriptionColor = "status" | "default"
51
52
  type AlertDirection = "ltr" | "rtl"
52
53
 
53
54
  type AlertMessages = {
@@ -75,7 +76,11 @@ type AlertProps = Omit<
75
76
  actions?: AlertAction[]
76
77
  closeLabel?: string
77
78
  description?: React.ReactNode
79
+ /** "default" sets the description in content/default instead of the status colour. */
80
+ descriptionColor?: AlertDescriptionColor
78
81
  dir?: AlertDirection
82
+ /** Replaces the status icon. */
83
+ icon?: React.ReactNode
79
84
  messages?: Partial<AlertMessages>
80
85
  onClose?: () => void
81
86
  showActions?: boolean
@@ -91,7 +96,9 @@ const Alert = React.forwardRef<HTMLElement, AlertProps>(function Alert(
91
96
  className,
92
97
  closeLabel,
93
98
  description,
99
+ descriptionColor = "status",
94
100
  dir,
101
+ icon,
95
102
  isExpandable = false,
96
103
  messages: messageOverrides,
97
104
  onClose,
@@ -119,7 +126,10 @@ const Alert = React.forwardRef<HTMLElement, AlertProps>(function Alert(
119
126
  const requestedActions = (showActions ?? Boolean(actions?.length))
120
127
  ? (actions ?? [])
121
128
  : []
122
- const expanded = Boolean(isExpandable && requestedActions.length > 0)
129
+ // The expanded layout shows the description, with or without actions.
130
+ const expanded = Boolean(
131
+ isExpandable && (description !== undefined || requestedActions.length > 0)
132
+ )
123
133
  const visibleActions = requestedActions.slice(0, expanded ? 2 : 1)
124
134
  const visibleCloseButton = showCloseButton ?? Boolean(onClose)
125
135
  const resolvedCloseLabel =
@@ -161,7 +171,7 @@ const Alert = React.forwardRef<HTMLElement, AlertProps>(function Alert(
161
171
  data-slot="alert-icon"
162
172
  className="inline-flex size-[var(--bh-alert-icon-slot-size)] shrink-0 items-center justify-center text-[var(--alert-icon)]"
163
173
  >
164
- <AlertStatusIcon status={resolvedStatus} />
174
+ {icon ?? <AlertStatusIcon status={resolvedStatus} />}
165
175
  </span>
166
176
  <div
167
177
  data-slot="alert-copy"
@@ -195,7 +205,12 @@ const Alert = React.forwardRef<HTMLElement, AlertProps>(function Alert(
195
205
  <p
196
206
  data-slot="alert-description"
197
207
  dir="auto"
198
- className="m-0 w-full break-words text-[length:var(--bh-text-body-sm-regular-font-size)] font-[var(--bh-font-weight-regular)] leading-[var(--bh-text-body-sm-regular-line-height)] tracking-[var(--bh-text-body-sm-regular-letter-spacing)] text-[var(--alert-description)]"
208
+ className={cn(
209
+ "m-0 w-full break-words text-[length:var(--bh-text-body-sm-regular-font-size)] font-[var(--bh-font-weight-regular)] leading-[var(--bh-text-body-sm-regular-line-height)] tracking-[var(--bh-text-body-sm-regular-letter-spacing)]",
210
+ descriptionColor === "default"
211
+ ? "text-[var(--bh-content-default)]"
212
+ : "text-[var(--alert-description)]"
213
+ )}
199
214
  >
200
215
  {description}
201
216
  </p>
@@ -286,6 +301,7 @@ export {
286
301
  Alert,
287
302
  alertVariants,
288
303
  type AlertAction,
304
+ type AlertDescriptionColor,
289
305
  type AlertDirection,
290
306
  type AlertMessages,
291
307
  type AlertProps,
@@ -0,0 +1,151 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Slot } from "@radix-ui/react-slot"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ type AppHeaderProps = React.ComponentProps<"header">
9
+ type AppHeaderSectionProps = React.ComponentProps<"div">
10
+ type AppHeaderSearchProps = React.ComponentProps<"form">
11
+ type AppHeaderActionSize = "sm" | "md"
12
+ type AppHeaderActionProps = React.ComponentProps<"button"> & {
13
+ asChild?: boolean
14
+ icon?: React.ReactNode
15
+ size?: AppHeaderActionSize
16
+ }
17
+
18
+ /** Workspace bar above page content: a location trail at the start and workspace tools at the end. */
19
+ function AppHeader({ className, ...props }: AppHeaderProps) {
20
+ return (
21
+ <header
22
+ data-slot="app-header"
23
+ className={cn(
24
+ // The rule is an inset shadow, like a Figma inside stroke, so content centers on the full height.
25
+ "flex min-h-[var(--bh-app-header-height)] w-full min-w-0 shrink-0 items-center gap-[var(--bh-app-header-gap)] bg-[var(--bh-app-header-bg)] px-[var(--bh-app-header-padding-x)] text-[var(--bh-content-default)] shadow-[inset_0_calc(var(--bh-border-width-default)*-1)_0_0_var(--bh-app-header-border)]",
26
+ "[--bh-breadcrumbs-current-content:var(--bh-app-header-location-content)]",
27
+ className
28
+ )}
29
+ {...props}
30
+ />
31
+ )
32
+ }
33
+
34
+ /** Flexible leading region for Breadcrumbs or a short location label. It keeps a minimum width so search shrinks first. */
35
+ function AppHeaderStart({ className, ...props }: AppHeaderSectionProps) {
36
+ return (
37
+ <div
38
+ data-slot="app-header-start"
39
+ className={cn(
40
+ "flex min-w-[min(var(--bh-app-header-start-min-width),100%)] flex-1 items-center gap-[var(--bh-app-header-start-gap)]",
41
+ "text-[length:var(--bh-text-body-sm-medium-font-size)] font-[var(--bh-text-body-sm-medium-font-weight)] leading-[var(--bh-text-body-sm-medium-line-height)] tracking-[var(--bh-text-body-sm-medium-letter-spacing)] text-[var(--bh-app-header-location-content)]",
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ )
47
+ }
48
+
49
+ /** Trailing group of search and actions, spaced on the header gap. */
50
+ function AppHeaderEnd({ className, ...props }: AppHeaderSectionProps) {
51
+ return (
52
+ <div
53
+ data-slot="app-header-end"
54
+ className={cn(
55
+ "flex min-w-0 items-center gap-[var(--bh-app-header-gap)]",
56
+ className
57
+ )}
58
+ {...props}
59
+ />
60
+ )
61
+ }
62
+
63
+ /** Search landmark that sizes a nested Input to the header search width. */
64
+ function AppHeaderSearch({
65
+ className,
66
+ role = "search",
67
+ ...props
68
+ }: AppHeaderSearchProps) {
69
+ return (
70
+ <form
71
+ data-slot="app-header-search"
72
+ role={role}
73
+ className={cn(
74
+ "flex w-[var(--bh-app-header-search-width)] min-w-0 max-w-full shrink items-center [--bh-input-width:100%]",
75
+ className
76
+ )}
77
+ {...props}
78
+ />
79
+ )
80
+ }
81
+
82
+ /**
83
+ * Flush header action. At rest it occupies only its icon or label, as in the
84
+ * workspace bar design; padding and a matching negative margin extend the
85
+ * hover surface and pointer target past that footprint without moving it.
86
+ */
87
+ const AppHeaderAction = React.forwardRef<
88
+ HTMLButtonElement,
89
+ AppHeaderActionProps
90
+ >(function AppHeaderAction(
91
+ {
92
+ asChild = false,
93
+ children,
94
+ className,
95
+ icon,
96
+ size = "md",
97
+ type = "button",
98
+ ...props
99
+ },
100
+ ref
101
+ ) {
102
+ const Comp = asChild ? Slot : "button"
103
+ const iconOnly = icon != null && children == null
104
+
105
+ return (
106
+ <Comp
107
+ data-icon-only={iconOnly ? "true" : undefined}
108
+ data-size={size}
109
+ data-slot="app-header-action"
110
+ ref={ref}
111
+ type={asChild ? undefined : type}
112
+ className={cn(
113
+ "m-[calc(var(--bh-app-header-action-hit-inset)*-1)] inline-flex shrink-0 cursor-pointer items-center gap-[var(--bh-space-xs-4)] whitespace-nowrap rounded-[var(--bh-app-header-action-radius)] border-0 bg-transparent p-[var(--bh-app-header-action-hit-inset)] no-underline outline-none transition-[background-color,box-shadow]",
114
+ "hover:bg-[var(--bh-app-header-action-bg-hover)] focus-visible:shadow-[var(--elevation-button-focus)]",
115
+ "disabled:cursor-not-allowed disabled:bg-transparent disabled:text-[var(--bh-content-disabled)]",
116
+ "[&_svg]:size-[var(--bh-app-header-icon-size)] [&_svg]:shrink-0 [&>img]:size-[var(--bh-app-header-icon-size)] [&>img]:shrink-0",
117
+ iconOnly
118
+ ? "justify-center text-[var(--bh-app-header-action-content)]"
119
+ : size === "sm"
120
+ ? "min-w-[calc(var(--bh-app-header-action-sm-min-width)+var(--bh-app-header-action-hit-inset)*2)] justify-end text-[length:var(--bh-text-body-sm-medium-font-size)] font-[var(--bh-text-body-sm-medium-font-weight)] leading-[var(--bh-text-body-sm-medium-line-height)] tracking-[var(--bh-text-body-sm-medium-letter-spacing)] text-[var(--bh-app-header-action-sm-content)]"
121
+ : "justify-center text-[length:var(--bh-text-body-md-medium-font-size)] font-[var(--bh-text-body-md-medium-font-weight)] leading-[var(--bh-text-body-md-medium-line-height)] tracking-[var(--bh-text-body-md-medium-letter-spacing)] text-[var(--bh-app-header-action-content)]",
122
+ className
123
+ )}
124
+ {...props}
125
+ >
126
+ {asChild ? (
127
+ children
128
+ ) : (
129
+ <>
130
+ {icon}
131
+ {children}
132
+ </>
133
+ )}
134
+ </Comp>
135
+ )
136
+ })
137
+
138
+ export {
139
+ AppHeader,
140
+ AppHeaderAction,
141
+ AppHeaderEnd,
142
+ AppHeaderSearch,
143
+ AppHeaderStart,
144
+ }
145
+ export type {
146
+ AppHeaderActionProps,
147
+ AppHeaderActionSize,
148
+ AppHeaderProps,
149
+ AppHeaderSearchProps,
150
+ AppHeaderSectionProps,
151
+ }