Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 47572b0

Browse files
committedFeb 15, 2025·
fix: theme
1 parent 4b40d28 commit 47572b0

14 files changed

+1635
-178
lines changed
 

‎components.json

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.ts",
8+
"css": "src/app/globals.css",
9+
"baseColor": "zinc",
10+
"cssVariables": true,
11+
"prefix": ""
12+
},
13+
"aliases": {
14+
"components": "@/components",
15+
"utils": "@/lib/utils",
16+
"ui": "@/components/ui",
17+
"lib": "@/lib",
18+
"hooks": "@/hooks"
19+
},
20+
"iconLibrary": "lucide"
21+
}

‎eslint.config.mjs

+89-15
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,90 @@
1-
import { dirname } from "path";
2-
import { fileURLToPath } from "url";
3-
import { FlatCompat } from "@eslint/eslintrc";
1+
import js from '@eslint/js';
2+
import ts from 'typescript-eslint';
3+
import stylistic from '@stylistic/eslint-plugin';
4+
import next from '@next/eslint-plugin-next';
5+
import tailwind from 'eslint-plugin-readable-tailwind';
6+
import importX from 'eslint-plugin-import-x';
47

5-
const __filename = fileURLToPath(import.meta.url);
6-
const __dirname = dirname(__filename);
7-
8-
const compat = new FlatCompat({
9-
baseDirectory: __dirname,
10-
});
11-
12-
const eslintConfig = [
13-
...compat.extends("next/core-web-vitals", "next/typescript"),
14-
];
15-
16-
export default eslintConfig;
8+
export default ts.config(
9+
{
10+
ignores: ['eslint.config.mjs'],
11+
},
12+
js.configs.recommended,
13+
...ts.configs.recommendedTypeChecked,
14+
stylistic.configs.customize({
15+
arrowParens: true,
16+
semi: true,
17+
jsx: true,
18+
flat: true,
19+
}),
20+
importX.flatConfigs.recommended,
21+
importX.flatConfigs.typescript,
22+
{
23+
settings: {
24+
'import/resolver': {
25+
typescript: {
26+
project: './tsconfig.json',
27+
},
28+
node: true,
29+
},
30+
},
31+
rules: {
32+
'import-x/no-duplicates': 'warn',
33+
'import-x/order': [
34+
'warn',
35+
{
36+
groups: [
37+
'builtin',
38+
'external',
39+
'internal',
40+
['parent', 'sibling', 'index'],
41+
'unknown',
42+
'type',
43+
],
44+
'newlines-between': 'always',
45+
},
46+
],
47+
'sort-imports': [
48+
'warn',
49+
{
50+
allowSeparatedGroups: true,
51+
ignoreDeclarationSort: true,
52+
},
53+
],
54+
},
55+
},
56+
{
57+
languageOptions: {
58+
parserOptions: {
59+
project: './tsconfig.json',
60+
tsconfigRootDir: import.meta.dirname,
61+
},
62+
},
63+
rules: {
64+
'@typescript-eslint/no-unsafe-assignment': 'warn',
65+
'@typescript-eslint/no-unsafe-member-access': 'warn',
66+
'@typescript-eslint/no-unsafe-argument': 'warn',
67+
'@typescript-eslint/restrict-template-expressions': 'warn',
68+
},
69+
},
70+
{
71+
plugins: { '@next/next': next },
72+
rules: {
73+
...next.configs.recommended.rules,
74+
...next.configs['core-web-vitals'].rules,
75+
},
76+
},
77+
{
78+
files: ['**/*.{jsx,tsx}'],
79+
languageOptions: {
80+
parserOptions: {
81+
ecmaFeatures: { jsx: true },
82+
},
83+
},
84+
plugins: { 'readable-tailwind': tailwind },
85+
rules: {
86+
...tailwind.configs.warning.rules,
87+
'readable-tailwind/multiline': ['warn',{group: 'newLine'}],
88+
},
89+
}
90+
);

‎package-lock.json

