@vvedantb/ui 0.17.1 → 0.18.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.
Files changed (130) hide show
  1. package/README.md +251 -3
  2. package/package.json +13 -5
  3. package/shell/src/SidebarResize.tsx +1 -4
  4. package/src/ai-elements/__snapshots__/queue.test.tsx.snap +11 -0
  5. package/src/ai-elements/activity-question-utils.ts +26 -0
  6. package/src/ai-elements/activity-question.tsx +159 -0
  7. package/src/ai-elements/activity-shared.tsx +459 -0
  8. package/src/ai-elements/activity-step-detail.tsx +115 -0
  9. package/src/ai-elements/activity-step-label.ts +981 -0
  10. package/src/ai-elements/activity-tasks-utils.ts +199 -0
  11. package/src/ai-elements/activity-tasks.tsx +770 -0
  12. package/src/ai-elements/artifact.tsx +162 -52
  13. package/src/ai-elements/attachments.tsx +62 -32
  14. package/src/ai-elements/code-block.tsx +76 -22
  15. package/src/ai-elements/context.tsx +370 -0
  16. package/src/ai-elements/conversation.tsx +186 -62
  17. package/src/ai-elements/file-chip.tsx +53 -0
  18. package/src/ai-elements/message.tsx +161 -50
  19. package/src/ai-elements/model-picker-content.tsx +505 -0
  20. package/src/ai-elements/model-picker-types.ts +56 -0
  21. package/src/ai-elements/model-picker.tsx +301 -0
  22. package/src/ai-elements/model-selector.tsx +102 -21
  23. package/src/ai-elements/plan.tsx +130 -0
  24. package/src/ai-elements/prompt-input-settings.tsx +144 -0
  25. package/src/ai-elements/prompt-input-speech.tsx +189 -0
  26. package/src/ai-elements/prompt-input-traits.tsx +281 -0
  27. package/src/ai-elements/prompt-input.tsx +320 -134
  28. package/src/ai-elements/provider-icon.tsx +133 -0
  29. package/src/ai-elements/queue.tsx +293 -9
  30. package/src/ai-elements/shimmer.tsx +72 -1
  31. package/src/ai-elements/simple-model-ladder.tsx +108 -0
  32. package/src/ai-elements/speech-input.tsx +5 -3
  33. package/src/ai-elements/test-results.tsx +510 -0
  34. package/src/ai-elements/trace.tsx +243 -0
  35. package/src/ai-elements/use-streaming-disclosure.ts +50 -0
  36. package/src/ai-elements/web-preview.tsx +349 -0
  37. package/src/composer/input-area/ComposerSlashMenu.tsx +1 -1
  38. package/src/index.ts +66 -12
  39. package/src/kibo/avatar-stack.tsx +40 -0
  40. package/src/kibo/contribution-graph.tsx +539 -0
  41. package/src/kibo/cursor.tsx +72 -0
  42. package/src/kibo/gantt-features.tsx +439 -0
  43. package/src/kibo/gantt-header.tsx +181 -0
  44. package/src/kibo/gantt-provider.tsx +657 -0
  45. package/src/kibo/gantt-sidebar.tsx +157 -0
  46. package/src/kibo/gantt-timeline.tsx +154 -0
  47. package/src/kibo/index.ts +151 -0
  48. package/src/kibo/kanban.tsx +338 -0
  49. package/src/kibo/kanbanDropTarget.ts +25 -0
  50. package/src/kibo/list.tsx +162 -0
  51. package/src/kibo/theme-switcher.tsx +109 -0
  52. package/src/kibo/video-player.tsx +237 -0
  53. package/src/markdown/Markdown.tsx +112 -0
  54. package/src/markdown/MarkdownBlockquote.tsx +45 -0
  55. package/src/markdown/MarkdownCodeBlock.tsx +197 -0
  56. package/src/markdown/MarkdownInlineCode.tsx +40 -0
  57. package/src/markdown/MarkdownLink.tsx +92 -0
  58. package/src/markdown/codeFence.ts +57 -0
  59. package/src/markdown/codeHighlight.ts +138 -0
  60. package/src/markdown/incrementalBlocks.ts +182 -0
  61. package/src/markdown/index.ts +3 -0
  62. package/src/markdown/markdownClipboard.ts +178 -0
  63. package/src/markdown/mermaid.ts +155 -0
  64. package/src/markdown/rehypeGithubAlerts.ts +80 -0
  65. package/src/motion/presets.ts +24 -0
  66. package/src/ui/__snapshots__/alt-wrappers.test.tsx.snap +19 -0
  67. package/src/ui/__snapshots__/sonner-dom.test.tsx.snap +5 -0
  68. package/src/ui/_menu-parts.tsx +29 -0
  69. package/src/ui/accordion.alt.tsx +46 -53
  70. package/src/ui/accordion.tsx +59 -10
  71. package/src/ui/avatar.tsx +56 -0
  72. package/src/ui/badge.tsx +45 -21
  73. package/src/ui/border-beam.tsx +97 -0
  74. package/src/ui/button-group.tsx +50 -21
  75. package/src/ui/button.tsx +67 -45
  76. package/src/ui/calendar.tsx +48 -5
  77. package/src/ui/card.tsx +77 -8
  78. package/src/ui/carousel.tsx +60 -0
  79. package/src/ui/centered-spinner.tsx +30 -0
  80. package/src/ui/checkbox.tsx +34 -16
  81. package/src/ui/clear-input.alt.tsx +33 -75
  82. package/src/ui/clear-input.tsx +140 -15
  83. package/src/ui/collapsible.tsx +40 -2
  84. package/src/ui/command.tsx +143 -70
  85. package/src/ui/context-menu.tsx +159 -93
  86. package/src/ui/crossfade-icon.tsx +123 -0
  87. package/src/ui/dialog.tsx +154 -70
  88. package/src/ui/dropdown-menu.tsx +177 -117
  89. package/src/ui/hover-card.tsx +59 -17
  90. package/src/ui/input-group.alt.tsx +83 -83
  91. package/src/ui/input-group.tsx +129 -58
  92. package/src/ui/input.tsx +13 -4
  93. package/src/ui/label.tsx +11 -8
  94. package/src/ui/list-row.tsx +232 -0
  95. package/src/ui/loading-state.tsx +215 -0
  96. package/src/ui/popover.tsx +56 -23
  97. package/src/ui/progress.tsx +28 -16
  98. package/src/ui/refresh-spin-icon.tsx +41 -0
  99. package/src/ui/search-input.tsx +66 -15
  100. package/src/ui/select.tsx +171 -100
  101. package/src/ui/separator.tsx +23 -14
  102. package/src/ui/slider.tsx +21 -7
  103. package/src/ui/sonner.tsx +68 -30
  104. package/src/ui/spinner.tsx +229 -11
  105. package/src/ui/status-dot.tsx +84 -0
  106. package/src/ui/surface.tsx +49 -0
  107. package/src/ui/switch.tsx +29 -15
  108. package/src/ui/table.tsx +38 -16
  109. package/src/ui/tabs.tsx +123 -35
  110. package/src/ui/tabsSliding.ts +33 -1
  111. package/src/ui/textarea.tsx +13 -4
  112. package/src/ui/tooltip.tsx +50 -14
  113. package/src/ui/ui-config.tsx +149 -0
  114. package/src/utils/cat-color.ts +123 -0
  115. package/src/utils/dayjs.ts +6 -0
  116. package/src/utils/gesture.ts +59 -0
  117. package/src/utils/runtimeVisibility.ts +167 -0
  118. package/src/utils/sharedClock.ts +96 -0
  119. package/src/utils/surface-radius.ts +16 -0
  120. package/src/utils/useDragSensors.ts +58 -0
  121. package/tokens/examples/tokens-moved/Fixture.tsx +283 -0
  122. package/tokens/examples/tokens-moved/README.md +20 -0
  123. package/tokens/examples/tokens-moved/index.html +12 -0
  124. package/tokens/examples/tokens-moved/main.tsx +6 -0
  125. package/tokens/examples/tokens-moved/package.json +14 -0
  126. package/tokens/examples/tokens-moved/styles.css +12 -0
  127. package/tokens/examples/tokens-moved/tsconfig.json +14 -0
  128. package/tokens/examples/tokens-moved/vite.config.ts +11 -0
  129. package/tokens/src/css/border-beam.css +116 -0
  130. package/tokens/src/css/markdown.css +318 -0
