@nocobase/portal-template-default 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +10 -0
- package/Dockerfile +36 -0
- package/README.MD +50 -0
- package/components.json +25 -0
- package/eslint.config.js +28 -0
- package/index.html +39 -0
- package/package.json +84 -0
- package/pnpm-workspace.yaml +2 -0
- package/public/favicon.ico +0 -0
- package/public/logo-dark.png +0 -0
- package/public/logo-mark-dark.png +0 -0
- package/public/logo-mark.png +0 -0
- package/public/logo.png +0 -0
- package/src/App.css +158 -0
- package/src/App.tsx +151 -0
- package/src/components/app-shell/brand.tsx +70 -0
- package/src/components/app-shell/breadcrumb.tsx +81 -0
- package/src/components/app-shell/document-title-handler.tsx +169 -0
- package/src/components/app-shell/error-component.tsx +134 -0
- package/src/components/app-shell/header.tsx +127 -0
- package/src/components/app-shell/layout.tsx +39 -0
- package/src/components/app-shell/loading-overlay.tsx +36 -0
- package/src/components/app-shell/sidebar.tsx +351 -0
- package/src/components/app-shell/user-avatar.tsx +42 -0
- package/src/components/app-shell/user-info.tsx +53 -0
- package/src/components/auth/auth-layout.tsx +82 -0
- package/src/components/auth/forgot-password-form.tsx +58 -0
- package/src/components/auth/input-password.tsx +38 -0
- package/src/components/auth/sign-in-form.tsx +111 -0
- package/src/components/auth/sign-up-form.tsx +127 -0
- package/src/components/data-table/data-table-filter.tsx +994 -0
- package/src/components/data-table/data-table-pagination.tsx +146 -0
- package/src/components/data-table/data-table-sorter.tsx +47 -0
- package/src/components/data-table/data-table.tsx +321 -0
- package/src/components/notifications/toaster.tsx +23 -0
- package/src/components/notifications/undoable-notification.tsx +84 -0
- package/src/components/notifications/use-notification-provider.tsx +62 -0
- package/src/components/resources/buttons/clone.tsx +81 -0
- package/src/components/resources/buttons/create.tsx +70 -0
- package/src/components/resources/buttons/delete.tsx +113 -0
- package/src/components/resources/buttons/edit.tsx +81 -0
- package/src/components/resources/buttons/list.tsx +70 -0
- package/src/components/resources/buttons/refresh.tsx +81 -0
- package/src/components/resources/buttons/show.tsx +81 -0
- package/src/components/resources/status-badge.tsx +37 -0
- package/src/components/resources/views/create-view.tsx +81 -0
- package/src/components/resources/views/edit-view.tsx +104 -0
- package/src/components/resources/views/list-view.tsx +86 -0
- package/src/components/resources/views/show-view.tsx +108 -0
- package/src/components/theme/theme-provider.tsx +71 -0
- package/src/components/theme/theme-select.tsx +97 -0
- package/src/components/theme/theme-toggle.tsx +66 -0
- package/src/components/ui/accordion.tsx +72 -0
- package/src/components/ui/alert-dialog.tsx +185 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/avatar.tsx +107 -0
- package/src/components/ui/badge.tsx +52 -0
- package/src/components/ui/breadcrumb.tsx +125 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/calendar.tsx +221 -0
- package/src/components/ui/card.tsx +103 -0
- package/src/components/ui/carousel.tsx +240 -0
- package/src/components/ui/chart.tsx +373 -0
- package/src/components/ui/checkbox.tsx +29 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/command.tsx +192 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/dialog.tsx +160 -0
- package/src/components/ui/drawer.tsx +226 -0
- package/src/components/ui/dropdown-menu.tsx +268 -0
- package/src/components/ui/form.tsx +174 -0
- package/src/components/ui/hover-card.tsx +49 -0
- package/src/components/ui/input-group.tsx +158 -0
- package/src/components/ui/input-otp.tsx +87 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/label.tsx +18 -0
- package/src/components/ui/menubar.tsx +278 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/pagination.tsx +130 -0
- package/src/components/ui/popover.tsx +91 -0
- package/src/components/ui/progress.tsx +81 -0
- package/src/components/ui/radio-group.tsx +38 -0
- package/src/components/ui/resizable.tsx +48 -0
- package/src/components/ui/scroll-area.tsx +55 -0
- package/src/components/ui/select.tsx +199 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/sheet.tsx +136 -0
- package/src/components/ui/sidebar.tsx +723 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +52 -0
- package/src/components/ui/sonner.tsx +47 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/table.tsx +114 -0
- package/src/components/ui/tabs.tsx +82 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +87 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +66 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.tsx +17 -0
- package/src/lib/utils.ts +19 -0
- package/src/pages/blog-posts/create.tsx +177 -0
- package/src/pages/blog-posts/edit.tsx +197 -0
- package/src/pages/blog-posts/index.ts +4 -0
- package/src/pages/blog-posts/list.tsx +131 -0
- package/src/pages/blog-posts/show.tsx +81 -0
- package/src/pages/categories/create.tsx +73 -0
- package/src/pages/categories/edit.tsx +73 -0
- package/src/pages/categories/index.ts +4 -0
- package/src/pages/categories/list.tsx +86 -0
- package/src/pages/categories/show.tsx +37 -0
- package/src/pages/forgot-password/index.tsx +6 -0
- package/src/pages/forgotPassword/index.tsx +5 -0
- package/src/pages/login/index.tsx +5 -0
- package/src/pages/register/index.tsx +5 -0
- package/src/providers/auth.ts +204 -0
- package/src/providers/constants.ts +75 -0
- package/src/providers/data.ts +232 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +25 -0
- package/tsconfig.node.json +21 -0
- package/vite.config.ts +74 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import {
|
|
5
|
+
useBack,
|
|
6
|
+
useResourceParams,
|
|
7
|
+
useUserFriendlyName,
|
|
8
|
+
} from "@refinedev/core";
|
|
9
|
+
import type { PropsWithChildren } from "react";
|
|
10
|
+
import { Breadcrumb } from "@/components/app-shell/breadcrumb";
|
|
11
|
+
import { Button } from "@/components/ui/button";
|
|
12
|
+
import { RefreshButton } from "@/components/resources/buttons/refresh";
|
|
13
|
+
import { ArrowLeftIcon } from "lucide-react";
|
|
14
|
+
|
|
15
|
+
type EditViewProps = PropsWithChildren<{
|
|
16
|
+
className?: string;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
export function EditView({ children, className }: EditViewProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div className={cn("flex flex-col", "gap-6", className)}>
|
|
22
|
+
<EditViewHeader />
|
|
23
|
+
{children}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type EditViewHeaderProps = PropsWithChildren<{
|
|
29
|
+
resource?: string;
|
|
30
|
+
title?: string;
|
|
31
|
+
wrapperClassName?: string;
|
|
32
|
+
headerClassName?: string;
|
|
33
|
+
actionsSlot?: React.ReactNode;
|
|
34
|
+
}>;
|
|
35
|
+
|
|
36
|
+
export const EditViewHeader = ({
|
|
37
|
+
resource: resourceFromProps,
|
|
38
|
+
title: titleFromProps,
|
|
39
|
+
actionsSlot,
|
|
40
|
+
wrapperClassName,
|
|
41
|
+
headerClassName,
|
|
42
|
+
}: EditViewHeaderProps) => {
|
|
43
|
+
const back = useBack();
|
|
44
|
+
|
|
45
|
+
const getUserFriendlyName = useUserFriendlyName();
|
|
46
|
+
|
|
47
|
+
const { resource, identifier } = useResourceParams({
|
|
48
|
+
resource: resourceFromProps,
|
|
49
|
+
});
|
|
50
|
+
const { id: recordItemId } = useResourceParams();
|
|
51
|
+
|
|
52
|
+
const resourceName = resource?.name ?? identifier;
|
|
53
|
+
|
|
54
|
+
const resourceTitle = getUserFriendlyName(
|
|
55
|
+
resource?.meta?.label ?? identifier ?? resource?.name,
|
|
56
|
+
"singular"
|
|
57
|
+
);
|
|
58
|
+
const title = titleFromProps ?? `Edit ${resourceTitle}`;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className={cn("flex flex-col", "gap-3", wrapperClassName)}>
|
|
62
|
+
<div className="flex items-center text-muted-foreground">
|
|
63
|
+
<Breadcrumb />
|
|
64
|
+
</div>
|
|
65
|
+
<div
|
|
66
|
+
className={cn(
|
|
67
|
+
"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between",
|
|
68
|
+
headerClassName
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
<div className="flex items-start gap-2">
|
|
72
|
+
<Button
|
|
73
|
+
variant="ghost"
|
|
74
|
+
size="icon"
|
|
75
|
+
className="mt-0.5 rounded-lg"
|
|
76
|
+
onClick={back}
|
|
77
|
+
>
|
|
78
|
+
<ArrowLeftIcon className="h-4 w-4" />
|
|
79
|
+
</Button>
|
|
80
|
+
<div>
|
|
81
|
+
<h2 className="text-3xl font-semibold tracking-[-0.035em]">
|
|
82
|
+
{title}
|
|
83
|
+
</h2>
|
|
84
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
85
|
+
Update this {resourceTitle.toLowerCase()} while NocoBase keeps
|
|
86
|
+
the data consistent.
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="flex items-center gap-2">
|
|
92
|
+
{actionsSlot}
|
|
93
|
+
<RefreshButton
|
|
94
|
+
variant="outline"
|
|
95
|
+
recordItemId={recordItemId}
|
|
96
|
+
resource={resourceName}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
EditView.displayName = "EditView";
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { PropsWithChildren } from "react";
|
|
4
|
+
|
|
5
|
+
import { useResourceParams, useUserFriendlyName } from "@refinedev/core";
|
|
6
|
+
import { Breadcrumb } from "@/components/app-shell/breadcrumb";
|
|
7
|
+
import { CreateButton } from "@/components/resources/buttons/create";
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
|
|
10
|
+
type ListViewProps = PropsWithChildren<{
|
|
11
|
+
className?: string;
|
|
12
|
+
}>;
|
|
13
|
+
|
|
14
|
+
export function ListView({ children, className }: ListViewProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div className={cn("flex flex-col", "gap-6", className)}>
|
|
17
|
+
<ListViewHeader />
|
|
18
|
+
{children}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type ListHeaderProps = PropsWithChildren<{
|
|
24
|
+
resource?: string;
|
|
25
|
+
title?: string;
|
|
26
|
+
canCreate?: boolean;
|
|
27
|
+
headerClassName?: string;
|
|
28
|
+
wrapperClassName?: string;
|
|
29
|
+
}>;
|
|
30
|
+
|
|
31
|
+
export const ListViewHeader = ({
|
|
32
|
+
canCreate,
|
|
33
|
+
resource: resourceFromProps,
|
|
34
|
+
title: titleFromProps,
|
|
35
|
+
wrapperClassName,
|
|
36
|
+
headerClassName,
|
|
37
|
+
}: ListHeaderProps) => {
|
|
38
|
+
const getUserFriendlyName = useUserFriendlyName();
|
|
39
|
+
|
|
40
|
+
const { resource, identifier } = useResourceParams({
|
|
41
|
+
resource: resourceFromProps,
|
|
42
|
+
});
|
|
43
|
+
const resourceName = identifier ?? resource?.name;
|
|
44
|
+
|
|
45
|
+
const isCreateButtonVisible = canCreate ?? !!resource?.create;
|
|
46
|
+
|
|
47
|
+
const title =
|
|
48
|
+
titleFromProps ??
|
|
49
|
+
getUserFriendlyName(
|
|
50
|
+
resource?.meta?.label ?? identifier ?? resource?.name,
|
|
51
|
+
"plural"
|
|
52
|
+
);
|
|
53
|
+
const description = resource?.meta?.description as string | undefined;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className={cn("flex flex-col", "gap-3", wrapperClassName)}>
|
|
57
|
+
<div className="flex items-center text-muted-foreground">
|
|
58
|
+
<Breadcrumb />
|
|
59
|
+
</div>
|
|
60
|
+
<div
|
|
61
|
+
className={cn(
|
|
62
|
+
"flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between",
|
|
63
|
+
headerClassName
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
<div>
|
|
67
|
+
<h2 className="text-3xl font-semibold tracking-[-0.035em]">
|
|
68
|
+
{title}
|
|
69
|
+
</h2>
|
|
70
|
+
{description && (
|
|
71
|
+
<p className="mt-2 max-w-2xl text-sm leading-6 text-muted-foreground">
|
|
72
|
+
{description}
|
|
73
|
+
</p>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
{isCreateButtonVisible && (
|
|
77
|
+
<div className="flex items-center gap-2">
|
|
78
|
+
<CreateButton resource={resourceName} />
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
ListView.displayName = "ListView";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { PropsWithChildren } from "react";
|
|
4
|
+
|
|
5
|
+
import { ArrowLeftIcon } from "lucide-react";
|
|
6
|
+
import {
|
|
7
|
+
useBack,
|
|
8
|
+
useResourceParams,
|
|
9
|
+
useUserFriendlyName,
|
|
10
|
+
} from "@refinedev/core";
|
|
11
|
+
import { Breadcrumb } from "@/components/app-shell/breadcrumb";
|
|
12
|
+
import { Button } from "@/components/ui/button";
|
|
13
|
+
import { RefreshButton } from "@/components/resources/buttons/refresh";
|
|
14
|
+
import { cn } from "@/lib/utils";
|
|
15
|
+
import { EditButton } from "../buttons/edit";
|
|
16
|
+
|
|
17
|
+
type ShowViewProps = PropsWithChildren<{
|
|
18
|
+
className?: string;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
export function ShowView({ children, className }: ShowViewProps) {
|
|
22
|
+
return (
|
|
23
|
+
<div className={cn("flex flex-col", "gap-6", className)}>
|
|
24
|
+
<ShowViewHeader />
|
|
25
|
+
{children}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type ShowViewHeaderProps = PropsWithChildren<{
|
|
31
|
+
resource?: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
wrapperClassName?: string;
|
|
34
|
+
headerClassName?: string;
|
|
35
|
+
}>;
|
|
36
|
+
|
|
37
|
+
export const ShowViewHeader = ({
|
|
38
|
+
resource: resourceFromProps,
|
|
39
|
+
title: titleFromProps,
|
|
40
|
+
wrapperClassName,
|
|
41
|
+
headerClassName,
|
|
42
|
+
}: ShowViewHeaderProps) => {
|
|
43
|
+
const back = useBack();
|
|
44
|
+
|
|
45
|
+
const getUserFriendlyName = useUserFriendlyName();
|
|
46
|
+
|
|
47
|
+
const { resource, identifier } = useResourceParams({
|
|
48
|
+
resource: resourceFromProps,
|
|
49
|
+
});
|
|
50
|
+
const { id: recordItemId } = useResourceParams();
|
|
51
|
+
|
|
52
|
+
const resourceName = resource?.name ?? identifier;
|
|
53
|
+
|
|
54
|
+
const title =
|
|
55
|
+
titleFromProps ??
|
|
56
|
+
getUserFriendlyName(
|
|
57
|
+
resource?.meta?.label ?? identifier ?? resource?.name,
|
|
58
|
+
"singular"
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={cn("flex flex-col", "gap-3", wrapperClassName)}>
|
|
63
|
+
<div className="flex items-center text-muted-foreground">
|
|
64
|
+
<Breadcrumb />
|
|
65
|
+
</div>
|
|
66
|
+
<div
|
|
67
|
+
className={cn(
|
|
68
|
+
"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between",
|
|
69
|
+
headerClassName
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
<div className="flex items-start gap-2">
|
|
73
|
+
<Button
|
|
74
|
+
variant="ghost"
|
|
75
|
+
size="icon"
|
|
76
|
+
className="mt-0.5 rounded-lg"
|
|
77
|
+
onClick={back}
|
|
78
|
+
>
|
|
79
|
+
<ArrowLeftIcon className="h-4 w-4" />
|
|
80
|
+
</Button>
|
|
81
|
+
<div>
|
|
82
|
+
<h2 className="text-3xl font-semibold tracking-[-0.035em]">
|
|
83
|
+
{title}
|
|
84
|
+
</h2>
|
|
85
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
86
|
+
Review the record and its NocoBase-managed data.
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="flex items-center gap-2">
|
|
92
|
+
<RefreshButton
|
|
93
|
+
variant="outline"
|
|
94
|
+
recordItemId={recordItemId}
|
|
95
|
+
resource={resourceName}
|
|
96
|
+
/>
|
|
97
|
+
<EditButton
|
|
98
|
+
variant="outline"
|
|
99
|
+
recordItemId={recordItemId}
|
|
100
|
+
resource={resourceName}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
ShowView.displayName = "ShowView";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
type Theme = "dark" | "light";
|
|
6
|
+
|
|
7
|
+
type ThemeProviderProps = {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
defaultTheme?: Theme;
|
|
10
|
+
storageKey?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type ThemeProviderState = {
|
|
14
|
+
theme: Theme;
|
|
15
|
+
setTheme: (theme: Theme) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const initialState: ThemeProviderState = {
|
|
19
|
+
theme: "light",
|
|
20
|
+
setTheme: () => null,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
|
24
|
+
|
|
25
|
+
export function ThemeProvider({
|
|
26
|
+
children,
|
|
27
|
+
defaultTheme = "light",
|
|
28
|
+
storageKey = "nocobase-theme",
|
|
29
|
+
...props
|
|
30
|
+
}: ThemeProviderProps) {
|
|
31
|
+
const [theme, setTheme] = useState<Theme>(() => {
|
|
32
|
+
const storedTheme = localStorage.getItem(storageKey);
|
|
33
|
+
return storedTheme === "dark" || storedTheme === "light"
|
|
34
|
+
? storedTheme
|
|
35
|
+
: defaultTheme;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const root = window.document.documentElement;
|
|
40
|
+
|
|
41
|
+
root.classList.remove("light", "dark");
|
|
42
|
+
|
|
43
|
+
root.classList.add(theme);
|
|
44
|
+
}, [theme]);
|
|
45
|
+
|
|
46
|
+
const value = {
|
|
47
|
+
theme,
|
|
48
|
+
setTheme: (theme: Theme) => {
|
|
49
|
+
localStorage.setItem(storageKey, theme);
|
|
50
|
+
setTheme(theme);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<ThemeProviderContext.Provider {...props} value={value}>
|
|
56
|
+
{children}
|
|
57
|
+
</ThemeProviderContext.Provider>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function useTheme() {
|
|
62
|
+
const context = useContext(ThemeProviderContext);
|
|
63
|
+
|
|
64
|
+
if (context === undefined) {
|
|
65
|
+
console.error("useTheme must be used within a ThemeProvider");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return context;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
ThemeProvider.displayName = "ThemeProvider";
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { useTheme } from "./theme-provider";
|
|
5
|
+
import {
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from "@/components/ui/dropdown-menu";
|
|
11
|
+
import { Button } from "@/components/ui/button";
|
|
12
|
+
import { Moon, Sun, ChevronDown, Check } from "lucide-react";
|
|
13
|
+
import { cn } from "@/lib/utils";
|
|
14
|
+
|
|
15
|
+
type ThemeOption = {
|
|
16
|
+
value: "light" | "dark";
|
|
17
|
+
label: string;
|
|
18
|
+
icon: React.ReactNode;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const themeOptions: ThemeOption[] = [
|
|
22
|
+
{
|
|
23
|
+
value: "light",
|
|
24
|
+
label: "Light",
|
|
25
|
+
icon: <Sun className="h-4 w-4" />,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
value: "dark",
|
|
29
|
+
label: "Dark",
|
|
30
|
+
icon: <Moon className="h-4 w-4" />,
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export function ThemeSelect() {
|
|
35
|
+
const { theme, setTheme } = useTheme();
|
|
36
|
+
|
|
37
|
+
const currentTheme = themeOptions.find((option) => option.value === theme);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<DropdownMenu>
|
|
41
|
+
<DropdownMenuTrigger
|
|
42
|
+
render={
|
|
43
|
+
<Button
|
|
44
|
+
variant="ghost"
|
|
45
|
+
size="lg"
|
|
46
|
+
className={cn(
|
|
47
|
+
"w-full",
|
|
48
|
+
"justify-between",
|
|
49
|
+
"px-3",
|
|
50
|
+
"text-left",
|
|
51
|
+
"text-sm",
|
|
52
|
+
"font-normal",
|
|
53
|
+
"text-foreground",
|
|
54
|
+
"hover:bg-accent",
|
|
55
|
+
"hover:text-accent-foreground",
|
|
56
|
+
"focus-visible:outline-none",
|
|
57
|
+
"focus-visible:ring-2",
|
|
58
|
+
"focus-visible:ring-ring"
|
|
59
|
+
)}
|
|
60
|
+
/>
|
|
61
|
+
}
|
|
62
|
+
>
|
|
63
|
+
<div className="flex items-center gap-2">
|
|
64
|
+
{currentTheme?.icon}
|
|
65
|
+
<span>{currentTheme?.label}</span>
|
|
66
|
+
</div>
|
|
67
|
+
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
68
|
+
</DropdownMenuTrigger>
|
|
69
|
+
<DropdownMenuContent align="end" className="min-w-40 space-y-1">
|
|
70
|
+
{themeOptions.map((option) => {
|
|
71
|
+
const isSelected = theme === option.value;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<DropdownMenuItem
|
|
75
|
+
key={option.value}
|
|
76
|
+
onClick={() => setTheme(option.value)}
|
|
77
|
+
className={cn(
|
|
78
|
+
"flex items-center gap-2 cursor-pointer relative pr-8",
|
|
79
|
+
{
|
|
80
|
+
"bg-accent text-accent-foreground": isSelected,
|
|
81
|
+
}
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
{option.icon}
|
|
85
|
+
<span>{option.label}</span>
|
|
86
|
+
{isSelected && (
|
|
87
|
+
<Check className="h-4 w-4 absolute right-2 text-primary" />
|
|
88
|
+
)}
|
|
89
|
+
</DropdownMenuItem>
|
|
90
|
+
);
|
|
91
|
+
})}
|
|
92
|
+
</DropdownMenuContent>
|
|
93
|
+
</DropdownMenu>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
ThemeSelect.displayName = "ThemeSelect";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTheme } from "@/components/theme/theme-provider";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { Moon, Sun } from "lucide-react";
|
|
7
|
+
|
|
8
|
+
type ThemeToggleProps = {
|
|
9
|
+
className?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function ThemeToggle({ className }: ThemeToggleProps) {
|
|
13
|
+
const { theme, setTheme } = useTheme();
|
|
14
|
+
|
|
15
|
+
const toggleTheme = () => setTheme(theme === "dark" ? "light" : "dark");
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Button
|
|
19
|
+
variant="outline"
|
|
20
|
+
size="icon"
|
|
21
|
+
onClick={toggleTheme}
|
|
22
|
+
className={cn(
|
|
23
|
+
"rounded-xl",
|
|
24
|
+
"border-border/70",
|
|
25
|
+
"bg-background/60",
|
|
26
|
+
className,
|
|
27
|
+
"h-10",
|
|
28
|
+
"w-10"
|
|
29
|
+
)}
|
|
30
|
+
>
|
|
31
|
+
<Sun
|
|
32
|
+
className={cn(
|
|
33
|
+
"h-[1.2rem]",
|
|
34
|
+
"w-[1.2rem]",
|
|
35
|
+
"rotate-0",
|
|
36
|
+
"scale-100",
|
|
37
|
+
"transition-all",
|
|
38
|
+
"duration-200",
|
|
39
|
+
{
|
|
40
|
+
"-rotate-90 scale-0": theme === "dark",
|
|
41
|
+
}
|
|
42
|
+
)}
|
|
43
|
+
/>
|
|
44
|
+
<Moon
|
|
45
|
+
className={cn(
|
|
46
|
+
"absolute",
|
|
47
|
+
"h-[1.2rem]",
|
|
48
|
+
"w-[1.2rem]",
|
|
49
|
+
"rotate-90",
|
|
50
|
+
"scale-0",
|
|
51
|
+
"transition-all",
|
|
52
|
+
"duration-200",
|
|
53
|
+
{
|
|
54
|
+
"rotate-0 scale-100": theme === "dark",
|
|
55
|
+
"rotate-90 scale-0": theme === "light",
|
|
56
|
+
}
|
|
57
|
+
)}
|
|
58
|
+
/>
|
|
59
|
+
<span className="sr-only">
|
|
60
|
+
Switch to {theme === "dark" ? "light" : "dark"} mode
|
|
61
|
+
</span>
|
|
62
|
+
</Button>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ThemeToggle.displayName = "ThemeToggle";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
|
|
7
|
+
return (
|
|
8
|
+
<AccordionPrimitive.Root
|
|
9
|
+
data-slot="accordion"
|
|
10
|
+
className={cn("flex w-full flex-col", className)}
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
|
|
17
|
+
return (
|
|
18
|
+
<AccordionPrimitive.Item
|
|
19
|
+
data-slot="accordion-item"
|
|
20
|
+
className={cn("not-last:border-b", className)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function AccordionTrigger({
|
|
27
|
+
className,
|
|
28
|
+
children,
|
|
29
|
+
...props
|
|
30
|
+
}: AccordionPrimitive.Trigger.Props) {
|
|
31
|
+
return (
|
|
32
|
+
<AccordionPrimitive.Header className="flex">
|
|
33
|
+
<AccordionPrimitive.Trigger
|
|
34
|
+
data-slot="accordion-trigger"
|
|
35
|
+
className={cn(
|
|
36
|
+
"group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground",
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
<ChevronDownIcon data-slot="accordion-trigger-icon" className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden" />
|
|
43
|
+
<ChevronUpIcon data-slot="accordion-trigger-icon" className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline" />
|
|
44
|
+
</AccordionPrimitive.Trigger>
|
|
45
|
+
</AccordionPrimitive.Header>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function AccordionContent({
|
|
50
|
+
className,
|
|
51
|
+
children,
|
|
52
|
+
...props
|
|
53
|
+
}: AccordionPrimitive.Panel.Props) {
|
|
54
|
+
return (
|
|
55
|
+
<AccordionPrimitive.Panel
|
|
56
|
+
data-slot="accordion-content"
|
|
57
|
+
className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
|
|
58
|
+
{...props}
|
|
59
|
+
>
|
|
60
|
+
<div
|
|
61
|
+
className={cn(
|
|
62
|
+
"h-(--accordion-panel-height) pt-0 pb-2.5 data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
|
|
63
|
+
className
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
{children}
|
|
67
|
+
</div>
|
|
68
|
+
</AccordionPrimitive.Panel>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|