+913-115
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+19-5
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,33 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12+
"@radix-ui/react-dropdown-menu": "^2.1.6",
13+
"@radix-ui/react-slot": "^1.1.2",
14+
"class-variance-authority": "^0.7.1",
15+
"clsx": "^2.1.1",
16+
"lucide-react": "^0.475.0",
17+
"next": "15.1.7",
18+
"next-themes": "^0.4.4",
1219
"react": "^19.0.0",
1320
"react-dom": "^19.0.0",
14-
"next": "15.1.7"
21+
"tailwind-merge": "^3.0.1",
22+
"tailwindcss-animate": "^1.0.7"
1523
},
1624
"devDependencies": {
17-
"typescript": "^5",
25+
"@eslint/eslintrc": "^3",
26+
"@eslint/js": "^9.20.0",
27+
"@next/eslint-plugin-next": "^15.1.7",
28+
"@stylistic/eslint-plugin": "^3.1.0",
1829
"@types/node": "^20",
1930
"@types/react": "^19",
2031
"@types/react-dom": "^19",
21-
"postcss": "^8",
22-
"tailwindcss": "^3.4.1",
2332
"eslint": "^9",
2433
"eslint-config-next": "15.1.7",
25-
"@eslint/eslintrc": "^3"
34+
"eslint-plugin-import-x": "^4.6.1",
35+
"eslint-plugin-readable-tailwind": "^1.9.0",
36+
"postcss": "^8",
37+
"tailwindcss": "^3.4.1",
38+
"typescript": "^5",
39+
"typescript-eslint": "^8.24.0"
2640
}
2741
}

‎src/app/globals.css

+80-11
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,89 @@
22
@tailwind components;
33
@tailwind utilities;
44