@@ -0,0 +1,108 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { IconChevronRight } from "@tabler/icons-react";
5
+ import { useCn } from "../ui/ui-config";
6
+ import { Slider } from "../ui/slider";
7
+ import type { ModelOption } from "./model-picker-types";
8
+
9
+ /**
10
+ * Discrete capability slider for simple view. Ticks are the available ladder
11
+ * models, cheapest on the left. Resting chrome is Advanced; a held drag swaps
12
+ * that for Faster / Smarter so the thumb is the only control.
13
+ */
14
+ export function SimpleModelLadder<TModel extends string>({
15
+ value,
16
+ steps,
17
+ snappedId,
18
+ disabled,
19
+ onValueChange,
20
+ onAdvanced,
21
+ }: {
22
+ value: TModel;
23
+ steps: ReadonlyArray<ModelOption<TModel>>;
24
+ /** Ladder id to park the thumb on when `value` is off-ladder. */
25
+ snappedId: TModel;
26
+ disabled?: boolean;
27
+ onValueChange: (model: TModel) => void;
28
+ onAdvanced: () => void;
29
+ }) {
30
+ const merge = useCn();
31
+ const [dragging, setDragging] = useState(false);
32
+ const lastIndex = steps.length - 1;
33
+ let index = 0;
34
+ for (let i = 0; i < steps.length; i++) {
35
+ const step = steps[i];
36
+ if (step && (step.id === value || step.id === snappedId)) {
37
+ index = i;
38
+ if (step.id === value) break;
39
+ }
40
+ }
41
+ const selected = steps[index];
42
+
43
+ const applyIndex = (next: number) => {
44
+ const step = steps[next];
45
+ if (!step) return;
46
+ onValueChange(step.id);
47
+ };
48
+
49
+ return (
50
+ <div className="flex flex-col gap-3">
51
+ <Slider
52
+ min={0}
53
+ max={lastIndex}
54
+ step={1}
55
+ value={[index]}
56
+ disabled={disabled || lastIndex <= 0}
57
+ aria-label="Model"
58
+ aria-valuetext={selected?.label}
59
+ onValueChange={(values) => {
60
+ const next = values[0];
61
+ if (next === undefined) return;
62
+ applyIndex(next);
63
+ }}
64
+ onPointerDown={() => setDragging(true)}
65
+ onPointerUp={() => setDragging(false)}
66
+ onPointerCancel={() => setDragging(false)}
67
+ onValueCommit={() => setDragging(false)}
68
+ >
69
+ <div className="pointer-events-none absolute inset-x-4 top-1/2 h-5 -translate-y-1/2">
70
+ {steps.map((step, tickIndex) => (
71
+ <span
72
+ key={step.id}
73
+ className={merge(
74
+ "absolute top-1/2 size-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full",
75
+ tickIndex === index
76
+ ? "opacity-0"
77
+ : tickIndex < index
78
+ ? "bg-primary-foreground/55"
79
+ : "bg-muted-foreground/40",
80
+ )}
81
+ style={{
82
+ left: `${lastIndex <= 0 ? 0 : (tickIndex / lastIndex) * 100}%`,
83
+ }}
84
+ aria-hidden
85
+ />
86
+ ))}
87
+ </div>
88
+ </Slider>
89
+ <div className="flex min-h-5 items-center">
90
+ {dragging ? (
91
+ <div className="flex w-full justify-between text-2xs font-medium text-muted-foreground">
92
+ <span>Faster</span>
93
+ <span>Smarter</span>
94
+ </div>
95
+ ) : (
96
+ <button
97
+ type="button"
98
+ onClick={onAdvanced}
99
+ className="motion-press inline-flex items-center gap-0.5 text-xs font-medium text-muted-foreground hover:text-foreground active:scale-[0.98]"
100
+ >
101
+ Advanced
102
+ <IconChevronRight size={12} className="opacity-70" aria-hidden />
103
+ </button>
104
+ )}
105
+ </div>
106
+ </div>
107
+ );
108
+ }
@@ -35,15 +35,17 @@ interface SpeechRecognitionAlternative {
35
35
  confidence: number;
36
36
  }
