@preset.nz/ux-kit 0.1.0 → 0.1.2

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 (60) hide show
  1. package/README.md +31 -42
  2. package/THIRD_PARTY_NOTICES.md +253 -0
  3. package/package.json +13 -13
  4. package/src/chat/index.ts +9 -0
  5. package/src/chat/message.tsx +80 -0
  6. package/src/chat/prompt-input.tsx +121 -0
  7. package/src/chat/reasoning.tsx +182 -0
  8. package/src/chat/scroller.tsx +147 -0
  9. package/src/chat/suggestion.tsx +65 -0
  10. package/src/chat/tool.tsx +143 -0
  11. package/src/chat/types.ts +8 -0
  12. package/src/components/alert-shell.tsx +66 -0
  13. package/src/components/avatar.tsx +1 -1
  14. package/src/components/badge.tsx +6 -6
  15. package/src/components/breadcrumb.tsx +8 -10
  16. package/src/components/button.tsx +1 -1
  17. package/src/components/card.tsx +4 -21
  18. package/src/components/checkbox.tsx +3 -5
  19. package/src/components/color-field.tsx +20 -7
  20. package/src/components/color-swatch.tsx +3 -3
  21. package/src/components/confirm-dialog.tsx +93 -0
  22. package/src/components/context-menu.tsx +16 -13
  23. package/src/components/dialog.tsx +8 -9
  24. package/src/components/download-prompt.tsx +108 -0
  25. package/src/components/dropdown-menu.tsx +16 -13
  26. package/src/components/empty-state.tsx +6 -3
  27. package/src/components/input.tsx +2 -2
  28. package/src/components/label.tsx +3 -2
  29. package/src/components/list.tsx +205 -0
  30. package/src/components/navigation-menu.tsx +17 -10
  31. package/src/components/number-field.tsx +30 -26
  32. package/src/components/popover.tsx +6 -10
  33. package/src/components/progress.tsx +77 -0
  34. package/src/components/select.tsx +16 -28
  35. package/src/components/separator.tsx +2 -6
  36. package/src/components/sheet.tsx +16 -8
  37. package/src/components/side-panel.tsx +31 -8
  38. package/src/components/sidebar.tsx +19 -52
  39. package/src/components/skeleton.tsx +1 -1
  40. package/src/components/slider.tsx +4 -8
  41. package/src/components/snackbar.tsx +39 -36
  42. package/src/components/status-bar.tsx +10 -3
  43. package/src/components/tabs.tsx +5 -6
  44. package/src/components/textarea.tsx +2 -2
  45. package/src/components/toggle-group.tsx +4 -4
  46. package/src/components/toggle.tsx +3 -1
  47. package/src/components/toolbar.tsx +8 -11
  48. package/src/components/tooltip.tsx +1 -1
  49. package/src/graphics/adsr.ts +21 -13
  50. package/src/graphics/curve-editor.tsx +56 -14
  51. package/src/graphics/curve-edits.ts +20 -3
  52. package/src/graphics/index.ts +6 -6
  53. package/src/graphics/scale.ts +4 -1
  54. package/src/graphics/use-canvas.ts +1 -1
  55. package/src/graphics/use-tokens.ts +4 -1
  56. package/src/index.css +9 -1
  57. package/src/index.ts +7 -5
  58. package/src/lib/use-mobile.ts +1 -1
  59. package/src/lib/utils.ts +1 -1
  60. package/src/shadcn-tailwind.css +665 -0
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Copyright 2023 Vercel, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ *
16
+ * Adapted from Vercel AI Elements (https://github.com/vercel/ai-elements), reasoning.tsx.
17
+ * Modified by preset.nz, 2026-10-14: own state instead of Radix's useControllableState, Base UI Collapsible, CSS shimmer instead of motion, streamdown core only, Phosphor icons, NZ English strings.
18
+ */
19
+ import { Collapsible } from "@base-ui/react/collapsible"
20
+ import { BrainIcon, CaretDownIcon } from "@phosphor-icons/react"
21
+ import type { ReactNode } from "react"
22
+ import { createContext, memo, useContext, useEffect, useMemo, useRef, useState } from "react"
23
+ import { Streamdown } from "streamdown"
24
+
25
+ import { cn } from "../lib/utils"
26
+
27
+ type ReasoningContextValue = {
28
+ isStreaming: boolean
29
+ isOpen: boolean
30
+ duration: number | undefined
31
+ }
32
+
33
+ const ReasoningContext = createContext<ReasoningContextValue | null>(null)
34
+
35
+ function useReasoning() {
36
+ const context = useContext(ReasoningContext)
37
+ if (!context) throw new Error("Reasoning parts must sit inside <Reasoning>")
38
+ return context
39
+ }
40
+
41
+ export type ReasoningProps = Omit<Collapsible.Root.Props, "open" | "defaultOpen"> & {
42
+ /** True while the thinking is still arriving. Opens the block, and folds it again a moment after it ends. */
43
+ isStreaming?: boolean
44
+ open?: boolean
45
+ defaultOpen?: boolean
46
+ /** Seconds spent thinking. Measured from `isStreaming` when not given. */
47
+ duration?: number
48
+ }
49
+
50
+ const AUTO_CLOSE_DELAY = 1000
51
+
52
+ /** A folded "Thinking…" block. */
53
+ export const Reasoning = memo(function Reasoning({
54
+ className,
55
+ isStreaming = false,
56
+ open,
57
+ defaultOpen,
58
+ onOpenChange,
59
+ duration: durationProp,
60
+ children,
61
+ ...props
62
+ }: ReasoningProps) {
63
+ const [innerOpen, setInnerOpen] = useState(defaultOpen ?? isStreaming)
64
+ const [measured, setMeasured] = useState<number | undefined>(undefined)
65
+ const isOpen = open ?? innerOpen
66
+ const duration = durationProp ?? measured
67
+
68
+ const everStreamed = useRef(isStreaming)
69
+ const autoClosed = useRef(false)
70
+ const startedAt = useRef<number | null>(null)
71
+
72
+ // A host that passes `open` owns it; the automatic open and fold only run when it does not.
73
+ const uncontrolled = open === undefined
74
+ const setAuto = (next: boolean) => setInnerOpen(next)
75
+ const setAutoRef = useRef(setAuto)
76
+ setAutoRef.current = setAuto
77
+
78
+ useEffect(() => {
79
+ if (isStreaming) {
80
+ everStreamed.current = true
81
+ startedAt.current ??= Date.now()
82
+ if (uncontrolled && defaultOpen !== false && !isOpen) setAutoRef.current(true)
83
+ } else if (startedAt.current !== null) {
84
+ setMeasured(Math.ceil((Date.now() - startedAt.current) / 1000))
85
+ startedAt.current = null
86
+ }
87
+ }, [isStreaming, isOpen, defaultOpen, uncontrolled])
88
+
89
+ useEffect(() => {
90
+ if (!uncontrolled || !everStreamed.current || isStreaming || !isOpen || autoClosed.current) {
91
+ return
92
+ }
93
+ const timer = setTimeout(() => {
94
+ autoClosed.current = true
95
+ setAutoRef.current(false)
96
+ }, AUTO_CLOSE_DELAY)
97
+ return () => clearTimeout(timer)
98
+ }, [isStreaming, isOpen, uncontrolled])
99
+
100
+ const value = useMemo(() => ({ duration, isOpen, isStreaming }), [duration, isOpen, isStreaming])
101
+
102
+ return (
103
+ <ReasoningContext.Provider value={value}>
104
+ <Collapsible.Root
105
+ data-slot="reasoning"
106
+ className={cn("w-full", className)}
107
+ open={isOpen}
108
+ onOpenChange={(next, details) => {
109
+ setInnerOpen(next)
110
+ onOpenChange?.(next, details)
111
+ }}
112
+ {...props}
113
+ >
114
+ {children}
115
+ </Collapsible.Root>
116
+ </ReasoningContext.Provider>
117
+ )
118
+ })
119
+
120
+ function defaultThinkingMessage(isStreaming: boolean, duration?: number): ReactNode {
121
+ if (isStreaming || duration === 0) return <span className="shimmer">Thinking…</span>
122
+ if (duration === undefined) return <span>Thought for a few seconds</span>
123
+ return <span>Thought for {duration} seconds</span>
124
+ }
125
+
126
+ export type ReasoningTriggerProps = Collapsible.Trigger.Props & {
127
+ getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode
128
+ }
129
+
130
+ export const ReasoningTrigger = memo(function ReasoningTrigger({
131
+ className,
132
+ children,
133
+ getThinkingMessage = defaultThinkingMessage,
134
+ ...props
135
+ }: ReasoningTriggerProps) {
136
+ const { isStreaming, isOpen, duration } = useReasoning()
137
+ return (
138
+ <Collapsible.Trigger
139
+ data-slot="reasoning-trigger"
140
+ className={cn(
141
+ "flex w-full items-center gap-2 text-xs text-muted-foreground transition-colors outline-none hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring/50",
142
+ className,
143
+ )}
144
+ {...props}
145
+ >
146
+ {children ?? (
147
+ <>
148
+ <BrainIcon className="size-3.5" />
149
+ {getThinkingMessage(isStreaming, duration)}
150
+ <CaretDownIcon
151
+ className={cn("size-3.5 transition-transform", isOpen ? "rotate-180" : "rotate-0")}
152
+ />
153
+ </>
154
+ )}
155
+ </Collapsible.Trigger>
156
+ )
157
+ })
158
+
159
+ export type ReasoningContentProps = Omit<Collapsible.Panel.Props, "children"> & {
160
+ /** The thinking so far, as markdown. */
161
+ children: string
162
+ }
163
+
164
+ export const ReasoningContent = memo(function ReasoningContent({
165
+ className,
166
+ children,
167
+ ...props
168
+ }: ReasoningContentProps) {
169
+ const { isStreaming } = useReasoning()
170
+ return (
171
+ <Collapsible.Panel
172
+ data-slot="reasoning-content"
173
+ className={cn(
174
+ "mt-2 border-l border-border pl-3 text-xs text-muted-foreground outline-none",
175
+ className,
176
+ )}
177
+ {...props}
178
+ >
179
+ <Streamdown isAnimating={isStreaming}>{children}</Streamdown>
180
+ </Collapsible.Panel>
181
+ )
182
+ })
@@ -0,0 +1,147 @@
1
+ /**
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2023 shadcn
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ *
24
+ * Adapted from shadcn/ui's MessageScroller (https://github.com/shadcn-ui/ui),
25
+ * on the headless core in @shadcn/react.
26
+ * Modified by preset.nz, 2026-10-14: one ChatScroller that assembles the parts,
27
+ * Phosphor icon, ux-kit Button, no utilities the kit's stylesheet lacks.
28
+ */
29
+
30
+ import { ArrowDownIcon } from "@phosphor-icons/react"
31
+ import {
32
+ MessageScroller as Primitive,
33
+ useMessageScroller,
34
+ useMessageScrollerScrollable,
35
+ useMessageScrollerVisibility,
36
+ } from "@shadcn/react/message-scroller"
37
+ import type { ComponentProps } from "react"
38
+
39
+ import { Button } from "../components/button"
40
+ import { cn } from "../lib/utils"
41
+
42
+ export type ChatScrollerProps = ComponentProps<typeof Primitive.Root> &
43
+ Pick<
44
+ ComponentProps<typeof Primitive.Provider>,
45
+ "autoScroll" | "defaultScrollPosition" | "scrollEdgeThreshold"
46
+ > & {
47
+ /** Classes for the scrolling list inside, which holds the items. */
48
+ contentClassName?: string
49
+ /** Show the jump-to-latest button when scrolled away from the end. Default true. */
50
+ scrollButton?: boolean
51
+ }
52
+
53
+ /**
54
+ * The conversation's scrolling container. It follows a streamed reply to the
55
+ * end and lets go as soon as the person scrolls up. Fill it with
56
+ * `ChatScrollerItem`s; give it a height (it fills its parent).
57
+ */
58
+ export function ChatScroller({
59
+ className,
60
+ contentClassName,
61
+ children,
62
+ autoScroll = true,
63
+ defaultScrollPosition = "end",
64
+ scrollEdgeThreshold,
65
+ scrollButton = true,
66
+ ...props
67
+ }: ChatScrollerProps) {
68
+ return (
69
+ <Primitive.Provider
70
+ autoScroll={autoScroll}
71
+ defaultScrollPosition={defaultScrollPosition}
72
+ scrollEdgeThreshold={scrollEdgeThreshold}
73
+ >
74
+ <Primitive.Root
75
+ data-slot="chat-scroller"
76
+ className={cn("relative flex size-full min-h-0 flex-col overflow-hidden", className)}
77
+ {...props}
78
+ >
79
+ <Primitive.Viewport
80
+ data-slot="chat-scroller-viewport"
81
+ className="size-full min-h-0 min-w-0 overflow-y-auto overscroll-contain data-pending-scroll:invisible"
82
+ >
83
+ <Primitive.Content
84
+ data-slot="chat-scroller-content"
85
+ className={cn("flex h-max min-h-full flex-col gap-4 p-3", contentClassName)}
86
+ >
87
+ {children}
88
+ </Primitive.Content>
89
+ </Primitive.Viewport>
90
+ {scrollButton && <ChatScrollerButton />}
91
+ </Primitive.Root>
92
+ </Primitive.Provider>
93
+ )
94
+ }
95
+
96
+ export type ChatScrollerItemProps = ComponentProps<typeof Primitive.Item>
97
+
98
+ /** One entry in the list. Mark a user turn with `scrollAnchor` to pin the view to it as the reply grows. */
99
+ export function ChatScrollerItem({
100
+ className,
101
+ scrollAnchor = false,
102
+ ...props
103
+ }: ChatScrollerItemProps) {
104
+ return (
105
+ <Primitive.Item
106
+ data-slot="chat-scroller-item"
107
+ scrollAnchor={scrollAnchor}
108
+ className={cn("min-w-0 shrink-0", className)}
109
+ {...props}
110
+ />
111
+ )
112
+ }
113
+
114
+ export type ChatScrollerButtonProps = Omit<ComponentProps<typeof Primitive.Button>, "render">
115
+
116
+ /** The jump-to-latest button. `ChatScroller` includes one; use this to place your own. */
117
+ export function ChatScrollerButton({
118
+ direction = "end",
119
+ className,
120
+ children,
121
+ ...props
122
+ }: ChatScrollerButtonProps) {
123
+ return (
124
+ <Primitive.Button
125
+ data-slot="chat-scroller-button"
126
+ data-direction={direction}
127
+ direction={direction}
128
+ render={<Button variant="outline" size="icon-sm" />}
129
+ className={cn(
130
+ "absolute left-1/2 -translate-x-1/2 transition-[translate,scale,opacity] duration-200 data-[active=false]:pointer-events-none data-[active=false]:scale-95 data-[active=false]:opacity-0 data-[direction=end]:bottom-3 data-[direction=start]:top-3 data-[direction=start]:[&_svg]:rotate-180",
131
+ className,
132
+ )}
133
+ {...props}
134
+ >
135
+ {children ?? (
136
+ <>
137
+ <ArrowDownIcon />
138
+ <span className="sr-only">
139
+ {direction === "end" ? "Jump to latest" : "Jump to start"}
140
+ </span>
141
+ </>
142
+ )}
143
+ </Primitive.Button>
144
+ )
145
+ }
146
+
147
+ export { useMessageScroller, useMessageScrollerScrollable, useMessageScrollerVisibility }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Copyright 2023 Vercel, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ *
16
+ * Adapted from Vercel AI Elements (https://github.com/vercel/ai-elements), suggestion.tsx.
17
+ * Modified by preset.nz, 2026-10-14: own Button and a plain scrolling row instead of ScrollArea, ux-kit tokens.
18
+ */
19
+ import type { ComponentProps } from "react"
20
+
21
+ import { Button } from "../components/button"
22
+ import { cn } from "../lib/utils"
23
+
24
+ export type SuggestionsProps = ComponentProps<"div">
25
+
26
+ /** A row of starter prompts. Scrolls sideways when it does not fit. */
27
+ export function Suggestions({ className, ...props }: SuggestionsProps) {
28
+ return (
29
+ <div
30
+ data-slot="suggestions"
31
+ className={cn("flex w-full flex-nowrap items-center gap-2 overflow-x-auto pb-1", className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ export type SuggestionProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
38
+ /** The text handed to `onClick`. Shown too, unless children are given. */
39
+ suggestion: string
40
+ onClick?: (suggestion: string) => void
41
+ }
42
+
43
+ export function Suggestion({
44
+ suggestion,
45
+ onClick,
46
+ className,
47
+ variant = "outline",
48
+ size = "sm",
49
+ children,
50
+ ...props
51
+ }: SuggestionProps) {
52
+ return (
53
+ <Button
54
+ data-slot="suggestion"
55
+ type="button"
56
+ variant={variant}
57
+ size={size}
58
+ className={cn("shrink-0 rounded-full normal-case tracking-normal", className)}
59
+ onClick={() => onClick?.(suggestion)}
60
+ {...props}
61
+ >
62
+ {children || suggestion}
63
+ </Button>
64
+ )
65
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Copyright 2023 Vercel, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ *
16
+ * Adapted from Vercel AI Elements (https://github.com/vercel/ai-elements), tool.tsx.
17
+ * Modified by preset.nz, 2026-10-14: own types instead of the `ai` package, Base UI Collapsible, Phosphor icons, no code-block dependency.
18
+ */
19
+ import { Collapsible } from "@base-ui/react/collapsible"
20
+ import {
21
+ CaretDownIcon,
22
+ CheckCircleIcon,
23
+ ClockIcon,
24
+ ProhibitIcon,
25
+ WrenchIcon,
26
+ } from "@phosphor-icons/react"
27
+ import type { ComponentProps, ReactNode } from "react"
28
+
29
+ import { cn } from "../lib/utils"
30
+ import type { ToolCallState } from "./types"
31
+
32
+ const STATE_LABELS: Record<ToolCallState, string> = {
33
+ running: "Running",
34
+ done: "Done",
35
+ refused: "Refused",
36
+ }
37
+
38
+ const STATE_ICONS: Record<ToolCallState, ReactNode> = {
39
+ running: <ClockIcon className="size-3.5 animate-pulse" />,
40
+ done: <CheckCircleIcon className="size-3.5" weight="fill" />,
41
+ refused: <ProhibitIcon className="size-3.5 text-destructive" weight="fill" />,
42
+ }
43
+
44
+ export type ToolCallProps = Collapsible.Root.Props
45
+
46
+ /** A collapsible row for one tool call: a header that always shows, a body that folds. */
47
+ export function ToolCall({ className, ...props }: ToolCallProps) {
48
+ return (
49
+ <Collapsible.Root
50
+ data-slot="tool-call"
51
+ className={cn("group w-full rounded-md border border-border", className)}
52
+ {...props}
53
+ />
54
+ )
55
+ }
56
+
57
+ export type ToolCallHeaderProps = Omit<Collapsible.Trigger.Props, "children"> & {
58
+ /** The tool's name, as the model called it. */
59
+ name: string
60
+ state: ToolCallState
61
+ }
62
+
63
+ export function ToolCallHeader({ className, name, state, ...props }: ToolCallHeaderProps) {
64
+ return (
65
+ <Collapsible.Trigger
66
+ data-slot="tool-call-header"
67
+ className={cn(
68
+ "flex w-full items-center justify-between gap-2 p-2 text-left outline-none focus-visible:ring-1 focus-visible:ring-ring/50",
69
+ className,
70
+ )}
71
+ {...props}
72
+ >
73
+ <span className="flex min-w-0 items-center gap-2">
74
+ <WrenchIcon className="size-3.5 shrink-0 text-muted-foreground" />
75
+ <span className="truncate font-mono text-xs font-medium">{name}</span>
76
+ </span>
77
+ <span className="flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground">
78
+ {STATE_ICONS[state]}
79
+ {STATE_LABELS[state]}
80
+ <CaretDownIcon className="size-3.5 transition-transform group-data-[open]:rotate-180" />
81
+ </span>
82
+ </Collapsible.Trigger>
83
+ )
84
+ }
85
+
86
+ export type ToolCallContentProps = Collapsible.Panel.Props
87
+
88
+ export function ToolCallContent({ className, ...props }: ToolCallContentProps) {
89
+ return (
90
+ <Collapsible.Panel
91
+ data-slot="tool-call-content"
92
+ className={cn("space-y-3 p-2 pt-0 text-popover-foreground outline-none", className)}
93
+ {...props}
94
+ />
95
+ )
96
+ }
97
+
98
+ function asText(value: unknown): string {
99
+ return typeof value === "string" ? value : JSON.stringify(value, null, 2)
100
+ }
101
+
102
+ export type ToolCallInputProps = ComponentProps<"div"> & {
103
+ /** The arguments the model sent. Objects are shown as JSON. */
104
+ input: unknown
105
+ }
106
+
107
+ export function ToolCallInput({ className, input, ...props }: ToolCallInputProps) {
108
+ return (
109
+ <div className={cn("space-y-1.5 overflow-hidden", className)} {...props}>
110
+ <h4 className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
111
+ Arguments
112
+ </h4>
113
+ <pre className="overflow-x-auto rounded-sm bg-muted/50 p-2 font-mono text-xs">
114
+ {asText(input)}
115
+ </pre>
116
+ </div>
117
+ )
118
+ }
119
+
120
+ export type ToolCallOutputProps = ComponentProps<"div"> & {
121
+ output?: unknown
122
+ /** Set when the call failed or was refused; shown in place of the output. */
123
+ errorText?: string
124
+ }
125
+
126
+ export function ToolCallOutput({ className, output, errorText, ...props }: ToolCallOutputProps) {
127
+ if (output === undefined && !errorText) return null
128
+ return (
129
+ <div className={cn("space-y-1.5", className)} {...props}>
130
+ <h4 className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
131
+ {errorText ? "Error" : "Result"}
132
+ </h4>
133
+ <pre
134
+ className={cn(
135
+ "overflow-x-auto rounded-sm p-2 font-mono text-xs",
136
+ errorText ? "bg-destructive/10 text-destructive" : "bg-muted/50 text-foreground",
137
+ )}
138
+ >
139
+ {errorText ?? asText(output)}
140
+ </pre>
141
+ </div>
142
+ )
143
+ }
@@ -0,0 +1,8 @@
1
+ /** Who a message is from. */
2
+ export type ChatRole = "user" | "assistant"
3
+
4
+ /** Where a tool call is: still running, finished, or declined by the host. */
5
+ export type ToolCallState = "running" | "done" | "refused"
6
+
7
+ /** What the conversation is doing. The host owns it; the components only read it. */
8
+ export type ChatStatus = "idle" | "streaming" | "stopped" | "error"
@@ -0,0 +1,66 @@
1
+ import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"
2
+ import type * as React from "react"
3
+ import { cn } from "../lib/utils"
4
+ import { DialogFooter, DialogHeader, DialogOverlay } from "./dialog"
5
+
6
+ // AlertShell: the markup ConfirmDialog and DownloadPrompt share. Internal, not
7
+ // exported from the package. It is answered, not dismissed: no close "x", and a
8
+ // click outside does nothing, so the footer buttons and Escape are the ways out.
9
+
10
+ interface AlertShellProps {
11
+ open: boolean
12
+ /** Escape. The caller decides what that means (and may ignore it). */
13
+ onDismiss: () => void
14
+ title: string
15
+ /** The body, below the title. */
16
+ children?: React.ReactNode
17
+ /** The buttons, secondary first and primary last. */
18
+ footer: React.ReactNode
19
+ /** The element that takes focus when the dialog opens. */
20
+ initialFocus?: React.RefObject<HTMLElement | null>
21
+ }
22
+
23
+ function AlertShell({ open, onDismiss, title, children, footer, initialFocus }: AlertShellProps) {
24
+ return (
25
+ <AlertDialogPrimitive.Root
26
+ data-slot="confirm-dialog"
27
+ open={open}
28
+ onOpenChange={(next) => {
29
+ if (!next) onDismiss()
30
+ }}
31
+ >
32
+ <AlertDialogPrimitive.Portal data-slot="confirm-dialog-portal">
33
+ <DialogOverlay />
34
+ <AlertDialogPrimitive.Popup
35
+ data-slot="confirm-dialog-content"
36
+ initialFocus={initialFocus}
37
+ className={cn(
38
+ "fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-6 rounded-xl bg-popover p-6 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-md data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
39
+ )}
40
+ >
41
+ <DialogHeader>
42
+ <AlertDialogPrimitive.Title
43
+ data-slot="dialog-title"
44
+ className="font-heading leading-none font-medium"
45
+ >
46
+ {title}
47
+ </AlertDialogPrimitive.Title>
48
+ {children ? (
49
+ <AlertDialogPrimitive.Description
50
+ render={<div />}
51
+ data-slot="dialog-description"
52
+ className="text-sm text-muted-foreground"
53
+ >
54
+ {children}
55
+ </AlertDialogPrimitive.Description>
56
+ ) : null}
57
+ </DialogHeader>
58
+ <DialogFooter>{footer}</DialogFooter>
59
+ </AlertDialogPrimitive.Popup>
60
+ </AlertDialogPrimitive.Portal>
61
+ </AlertDialogPrimitive.Root>
62
+ )
63
+ }
64
+
65
+ export type { AlertShellProps }
66
+ export { AlertShell }
@@ -32,4 +32,4 @@ function AvatarFallback({ className, ...props }: AvatarPrimitive.Fallback.Props)
32
32
  )
33
33
  }
34
34
 
35
- export { Avatar, AvatarImage, AvatarFallback }
35
+ export { Avatar, AvatarFallback, AvatarImage }
@@ -4,13 +4,16 @@ import { cva, type VariantProps } from "class-variance-authority"
4
4
 
5
5
  import { cn } from "../lib/utils"
6
6
 
7
+ // Badge: a plain span. The `render` prop replaces Radix's `asChild`.
8
+
7
9
  const badgeVariants = cva(
8
10
  "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-md border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
9
11
  {
10
12
  variants: {
11
13
  variant: {
12
14
  default: "border-transparent bg-primary text-primary-foreground [a]:hover:bg-primary/90",
13
- secondary: "border-transparent bg-secondary text-secondary-foreground [a]:hover:bg-secondary/90",
15
+ secondary:
16
+ "border-transparent bg-secondary text-secondary-foreground [a]:hover:bg-secondary/90",
14
17
  destructive:
15
18
  "border-transparent bg-destructive text-destructive-foreground focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/90",
16
19
  outline: "text-foreground [a]:hover:bg-accent [a]:hover:text-accent-foreground",
@@ -19,7 +22,7 @@ const badgeVariants = cva(
19
22
  defaultVariants: {
20
23
  variant: "default",
21
24
  },
22
- }
25
+ },
23
26
  )
24
27
 
25
28
  function Badge({
@@ -30,10 +33,7 @@ function Badge({
30
33
  }: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
31
34
  return useRender({
32
35
  defaultTagName: "span",
33
- props: mergeProps<"span">(
34
- { className: cn(badgeVariants({ variant }), className) },
35
- props
36
- ),
36
+ props: mergeProps<"span">({ className: cn(badgeVariants({ variant }), className) }, props),
37
37
  render,
38
38
  state: { slot: "badge", variant },
39
39
  })