5-
:root {
6-
--background: #ffffff;
7-
--foreground: #171717;
5+
body {
6+
font-family: Arial, Helvetica, sans-serif;
87
}
98

10-
@media (prefers-color-scheme: dark) {
9+
@layer base {
1110
:root {
12-
--background: #0a0a0a;
13-
--foreground: #ededed;
11+
--background: 0 0% 100%;
12+
--foreground: 240 10% 3.9%;
13+
--card: 0 0% 100%;
14+
--card-foreground: 240 10% 3.9%;
15+
--popover: 0 0% 100%;
16+
--popover-foreground: 240 10% 3.9%;
17+
--primary: 240 5.9% 10%;
18+
--primary-foreground: 0 0% 98%;
19+
--secondary: 240 4.8% 95.9%;
20+
--secondary-foreground: 240 5.9% 10%;
21+
--muted: 240 4.8% 95.9%;
22+
--muted-foreground: 240 3.8% 46.1%;
23+
--accent: 240 4.8% 95.9%;
24+
--accent-foreground: 240 5.9% 10%;
25+
--destructive: 0 84.2% 60.2%;
26+
--destructive-foreground: 0 0% 98%;
27+
--border: 240 5.9% 90%;
28+
--input: 240 5.9% 90%;
29+
--ring: 240 10% 3.9%;
30+
--radius: 0.5rem;
31+
--chart-1: 12 76% 61%;
32+
--chart-2: 173 58% 39%;
33+
--chart-3: 197 37% 24%;
34+
--chart-4: 43 74% 66%;
35+
--chart-5: 27 87% 67%;
36+
--sidebar-background: 0 0% 98%;
37+
--sidebar-foreground: 240 5.3% 26.1%;
38+
--sidebar-primary: 240 5.9% 10%;
39+
--sidebar-primary-foreground: 0 0% 98%;
40+
--sidebar-accent: 240 4.8% 95.9%;
41+
--sidebar-accent-foreground: 240 5.9% 10%;
42+
--sidebar-border: 220 13% 91%;
43+
--sidebar-ring: 217.2 91.2% 59.8%;
1444
}
15-
}
1645

17-
body {
18-
color: var(--foreground);
19-
background: var(--background);
20-
font-family: Arial, Helvetica, sans-serif;
46+
.dark {
47+
--background: 240 10% 3.9%;
48+
--foreground: 0 0% 98%;
49+
--card: 240 10% 3.9%;
50+
--card-foreground: 0 0% 98%;
51+
--popover: 240 10% 3.9%;
52+
--popover-foreground: 0 0% 98%;
53+
--primary: 0 0% 98%;
54+
--primary-foreground: 240 5.9% 10%;
55+
--secondary: 240 3.7% 15.9%;
56+
--secondary-foreground: 0 0% 98%;
57+
--muted: 240 3.7% 15.9%;
58+
--muted-foreground: 240 5% 64.9%;
59+
--accent: 240 3.7% 15.9%;
60+
--accent-foreground: 0 0% 98%;
61+
--destructive: 0 62.8% 30.6%;
62+
--destructive-foreground: 0 0% 98%;
63+
--border: 240 3.7% 15.9%;
64+
--input: 240 3.7% 15.9%;
65+
--ring: 240 4.9% 83.9%;
66+
--chart-1: 220 70% 50%;
67+
--chart-2: 160 60% 45%;
68+
--chart-3: 30 80% 55%;
69+
--chart-4: 280 65% 60%;
70+
--chart-5: 340 75% 55%;
71+
--sidebar-background: 240 5.9% 10%;
72+
--sidebar-foreground: 240 4.8% 95.9%;
73+
--sidebar-primary: 224.3 76.3% 48%;
74+
--sidebar-primary-foreground: 0 0% 100%;
75+
--sidebar-accent: 240 3.7% 15.9%;
76+
--sidebar-accent-foreground: 240 4.8% 95.9%;
77+
--sidebar-border: 240 3.7% 15.9%;
78+
--sidebar-ring: 217.2 91.2% 59.8%;
79+
}
2180
}
81+
82+
@layer base {
83+
* {
84+
@apply border-border;
85+
}
86+
87+
body {
88+
@apply bg-background text-foreground;
89+
}
90+
}

‎src/app/layout.tsx

+35-24
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,44 @@
1-
import type { Metadata } from "next";
2-
import { Geist, Geist_Mono } from "next/font/google";
3-
import "./globals.css";
1+
'use client';
42

5-
const geistSans = Geist({
6-
variable: "--font-geist-sans",
7-
subsets: ["latin"],
8-
});
3+
import { usePathname } from 'next/navigation';
94

10-
const geistMono = Geist_Mono({
11-
variable: "--font-geist-mono",
12-
subsets: ["latin"],
13-
});
5+
import AppFooter from '@/components/app/footer';
6+
import AppHeader from '@/components/app/header';
7+
import { ThemeProvider } from '@/components/theme/provider';
148

15-
export const metadata: Metadata = {
16-
title: "Create Next App",
17-
description: "Generated by create next app",
18-
};
9+
import '@/app/globals.css';
1910

20-
export default function RootLayout({
21-
children,
22-
}: Readonly<{
11+
type Props = Readonly<{
2312
children: React.ReactNode;
24-
}>) {
13+
}>;
14+
15+
export default function RootLayout({ children }: Props) {
16+
const pathname = usePathname();
17+
const isDashboard = pathname === '/dashboard';
18+
2519
return (
26-
<html lang="en">
27-
<body
28-
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
29-
>
30-
{children}
20+
<html lang="zh-TW" suppressHydrationWarning>
21+
<title>Weather Dashboard</title>
22+
<meta name="description" content="Weather Dashboard" />
23+
24+
<body className="flex flex-col">
25+
<ThemeProvider
26+
attribute="class"
27+
defaultTheme="system"
28+
enableSystem
29+
>
30+
{!isDashboard && <AppHeader />}
31+
<main className={`
32+
flex min-h-dvh flex-col items-center
33+
${!isDashboard
34+
? `pt-16`
35+
: ''}
36+
`}
37+
>
38+
{children}
39+
</main>
40+
{!isDashboard && <AppFooter />}
41+
</ThemeProvider>
3142
</body>
3243
</html>
3344
);

‎src/components/app/footer.tsx

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export default function AppFooter({ className }: { className?: string }) {
2+
return (
3+
<footer
4+
className={`
5+
border-t bg-background px-8 py-4 text-muted-foreground
6+
md:px-16 md:py-8
7+
${className}
8+
`}
9+
>
10+
&copy; 2024 Design by NKUST ISLab
11+
</footer>
12+
);
13+
}

‎src/components/app/header.tsx

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use client';
2+
3+
import { Home, Radar } from 'lucide-react';
4+
import { usePathname } from 'next/navigation';
5+
import Link from 'next/link';
6+
7+
import { Button } from '@/components/ui/button';
8+
import ThemeSelector from '@/components/theme/theme-selector';
9+
10+
export default function AppHeader() {
11+
const pathname = usePathname();
12+
13+
const navigationItems = [
14+
{
15+
href: '/',
16+
icon: Home,
17+
text: '首頁',
18+
isActive: pathname === '/',
19+
},
20+
{
21+
href: '/dashboard',
22+
icon: Radar,
23+
text: '面板',
24+
isActive: pathname === '/dashboard',
25+
},
26+
];
27+
28+
return (
29+
<header className={`
30+
fixed inset-x-0 top-0 z-50 flex h-16 items-center justify-center border-b
31+
bg-background/95 backdrop-blur
32+
supports-[backdrop-filter]:bg-background/60
33+
`}
34+
>
35+
<div className="container flex items-center justify-between px-4">
36+
<nav className={`
37+
flex gap-1
38+
sm:gap-2
39+
`}
40+
>
41+
{navigationItems.map((item) => (
42+
<Button
43+
key={item.href}
44+
variant={item.isActive ? 'default' : 'ghost'}
45+
className={`
46+
flex items-center gap-2 px-2
47+
sm:px-4
48+
`}
49+
asChild
50+
>
51+
<Link href={item.href}>
52+
<item.icon className={`
53+
h-4 w-4
54+
sm:h-5 sm:w-5
55+
`}
56+
/>
57+
<span className={`
58+
hidden
59+
sm:inline
60+
`}
61+
>
62+
{item.text}
63+
</span>
64+
</Link>
65+
</Button>
66+
))}
67+
</nav>
68+
<ThemeSelector />
69+
</div>
70+
</header>
71+
);
72+
}

‎src/components/theme/provider.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use client';
2+
3+
import * as React from 'react';
4+
import { ThemeProvider as NextThemesProvider } from 'next-themes';
5+
6+
type Props = React.ComponentProps<typeof NextThemesProvider>;
7+
8+
export function ThemeProvider({ children, ...props }: Props) {
9+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
10+
}
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use client';
2+
3+
import { Computer, Moon, Sun } from 'lucide-react';
4+
import { useEffect, useState } from 'react';
5+
import { useTheme } from 'next-themes';
6+
7+
import {
8+
DropdownMenu,
9+
DropdownMenuContent,
10+
DropdownMenuItem,
11+
DropdownMenuTrigger,
12+
} from '@/components/ui/dropdown-menu';
13+
import { Button } from '@/components/ui/button';
14+
15+
const themes = [
16+
{ value: 'light', label: '淺色', icon: Sun },
17+
{ value: 'dark', label: '深色', icon: Moon },
18+
{ value: 'system', label: '系統', icon: Computer },
19+
];
20+
21+
export default function ThemeSelector() {
22+
const [mounted, setMounted] = useState(false);
23+
const { theme, setTheme } = useTheme();
24+
25+
useEffect(() => {
26+
setMounted(true);
27+
}, []);
28+
29+
if (!mounted)
30+
return (
31+
<Button variant="outline" size="icon" disabled>
32+
<Computer />
33+
</Button>
34+
);
35+
36+
const current = themes.find((v) => v.value == theme) || themes[2];
37+
38+
return (
39+
<DropdownMenu>
40+
<DropdownMenuTrigger asChild>
41+
<Button variant="outline" size="icon">
42+
<current.icon />
43+
</Button>
44+
</DropdownMenuTrigger>
45+
<DropdownMenuContent>
46+
{themes.map(({ value, label, icon: Icon }) => (
47+
<DropdownMenuItem key={value} onClick={() => setTheme(value)}>
48+
<Icon />
49+
{label}
50+
</DropdownMenuItem>
51+
))}
52+
</DropdownMenuContent>
53+
</DropdownMenu>
54+
);
55+
}

‎src/components/ui/button.tsx

+57
Original file line numberDiff line numberDiff line change
@@ -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 gap-2 whitespace-nowrap 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
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-background 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 }

‎src/components/ui/dropdown-menu.tsx

+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5+
import { Check, ChevronRight, Circle } from "lucide-react"
6+
7+
import { cn } from "@/lib/utils"
8+
9+
const DropdownMenu = DropdownMenuPrimitive.Root
10+
11+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12+
13+
const DropdownMenuGroup = DropdownMenuPrimitive.Group
14+
15+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16+
17+
const DropdownMenuSub = DropdownMenuPrimitive.Sub
18+
19+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20+
21+
const DropdownMenuSubTrigger = React.forwardRef<
22+
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
23+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
24+
inset?: boolean
25+
}
26+
>(({ className, inset, children, ...props }, ref) => (
27+
<DropdownMenuPrimitive.SubTrigger
28+
ref={ref}
29+
className={cn(
30+
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
31+
inset && "pl-8",
32+
className
33+
)}
34+
{...props}
35+
>
36+
{children}
37+
<ChevronRight className="ml-auto" />
38+
</DropdownMenuPrimitive.SubTrigger>
39+
))
40+
DropdownMenuSubTrigger.displayName =
41+
DropdownMenuPrimitive.SubTrigger.displayName
42+
43+
const DropdownMenuSubContent = React.forwardRef<
44+
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
45+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
46+
>(({ className, ...props }, ref) => (
47+
<DropdownMenuPrimitive.SubContent
48+
ref={ref}
49+
className={cn(
50+
"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",
51+
className
52+
)}
53+
{...props}
54+
/>
55+
))
56+
DropdownMenuSubContent.displayName =
57+
DropdownMenuPrimitive.SubContent.displayName
58+
59+
const DropdownMenuContent = React.forwardRef<
60+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
61+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
62+
>(({ className, sideOffset = 4, ...props }, ref) => (
63+
<DropdownMenuPrimitive.Portal>
64+
<DropdownMenuPrimitive.Content
65+
ref={ref}
66+
sideOffset={sideOffset}
67+
className={cn(
68+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
69+
"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",
70+
className
71+
)}
72+
{...props}
73+
/>
74+
</DropdownMenuPrimitive.Portal>
75+
))
76+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
77+
78+
const DropdownMenuItem = React.forwardRef<
79+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
80+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
81+
inset?: boolean
82+
}
83+
>(({ className, inset, ...props }, ref) => (
84+
<DropdownMenuPrimitive.Item
85+
ref={ref}
86+
className={cn(
87+
"relative flex cursor-default select-none items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0",
88+
inset && "pl-8",
89+
className
90+
)}
91+
{...props}
92+
/>
93+
))
94+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
95+
96+
const DropdownMenuCheckboxItem = React.forwardRef<
97+
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
98+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
99+
>(({ className, children, checked, ...props }, ref) => (
100+
<DropdownMenuPrimitive.CheckboxItem
101+
ref={ref}
102+
className={cn(
103+
"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",
104+
className
105+
)}
106+
checked={checked}
107+
{...props}
108+
>
109+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
110+
<DropdownMenuPrimitive.ItemIndicator>
111+
<Check className="h-4 w-4" />
112+
</DropdownMenuPrimitive.ItemIndicator>
113+
</span>
114+
{children}
115+
</DropdownMenuPrimitive.CheckboxItem>
116+
))
117+
DropdownMenuCheckboxItem.displayName =
118+
DropdownMenuPrimitive.CheckboxItem.displayName
119+
120+
const DropdownMenuRadioItem = React.forwardRef<
121+
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
122+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
123+
>(({ className, children, ...props }, ref) => (
124+
<DropdownMenuPrimitive.RadioItem
125+
ref={ref}
126+
className={cn(
127+
"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",
128+
className
129+
)}
130+
{...props}
131+
>
132+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
133+
<DropdownMenuPrimitive.ItemIndicator>
134+
<Circle className="h-2 w-2 fill-current" />
135+
</DropdownMenuPrimitive.ItemIndicator>
136+
</span>
137+
{children}
138+
</DropdownMenuPrimitive.RadioItem>
139+
))
140+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
141+
142+
const DropdownMenuLabel = React.forwardRef<
143+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
144+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
145+
inset?: boolean
146+
}
147+
>(({ className, inset, ...props }, ref) => (
148+
<DropdownMenuPrimitive.Label
149+
ref={ref}
150+
className={cn(
151+
"px-2 py-1.5 text-sm font-semibold",
152+
inset && "pl-8",
153+
className
154+
)}
155+
{...props}
156+
/>
157+
))
158+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
159+
160+
const DropdownMenuSeparator = React.forwardRef<
161+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
162+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
163+
>(({ className, ...props }, ref) => (
164+
<DropdownMenuPrimitive.Separator
165+
ref={ref}
166+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
167+
{...props}
168+
/>
169+
))
170+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
171+
172+
const DropdownMenuShortcut = ({
173+
className,
174+
...props
175+
}: React.HTMLAttributes<HTMLSpanElement>) => {
176+
return (
177+
<span
178+
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
179+
{...props}
180+
/>
181+
)
182+
}
183+
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
184+
185+
export {
186+
DropdownMenu,
187+
DropdownMenuTrigger,
188+
DropdownMenuContent,
189+
DropdownMenuItem,
190+
DropdownMenuCheckboxItem,
191+
DropdownMenuRadioItem,
192+
DropdownMenuLabel,
193+
DropdownMenuSeparator,
194+
DropdownMenuShortcut,
195+
DropdownMenuGroup,
196+
DropdownMenuPortal,
197+
DropdownMenuSub,
198+
DropdownMenuSubContent,
199+
DropdownMenuSubTrigger,
200+
DropdownMenuRadioGroup,
201+
}

