XspecT 0.4.1__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
Potentially problematic release.
This version of XspecT might be problematic. Click here for more details.
- xspect/classify.py +32 -0
- xspect/file_io.py +3 -9
- xspect/filter_sequences.py +56 -0
- xspect/main.py +13 -18
- xspect/mlst_feature/mlst_helper.py +102 -13
- xspect/mlst_feature/pub_mlst_handler.py +32 -6
- xspect/models/probabilistic_filter_mlst_model.py +160 -32
- xspect/models/probabilistic_filter_model.py +1 -0
- xspect/ncbi.py +8 -6
- xspect/train.py +13 -5
- xspect/web.py +173 -0
- xspect/xspect-web/.gitignore +24 -0
- xspect/xspect-web/README.md +54 -0
- xspect/xspect-web/components.json +21 -0
- xspect/xspect-web/dist/assets/index-CMG4V7fZ.js +290 -0
- xspect/xspect-web/dist/assets/index-jIKg1HIy.css +1 -0
- xspect/xspect-web/dist/index.html +14 -0
- xspect/xspect-web/dist/vite.svg +1 -0
- xspect/xspect-web/eslint.config.js +28 -0
- xspect/xspect-web/index.html +13 -0
- xspect/xspect-web/package-lock.json +6865 -0
- xspect/xspect-web/package.json +58 -0
- xspect/xspect-web/pnpm-lock.yaml +4317 -0
- xspect/xspect-web/public/vite.svg +1 -0
- xspect/xspect-web/src/App.tsx +29 -0
- xspect/xspect-web/src/api.tsx +62 -0
- xspect/xspect-web/src/assets/react.svg +1 -0
- xspect/xspect-web/src/components/classification-form.tsx +284 -0
- xspect/xspect-web/src/components/classify.tsx +18 -0
- xspect/xspect-web/src/components/data-table.tsx +78 -0
- xspect/xspect-web/src/components/dropdown-checkboxes.tsx +63 -0
- xspect/xspect-web/src/components/dropdown-slider.tsx +42 -0
- xspect/xspect-web/src/components/filter-form.tsx +423 -0
- xspect/xspect-web/src/components/filter.tsx +15 -0
- xspect/xspect-web/src/components/header.tsx +46 -0
- xspect/xspect-web/src/components/landing.tsx +7 -0
- xspect/xspect-web/src/components/models-details.tsx +138 -0
- xspect/xspect-web/src/components/models.tsx +53 -0
- xspect/xspect-web/src/components/result-chart.tsx +44 -0
- xspect/xspect-web/src/components/result.tsx +155 -0
- xspect/xspect-web/src/components/spinner.tsx +30 -0
- xspect/xspect-web/src/components/ui/accordion.tsx +64 -0
- xspect/xspect-web/src/components/ui/button.tsx +59 -0
- xspect/xspect-web/src/components/ui/card.tsx +92 -0
- xspect/xspect-web/src/components/ui/chart.tsx +351 -0
- xspect/xspect-web/src/components/ui/command.tsx +175 -0
- xspect/xspect-web/src/components/ui/dialog.tsx +135 -0
- xspect/xspect-web/src/components/ui/dropdown-menu.tsx +255 -0
- xspect/xspect-web/src/components/ui/file-upload.tsx +1459 -0
- xspect/xspect-web/src/components/ui/form.tsx +165 -0
- xspect/xspect-web/src/components/ui/input.tsx +21 -0
- xspect/xspect-web/src/components/ui/label.tsx +24 -0
- xspect/xspect-web/src/components/ui/navigation-menu.tsx +168 -0
- xspect/xspect-web/src/components/ui/popover.tsx +46 -0
- xspect/xspect-web/src/components/ui/select.tsx +183 -0
- xspect/xspect-web/src/components/ui/separator.tsx +26 -0
- xspect/xspect-web/src/components/ui/slider.tsx +61 -0
- xspect/xspect-web/src/components/ui/switch.tsx +29 -0
- xspect/xspect-web/src/components/ui/table.tsx +113 -0
- xspect/xspect-web/src/components/ui/tabs.tsx +64 -0
- xspect/xspect-web/src/index.css +120 -0
- xspect/xspect-web/src/lib/utils.ts +6 -0
- xspect/xspect-web/src/main.tsx +10 -0
- xspect/xspect-web/src/types.tsx +34 -0
- xspect/xspect-web/src/utils.tsx +6 -0
- xspect/xspect-web/src/vite-env.d.ts +1 -0
- xspect/xspect-web/tsconfig.app.json +32 -0
- xspect/xspect-web/tsconfig.json +13 -0
- xspect/xspect-web/tsconfig.node.json +24 -0
- xspect/xspect-web/vite.config.ts +24 -0
- {xspect-0.4.1.dist-info → xspect-0.5.0.dist-info}/METADATA +6 -8
- xspect-0.5.0.dist-info/RECORD +85 -0
- {xspect-0.4.1.dist-info → xspect-0.5.0.dist-info}/WHEEL +1 -1
- xspect/fastapi.py +0 -102
- xspect-0.4.1.dist-info/RECORD +0 -24
- {xspect-0.4.1.dist-info → xspect-0.5.0.dist-info}/entry_points.txt +0 -0
- {xspect-0.4.1.dist-info → xspect-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {xspect-0.4.1.dist-info → xspect-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Model, ModelTableEntry } from "@/types/models";
|
|
2
|
+
import { ColumnDef } from "@tanstack/react-table"
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { DataTable } from "@/components/data-table"
|
|
5
|
+
import { getModels } from "../api";
|
|
6
|
+
import { Separator } from "./ui/separator";
|
|
7
|
+
import { ArrowRight } from "lucide-react";
|
|
8
|
+
import { Link } from "react-router-dom";
|
|
9
|
+
|
|
10
|
+
export default function Models() {
|
|
11
|
+
const [tableData, setTableData] = useState<ModelTableEntry[] | null>(null);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
getModels()
|
|
15
|
+
.then((data: Model[]) => {
|
|
16
|
+
const tableEntries: ModelTableEntry[] = [];
|
|
17
|
+
Object.entries(data).forEach(([modelType, modelList]) => {
|
|
18
|
+
(modelList as string[]).forEach((modelName: string) => {
|
|
19
|
+
tableEntries.push({ model_type: modelType, name: modelName });
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
setTableData(tableEntries);
|
|
23
|
+
})
|
|
24
|
+
.catch((err) => console.error('Fetch error:', err));
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
const columns: ColumnDef<ModelTableEntry>[] = [
|
|
28
|
+
{ header: "Model Name", accessorKey: "name" },
|
|
29
|
+
{ header: "Model Type", accessorKey: "model_type"},
|
|
30
|
+
{
|
|
31
|
+
id: "details",
|
|
32
|
+
cell: ({ row }) => (
|
|
33
|
+
<Link to={`/models/${row.getValue("name")}-${row.getValue("model_type")}`.toLowerCase()} className="">
|
|
34
|
+
<ArrowRight />
|
|
35
|
+
</Link>
|
|
36
|
+
),
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<main className="flex-1 flex flex-col items-center justify-center p-4">
|
|
42
|
+
<div className="w-1/2">
|
|
43
|
+
<h1 className="text-xl font-bold">Available models</h1>
|
|
44
|
+
<p>The following models are available for classification and filtering.</p>
|
|
45
|
+
<Separator className="my-4" />
|
|
46
|
+
<DataTable
|
|
47
|
+
columns={columns}
|
|
48
|
+
data={tableData || []}
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
</main>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from "recharts"
|
|
3
|
+
import {
|
|
4
|
+
ChartConfig,
|
|
5
|
+
ChartContainer,
|
|
6
|
+
ChartTooltip,
|
|
7
|
+
ChartTooltipContent,
|
|
8
|
+
} from "@/components/ui/chart"
|
|
9
|
+
|
|
10
|
+
const chartConfig = {
|
|
11
|
+
score: {
|
|
12
|
+
label: "Score",
|
|
13
|
+
color: "hsl(var(--chart-1))",
|
|
14
|
+
},
|
|
15
|
+
} satisfies ChartConfig
|
|
16
|
+
|
|
17
|
+
export interface ResultChartProps {
|
|
18
|
+
data: {
|
|
19
|
+
taxon: string
|
|
20
|
+
score: number
|
|
21
|
+
}[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ResultChart({ data }: ResultChartProps) {
|
|
25
|
+
return (
|
|
26
|
+
<ChartContainer
|
|
27
|
+
config={chartConfig}
|
|
28
|
+
className="mx-auto aspect-square max-h-[1000px]"
|
|
29
|
+
>
|
|
30
|
+
<RadarChart data={data}>
|
|
31
|
+
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
|
32
|
+
<PolarAngleAxis dataKey="taxon" />
|
|
33
|
+
<PolarGrid gridType="circle"/>
|
|
34
|
+
<Radar
|
|
35
|
+
dataKey="score"
|
|
36
|
+
fill="var(--primary)"
|
|
37
|
+
fillOpacity={0.6}
|
|
38
|
+
animationDuration={500}
|
|
39
|
+
max={1}
|
|
40
|
+
/>
|
|
41
|
+
</RadarChart>
|
|
42
|
+
</ChartContainer>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Link, useParams } from "react-router-dom"
|
|
2
|
+
import { getClassificationResult, getModelMetadata } from "../api";
|
|
3
|
+
import { ModelMetadata, ClassificationResult } from "../types";
|
|
4
|
+
import { useState, useEffect, use } from "react";
|
|
5
|
+
import { LoadingSpinner } from "./spinner";
|
|
6
|
+
import { ResultChart, ResultChartProps } from "./result-chart";
|
|
7
|
+
import { Separator } from "@/components/ui/separator";
|
|
8
|
+
import { Button } from "./ui/button";
|
|
9
|
+
import { DropdownMenuCheckboxes } from "./dropdown-checkboxes";
|
|
10
|
+
import { DropdownMenuSlider } from "./dropdown-slider";
|
|
11
|
+
|
|
12
|
+
type CheckboxItem = {
|
|
13
|
+
id: string;
|
|
14
|
+
label: string;
|
|
15
|
+
checked: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const useCheckboxItems = () => {
|
|
19
|
+
const [items, setItems] = useState<CheckboxItem[]>([]);
|
|
20
|
+
|
|
21
|
+
const handleCheckedChange = (id: string, checked: boolean) => {
|
|
22
|
+
setItems(items.map(item =>
|
|
23
|
+
item.id === id ? { ...item, checked } : item
|
|
24
|
+
));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const checkboxItems = items.map(item => ({
|
|
28
|
+
...item,
|
|
29
|
+
onCheckedChange: (checked: boolean) => handleCheckedChange(item.id, checked)
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
return { checkboxItems, setItems };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
export default function Result() {
|
|
37
|
+
const { checkboxItems: contig_checkbox_items, setItems: setCheckboxItems } = useCheckboxItems();
|
|
38
|
+
const { classification_uuid } = useParams();
|
|
39
|
+
const [classificationResult, setClassificationResult] = useState<ClassificationResult | null>(null);
|
|
40
|
+
const [chartData, setChartData] = useState<ResultChartProps[] | null>(null);
|
|
41
|
+
const [modelMetadata, setModelMetadata] = useState<ModelMetadata | null>(null);
|
|
42
|
+
const [numResults, setNumResults] = useState(15);
|
|
43
|
+
|
|
44
|
+
const checkedItemsState = JSON.stringify(
|
|
45
|
+
contig_checkbox_items.map(item => ({ id: item.id, checked: item.checked }))
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const fetchResult = () => {
|
|
50
|
+
if (classification_uuid) {
|
|
51
|
+
getClassificationResult(classification_uuid).then((data) => {
|
|
52
|
+
setClassificationResult(data);
|
|
53
|
+
getModelMetadata(data.model_slug).then((modelMetadata) => {
|
|
54
|
+
setModelMetadata(modelMetadata);
|
|
55
|
+
}).catch((error) => {
|
|
56
|
+
console.error('Error fetching model metadata:', error);
|
|
57
|
+
setTimeout(fetchResult, 500);
|
|
58
|
+
});
|
|
59
|
+
}).catch((error) => {
|
|
60
|
+
console.error('Error fetching classification result:', error);
|
|
61
|
+
setTimeout(fetchResult, 500);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
fetchResult();
|
|
66
|
+
}, [classification_uuid, setClassificationResult, setModelMetadata]);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (classificationResult) {
|
|
70
|
+
const contigNames = Object.keys(classificationResult.scores);
|
|
71
|
+
if (contigNames.length <= 20) {
|
|
72
|
+
const initialCheckboxItems = contigNames
|
|
73
|
+
.filter(name => name !== "total")
|
|
74
|
+
.map((name) => ({
|
|
75
|
+
id: name,
|
|
76
|
+
label: name,
|
|
77
|
+
checked: true,
|
|
78
|
+
}));
|
|
79
|
+
setCheckboxItems(initialCheckboxItems);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}, [classificationResult, setCheckboxItems]);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (classificationResult && modelMetadata) {
|
|
86
|
+
const selectedContigs = contig_checkbox_items
|
|
87
|
+
.filter(item => item.checked)
|
|
88
|
+
.map(item => item.id);
|
|
89
|
+
console.log('selectedContigs', selectedContigs);
|
|
90
|
+
const filtered_hits = Object.entries(classificationResult.hits).filter(([key]) => selectedContigs.includes(key));
|
|
91
|
+
const total_hits = {}
|
|
92
|
+
filtered_hits.forEach(([, value]) => {
|
|
93
|
+
Object.entries(value).forEach(([label, hits]) => {
|
|
94
|
+
if (!total_hits[label]) {
|
|
95
|
+
total_hits[label] = 0;
|
|
96
|
+
}
|
|
97
|
+
total_hits[label] += hits;
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
const total_kmers = Object.entries(classificationResult.num_kmers)
|
|
101
|
+
.filter(([key]) => selectedContigs.includes(key))
|
|
102
|
+
.reduce((sum, [_, value]) => sum + value, 0);
|
|
103
|
+
const scores = Object.entries(total_hits).map(([label, hits]) => {
|
|
104
|
+
return {
|
|
105
|
+
taxon: modelMetadata.display_names[label].replace(modelMetadata.model_display_name, modelMetadata.model_display_name.charAt(0) + '.') || label,
|
|
106
|
+
score: hits / total_kmers,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
scores.sort((a, b) => b.score - a.score);
|
|
110
|
+
const topScores = scores.slice(0, numResults);
|
|
111
|
+
setChartData(topScores);
|
|
112
|
+
}
|
|
113
|
+
}, [classificationResult, modelMetadata, checkedItemsState, numResults]);
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<main className="flex-1 flex flex-col items-center justify-center p-4">
|
|
118
|
+
<div className="w-1/2">
|
|
119
|
+
{!chartData && (
|
|
120
|
+
<div className="flex items-center justify-center">
|
|
121
|
+
<LoadingSpinner className="size-10" />
|
|
122
|
+
<span className="m-2 text-xl">Classifying...</span>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
{chartData && (<>
|
|
126
|
+
<h1 className="text-xl font-bold">Classification Results</h1>
|
|
127
|
+
<p>{classificationResult?.input_source} was classified using the <Link className="font-medium underline underline-offset-4" to={`/models/${modelMetadata?.model_slug}`}>{modelMetadata?.model_display_name} {modelMetadata?.model_type.toLowerCase()}</Link> model. {classificationResult?.sparse_sampling_step === 1 ? 'No sparse sampling was used.' : 'Sparse sampling was used with step ' + classificationResult?.sparse_sampling_step}</p>
|
|
128
|
+
<p className="mt-4"><b>Prediction:</b> {modelMetadata?.display_names[classificationResult?.prediction] || 'No prediction available.'}</p>
|
|
129
|
+
<Button className="w-full mt-4" onClick={() => {
|
|
130
|
+
const blob = new Blob([JSON.stringify(classificationResult, null, 2)], { type: 'application/json' });
|
|
131
|
+
const href = URL.createObjectURL(blob);
|
|
132
|
+
|
|
133
|
+
const link = document.createElement("a");
|
|
134
|
+
link.href = href;
|
|
135
|
+
link.download = "result-" + classification_uuid + ".json";
|
|
136
|
+
document.body.appendChild(link);
|
|
137
|
+
link.click();
|
|
138
|
+
|
|
139
|
+
document.body.removeChild(link);
|
|
140
|
+
URL.revokeObjectURL(href);
|
|
141
|
+
}}>
|
|
142
|
+
Download Full Result
|
|
143
|
+
</Button>
|
|
144
|
+
<Separator className="my-4 h-px" />
|
|
145
|
+
<div className="flex justify-end space-x-2">
|
|
146
|
+
<DropdownMenuSlider triggerButtonText="#Results" value={numResults} onValueChange={setNumResults} max={modelMetadata?.display_names.length} step={1} disabled={false} />
|
|
147
|
+
<DropdownMenuCheckboxes triggerButtonText="Contigs" labelText="Select Contigs" items={contig_checkbox_items} disabled={contig_checkbox_items.length === 0} />
|
|
148
|
+
</div>
|
|
149
|
+
<ResultChart data={chartData} />
|
|
150
|
+
</>)}
|
|
151
|
+
|
|
152
|
+
</div>
|
|
153
|
+
</main>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils"
|
|
2
|
+
|
|
3
|
+
export interface ISVGProps extends React.SVGProps<SVGSVGElement> {
|
|
4
|
+
size?: number;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const LoadingSpinner = ({
|
|
9
|
+
size = 24,
|
|
10
|
+
className,
|
|
11
|
+
...props
|
|
12
|
+
}: ISVGProps) => {
|
|
13
|
+
return (
|
|
14
|
+
<svg
|
|
15
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
16
|
+
width={size}
|
|
17
|
+
height={size}
|
|
18
|
+
{...props}
|
|
19
|
+
viewBox="0 0 24 24"
|
|
20
|
+
fill="none"
|
|
21
|
+
stroke="currentColor"
|
|
22
|
+
strokeWidth="2"
|
|
23
|
+
strokeLinecap="round"
|
|
24
|
+
strokeLinejoin="round"
|
|
25
|
+
className={cn("animate-spin", className)}
|
|
26
|
+
>
|
|
27
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
28
|
+
</svg>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
|
3
|
+
import { ChevronDownIcon } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
function Accordion({
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
|
10
|
+
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function AccordionItem({
|
|
14
|
+
className,
|
|
15
|
+
...props
|
|
16
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
|
17
|
+
return (
|
|
18
|
+
<AccordionPrimitive.Item
|
|
19
|
+
data-slot="accordion-item"
|
|
20
|
+
className={cn("border-b last:border-b-0", className)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function AccordionTrigger({
|
|
27
|
+
className,
|
|
28
|
+
children,
|
|
29
|
+
...props
|
|
30
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
|
31
|
+
return (
|
|
32
|
+
<AccordionPrimitive.Header className="flex">
|
|
33
|
+
<AccordionPrimitive.Trigger
|
|
34
|
+
data-slot="accordion-trigger"
|
|
35
|
+
className={cn(
|
|
36
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
|
43
|
+
</AccordionPrimitive.Trigger>
|
|
44
|
+
</AccordionPrimitive.Header>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function AccordionContent({
|
|
49
|
+
className,
|
|
50
|
+
children,
|
|
51
|
+
...props
|
|
52
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
|
53
|
+
return (
|
|
54
|
+
<AccordionPrimitive.Content
|
|
55
|
+
data-slot="accordion-content"
|
|
56
|
+
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
|
60
|
+
</AccordionPrimitive.Content>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default:
|
|
13
|
+
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
|
14
|
+
destructive:
|
|
15
|
+
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
16
|
+
outline:
|
|
17
|
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
18
|
+
secondary:
|
|
19
|
+
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
|
20
|
+
ghost:
|
|
21
|
+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
22
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
23
|
+
},
|
|
24
|
+
size: {
|
|
25
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
26
|
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
27
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
28
|
+
icon: "size-9",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: {
|
|
32
|
+
variant: "default",
|
|
33
|
+
size: "default",
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
function Button({
|
|
39
|
+
className,
|
|
40
|
+
variant,
|
|
41
|
+
size,
|
|
42
|
+
asChild = false,
|
|
43
|
+
...props
|
|
44
|
+
}: React.ComponentProps<"button"> &
|
|
45
|
+
VariantProps<typeof buttonVariants> & {
|
|
46
|
+
asChild?: boolean
|
|
47
|
+
}) {
|
|
48
|
+
const Comp = asChild ? Slot : "button"
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Comp
|
|
52
|
+
data-slot="button"
|
|
53
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
data-slot="card"
|
|
9
|
+
className={cn(
|
|
10
|
+
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
data-slot="card-header"
|
|
22
|
+
className={cn(
|
|
23
|
+
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
data-slot="card-title"
|
|
35
|
+
className={cn("leading-none font-semibold", className)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
data-slot="card-description"
|
|
45
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
data-slot="card-action"
|
|
55
|
+
className={cn(
|
|
56
|
+
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
57
|
+
className
|
|
58
|
+
)}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
data-slot="card-content"
|
|
68
|
+
className={cn("px-6", className)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
data-slot="card-footer"
|
|
78
|
+
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
Card,
|
|
86
|
+
CardHeader,
|
|
87
|
+
CardFooter,
|
|
88
|
+
CardTitle,
|
|
89
|
+
CardAction,
|
|
90
|
+
CardDescription,
|
|
91
|
+
CardContent,
|
|
92
|
+
}
|