37
37
  // Read the constructors through a local type: a package must not augment the
38
- // global Window, which would clash with apps and the DOM lib.
39
- type SpeechRecognitionWindow = Window & {
38
+ // global Window. Apps often declare their own `Window.SpeechRecognition`, so
39
+ // `window` is cast to this shape rather than assigned (see
40
+ // consumer/window-speech-global.d.ts).
41
+ type SpeechRecognitionWindow = {
40
42
  SpeechRecognition?: SpeechRecognitionConstructor;
41
43
  webkitSpeechRecognition?: SpeechRecognitionConstructor;
42
44
  };
43
45
  const getSpeechRecognitionConstructor = ():
44
46
  | SpeechRecognitionConstructor
45
47
  | undefined => {
46
- const speechWindow: SpeechRecognitionWindow = window;
48
+ const speechWindow = window as unknown as SpeechRecognitionWindow;
47
49
  return speechWindow.SpeechRecognition || speechWindow.webkitSpeechRecognition;
48
50
  };
49
51
  type SpeechInputMode = "speech-recognition" | "media-recorder" | "none";
@@ -0,0 +1,510 @@
1
+ "use client";
2
+
3
+ import type { ComponentProps, HTMLAttributes } from "react";
4
+ import { createContext, useContext, useMemo } from "react";
5
+ import {
6
+ IconCircleCheck,
7
+ IconChevronRight,
8
+ IconCircleDot,
9
+ IconCircle,
10
+ IconCircleX,
11
+ } from "@tabler/icons-react";
12
+ import { Badge } from "../ui/badge";
13
+ import {
14
+ Collapsible,
15
+ CollapsibleContent,
16
+ CollapsibleTrigger,
17
+ } from "../ui/collapsible";
18
+ import { useCn } from "../ui/ui-config";
19
+ import { SURFACE_RADIUS_CLASS } from "../utils/surface-radius";
20
+
21
+ type TestStatus = "passed" | "failed" | "skipped" | "running";
22
+
23
+ interface TestResultsSummary {
24
+ passed: number;
25
+ failed: number;
26
+ skipped: number;
27
+ total: number;
28
+ duration?: number;
29
+ }
30
+
31
+ interface TestResultsContextType {
32
+ summary?: TestResultsSummary;
33
+ }
34
+
35
+ const TestResultsContext = createContext<TestResultsContextType>({});
36
+
37
+ const formatDuration = (ms: number) => {
38
+ if (ms < 1000) return `${ms}ms`;
39
+ return `${(ms / 1000).toFixed(2)}s`;
40
+ };
41
+
42
+ export type TestResultsProps = HTMLAttributes<HTMLDivElement> & {
43
+ summary?: TestResultsSummary;
44
+ };
45
+
46
+ export const TestResults = ({
47
+ summary,
48
+ className,
49
+ children,
50
+ ...props
51
+ }: TestResultsProps) => {
52
+ const merge = useCn();
53
+ const contextValue = useMemo(() => ({ summary }), [summary]);
54
+
55
+ return (
56
+ <TestResultsContext.Provider value={contextValue}>
57
+ <div
58
+ className={merge("border bg-background", SURFACE_RADIUS_CLASS, className)}
59
+ {...props}
60
+ >
61
+ {children ??
62
+ (summary && (
63
+ <TestResultsHeader>
64
+ <TestResultsSummary />
65
+ <TestResultsDuration />
66
+ </TestResultsHeader>
67
+ ))}
68
+ </div>
69
+ </TestResultsContext.Provider>
70
+ );
71
+ };
72
+
73
+ export type TestResultsHeaderProps = HTMLAttributes<HTMLDivElement>;
74
+
75
+ export const TestResultsHeader = ({
76
+ className,
77
+ children,
78
+ ...props
79
+ }: TestResultsHeaderProps) => {
80
+ const merge = useCn();
81
+ return (
82
+ <div
83
+ className={merge(
84
+ "flex items-center justify-between border-b px-4 py-3",
85
+ className,
86
+ )}
87
+ {...props}
88
+ >
89
+ {children}
90
+ </div>
91
+ );
92
+ };
93
+
94
+ export type TestResultsSummaryProps = HTMLAttributes<HTMLDivElement>;
95
+
96
+ export const TestResultsSummary = ({
97
+ className,
98
+ children,
99
+ ...props
100
+ }: TestResultsSummaryProps) => {
101
+ const merge = useCn();
102
+ const { summary } = useContext(TestResultsContext);
103
+
104
+ if (!summary) return null;
105
+
106
+ return (
107
+ <div className={merge("flex items-center gap-3", className)} {...props}>
108
+ {children ?? (
109
+ <>
110
+ <Badge className="gap-1" variant="success">
111
+ <IconCircleCheck className="size-3" />
112
+ {summary.passed} passed
113
+ </Badge>
114
+ {summary.failed > 0 && (
115
+ <Badge className="gap-1" variant="destructive">
116
+ <IconCircleX className="size-3" />
117
+ {summary.failed} failed
118
+ </Badge>
119
+ )}
120
+ {summary.skipped > 0 && (
121
+ <Badge className="gap-1" variant="warning">
122
+ <IconCircle className="size-3" />
123
+ {summary.skipped} skipped
124
+ </Badge>
125
+ )}
126
+ </>
127
+ )}
128
+ </div>
129
+ );
130
+ };
131
+
132
+ export type TestResultsDurationProps = HTMLAttributes<HTMLSpanElement>;
133
+
134
+ export const TestResultsDuration = ({
135
+ className,
136
+ children,
137
+ ...props
138
+ }: TestResultsDurationProps) => {
139
+ const merge = useCn();
140
+ const { summary } = useContext(TestResultsContext);
141
+
142
+ if (!summary?.duration) return null;
143
+
144
+ return (
145
+ <span className={merge("text-muted-foreground text-sm", className)} {...props}>
146
+ {children ?? formatDuration(summary.duration)}
147
+ </span>
148
+ );
149
+ };
150
+
151
+ export type TestResultsProgressProps = HTMLAttributes<HTMLDivElement>;
152
+
153
+ export const TestResultsProgress = ({
154
+ className,
155
+ children,
156
+ ...props
157
+ }: TestResultsProgressProps) => {
158
+ const merge = useCn();
159
+ const { summary } = useContext(TestResultsContext);
160
+
161
+ if (!summary) return null;
162
+
163
+ const passedPercent = (summary.passed / summary.total) * 100;
164
+ const failedPercent = (summary.failed / summary.total) * 100;
165
+
166
+ return (
167
+ <div className={merge("space-y-2", className)} {...props}>
168
+ {children ?? (
169
+ <>
170
+ {/* Two full-width layers scaled from the left edge rather than two
171
+ flex siblings sized by `width`: the flex version relayouted the
172
+ track on every frame. The failed layer spans passed+failed and sits
173
+ underneath, so the passed layer painted over it leaves the same
174
+ two-tone bar while both segments only ever scale. */}
175
+ <div className="relative h-2 overflow-hidden rounded-full bg-surface-secondary">
176
+ <div
177
+ className="absolute inset-0 origin-left bg-destructive transition-transform duration-[var(--motion-base)]"
178
+ style={{
179
+ transform: `scaleX(${(passedPercent + failedPercent) / 100})`,
180
+ }}
181
+ />
182
+ <div
183
+ className="absolute inset-0 origin-left bg-success transition-transform duration-[var(--motion-base)]"
184
+ style={{ transform: `scaleX(${passedPercent / 100})` }}
185
+ />
186
+ </div>
187
+ <div className="flex justify-between text-muted-foreground text-xs tabular-nums">
188
+ <span>
189
+ {summary.passed}/{summary.total} tests passed
190
+ </span>
191
+ <span>{passedPercent.toFixed(0)}%</span>
192
+ </div>
193
+ </>
194
+ )}
195
+ </div>
196
+ );
197
+ };
198
+
199
+ export type TestResultsContentProps = HTMLAttributes<HTMLDivElement>;
200
+
201
+ export const TestResultsContent = ({
202
+ className,
203
+ children,
204
+ ...props
205
+ }: TestResultsContentProps) => {
206
+ const merge = useCn();
207
+ return (
208
+ <div className={merge("space-y-2 p-4", className)} {...props}>
209
+ {children}
210
+ </div>
211
+ );
212
+ };
213
+
214
+ interface TestSuiteContextType {
215
+ name: string;
216
+ status: TestStatus;
217
+ }
218
+
219
+ const TestSuiteContext = createContext<TestSuiteContextType>({
220
+ name: "",
221
+ status: "passed",
222
+ });
223
+
224
+ export type TestSuiteProps = ComponentProps<typeof Collapsible> & {
225
+ name: string;
226
+ status: TestStatus;
227
+ };
228
+
229
+ export const TestSuite = ({
230
+ name,
231
+ status,
232
+ className,
233
+ children,
234
+ ...props
235
+ }: TestSuiteProps) => {
236
+ const merge = useCn();
237
+ const contextValue = useMemo(() => ({ name, status }), [name, status]);
238
+
239
+ return (
240
+ <TestSuiteContext.Provider value={contextValue}>
241
+ <Collapsible
242
+ className={merge("border", SURFACE_RADIUS_CLASS, className)}
243
+ {...props}
244
+ >
245
+ {children}
246
+ </Collapsible>
247
+ </TestSuiteContext.Provider>
248
+ );
249
+ };
250
+
251
+ export type TestSuiteNameProps = ComponentProps<typeof CollapsibleTrigger>;
252
+
253
+ export const TestSuiteName = ({
254
+ className,
255
+ children,
256
+ ...props
257
+ }: TestSuiteNameProps) => {
258
+ const merge = useCn();
259
+ const { name, status } = useContext(TestSuiteContext);
260
+
261
+ return (
262
+ <CollapsibleTrigger
263
+ className={merge(
264
+ "group flex w-full items-center gap-2 px-4 py-3 text-left transition-colors hover:bg-surface-secondary/50",
265
+ className,
266
+ )}
267
+ {...props}
268
+ >
269
+ <IconChevronRight className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
270
+ <TestStatusIcon status={status} />
271
+ <span className="font-medium text-sm">{children ?? name}</span>
272
+ </CollapsibleTrigger>
273
+ );
274
+ };
275
+
276
+ export type TestSuiteStatsProps = HTMLAttributes<HTMLDivElement> & {
277
+ passed?: number;
278
+ failed?: number;
279
+ skipped?: number;
280
+ };
281
+
282
+ export const TestSuiteStats = ({
283
+ passed = 0,
284
+ failed = 0,
285
+ skipped = 0,
286
+ className,
287
+ children,
288
+ ...props
289
+ }: TestSuiteStatsProps) => {
290
+ const merge = useCn();
291
+ return (
292
+ <div
293
+ className={merge("ml-auto flex items-center gap-2 text-xs", className)}
294
+ {...props}
295
+ >
296
+ {children ?? (
297
+ <>
298
+ {passed > 0 && <span className="text-success">{passed} passed</span>}
299
+ {failed > 0 && (
300
+ <span className="text-destructive">{failed} failed</span>
301
+ )}
302
+ {skipped > 0 && <span className="text-warning">{skipped} skipped</span>}
303
+ </>
304
+ )}
305
+ </div>
306
+ );
307
+ };
308
+
309
+ export type TestSuiteContentProps = ComponentProps<typeof CollapsibleContent>;
310
+
311
+ export const TestSuiteContent = ({
312
+ className,
313
+ children,
314
+ ...props
315
+ }: TestSuiteContentProps) => {
316
+ const merge = useCn();
317
+ return (
318
+ <CollapsibleContent className={merge("border-t", className)} {...props}>
319
+ <div className="divide-y">{children}</div>
320
+ </CollapsibleContent>
321
+ );
322
+ };
323
+
324
+ interface TestContextType {
325
+ name: string;
326
+ status: TestStatus;
327
+ duration?: number;
328
+ }
329
+
330
+ const TestContext = createContext<TestContextType>({
331
+ name: "",
332
+ status: "passed",
333
+ });
334
+
335
+ export type TestResultProps = HTMLAttributes<HTMLDivElement> & {
336
+ name: string;
337
+ status: TestStatus;
338
+ duration?: number;
339
+ };
340
+
341
+ export const TestResult = ({
342
+ name,
343
+ status,
344
+ duration,
345
+ className,
346
+ children,
347
+ ...props
348
+ }: TestResultProps) => {
349
+ const merge = useCn();
350
+ const contextValue = useMemo(
351
+ () => ({ duration, name, status }),
352
+ [duration, name, status],
353
+ );
354
+
355
+ return (
356
+ <TestContext.Provider value={contextValue}>
357
+ <div
358
+ className={merge("flex items-center gap-2 px-4 py-2 text-sm", className)}
359
+ {...props}
360
+ >
361
+ {children ?? (
362
+ <>
363
+ <TestResultStatusIndicator />
364
+ <TestResultName />
365
+ {duration !== undefined && <TestResultDuration />}
366
+ </>
367
+ )}
368
+ </div>
369
+ </TestContext.Provider>
370
+ );
371
+ };
372
+
373
+ const statusStyles: Record<TestStatus, string> = {
374
+ failed: "text-destructive",
375
+ passed: "text-success",
376
+ running: "text-primary",
377
+ skipped: "text-warning",
378
+ };
379
+
380
+ const statusIcons: Record<TestStatus, React.ReactNode> = {
381
+ failed: <IconCircleX className="size-4" />,
382
+ passed: <IconCircleCheck className="size-4" />,
383
+ running: <IconCircleDot className="size-4" />,
384
+ skipped: <IconCircle className="size-4" />,
385
+ };
386
+
387
+ const TestStatusIcon = ({ status }: { status: TestStatus }) => {
388
+ const merge = useCn();
389
+ return (
390
+ <span className={merge("shrink-0", statusStyles[status])}>
391
+ {statusIcons[status]}
392
+ </span>
393
+ );
394
+ };
395
+
396
+ export type TestResultStatusIndicatorProps = HTMLAttributes<HTMLSpanElement>;
397
+
398
+ export const TestResultStatusIndicator = ({
399
+ className,
400
+ children,
401
+ ...props
402
+ }: TestResultStatusIndicatorProps) => {
403
+ const merge = useCn();
404
+ const { status } = useContext(TestContext);
405
+
406
+ return (
407
+ <span
408
+ className={merge("shrink-0", statusStyles[status], className)}
409
+ {...props}
410
+ >
411
+ {children ?? statusIcons[status]}
412
+ </span>
413
+ );
414
+ };
415
+
416
+ export type TestResultNameProps = HTMLAttributes<HTMLSpanElement>;
417
+
418
+ export const TestResultName = ({ className, children, ...props }: TestResultNameProps) => {
419
+ const merge = useCn();
420
+ const { name } = useContext(TestContext);
421
+
422
+ return (
423
+ <span className={merge("flex-1", className)} {...props}>
424
+ {children ?? name}
425
+ </span>
426
+ );
427
+ };
428
+
429
+ export type TestResultDurationProps = HTMLAttributes<HTMLSpanElement>;
430
+
431
+ export const TestResultDuration = ({
432
+ className,
433
+ children,
434
+ ...props
435
+ }: TestResultDurationProps) => {
436
+ const merge = useCn();
437
+ const { duration } = useContext(TestContext);
438
+
439
+ if (duration === undefined) return null;
440
+
441
+ return (
442
+ <span
443
+ className={merge("ml-auto text-muted-foreground text-xs", className)}
444
+ {...props}
445
+ >
446
+ {children ?? `${duration}ms`}
447
+ </span>
448
+ );
449
+ };
450
+
451
+ export type TestResultErrorProps = HTMLAttributes<HTMLDivElement>;
452
+
453
+ export const TestResultError = ({
454
+ className,
455
+ children,
456
+ ...props
457
+ }: TestResultErrorProps) => {
458
+ const merge = useCn();
459
+ return (
460
+ <div
461
+ className={merge(
462
+ "mt-2 bg-destructive/10 p-3",
463
+ SURFACE_RADIUS_CLASS,
464
+ className,
465
+ )}
466
+ {...props}
467
+ >
468
+ {children}
469
+ </div>
470
+ );
471
+ };
472
+
473
+ export type TestResultErrorMessageProps = HTMLAttributes<HTMLParagraphElement>;
474
+
475
+ export const TestResultErrorMessage = ({
476
+ className,
477
+ children,
478
+ ...props
479
+ }: TestResultErrorMessageProps) => {
480
+ const merge = useCn();
481
+ return (
482
+ <p
483
+ className={merge("text-sm font-medium text-destructive", className)}
484
+ {...props}
485
+ >
486
+ {children}
487
+ </p>
488
+ );
489
+ };
490
+
491
+ export type TestResultErrorStackProps = HTMLAttributes<HTMLPreElement>;
492
+
493
+ export const TestResultErrorStack = ({
494
+ className,
495
+ children,
496
+ ...props
497
+ }: TestResultErrorStackProps) => {
498
+ const merge = useCn();
499
+ return (
500
+ <pre
501
+ className={merge(
502
+ "mt-2 overflow-auto font-mono text-xs text-destructive",
503
+ className,
504
+ )}
505
+ {...props}
506
+ >
507
+ {children}
508
+ </pre>
509
+ );
510
+ };