‎src/lib/utils.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { type ClassValue, clsx } from 'clsx';
2+
import { twMerge } from 'tailwind-merge';
3+
4+
export function cn(...inputs: ClassValue[]) {
5+
return twMerge(clsx(inputs));
6+
}

‎tailwind.config.ts

+64-8
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,74 @@
1-
import type { Config } from "tailwindcss";
1+
import type { Config } from 'tailwindcss';
22

3-
export default {
3+
const config = {
4+
darkMode: ['class'],
45
content: [
5-
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6-
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7-
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
6+
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
7+
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
8+
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
89
],
910
theme: {
1011
extend: {
1112
colors: {
12-
background: "var(--background)",
13-
foreground: "var(--foreground)",
13+
border: 'hsl(var(--border))',
14+
input: 'hsl(var(--input))',
15+
ring: 'hsl(var(--ring))',
16+
background: 'hsl(var(--background))',
17+
foreground: 'hsl(var(--foreground))',
18+
primary: {
19+
DEFAULT: 'hsl(var(--primary))',
20+
foreground: 'hsl(var(--primary-foreground))',
21+
},
22+
secondary: {
23+
DEFAULT: 'hsl(var(--secondary))',
24+
foreground: 'hsl(var(--secondary-foreground))',
25+
},
26+
destructive: {
27+
DEFAULT: 'hsl(var(--destructive))',
28+
foreground: 'hsl(var(--destructive-foreground))',
29+
},
30+
muted: {
31+
DEFAULT: 'hsl(var(--muted))',
32+
foreground: 'hsl(var(--muted-foreground))',
33+
},
34+
accent: {
35+
DEFAULT: 'hsl(var(--accent))',
36+
foreground: 'hsl(var(--accent-foreground))',
37+
},
38+
popover: {
39+
DEFAULT: 'hsl(var(--popover))',
40+
foreground: 'hsl(var(--popover-foreground))',
41+
},
42+
card: {
43+
DEFAULT: 'hsl(var(--card))',
44+
foreground: 'hsl(var(--card-foreground))',
45+
},
46+
chart: {
47+
1: 'hsl(var(--chart-1))',
48+
2: 'hsl(var(--chart-2))',
49+
3: 'hsl(var(--chart-3))',
50+
4: 'hsl(var(--chart-4))',
51+
5: 'hsl(var(--chart-5))',
52+
},
53+
sidebar: {
54+
'DEFAULT': 'hsl(var(--sidebar-background))',
55+
'foreground': 'hsl(var(--sidebar-foreground))',
56+
'primary': 'hsl(var(--sidebar-primary))',
57+
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
58+
'accent': 'hsl(var(--sidebar-accent))',
59+
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
60+
'border': 'hsl(var(--sidebar-border))',
61+
'ring': 'hsl(var(--sidebar-ring))',
62+
},
63+
},
64+
borderRadius: {
65+
lg: 'var(--radius)',
66+
md: 'calc(var(--radius) - 2px)',
67+
sm: 'calc(var(--radius) - 4px)',
1468
},
1569
},
1670
},
17-
plugins: [],
71+
plugins: [require('tailwindcss-animate')],
1872
} satisfies Config;
73+
74+
export default config;

0 commit comments

Comments
 (0)
Please sign in to comment.