@vvedantb/ui 0.17.1 → 0.18.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/README.md +251 -3
- package/package.json +13 -5
- package/shell/src/SidebarResize.tsx +1 -4
- package/src/ai-elements/__snapshots__/queue.test.tsx.snap +11 -0
- package/src/ai-elements/activity-question-utils.ts +26 -0
- package/src/ai-elements/activity-question.tsx +159 -0
- package/src/ai-elements/activity-shared.tsx +459 -0
- package/src/ai-elements/activity-step-detail.tsx +115 -0
- package/src/ai-elements/activity-step-label.ts +981 -0
- package/src/ai-elements/activity-tasks-utils.ts +199 -0
- package/src/ai-elements/activity-tasks.tsx +770 -0
- package/src/ai-elements/artifact.tsx +162 -52
- package/src/ai-elements/attachments.tsx +62 -32
- package/src/ai-elements/code-block.tsx +76 -22
- package/src/ai-elements/context.tsx +370 -0
- package/src/ai-elements/conversation.tsx +186 -62
- package/src/ai-elements/file-chip.tsx +53 -0
- package/src/ai-elements/message.tsx +161 -50
- package/src/ai-elements/model-picker-content.tsx +505 -0
- package/src/ai-elements/model-picker-types.ts +56 -0
- package/src/ai-elements/model-picker.tsx +301 -0
- package/src/ai-elements/model-selector.tsx +102 -21
- package/src/ai-elements/plan.tsx +130 -0
- package/src/ai-elements/prompt-input-settings.tsx +144 -0
- package/src/ai-elements/prompt-input-speech.tsx +189 -0
- package/src/ai-elements/prompt-input-traits.tsx +281 -0
- package/src/ai-elements/prompt-input.tsx +320 -134
- package/src/ai-elements/provider-icon.tsx +133 -0
- package/src/ai-elements/queue.tsx +293 -9
- package/src/ai-elements/shimmer.tsx +72 -1
- package/src/ai-elements/simple-model-ladder.tsx +108 -0
- package/src/ai-elements/test-results.tsx +510 -0
- package/src/ai-elements/trace.tsx +243 -0
- package/src/ai-elements/use-streaming-disclosure.ts +50 -0
- package/src/ai-elements/web-preview.tsx +349 -0
- package/src/composer/input-area/ComposerSlashMenu.tsx +1 -1
- package/src/index.ts +66 -12
- package/src/kibo/avatar-stack.tsx +40 -0
- package/src/kibo/contribution-graph.tsx +539 -0
- package/src/kibo/cursor.tsx +72 -0
- package/src/kibo/gantt-features.tsx +439 -0
- package/src/kibo/gantt-header.tsx +181 -0
- package/src/kibo/gantt-provider.tsx +657 -0
- package/src/kibo/gantt-sidebar.tsx +157 -0
- package/src/kibo/gantt-timeline.tsx +154 -0
- package/src/kibo/index.ts +151 -0
- package/src/kibo/kanban.tsx +338 -0
- package/src/kibo/kanbanDropTarget.ts +25 -0
- package/src/kibo/list.tsx +162 -0
- package/src/kibo/theme-switcher.tsx +109 -0
- package/src/kibo/video-player.tsx +237 -0
- package/src/markdown/Markdown.tsx +112 -0
- package/src/markdown/MarkdownBlockquote.tsx +45 -0
- package/src/markdown/MarkdownCodeBlock.tsx +197 -0
- package/src/markdown/MarkdownInlineCode.tsx +40 -0
- package/src/markdown/MarkdownLink.tsx +92 -0
- package/src/markdown/codeFence.ts +57 -0
- package/src/markdown/codeHighlight.ts +138 -0
- package/src/markdown/incrementalBlocks.ts +182 -0
- package/src/markdown/index.ts +3 -0
- package/src/markdown/markdownClipboard.ts +178 -0
- package/src/markdown/mermaid.ts +155 -0
- package/src/markdown/rehypeGithubAlerts.ts +80 -0
- package/src/motion/presets.ts +24 -0
- package/src/ui/__snapshots__/alt-wrappers.test.tsx.snap +19 -0
- package/src/ui/__snapshots__/sonner-dom.test.tsx.snap +5 -0
- package/src/ui/_menu-parts.tsx +29 -0
- package/src/ui/accordion.alt.tsx +46 -53
- package/src/ui/accordion.tsx +59 -10
- package/src/ui/avatar.tsx +56 -0
- package/src/ui/badge.tsx +45 -21
- package/src/ui/border-beam.tsx +97 -0
- package/src/ui/button-group.tsx +50 -21
- package/src/ui/button.tsx +67 -45
- package/src/ui/calendar.tsx +48 -5
- package/src/ui/card.tsx +77 -8
- package/src/ui/carousel.tsx +60 -0
- package/src/ui/centered-spinner.tsx +30 -0
- package/src/ui/checkbox.tsx +34 -16
- package/src/ui/clear-input.alt.tsx +33 -75
- package/src/ui/clear-input.tsx +140 -15
- package/src/ui/collapsible.tsx +40 -2
- package/src/ui/command.tsx +143 -70
- package/src/ui/context-menu.tsx +159 -93
- package/src/ui/crossfade-icon.tsx +123 -0
- package/src/ui/dialog.tsx +154 -70
- package/src/ui/dropdown-menu.tsx +177 -117
- package/src/ui/hover-card.tsx +59 -17
- package/src/ui/input-group.alt.tsx +83 -83
- package/src/ui/input-group.tsx +129 -58
- package/src/ui/input.tsx +13 -4
- package/src/ui/label.tsx +11 -8
- package/src/ui/list-row.tsx +232 -0
- package/src/ui/loading-state.tsx +215 -0
- package/src/ui/popover.tsx +56 -23
- package/src/ui/progress.tsx +28 -16
- package/src/ui/refresh-spin-icon.tsx +41 -0
- package/src/ui/search-input.tsx +66 -15
- package/src/ui/select.tsx +171 -100
- package/src/ui/separator.tsx +23 -14
- package/src/ui/slider.tsx +21 -7
- package/src/ui/sonner.tsx +68 -30
- package/src/ui/spinner.tsx +229 -11
- package/src/ui/status-dot.tsx +84 -0
- package/src/ui/surface.tsx +49 -0
- package/src/ui/switch.tsx +29 -15
- package/src/ui/table.tsx +38 -16
- package/src/ui/tabs.tsx +123 -35
- package/src/ui/tabsSliding.ts +33 -1
- package/src/ui/textarea.tsx +13 -4
- package/src/ui/tooltip.tsx +50 -14
- package/src/ui/ui-config.tsx +149 -0
- package/src/utils/cat-color.ts +123 -0
- package/src/utils/dayjs.ts +6 -0
- package/src/utils/gesture.ts +59 -0
- package/src/utils/runtimeVisibility.ts +167 -0
- package/src/utils/sharedClock.ts +96 -0
- package/src/utils/surface-radius.ts +16 -0
- package/src/utils/useDragSensors.ts +58 -0
- package/tokens/examples/tokens-moved/Fixture.tsx +283 -0
- package/tokens/examples/tokens-moved/README.md +20 -0
- package/tokens/examples/tokens-moved/index.html +12 -0
- package/tokens/examples/tokens-moved/main.tsx +6 -0
- package/tokens/examples/tokens-moved/package.json +14 -0
- package/tokens/examples/tokens-moved/styles.css +12 -0
- package/tokens/examples/tokens-moved/tsconfig.json +14 -0
- package/tokens/examples/tokens-moved/vite.config.ts +11 -0
- package/tokens/src/css/border-beam.css +116 -0
- 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
|
+
}
|
|
@@ -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
|
+
};
|