Spaces:
Runtime error
Runtime error
initial commit
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .eslintrc.json +3 -0
- .gitignore +35 -0
- Dockerfile +65 -0
- README.md +2 -1
- app/api/detect/data.ts +21 -0
- app/api/detect/route.ts +58 -0
- app/api/detect/utils.ts +33 -0
- app/favicon.ico +0 -0
- app/globals.css +83 -0
- app/layout.tsx +29 -0
- app/page.tsx +35 -0
- components.json +16 -0
- components/ImageInput/Highlight/Highlight.tsx +53 -0
- components/ImageInput/Highlight/index.tsx +3 -0
- components/ImageInput/ImageInput.tsx +25 -0
- components/ImageInput/Preview/Markers/Markers.tsx +60 -0
- components/ImageInput/Preview/Markers/index.tsx +3 -0
- components/ImageInput/Preview/Preview.tsx +42 -0
- components/ImageInput/Preview/index.tsx +3 -0
- components/ImageInput/index.tsx +3 -0
- components/ModelSelector/ModelSelector.tsx +59 -0
- components/ModelSelector/index.tsx +3 -0
- components/Results/Filters/Filters.tsx +36 -0
- components/Results/Filters/index.tsx +3 -0
- components/Results/Item/Item.tsx +53 -0
- components/Results/Item/index.tsx +3 -0
- components/Results/Results.tsx +19 -0
- components/Results/index.tsx +3 -0
- components/layout/Error/Error.tsx +26 -0
- components/layout/Error/index.tsx +3 -0
- components/layout/Header/Header.tsx +27 -0
- components/layout/Header/index.tsx +3 -0
- components/layout/Loading/Loading.tsx +22 -0
- components/layout/Loading/index.tsx +3 -0
- components/layout/SuccessWrapper/SuccessWrapper.tsx +13 -0
- components/layout/SuccessWrapper/index.tsx +3 -0
- components/layout/ThemeProvider/ThemeProvider.tsx +11 -0
- components/layout/ThemeProvider/index.tsx +3 -0
- components/layout/ThemeToggle/ThemeToggle.tsx +37 -0
- components/layout/ThemeToggle/index.tsx +3 -0
- components/ui/alert.tsx +59 -0
- components/ui/button.tsx +57 -0
- components/ui/card.tsx +76 -0
- components/ui/dropdown-menu.tsx +205 -0
- components/ui/input.tsx +25 -0
- components/ui/label.tsx +26 -0
- components/ui/select.tsx +120 -0
- components/ui/skeleton.tsx +15 -0
- components/ui/slider.tsx +28 -0
- components/ui/switch.tsx +29 -0
.eslintrc.json
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"extends": "next/core-web-vitals"
|
3 |
+
}
|
.gitignore
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
2 |
+
|
3 |
+
# dependencies
|
4 |
+
/node_modules
|
5 |
+
/.pnp
|
6 |
+
.pnp.js
|
7 |
+
|
8 |
+
# testing
|
9 |
+
/coverage
|
10 |
+
|
11 |
+
# next.js
|
12 |
+
/.next/
|
13 |
+
/out/
|
14 |
+
|
15 |
+
# production
|
16 |
+
/build
|
17 |
+
|
18 |
+
# misc
|
19 |
+
.DS_Store
|
20 |
+
*.pem
|
21 |
+
|
22 |
+
# debug
|
23 |
+
npm-debug.log*
|
24 |
+
yarn-debug.log*
|
25 |
+
yarn-error.log*
|
26 |
+
|
27 |
+
# local env files
|
28 |
+
.env*.local
|
29 |
+
|
30 |
+
# vercel
|
31 |
+
.vercel
|
32 |
+
|
33 |
+
# typescript
|
34 |
+
*.tsbuildinfo
|
35 |
+
next-env.d.ts
|
Dockerfile
ADDED
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM node:18-alpine AS base
|
2 |
+
|
3 |
+
# Install dependencies only when needed
|
4 |
+
FROM base AS deps
|
5 |
+
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
6 |
+
RUN apk add --no-cache libc6-compat
|
7 |
+
WORKDIR /app
|
8 |
+
|
9 |
+
# Install dependencies based on the preferred package manager
|
10 |
+
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
11 |
+
RUN \
|
12 |
+
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
13 |
+
elif [ -f package-lock.json ]; then npm ci; \
|
14 |
+
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
|
15 |
+
else echo "Lockfile not found." && exit 1; \
|
16 |
+
fi
|
17 |
+
|
18 |
+
# Uncomment the following lines if you want to use a secret at buildtime,
|
19 |
+
# for example to access your private npm packages
|
20 |
+
# RUN --mount=type=secret,id=HF_EXAMPLE_SECRET,mode=0444,required=true \
|
21 |
+
# $(cat /run/secrets/HF_EXAMPLE_SECRET)
|
22 |
+
|
23 |
+
# Rebuild the source code only when needed
|
24 |
+
FROM base AS builder
|
25 |
+
WORKDIR /app
|
26 |
+
COPY --from=deps /app/node_modules ./node_modules
|
27 |
+
COPY . .
|
28 |
+
|
29 |
+
# Next.js collects completely anonymous telemetry data about general usage.
|
30 |
+
# Learn more here: https://nextjs.org/telemetry
|
31 |
+
# Uncomment the following line in case you want to disable telemetry during the build.
|
32 |
+
# ENV NEXT_TELEMETRY_DISABLED 1
|
33 |
+
|
34 |
+
# RUN yarn build
|
35 |
+
|
36 |
+
# If you use yarn, comment out this line and use the line above
|
37 |
+
RUN npm run build
|
38 |
+
|
39 |
+
# Production image, copy all the files and run next
|
40 |
+
FROM base AS runner
|
41 |
+
WORKDIR /app
|
42 |
+
|
43 |
+
ENV NODE_ENV production
|
44 |
+
# Uncomment the following line in case you want to disable telemetry during runtime.
|
45 |
+
# ENV NEXT_TELEMETRY_DISABLED 1
|
46 |
+
|
47 |
+
RUN addgroup --system --gid 1001 nodejs
|
48 |
+
RUN adduser --system --uid 1001 nextjs
|
49 |
+
|
50 |
+
COPY --from=builder /app/public ./public
|
51 |
+
|
52 |
+
# Automatically leverage output traces to reduce image size
|
53 |
+
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
54 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
55 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
56 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/cache ./.next/cache
|
57 |
+
# COPY --from=builder --chown=nextjs:nodejs /app/.next/cache/fetch-cache ./.next/cache/fetch-cache
|
58 |
+
|
59 |
+
USER nextjs
|
60 |
+
|
61 |
+
EXPOSE 3000
|
62 |
+
|
63 |
+
ENV PORT 3000
|
64 |
+
|
65 |
+
CMD ["node", "server.js"]
|
README.md
CHANGED
@@ -4,8 +4,9 @@ emoji: 🏢
|
|
4 |
colorFrom: green
|
5 |
colorTo: gray
|
6 |
sdk: docker
|
|
|
7 |
pinned: false
|
8 |
license: mit
|
9 |
---
|
10 |
|
11 |
-
|
|
|
4 |
colorFrom: green
|
5 |
colorTo: gray
|
6 |
sdk: docker
|
7 |
+
app_port: 3000
|
8 |
pinned: false
|
9 |
license: mit
|
10 |
---
|
11 |
|
12 |
+
|
app/api/detect/data.ts
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
type Data = {
|
2 |
+
score: number;
|
3 |
+
label: string;
|
4 |
+
box: {
|
5 |
+
xmin: number;
|
6 |
+
ymin: number;
|
7 |
+
xmax: number;
|
8 |
+
ymax: number;
|
9 |
+
};
|
10 |
+
}[];
|
11 |
+
export function processData(data: Data) {
|
12 |
+
return data.map((i, index) => ({
|
13 |
+
score: i.score,
|
14 |
+
label: i.label,
|
15 |
+
x: i.box.xmin,
|
16 |
+
y: i.box.ymin,
|
17 |
+
width: i.box.xmax - i.box.xmin,
|
18 |
+
height: i.box.ymax - i.box.ymin,
|
19 |
+
key: index,
|
20 |
+
}));
|
21 |
+
}
|
app/api/detect/route.ts
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextResponse } from "next/server";
|
2 |
+
import { extractPart, getImageDimensions, isImageValid } from "./utils";
|
3 |
+
import { processData } from "./data";
|
4 |
+
import constants from "@/constants";
|
5 |
+
import type { DetectResponse } from "@/types";
|
6 |
+
|
7 |
+
async function query(file: File, model: string) {
|
8 |
+
const response = await fetch(
|
9 |
+
`https://api-inference.huggingface.co/models/${model}`,
|
10 |
+
{
|
11 |
+
headers: {
|
12 |
+
Authorization: `Bearer ${process.env.HF_TOKEN}`,
|
13 |
+
},
|
14 |
+
method: "POST",
|
15 |
+
body: file,
|
16 |
+
}
|
17 |
+
);
|
18 |
+
const result = await response.json();
|
19 |
+
return result;
|
20 |
+
}
|
21 |
+
|
22 |
+
export async function POST(request: Request) {
|
23 |
+
const formData = await request.formData();
|
24 |
+
const image = formData.get("file");
|
25 |
+
const model = formData.get("model") as string;
|
26 |
+
|
27 |
+
if (image instanceof Blob) {
|
28 |
+
const buffer = await image.arrayBuffer();
|
29 |
+
const isValidImage = await isImageValid(buffer);
|
30 |
+
if (isValidImage) {
|
31 |
+
const { width, height } = await getImageDimensions(buffer);
|
32 |
+
const matches = [];
|
33 |
+
const labels: string[] = [];
|
34 |
+
|
35 |
+
const result = await query(image, model);
|
36 |
+
if (Array.isArray(result)) {
|
37 |
+
const processed = processData(result);
|
38 |
+
for (let i = 0; i < processed.length; i++) {
|
39 |
+
const item = processed[i];
|
40 |
+
const extract = await extractPart(buffer, item);
|
41 |
+
const labelIndex = labels.findIndex((i) => i === item.label);
|
42 |
+
let colorIndex;
|
43 |
+
if (labelIndex !== -1) {
|
44 |
+
colorIndex = labelIndex % constants.INDICATOR_COLORS.length;
|
45 |
+
} else {
|
46 |
+
colorIndex = labels.length % constants.INDICATOR_COLORS.length;
|
47 |
+
labels.push(item.label);
|
48 |
+
}
|
49 |
+
matches.push({ ...item, extract, colorIndex });
|
50 |
+
}
|
51 |
+
|
52 |
+
const res: DetectResponse = { width, height, matches, labels };
|
53 |
+
return NextResponse.json(res);
|
54 |
+
}
|
55 |
+
}
|
56 |
+
}
|
57 |
+
return NextResponse.json({ error: true });
|
58 |
+
}
|
app/api/detect/utils.ts
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sharp from "sharp";
|
2 |
+
|
3 |
+
export async function isImageValid(arrayBuffer: ArrayBuffer): Promise<boolean> {
|
4 |
+
try {
|
5 |
+
const metadata = await sharp(Buffer.from(arrayBuffer)).metadata();
|
6 |
+
return metadata.format !== undefined;
|
7 |
+
} catch (error) {
|
8 |
+
return false;
|
9 |
+
}
|
10 |
+
}
|
11 |
+
|
12 |
+
export async function getImageDimensions(
|
13 |
+
arrayBuffer: ArrayBuffer
|
14 |
+
): Promise<{ width: number; height: number }> {
|
15 |
+
const metadata = await sharp(Buffer.from(arrayBuffer)).metadata();
|
16 |
+
return { width: metadata.width || 0, height: metadata.height || 0 };
|
17 |
+
}
|
18 |
+
|
19 |
+
export async function extractPart(
|
20 |
+
arrayBuffer: ArrayBuffer,
|
21 |
+
{
|
22 |
+
x,
|
23 |
+
y,
|
24 |
+
width,
|
25 |
+
height,
|
26 |
+
}: { x: number; y: number; width: number; height: number }
|
27 |
+
) {
|
28 |
+
const out = await sharp(arrayBuffer)
|
29 |
+
.extract({ left: x, top: y, width, height })
|
30 |
+
.toBuffer();
|
31 |
+
const img = out.toString("base64");
|
32 |
+
return img;
|
33 |
+
}
|
app/favicon.ico
ADDED
app/globals.css
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@tailwind base;
|
2 |
+
@tailwind components;
|
3 |
+
@tailwind utilities;
|
4 |
+
|
5 |
+
@layer base {
|
6 |
+
:root {
|
7 |
+
--background: 0 0% 100%;
|
8 |
+
--foreground: 240 10% 3.9%;
|
9 |
+
|
10 |
+
--card: 0 0% 100%;
|
11 |
+
--card-foreground: 240 10% 3.9%;
|
12 |
+
|
13 |
+
--popover: 0 0% 100%;
|
14 |
+
--popover-foreground: 240 10% 3.9%;
|
15 |
+
|
16 |
+
--primary: 240 5.9% 10%;
|
17 |
+
--primary-foreground: 0 0% 98%;
|
18 |
+
|
19 |
+
--secondary: 240 4.8% 95.9%;
|
20 |
+
--secondary-foreground: 240 5.9% 10%;
|
21 |
+
|
22 |
+
--muted: 240 4.8% 95.9%;
|
23 |
+
--muted-foreground: 240 3.8% 46.1%;
|
24 |
+
|
25 |
+
--accent: 240 4.8% 95.9%;
|
26 |
+
--accent-foreground: 240 5.9% 10%;
|
27 |
+
|
28 |
+
--destructive: 0 84.2% 60.2%;
|
29 |
+
--destructive-foreground: 0 0% 98%;
|
30 |
+
|
31 |
+
--border: 240 5.9% 90%;
|
32 |
+
--input: 240 5.9% 90%;
|
33 |
+
--ring: 240 10% 3.9%;
|
34 |
+
|
35 |
+
--radius: 0.5rem;
|
36 |
+
}
|
37 |
+
|
38 |
+
.dark {
|
39 |
+
--background: 240 10% 3.9%;
|
40 |
+
--foreground: 0 0% 98%;
|
41 |
+
|
42 |
+
--card: 240 10% 3.9%;
|
43 |
+
--card-foreground: 0 0% 98%;
|
44 |
+
|
45 |
+
--popover: 240 10% 3.9%;
|
46 |
+
--popover-foreground: 0 0% 98%;
|
47 |
+
|
48 |
+
--primary: 0 0% 98%;
|
49 |
+
--primary-foreground: 240 5.9% 10%;
|
50 |
+
|
51 |
+
--secondary: 240 3.7% 15.9%;
|
52 |
+
--secondary-foreground: 0 0% 98%;
|
53 |
+
|
54 |
+
--muted: 240 3.7% 15.9%;
|
55 |
+
--muted-foreground: 240 5% 64.9%;
|
56 |
+
|
57 |
+
--accent: 240 3.7% 15.9%;
|
58 |
+
--accent-foreground: 0 0% 98%;
|
59 |
+
|
60 |
+
--destructive: 0 62.8% 30.6%;
|
61 |
+
--destructive-foreground: 0 0% 98%;
|
62 |
+
|
63 |
+
--border: 240 3.7% 15.9%;
|
64 |
+
--input: 240 3.7% 15.9%;
|
65 |
+
--ring: 240 4.9% 83.9%;
|
66 |
+
}
|
67 |
+
}
|
68 |
+
|
69 |
+
@layer base {
|
70 |
+
* {
|
71 |
+
@apply border-border;
|
72 |
+
}
|
73 |
+
body {
|
74 |
+
@apply bg-background text-foreground;
|
75 |
+
}
|
76 |
+
}
|
77 |
+
|
78 |
+
.dark .score-fg{
|
79 |
+
background-color: #3d8a21 ;
|
80 |
+
}
|
81 |
+
.light .score-fg{
|
82 |
+
background-color: #8ddf9a;
|
83 |
+
}
|
app/layout.tsx
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import "./globals.css";
|
2 |
+
import type { Metadata } from "next";
|
3 |
+
import { Inter } from "next/font/google";
|
4 |
+
import ThemeProvider from "@/components/layout/ThemeProvider";
|
5 |
+
import Header from "@/components/layout/Header";
|
6 |
+
|
7 |
+
const inter = Inter({ subsets: ["latin"] });
|
8 |
+
|
9 |
+
export const metadata: Metadata = {
|
10 |
+
title: "Object Detection",
|
11 |
+
description: "Object detection with different models",
|
12 |
+
};
|
13 |
+
|
14 |
+
export default function RootLayout({
|
15 |
+
children,
|
16 |
+
}: {
|
17 |
+
children: React.ReactNode;
|
18 |
+
}) {
|
19 |
+
return (
|
20 |
+
<html lang="en" suppressHydrationWarning>
|
21 |
+
<body className={inter.className}>
|
22 |
+
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
23 |
+
<Header />
|
24 |
+
<main className="max-w-5xl mx-auto pt-20 px-4">{children}</main>
|
25 |
+
</ThemeProvider>
|
26 |
+
</body>
|
27 |
+
</html>
|
28 |
+
);
|
29 |
+
}
|
app/page.tsx
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
2 |
+
import Error from "@/components/layout/Error";
|
3 |
+
import Loading from "@/components/layout/Loading";
|
4 |
+
import SuccessWrapper from "@/components/layout/SuccessWrapper";
|
5 |
+
import ImageInput from "@/components/ImageInput";
|
6 |
+
import Results from "@/components/Results";
|
7 |
+
import ModelSelector from "@/components/ModelSelector";
|
8 |
+
export default function Home() {
|
9 |
+
return (
|
10 |
+
<main className="pb-20">
|
11 |
+
<Card>
|
12 |
+
<CardContent className="px-4 pt-4">
|
13 |
+
<Card className="w-full mb-4">
|
14 |
+
<ModelSelector />
|
15 |
+
</Card>
|
16 |
+
<ImageInput />
|
17 |
+
</CardContent>
|
18 |
+
</Card>
|
19 |
+
<Card className="mt-6">
|
20 |
+
<CardHeader>
|
21 |
+
<div className="flex items-center justify-between">
|
22 |
+
<h3 className="tracking-tight font-bold text-xl">Results</h3>
|
23 |
+
</div>
|
24 |
+
</CardHeader>
|
25 |
+
<CardContent className="px-4">
|
26 |
+
<Loading />
|
27 |
+
<Error />
|
28 |
+
<SuccessWrapper>
|
29 |
+
<Results />
|
30 |
+
</SuccessWrapper>
|
31 |
+
</CardContent>
|
32 |
+
</Card>
|
33 |
+
</main>
|
34 |
+
);
|
35 |
+
}
|
components.json
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"$schema": "https://ui.shadcn.com/schema.json",
|
3 |
+
"style": "new-york",
|
4 |
+
"rsc": true,
|
5 |
+
"tsx": true,
|
6 |
+
"tailwind": {
|
7 |
+
"config": "tailwind.config.js",
|
8 |
+
"css": "app/globals.css",
|
9 |
+
"baseColor": "zinc",
|
10 |
+
"cssVariables": true
|
11 |
+
},
|
12 |
+
"aliases": {
|
13 |
+
"components": "@/components",
|
14 |
+
"utils": "@/lib/utils"
|
15 |
+
}
|
16 |
+
}
|
components/ImageInput/Highlight/Highlight.tsx
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import useStore from "@/store";
|
2 |
+
type CompProps = {
|
3 |
+
parentRef: React.MutableRefObject<HTMLDivElement | null>;
|
4 |
+
};
|
5 |
+
|
6 |
+
function getPos(ref: React.MutableRefObject<HTMLDivElement | null>) {
|
7 |
+
if (!ref.current) {
|
8 |
+
return [0, 0];
|
9 |
+
}
|
10 |
+
const rect = ref.current.getBoundingClientRect();
|
11 |
+
return [rect.x + rect.width + 24, rect.top + 24];
|
12 |
+
}
|
13 |
+
|
14 |
+
export default function Highlight({ parentRef }: CompProps) {
|
15 |
+
const item = useStore((state) => state.hovering);
|
16 |
+
|
17 |
+
if (item === null) {
|
18 |
+
return null;
|
19 |
+
}
|
20 |
+
|
21 |
+
const [left, top] = getPos(parentRef);
|
22 |
+
return (
|
23 |
+
<div
|
24 |
+
className="fixed z-50 w-52 bg-white shadow-md dark:bg-zinc-950"
|
25 |
+
style={{ top: `${top}px`, left: `${left}px` }}
|
26 |
+
>
|
27 |
+
<div className="border rounded-md overflow-hidden">
|
28 |
+
<div className="border-b py-1 px-2 text-center font-semibold capitalize">
|
29 |
+
{item.label}
|
30 |
+
</div>
|
31 |
+
<div className="p-2">
|
32 |
+
<div className="relative overflow-hidden aspect-square">
|
33 |
+
<img
|
34 |
+
src={`data:image/png;base64,${item.extract}`}
|
35 |
+
className="absolute left-0 top-0 w-full h-full object-contain"
|
36 |
+
/>
|
37 |
+
</div>
|
38 |
+
</div>
|
39 |
+
<div className="px-2 border-t pt-2">
|
40 |
+
<div className="bg-zinc-200 dark:bg-zinc-800 h-1 rounded-md">
|
41 |
+
<div
|
42 |
+
className="h-1 score-fg rounded-md"
|
43 |
+
style={{ width: `${Math.round(item.score * 100)}%` }}
|
44 |
+
/>
|
45 |
+
</div>
|
46 |
+
</div>
|
47 |
+
<div className="py-1 px-2 text-center">
|
48 |
+
Score : {item.score.toFixed(4)}
|
49 |
+
</div>
|
50 |
+
</div>
|
51 |
+
</div>
|
52 |
+
);
|
53 |
+
}
|
components/ImageInput/Highlight/index.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import Highlight from './Highlight';
|
2 |
+
|
3 |
+
export default Highlight;
|
components/ImageInput/ImageInput.tsx
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
import useStore from "@/store";
|
3 |
+
import Preview from "@/components/ImageInput/Preview";
|
4 |
+
|
5 |
+
type CompProps = {};
|
6 |
+
export default function ImageInput({}: CompProps) {
|
7 |
+
const handleImageChange = useStore((state) => state.handleImageChange);
|
8 |
+
return (
|
9 |
+
<>
|
10 |
+
<label
|
11 |
+
className="grow bg-zinc-100 dark:bg-zinc-900 rounded-md overflow-hidden grid place-items-center py-2"
|
12 |
+
htmlFor="picture"
|
13 |
+
>
|
14 |
+
<Preview />
|
15 |
+
<input
|
16 |
+
className="hidden"
|
17 |
+
id="picture"
|
18 |
+
type="file"
|
19 |
+
accept="image/*"
|
20 |
+
onChange={handleImageChange}
|
21 |
+
/>
|
22 |
+
</label>
|
23 |
+
</>
|
24 |
+
);
|
25 |
+
}
|
components/ImageInput/Preview/Markers/Markers.tsx
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import useStore from "@/store";
|
2 |
+
import constants from "@/constants";
|
3 |
+
import type { MatchItem } from "@/types";
|
4 |
+
type CompProps = {};
|
5 |
+
export default function Markers({}: CompProps) {
|
6 |
+
const { matches, width, height } = useStore(({ matches, width, height }) => ({
|
7 |
+
matches,
|
8 |
+
width,
|
9 |
+
height,
|
10 |
+
}));
|
11 |
+
|
12 |
+
return (
|
13 |
+
<>
|
14 |
+
<svg
|
15 |
+
className="absolute inset-x-0 top-0"
|
16 |
+
viewBox={`0 0 ${width} ${height}`}
|
17 |
+
fill="none"
|
18 |
+
xmlns="http://www.w3.org/2000/svg"
|
19 |
+
>
|
20 |
+
{matches.map((i) => (
|
21 |
+
<Marker item={i} key={i.key} />
|
22 |
+
))}
|
23 |
+
</svg>
|
24 |
+
</>
|
25 |
+
);
|
26 |
+
}
|
27 |
+
|
28 |
+
type MarkerProps = {
|
29 |
+
item: MatchItem;
|
30 |
+
};
|
31 |
+
function Marker({ item }: MarkerProps) {
|
32 |
+
const { setHovering, clearHovering } = useStore(
|
33 |
+
({ setHovering, clearHovering }) => ({ setHovering, clearHovering })
|
34 |
+
);
|
35 |
+
const selectedLabel = useStore((state) => state.selectedLabel);
|
36 |
+
const isolate = useStore((state) => state.isolate);
|
37 |
+
if (selectedLabel !== "all" && selectedLabel !== item.label) {
|
38 |
+
return null;
|
39 |
+
}
|
40 |
+
if (isolate !== null && isolate !== item.key) {
|
41 |
+
return null;
|
42 |
+
}
|
43 |
+
|
44 |
+
return (
|
45 |
+
<rect
|
46 |
+
x={item.x}
|
47 |
+
y={item.y}
|
48 |
+
width={item.width}
|
49 |
+
height={item.height}
|
50 |
+
className={constants.INDICATOR_COLORS[item.colorIndex]}
|
51 |
+
fill="currentColor"
|
52 |
+
fillOpacity="0.1"
|
53 |
+
stroke="currentColor"
|
54 |
+
strokeWidth="3"
|
55 |
+
key={item.key}
|
56 |
+
onMouseEnter={() => setHovering(item.key)}
|
57 |
+
onMouseLeave={() => clearHovering(item.key)}
|
58 |
+
/>
|
59 |
+
);
|
60 |
+
}
|
components/ImageInput/Preview/Markers/index.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import Markers from './Markers';
|
2 |
+
|
3 |
+
export default Markers;
|
components/ImageInput/Preview/Preview.tsx
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
import useStore from "@/store";
|
3 |
+
import SuccessWrapper from "@/components/layout/SuccessWrapper";
|
4 |
+
import Markers from "@/components/ImageInput/Preview/Markers";
|
5 |
+
import Highlight from "@/components/ImageInput/Highlight";
|
6 |
+
import { useRef } from "react";
|
7 |
+
type CompProps = {};
|
8 |
+
export default function Preview({}: CompProps) {
|
9 |
+
const previewURL = useStore((state) => state.previewURL);
|
10 |
+
const ref = useRef<HTMLDivElement | null>(null);
|
11 |
+
|
12 |
+
if (!previewURL) {
|
13 |
+
return <Placeholder />;
|
14 |
+
}
|
15 |
+
return (
|
16 |
+
<div className="relative inline-flex overflow-hidden max-w-lg" ref={ref}>
|
17 |
+
<img alt="" className="object-contain" src={previewURL} />
|
18 |
+
<SuccessWrapper>
|
19 |
+
<Highlight parentRef={ref} />
|
20 |
+
<Markers />
|
21 |
+
</SuccessWrapper>
|
22 |
+
</div>
|
23 |
+
);
|
24 |
+
}
|
25 |
+
|
26 |
+
function Placeholder() {
|
27 |
+
return (
|
28 |
+
<svg
|
29 |
+
fill="none"
|
30 |
+
viewBox="0 0 24 24"
|
31 |
+
strokeWidth={1.5}
|
32 |
+
stroke="currentColor"
|
33 |
+
className="w-60 h-60 text-zinc-200 dark:text-zinc-800"
|
34 |
+
>
|
35 |
+
<path
|
36 |
+
strokeLinecap="round"
|
37 |
+
strokeLinejoin="round"
|
38 |
+
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
|
39 |
+
/>
|
40 |
+
</svg>
|
41 |
+
);
|
42 |
+
}
|
components/ImageInput/Preview/index.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import Preview from './Preview';
|
2 |
+
|
3 |
+
export default Preview;
|
components/ImageInput/index.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import ImageInput from './ImageInput';
|
2 |
+
|
3 |
+
export default ImageInput;
|
components/ModelSelector/ModelSelector.tsx
ADDED
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
import {
|
3 |
+
Select,
|
4 |
+
SelectContent,
|
5 |
+
SelectGroup,
|
6 |
+
SelectItem,
|
7 |
+
SelectTrigger,
|
8 |
+
SelectValue,
|
9 |
+
} from "@/components/ui/select";
|
10 |
+
import useStore from "@/store";
|
11 |
+
|
12 |
+
type CompProps = {};
|
13 |
+
export default function ModelSelector({}: CompProps) {
|
14 |
+
return (
|
15 |
+
<div className="flex items-center px-4 py-2 justify-between">
|
16 |
+
<div className="inline-flex items-center space-x-2">
|
17 |
+
<div className="font-bold"> Model : </div>
|
18 |
+
<SelectModel />
|
19 |
+
</div>
|
20 |
+
</div>
|
21 |
+
);
|
22 |
+
}
|
23 |
+
|
24 |
+
const options = [
|
25 |
+
"facebook/detr-resnet-50",
|
26 |
+
"hustvl/yolos-tiny",
|
27 |
+
"facebook/detr-resnet-101",
|
28 |
+
"hustvl/yolos-small",
|
29 |
+
"valentinafeve/yolos-fashionpedia",
|
30 |
+
"keremberke/yolov5m-license-plate",
|
31 |
+
"facebook/detr-resnet-101-dc5",
|
32 |
+
"nickmuchi/yolos-small-finetuned-license-plate-detection",
|
33 |
+
"microsoft/table-transformer-structure-recognition",
|
34 |
+
"TahaDouaji/detr-doc-table-detection",
|
35 |
+
"hustvl/yolos-base",
|
36 |
+
"biglam/detr-resnet-50_fine_tuned_nls_chapbooks",
|
37 |
+
];
|
38 |
+
function SelectModel() {
|
39 |
+
const { model, setModel } = useStore((state) => ({
|
40 |
+
model: state.model,
|
41 |
+
setModel: state.setModel,
|
42 |
+
}));
|
43 |
+
return (
|
44 |
+
<Select value={model} onValueChange={setModel}>
|
45 |
+
<SelectTrigger className="w-[500px]">
|
46 |
+
<SelectValue placeholder="Select a fruit" />
|
47 |
+
</SelectTrigger>
|
48 |
+
<SelectContent className="w-full">
|
49 |
+
<SelectGroup>
|
50 |
+
{options.map((i) => (
|
51 |
+
<SelectItem value={i} key={i}>
|
52 |
+
{i}
|
53 |
+
</SelectItem>
|
54 |
+
))}
|
55 |
+
</SelectGroup>
|
56 |
+
</SelectContent>
|
57 |
+
</Select>
|
58 |
+
);
|
59 |
+
}
|
components/ModelSelector/index.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import ModelSelector from './ModelSelector';
|
2 |
+
|
3 |
+
export default ModelSelector;
|
components/Results/Filters/Filters.tsx
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import useStore from "@/store";
|
2 |
+
import { useCallback } from "react";
|
3 |
+
import clsx from "clsx";
|
4 |
+
|
5 |
+
export default function Filters() {
|
6 |
+
const labels = useStore((state) => state.labels);
|
7 |
+
const selectedLabel = useStore((state) => state.selectedLabel);
|
8 |
+
const setSelectedLabel = useStore((state) => state.setSelectedLabel);
|
9 |
+
|
10 |
+
const onClick = useCallback(
|
11 |
+
(e: React.MouseEvent<HTMLButtonElement>) => {
|
12 |
+
setSelectedLabel(e.currentTarget.id);
|
13 |
+
},
|
14 |
+
[setSelectedLabel]
|
15 |
+
);
|
16 |
+
return (
|
17 |
+
<div className="flex flex-wrap">
|
18 |
+
{["all", ...labels].map((i) => (
|
19 |
+
<div className="p-2" key={i}>
|
20 |
+
<button
|
21 |
+
className={clsx(
|
22 |
+
"capitalize px-6 py-1 rounded-full border",
|
23 |
+
selectedLabel === i
|
24 |
+
? "bg-zinc-300 dark:bg-zinc-700"
|
25 |
+
: "bg-transperant"
|
26 |
+
)}
|
27 |
+
id={i}
|
28 |
+
onClick={onClick}
|
29 |
+
>
|
30 |
+
{i}
|
31 |
+
</button>
|
32 |
+
</div>
|
33 |
+
))}
|
34 |
+
</div>
|
35 |
+
);
|
36 |
+
}
|
components/Results/Filters/index.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import Filters from './Filters';
|
2 |
+
|
3 |
+
export default Filters;
|
components/Results/Item/Item.tsx
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { MatchItem } from "@/types";
|
2 |
+
import useStore from "@/store";
|
3 |
+
import { useCallback } from "react";
|
4 |
+
import clsx from "clsx";
|
5 |
+
type CompProps = {
|
6 |
+
item: MatchItem;
|
7 |
+
selected: string;
|
8 |
+
};
|
9 |
+
export default function Item({ item, selected }: CompProps) {
|
10 |
+
const toggleIsolate = useStore((state) => state.toggleIsolate);
|
11 |
+
const isolate = useStore((state) => state.isolate);
|
12 |
+
const onClick = useCallback(() => {
|
13 |
+
toggleIsolate(item.key);
|
14 |
+
}, [toggleIsolate, item]);
|
15 |
+
if (selected !== "all" && selected !== item.label) {
|
16 |
+
return null;
|
17 |
+
}
|
18 |
+
|
19 |
+
return (
|
20 |
+
<button className="p-2 w-full sm:w-1/2 md:w-1/3 lg:w-1/4" onClick={onClick}>
|
21 |
+
<div
|
22 |
+
className={clsx(
|
23 |
+
"border rounded-md overflow-hidden",
|
24 |
+
isolate === item.key &&
|
25 |
+
"dark:border-green-800 dark:bg-green-800/10 border-green-300 bg-green-200/20"
|
26 |
+
)}
|
27 |
+
>
|
28 |
+
<div className="border-b py-1 px-2 text-center font-semibold capitalize">
|
29 |
+
{item.label}
|
30 |
+
</div>
|
31 |
+
<div className="p-2">
|
32 |
+
<div className="relative overflow-hidden aspect-square">
|
33 |
+
<img
|
34 |
+
src={`data:image/png;base64,${item.extract}`}
|
35 |
+
className="absolute left-0 top-0 w-full h-full object-contain"
|
36 |
+
/>
|
37 |
+
</div>
|
38 |
+
</div>
|
39 |
+
<div className="px-2 border-t pt-2">
|
40 |
+
<div className="bg-zinc-200 dark:bg-zinc-800 h-1 rounded-md">
|
41 |
+
<div
|
42 |
+
className="h-1 score-fg rounded-md"
|
43 |
+
style={{ width: `${Math.round(item.score * 100)}%` }}
|
44 |
+
/>
|
45 |
+
</div>
|
46 |
+
</div>
|
47 |
+
<div className="py-1 px-2 text-center">
|
48 |
+
Score : {item.score.toFixed(4)}
|
49 |
+
</div>
|
50 |
+
</div>
|
51 |
+
</button>
|
52 |
+
);
|
53 |
+
}
|
components/Results/Item/index.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import Item from './Item';
|
2 |
+
|
3 |
+
export default Item;
|
components/Results/Results.tsx
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
import useStore from "@/store";
|
3 |
+
import Filters from "@/components/Results/Filters";
|
4 |
+
import Item from "@/components/Results/Item";
|
5 |
+
type CompProps = {};
|
6 |
+
export default function Results({}: CompProps) {
|
7 |
+
const matches = useStore((state) => state.matches);
|
8 |
+
const selectedLabel = useStore((state) => state.selectedLabel);
|
9 |
+
return (
|
10 |
+
<>
|
11 |
+
<Filters />
|
12 |
+
<div className="flex flex-wrap">
|
13 |
+
{matches.map((item) => (
|
14 |
+
<Item key={item.key} item={item} selected={selectedLabel} />
|
15 |
+
))}
|
16 |
+
</div>
|
17 |
+
</>
|
18 |
+
);
|
19 |
+
}
|
components/Results/index.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import Results from './Results';
|
2 |
+
|
3 |
+
export default Results;
|
components/layout/Error/Error.tsx
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
|
3 |
+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
4 |
+
import useStore from "@/store";
|
5 |
+
type CompProps = {};
|
6 |
+
export default function Error({}: CompProps) {
|
7 |
+
const error = useStore((state) => state.error);
|
8 |
+
const retry = useStore((state) => state.retry);
|
9 |
+
|
10 |
+
if (!error) {
|
11 |
+
return null;
|
12 |
+
}
|
13 |
+
return (
|
14 |
+
<Alert variant="destructive">
|
15 |
+
<ExclamationTriangleIcon className="h-4 w-4" />
|
16 |
+
<AlertTitle>Error</AlertTitle>
|
17 |
+
<AlertDescription>
|
18 |
+
Unknown Error Occurred: This could be due to the model still loading.
|
19 |
+
<button className="underline" onClick={retry}>
|
20 |
+
Please retry
|
21 |
+
</button>{" "}
|
22 |
+
after a few minutes.
|
23 |
+
</AlertDescription>
|
24 |
+
</Alert>
|
25 |
+
);
|
26 |
+
}
|
components/layout/Error/index.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import Error from './Error';
|
2 |
+
|
3 |
+
export default Error;
|
components/layout/Header/Header.tsx
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import ThemeToggle from "@/components/layout/ThemeToggle";
|
2 |
+
|
3 |
+
export default function Header() {
|
4 |
+
return (
|
5 |
+
<div className="fixed top-0 left-0 right-0 supports-backdrop-blur:bg-background/60 border-b bg-background/95 backdrop-blur z-20">
|
6 |
+
<nav className="h-14 flex items-center justify-between max-w-5xl mx-auto px-4">
|
7 |
+
<div className="">
|
8 |
+
<svg
|
9 |
+
xmlns="http://www.w3.org/2000/svg"
|
10 |
+
fill="none"
|
11 |
+
viewBox="0 0 24 24"
|
12 |
+
strokeWidth={1.5}
|
13 |
+
stroke="currentColor"
|
14 |
+
className="w-6 h-6"
|
15 |
+
>
|
16 |
+
<path
|
17 |
+
strokeLinecap="round"
|
18 |
+
strokeLinejoin="round"
|
19 |
+
d="M12 12.75c1.148 0 2.278.08 3.383.237 1.037.146 1.866.966 1.866 2.013 0 3.728-2.35 6.75-5.25 6.75S6.75 18.728 6.75 15c0-1.046.83-1.867 1.866-2.013A24.204 24.204 0 0112 12.75zm0 0c2.883 0 5.647.508 8.207 1.44a23.91 23.91 0 01-1.152 6.06M12 12.75c-2.883 0-5.647.508-8.208 1.44.125 2.104.52 4.136 1.153 6.06M12 12.75a2.25 2.25 0 002.248-2.354M12 12.75a2.25 2.25 0 01-2.248-2.354M12 8.25c.995 0 1.971-.08 2.922-.236.403-.066.74-.358.795-.762a3.778 3.778 0 00-.399-2.25M12 8.25c-.995 0-1.97-.08-2.922-.236-.402-.066-.74-.358-.795-.762a3.734 3.734 0 01.4-2.253M12 8.25a2.25 2.25 0 00-2.248 2.146M12 8.25a2.25 2.25 0 012.248 2.146M8.683 5a6.032 6.032 0 01-1.155-1.002c.07-.63.27-1.222.574-1.747m.581 2.749A3.75 3.75 0 0115.318 5m0 0c.427-.283.815-.62 1.155-.999a4.471 4.471 0 00-.575-1.752M4.921 6a24.048 24.048 0 00-.392 3.314c1.668.546 3.416.914 5.223 1.082M19.08 6c.205 1.08.337 2.187.392 3.314a23.882 23.882 0 01-5.223 1.082"
|
20 |
+
/>
|
21 |
+
</svg>
|
22 |
+
</div>
|
23 |
+
<ThemeToggle />
|
24 |
+
</nav>
|
25 |
+
</div>
|
26 |
+
);
|
27 |
+
}
|
components/layout/Header/index.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import Header from './Header';
|
2 |
+
|
3 |
+
export default Header;
|
components/layout/Loading/Loading.tsx
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
import { range } from "@/utils";
|
3 |
+
import { Skeleton } from "@/components/ui/skeleton";
|
4 |
+
import useStore from "@/store";
|
5 |
+
type CompProps = {};
|
6 |
+
|
7 |
+
export default function Loading({}: CompProps) {
|
8 |
+
const loading = useStore((state) => state.loading);
|
9 |
+
|
10 |
+
if (!loading) {
|
11 |
+
return null;
|
12 |
+
}
|
13 |
+
return (
|
14 |
+
<div className="flex flex-wrap">
|
15 |
+
{range(8).map((i) => (
|
16 |
+
<div className="p-2 w-full sm:w-1/2 md:w-1/3 lg:w-1/4" key={i}>
|
17 |
+
<Skeleton className="aspect-square" />
|
18 |
+
</div>
|
19 |
+
))}
|
20 |
+
</div>
|
21 |
+
);
|
22 |
+
}
|
components/layout/Loading/index.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import Loading from './Loading';
|
2 |
+
|
3 |
+
export default Loading;
|
components/layout/SuccessWrapper/SuccessWrapper.tsx
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
import useStore from "@/store";
|
3 |
+
type CompProps = {
|
4 |
+
children: React.ReactNode;
|
5 |
+
};
|
6 |
+
export default function SuccessWrapper({ children }: CompProps) {
|
7 |
+
const success = useStore((state) => state.success);
|
8 |
+
|
9 |
+
if (!success) {
|
10 |
+
return null;
|
11 |
+
}
|
12 |
+
return children;
|
13 |
+
}
|
components/layout/SuccessWrapper/index.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import SuccessWrapper from './SuccessWrapper';
|
2 |
+
|
3 |
+
export default SuccessWrapper;
|
components/layout/ThemeProvider/ThemeProvider.tsx
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
|
3 |
+
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
4 |
+
import { type ThemeProviderProps } from "next-themes/dist/types";
|
5 |
+
|
6 |
+
export default function ThemeProvider({
|
7 |
+
children,
|
8 |
+
...props
|
9 |
+
}: ThemeProviderProps) {
|
10 |
+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
11 |
+
}
|
components/layout/ThemeProvider/index.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import ThemeProvider from './ThemeProvider';
|
2 |
+
|
3 |
+
export default ThemeProvider;
|
components/layout/ThemeToggle/ThemeToggle.tsx
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
|
3 |
+
import { useTheme } from "next-themes";
|
4 |
+
|
5 |
+
import { Button } from "@/components/ui/button";
|
6 |
+
import {
|
7 |
+
DropdownMenu,
|
8 |
+
DropdownMenuContent,
|
9 |
+
DropdownMenuItem,
|
10 |
+
DropdownMenuTrigger,
|
11 |
+
} from "@/components/ui/dropdown-menu";
|
12 |
+
type CompProps = {};
|
13 |
+
export default function ThemeToggle({}: CompProps) {
|
14 |
+
const { setTheme } = useTheme();
|
15 |
+
return (
|
16 |
+
<DropdownMenu>
|
17 |
+
<DropdownMenuTrigger asChild>
|
18 |
+
<Button variant="outline" size="icon">
|
19 |
+
<SunIcon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
20 |
+
<MoonIcon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
21 |
+
<span className="sr-only">Toggle theme</span>
|
22 |
+
</Button>
|
23 |
+
</DropdownMenuTrigger>
|
24 |
+
<DropdownMenuContent align="end">
|
25 |
+
<DropdownMenuItem onClick={() => setTheme("light")}>
|
26 |
+
Light
|
27 |
+
</DropdownMenuItem>
|
28 |
+
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
29 |
+
Dark
|
30 |
+
</DropdownMenuItem>
|
31 |
+
<DropdownMenuItem onClick={() => setTheme("system")}>
|
32 |
+
System
|
33 |
+
</DropdownMenuItem>
|
34 |
+
</DropdownMenuContent>
|
35 |
+
</DropdownMenu>
|
36 |
+
);
|
37 |
+
}
|
components/layout/ThemeToggle/index.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import ThemeToggle from './ThemeToggle';
|
2 |
+
|
3 |
+
export default ThemeToggle;
|
components/ui/alert.tsx
ADDED
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
3 |
+
|
4 |
+
import { cn } from "@/lib/utils"
|
5 |
+
|
6 |
+
const alertVariants = cva(
|
7 |
+
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
8 |
+
{
|
9 |
+
variants: {
|
10 |
+
variant: {
|
11 |
+
default: "bg-background text-foreground",
|
12 |
+
destructive:
|
13 |
+
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
14 |
+
},
|
15 |
+
},
|
16 |
+
defaultVariants: {
|
17 |
+
variant: "default",
|
18 |
+
},
|
19 |
+
}
|
20 |
+
)
|
21 |
+
|
22 |
+
const Alert = React.forwardRef<
|
23 |
+
HTMLDivElement,
|
24 |
+
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
25 |
+
>(({ className, variant, ...props }, ref) => (
|
26 |
+
<div
|
27 |
+
ref={ref}
|
28 |
+
role="alert"
|
29 |
+
className={cn(alertVariants({ variant }), className)}
|
30 |
+
{...props}
|
31 |
+
/>
|
32 |
+
))
|
33 |
+
Alert.displayName = "Alert"
|
34 |
+
|
35 |
+
const AlertTitle = React.forwardRef<
|
36 |
+
HTMLParagraphElement,
|
37 |
+
React.HTMLAttributes<HTMLHeadingElement>
|
38 |
+
>(({ className, ...props }, ref) => (
|
39 |
+
<h5
|
40 |
+
ref={ref}
|
41 |
+
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
42 |
+
{...props}
|
43 |
+
/>
|
44 |
+
))
|
45 |
+
AlertTitle.displayName = "AlertTitle"
|
46 |
+
|
47 |
+
const AlertDescription = React.forwardRef<
|
48 |
+
HTMLParagraphElement,
|
49 |
+
React.HTMLAttributes<HTMLParagraphElement>
|
50 |
+
>(({ className, ...props }, ref) => (
|
51 |
+
<div
|
52 |
+
ref={ref}
|
53 |
+
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
54 |
+
{...props}
|
55 |
+
/>
|
56 |
+
))
|
57 |
+
AlertDescription.displayName = "AlertDescription"
|
58 |
+
|
59 |
+
export { Alert, AlertTitle, AlertDescription }
|
components/ui/button.tsx
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
9 |
+
{
|
10 |
+
variants: {
|
11 |
+
variant: {
|
12 |
+
default:
|
13 |
+
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
14 |
+
destructive:
|
15 |
+
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
16 |
+
outline:
|
17 |
+
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
|
18 |
+
secondary:
|
19 |
+
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
20 |
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
21 |
+
link: "text-primary underline-offset-4 hover:underline",
|
22 |
+
},
|
23 |
+
size: {
|
24 |
+
default: "h-9 px-4 py-2",
|
25 |
+
sm: "h-8 rounded-md px-3 text-xs",
|
26 |
+
lg: "h-10 rounded-md px-8",
|
27 |
+
icon: "h-9 w-9",
|
28 |
+
},
|
29 |
+
},
|
30 |
+
defaultVariants: {
|
31 |
+
variant: "default",
|
32 |
+
size: "default",
|
33 |
+
},
|
34 |
+
}
|
35 |
+
)
|
36 |
+
|
37 |
+
export interface ButtonProps
|
38 |
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
39 |
+
VariantProps<typeof buttonVariants> {
|
40 |
+
asChild?: boolean
|
41 |
+
}
|
42 |
+
|
43 |
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
44 |
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
45 |
+
const Comp = asChild ? Slot : "button"
|
46 |
+
return (
|
47 |
+
<Comp
|
48 |
+
className={cn(buttonVariants({ variant, size, className }))}
|
49 |
+
ref={ref}
|
50 |
+
{...props}
|
51 |
+
/>
|
52 |
+
)
|
53 |
+
}
|
54 |
+
)
|
55 |
+
Button.displayName = "Button"
|
56 |
+
|
57 |
+
export { Button, buttonVariants }
|
components/ui/card.tsx
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
const Card = React.forwardRef<
|
6 |
+
HTMLDivElement,
|
7 |
+
React.HTMLAttributes<HTMLDivElement>
|
8 |
+
>(({ className, ...props }, ref) => (
|
9 |
+
<div
|
10 |
+
ref={ref}
|
11 |
+
className={cn(
|
12 |
+
"rounded-xl border bg-card text-card-foreground shadow",
|
13 |
+
className
|
14 |
+
)}
|
15 |
+
{...props}
|
16 |
+
/>
|
17 |
+
))
|
18 |
+
Card.displayName = "Card"
|
19 |
+
|
20 |
+
const CardHeader = React.forwardRef<
|
21 |
+
HTMLDivElement,
|
22 |
+
React.HTMLAttributes<HTMLDivElement>
|
23 |
+
>(({ className, ...props }, ref) => (
|
24 |
+
<div
|
25 |
+
ref={ref}
|
26 |
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
27 |
+
{...props}
|
28 |
+
/>
|
29 |
+
))
|
30 |
+
CardHeader.displayName = "CardHeader"
|
31 |
+
|
32 |
+
const CardTitle = React.forwardRef<
|
33 |
+
HTMLParagraphElement,
|
34 |
+
React.HTMLAttributes<HTMLHeadingElement>
|
35 |
+
>(({ className, ...props }, ref) => (
|
36 |
+
<h3
|
37 |
+
ref={ref}
|
38 |
+
className={cn("font-semibold leading-none tracking-tight", className)}
|
39 |
+
{...props}
|
40 |
+
/>
|
41 |
+
))
|
42 |
+
CardTitle.displayName = "CardTitle"
|
43 |
+
|
44 |
+
const CardDescription = React.forwardRef<
|
45 |
+
HTMLParagraphElement,
|
46 |
+
React.HTMLAttributes<HTMLParagraphElement>
|
47 |
+
>(({ className, ...props }, ref) => (
|
48 |
+
<p
|
49 |
+
ref={ref}
|
50 |
+
className={cn("text-sm text-muted-foreground", className)}
|
51 |
+
{...props}
|
52 |
+
/>
|
53 |
+
))
|
54 |
+
CardDescription.displayName = "CardDescription"
|
55 |
+
|
56 |
+
const CardContent = React.forwardRef<
|
57 |
+
HTMLDivElement,
|
58 |
+
React.HTMLAttributes<HTMLDivElement>
|
59 |
+
>(({ className, ...props }, ref) => (
|
60 |
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
61 |
+
))
|
62 |
+
CardContent.displayName = "CardContent"
|
63 |
+
|
64 |
+
const CardFooter = React.forwardRef<
|
65 |
+
HTMLDivElement,
|
66 |
+
React.HTMLAttributes<HTMLDivElement>
|
67 |
+
>(({ className, ...props }, ref) => (
|
68 |
+
<div
|
69 |
+
ref={ref}
|
70 |
+
className={cn("flex items-center p-6 pt-0", className)}
|
71 |
+
{...props}
|
72 |
+
/>
|
73 |
+
))
|
74 |
+
CardFooter.displayName = "CardFooter"
|
75 |
+
|
76 |
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
components/ui/dropdown-menu.tsx
ADDED
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
5 |
+
import {
|
6 |
+
CheckIcon,
|
7 |
+
ChevronRightIcon,
|
8 |
+
DotFilledIcon,
|
9 |
+
} from "@radix-ui/react-icons"
|
10 |
+
|
11 |
+
import { cn } from "@/lib/utils"
|
12 |
+
|
13 |
+
const DropdownMenu = DropdownMenuPrimitive.Root
|
14 |
+
|
15 |
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
16 |
+
|
17 |
+
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
18 |
+
|
19 |
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
20 |
+
|
21 |
+
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
22 |
+
|
23 |
+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
24 |
+
|
25 |
+
const DropdownMenuSubTrigger = React.forwardRef<
|
26 |
+
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
27 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
28 |
+
inset?: boolean
|
29 |
+
}
|
30 |
+
>(({ className, inset, children, ...props }, ref) => (
|
31 |
+
<DropdownMenuPrimitive.SubTrigger
|
32 |
+
ref={ref}
|
33 |
+
className={cn(
|
34 |
+
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
35 |
+
inset && "pl-8",
|
36 |
+
className
|
37 |
+
)}
|
38 |
+
{...props}
|
39 |
+
>
|
40 |
+
{children}
|
41 |
+
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
42 |
+
</DropdownMenuPrimitive.SubTrigger>
|
43 |
+
))
|
44 |
+
DropdownMenuSubTrigger.displayName =
|
45 |
+
DropdownMenuPrimitive.SubTrigger.displayName
|
46 |
+
|
47 |
+
const DropdownMenuSubContent = React.forwardRef<
|
48 |
+
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
49 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
50 |
+
>(({ className, ...props }, ref) => (
|
51 |
+
<DropdownMenuPrimitive.SubContent
|
52 |
+
ref={ref}
|
53 |
+
className={cn(
|
54 |
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
55 |
+
className
|
56 |
+
)}
|
57 |
+
{...props}
|
58 |
+
/>
|
59 |
+
))
|
60 |
+
DropdownMenuSubContent.displayName =
|
61 |
+
DropdownMenuPrimitive.SubContent.displayName
|
62 |
+
|
63 |
+
const DropdownMenuContent = React.forwardRef<
|
64 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
65 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
66 |
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
67 |
+
<DropdownMenuPrimitive.Portal>
|
68 |
+
<DropdownMenuPrimitive.Content
|
69 |
+
ref={ref}
|
70 |
+
sideOffset={sideOffset}
|
71 |
+
className={cn(
|
72 |
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
73 |
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
74 |
+
className
|
75 |
+
)}
|
76 |
+
{...props}
|
77 |
+
/>
|
78 |
+
</DropdownMenuPrimitive.Portal>
|
79 |
+
))
|
80 |
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
81 |
+
|
82 |
+
const DropdownMenuItem = React.forwardRef<
|
83 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
84 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
85 |
+
inset?: boolean
|
86 |
+
}
|
87 |
+
>(({ className, inset, ...props }, ref) => (
|
88 |
+
<DropdownMenuPrimitive.Item
|
89 |
+
ref={ref}
|
90 |
+
className={cn(
|
91 |
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
92 |
+
inset && "pl-8",
|
93 |
+
className
|
94 |
+
)}
|
95 |
+
{...props}
|
96 |
+
/>
|
97 |
+
))
|
98 |
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
99 |
+
|
100 |
+
const DropdownMenuCheckboxItem = React.forwardRef<
|
101 |
+
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
102 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
103 |
+
>(({ className, children, checked, ...props }, ref) => (
|
104 |
+
<DropdownMenuPrimitive.CheckboxItem
|
105 |
+
ref={ref}
|
106 |
+
className={cn(
|
107 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
108 |
+
className
|
109 |
+
)}
|
110 |
+
checked={checked}
|
111 |
+
{...props}
|
112 |
+
>
|
113 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
114 |
+
<DropdownMenuPrimitive.ItemIndicator>
|
115 |
+
<CheckIcon className="h-4 w-4" />
|
116 |
+
</DropdownMenuPrimitive.ItemIndicator>
|
117 |
+
</span>
|
118 |
+
{children}
|
119 |
+
</DropdownMenuPrimitive.CheckboxItem>
|
120 |
+
))
|
121 |
+
DropdownMenuCheckboxItem.displayName =
|
122 |
+
DropdownMenuPrimitive.CheckboxItem.displayName
|
123 |
+
|
124 |
+
const DropdownMenuRadioItem = React.forwardRef<
|
125 |
+
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
126 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
127 |
+
>(({ className, children, ...props }, ref) => (
|
128 |
+
<DropdownMenuPrimitive.RadioItem
|
129 |
+
ref={ref}
|
130 |
+
className={cn(
|
131 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
132 |
+
className
|
133 |
+
)}
|
134 |
+
{...props}
|
135 |
+
>
|
136 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
137 |
+
<DropdownMenuPrimitive.ItemIndicator>
|
138 |
+
<DotFilledIcon className="h-4 w-4 fill-current" />
|
139 |
+
</DropdownMenuPrimitive.ItemIndicator>
|
140 |
+
</span>
|
141 |
+
{children}
|
142 |
+
</DropdownMenuPrimitive.RadioItem>
|
143 |
+
))
|
144 |
+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
145 |
+
|
146 |
+
const DropdownMenuLabel = React.forwardRef<
|
147 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
148 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
149 |
+
inset?: boolean
|
150 |
+
}
|
151 |
+
>(({ className, inset, ...props }, ref) => (
|
152 |
+
<DropdownMenuPrimitive.Label
|
153 |
+
ref={ref}
|
154 |
+
className={cn(
|
155 |
+
"px-2 py-1.5 text-sm font-semibold",
|
156 |
+
inset && "pl-8",
|
157 |
+
className
|
158 |
+
)}
|
159 |
+
{...props}
|
160 |
+
/>
|
161 |
+
))
|
162 |
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
163 |
+
|
164 |
+
const DropdownMenuSeparator = React.forwardRef<
|
165 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
166 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
167 |
+
>(({ className, ...props }, ref) => (
|
168 |
+
<DropdownMenuPrimitive.Separator
|
169 |
+
ref={ref}
|
170 |
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
171 |
+
{...props}
|
172 |
+
/>
|
173 |
+
))
|
174 |
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
175 |
+
|
176 |
+
const DropdownMenuShortcut = ({
|
177 |
+
className,
|
178 |
+
...props
|
179 |
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
180 |
+
return (
|
181 |
+
<span
|
182 |
+
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
183 |
+
{...props}
|
184 |
+
/>
|
185 |
+
)
|
186 |
+
}
|
187 |
+
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
188 |
+
|
189 |
+
export {
|
190 |
+
DropdownMenu,
|
191 |
+
DropdownMenuTrigger,
|
192 |
+
DropdownMenuContent,
|
193 |
+
DropdownMenuItem,
|
194 |
+
DropdownMenuCheckboxItem,
|
195 |
+
DropdownMenuRadioItem,
|
196 |
+
DropdownMenuLabel,
|
197 |
+
DropdownMenuSeparator,
|
198 |
+
DropdownMenuShortcut,
|
199 |
+
DropdownMenuGroup,
|
200 |
+
DropdownMenuPortal,
|
201 |
+
DropdownMenuSub,
|
202 |
+
DropdownMenuSubContent,
|
203 |
+
DropdownMenuSubTrigger,
|
204 |
+
DropdownMenuRadioGroup,
|
205 |
+
}
|
components/ui/input.tsx
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
export interface InputProps
|
6 |
+
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
7 |
+
|
8 |
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
9 |
+
({ className, type, ...props }, ref) => {
|
10 |
+
return (
|
11 |
+
<input
|
12 |
+
type={type}
|
13 |
+
className={cn(
|
14 |
+
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
15 |
+
className
|
16 |
+
)}
|
17 |
+
ref={ref}
|
18 |
+
{...props}
|
19 |
+
/>
|
20 |
+
)
|
21 |
+
}
|
22 |
+
)
|
23 |
+
Input.displayName = "Input"
|
24 |
+
|
25 |
+
export { Input }
|
components/ui/label.tsx
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
5 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
6 |
+
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
|
9 |
+
const labelVariants = cva(
|
10 |
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
11 |
+
)
|
12 |
+
|
13 |
+
const Label = React.forwardRef<
|
14 |
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
15 |
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
16 |
+
VariantProps<typeof labelVariants>
|
17 |
+
>(({ className, ...props }, ref) => (
|
18 |
+
<LabelPrimitive.Root
|
19 |
+
ref={ref}
|
20 |
+
className={cn(labelVariants(), className)}
|
21 |
+
{...props}
|
22 |
+
/>
|
23 |
+
))
|
24 |
+
Label.displayName = LabelPrimitive.Root.displayName
|
25 |
+
|
26 |
+
export { Label }
|
components/ui/select.tsx
ADDED
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
|
5 |
+
import * as SelectPrimitive from "@radix-ui/react-select"
|
6 |
+
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
|
9 |
+
const Select = SelectPrimitive.Root
|
10 |
+
|
11 |
+
const SelectGroup = SelectPrimitive.Group
|
12 |
+
|
13 |
+
const SelectValue = SelectPrimitive.Value
|
14 |
+
|
15 |
+
const SelectTrigger = React.forwardRef<
|
16 |
+
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
17 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
18 |
+
>(({ className, children, ...props }, ref) => (
|
19 |
+
<SelectPrimitive.Trigger
|
20 |
+
ref={ref}
|
21 |
+
className={cn(
|
22 |
+
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
23 |
+
className
|
24 |
+
)}
|
25 |
+
{...props}
|
26 |
+
>
|
27 |
+
{children}
|
28 |
+
<SelectPrimitive.Icon asChild>
|
29 |
+
<CaretSortIcon className="h-4 w-4 opacity-50" />
|
30 |
+
</SelectPrimitive.Icon>
|
31 |
+
</SelectPrimitive.Trigger>
|
32 |
+
))
|
33 |
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
34 |
+
|
35 |
+
const SelectContent = React.forwardRef<
|
36 |
+
React.ElementRef<typeof SelectPrimitive.Content>,
|
37 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
38 |
+
>(({ className, children, position = "popper", ...props }, ref) => (
|
39 |
+
<SelectPrimitive.Portal>
|
40 |
+
<SelectPrimitive.Content
|
41 |
+
ref={ref}
|
42 |
+
className={cn(
|
43 |
+
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
44 |
+
position === "popper" &&
|
45 |
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
46 |
+
className
|
47 |
+
)}
|
48 |
+
position={position}
|
49 |
+
{...props}
|
50 |
+
>
|
51 |
+
<SelectPrimitive.Viewport
|
52 |
+
className={cn(
|
53 |
+
"p-1",
|
54 |
+
position === "popper" &&
|
55 |
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
56 |
+
)}
|
57 |
+
>
|
58 |
+
{children}
|
59 |
+
</SelectPrimitive.Viewport>
|
60 |
+
</SelectPrimitive.Content>
|
61 |
+
</SelectPrimitive.Portal>
|
62 |
+
))
|
63 |
+
SelectContent.displayName = SelectPrimitive.Content.displayName
|
64 |
+
|
65 |
+
const SelectLabel = React.forwardRef<
|
66 |
+
React.ElementRef<typeof SelectPrimitive.Label>,
|
67 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
68 |
+
>(({ className, ...props }, ref) => (
|
69 |
+
<SelectPrimitive.Label
|
70 |
+
ref={ref}
|
71 |
+
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
72 |
+
{...props}
|
73 |
+
/>
|
74 |
+
))
|
75 |
+
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
76 |
+
|
77 |
+
const SelectItem = React.forwardRef<
|
78 |
+
React.ElementRef<typeof SelectPrimitive.Item>,
|
79 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
80 |
+
>(({ className, children, ...props }, ref) => (
|
81 |
+
<SelectPrimitive.Item
|
82 |
+
ref={ref}
|
83 |
+
className={cn(
|
84 |
+
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
85 |
+
className
|
86 |
+
)}
|
87 |
+
{...props}
|
88 |
+
>
|
89 |
+
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
90 |
+
<SelectPrimitive.ItemIndicator>
|
91 |
+
<CheckIcon className="h-4 w-4" />
|
92 |
+
</SelectPrimitive.ItemIndicator>
|
93 |
+
</span>
|
94 |
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
95 |
+
</SelectPrimitive.Item>
|
96 |
+
))
|
97 |
+
SelectItem.displayName = SelectPrimitive.Item.displayName
|
98 |
+
|
99 |
+
const SelectSeparator = React.forwardRef<
|
100 |
+
React.ElementRef<typeof SelectPrimitive.Separator>,
|
101 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
102 |
+
>(({ className, ...props }, ref) => (
|
103 |
+
<SelectPrimitive.Separator
|
104 |
+
ref={ref}
|
105 |
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
106 |
+
{...props}
|
107 |
+
/>
|
108 |
+
))
|
109 |
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
110 |
+
|
111 |
+
export {
|
112 |
+
Select,
|
113 |
+
SelectGroup,
|
114 |
+
SelectValue,
|
115 |
+
SelectTrigger,
|
116 |
+
SelectContent,
|
117 |
+
SelectLabel,
|
118 |
+
SelectItem,
|
119 |
+
SelectSeparator,
|
120 |
+
}
|
components/ui/skeleton.tsx
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { cn } from "@/lib/utils"
|
2 |
+
|
3 |
+
function Skeleton({
|
4 |
+
className,
|
5 |
+
...props
|
6 |
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
7 |
+
return (
|
8 |
+
<div
|
9 |
+
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
10 |
+
{...props}
|
11 |
+
/>
|
12 |
+
)
|
13 |
+
}
|
14 |
+
|
15 |
+
export { Skeleton }
|
components/ui/slider.tsx
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as SliderPrimitive from "@radix-ui/react-slider"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
const Slider = React.forwardRef<
|
9 |
+
React.ElementRef<typeof SliderPrimitive.Root>,
|
10 |
+
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
11 |
+
>(({ className, ...props }, ref) => (
|
12 |
+
<SliderPrimitive.Root
|
13 |
+
ref={ref}
|
14 |
+
className={cn(
|
15 |
+
"relative flex w-full touch-none select-none items-center",
|
16 |
+
className
|
17 |
+
)}
|
18 |
+
{...props}
|
19 |
+
>
|
20 |
+
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
|
21 |
+
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
22 |
+
</SliderPrimitive.Track>
|
23 |
+
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
|
24 |
+
</SliderPrimitive.Root>
|
25 |
+
))
|
26 |
+
Slider.displayName = SliderPrimitive.Root.displayName
|
27 |
+
|
28 |
+
export { Slider }
|
components/ui/switch.tsx
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
const Switch = React.forwardRef<
|
9 |
+
React.ElementRef<typeof SwitchPrimitives.Root>,
|
10 |
+
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
11 |
+
>(({ className, ...props }, ref) => (
|
12 |
+
<SwitchPrimitives.Root
|
13 |
+
className={cn(
|
14 |
+
"peer inline-flex h-[20px] w-[36px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
15 |
+
className
|
16 |
+
)}
|
17 |
+
{...props}
|
18 |
+
ref={ref}
|
19 |
+
>
|
20 |
+
<SwitchPrimitives.Thumb
|
21 |
+
className={cn(
|
22 |
+
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
23 |
+
)}
|
24 |
+
/>
|
25 |
+
</SwitchPrimitives.Root>
|
26 |
+
))
|
27 |
+
Switch.displayName = SwitchPrimitives.Root.displayName
|
28 |
+
|
29 |
+
export { Switch }
|