@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.
Files changed (123) hide show
  1. package/.env.example +10 -0
  2. package/Dockerfile +36 -0
  3. package/README.MD +50 -0
  4. package/components.json +25 -0
  5. package/eslint.config.js +28 -0
  6. package/index.html +39 -0
  7. package/package.json +84 -0
  8. package/pnpm-workspace.yaml +2 -0
  9. package/public/favicon.ico +0 -0
  10. package/public/logo-dark.png +0 -0
  11. package/public/logo-mark-dark.png +0 -0
  12. package/public/logo-mark.png +0 -0
  13. package/public/logo.png +0 -0
  14. package/src/App.css +158 -0
  15. package/src/App.tsx +151 -0
  16. package/src/components/app-shell/brand.tsx +70 -0
  17. package/src/components/app-shell/breadcrumb.tsx +81 -0
  18. package/src/components/app-shell/document-title-handler.tsx +169 -0
  19. package/src/components/app-shell/error-component.tsx +134 -0
  20. package/src/components/app-shell/header.tsx +127 -0
  21. package/src/components/app-shell/layout.tsx +39 -0
  22. package/src/components/app-shell/loading-overlay.tsx +36 -0
  23. package/src/components/app-shell/sidebar.tsx +351 -0
  24. package/src/components/app-shell/user-avatar.tsx +42 -0
  25. package/src/components/app-shell/user-info.tsx +53 -0
  26. package/src/components/auth/auth-layout.tsx +82 -0
  27. package/src/components/auth/forgot-password-form.tsx +58 -0
  28. package/src/components/auth/input-password.tsx +38 -0
  29. package/src/components/auth/sign-in-form.tsx +111 -0
  30. package/src/components/auth/sign-up-form.tsx +127 -0
  31. package/src/components/data-table/data-table-filter.tsx +994 -0
  32. package/src/components/data-table/data-table-pagination.tsx +146 -0
  33. package/src/components/data-table/data-table-sorter.tsx +47 -0
  34. package/src/components/data-table/data-table.tsx +321 -0
  35. package/src/components/notifications/toaster.tsx +23 -0
  36. package/src/components/notifications/undoable-notification.tsx +84 -0
  37. package/src/components/notifications/use-notification-provider.tsx +62 -0
  38. package/src/components/resources/buttons/clone.tsx +81 -0
  39. package/src/components/resources/buttons/create.tsx +70 -0
  40. package/src/components/resources/buttons/delete.tsx +113 -0
  41. package/src/components/resources/buttons/edit.tsx +81 -0
  42. package/src/components/resources/buttons/list.tsx +70 -0
  43. package/src/components/resources/buttons/refresh.tsx +81 -0
  44. package/src/components/resources/buttons/show.tsx +81 -0
  45. package/src/components/resources/status-badge.tsx +37 -0
  46. package/src/components/resources/views/create-view.tsx +81 -0
  47. package/src/components/resources/views/edit-view.tsx +104 -0
  48. package/src/components/resources/views/list-view.tsx +86 -0
  49. package/src/components/resources/views/show-view.tsx +108 -0
  50. package/src/components/theme/theme-provider.tsx +71 -0
  51. package/src/components/theme/theme-select.tsx +97 -0
  52. package/src/components/theme/theme-toggle.tsx +66 -0
  53. package/src/components/ui/accordion.tsx +72 -0
  54. package/src/components/ui/alert-dialog.tsx +185 -0
  55. package/src/components/ui/alert.tsx +76 -0
  56. package/src/components/ui/aspect-ratio.tsx +22 -0
  57. package/src/components/ui/avatar.tsx +107 -0
  58. package/src/components/ui/badge.tsx +52 -0
  59. package/src/components/ui/breadcrumb.tsx +125 -0
  60. package/src/components/ui/button.tsx +58 -0
  61. package/src/components/ui/calendar.tsx +221 -0
  62. package/src/components/ui/card.tsx +103 -0
  63. package/src/components/ui/carousel.tsx +240 -0
  64. package/src/components/ui/chart.tsx +373 -0
  65. package/src/components/ui/checkbox.tsx +29 -0
  66. package/src/components/ui/collapsible.tsx +19 -0
  67. package/src/components/ui/command.tsx +192 -0
  68. package/src/components/ui/context-menu.tsx +271 -0
  69. package/src/components/ui/dialog.tsx +160 -0
  70. package/src/components/ui/drawer.tsx +226 -0
  71. package/src/components/ui/dropdown-menu.tsx +268 -0
  72. package/src/components/ui/form.tsx +174 -0
  73. package/src/components/ui/hover-card.tsx +49 -0
  74. package/src/components/ui/input-group.tsx +158 -0
  75. package/src/components/ui/input-otp.tsx +87 -0
  76. package/src/components/ui/input.tsx +20 -0
  77. package/src/components/ui/label.tsx +18 -0
  78. package/src/components/ui/menubar.tsx +278 -0
  79. package/src/components/ui/navigation-menu.tsx +168 -0
  80. package/src/components/ui/pagination.tsx +130 -0
  81. package/src/components/ui/popover.tsx +91 -0
  82. package/src/components/ui/progress.tsx +81 -0
  83. package/src/components/ui/radio-group.tsx +38 -0
  84. package/src/components/ui/resizable.tsx +48 -0
  85. package/src/components/ui/scroll-area.tsx +55 -0
  86. package/src/components/ui/select.tsx +199 -0
  87. package/src/components/ui/separator.tsx +25 -0
  88. package/src/components/ui/sheet.tsx +136 -0
  89. package/src/components/ui/sidebar.tsx +723 -0
  90. package/src/components/ui/skeleton.tsx +13 -0
  91. package/src/components/ui/slider.tsx +52 -0
  92. package/src/components/ui/sonner.tsx +47 -0
  93. package/src/components/ui/switch.tsx +32 -0
  94. package/src/components/ui/table.tsx +114 -0
  95. package/src/components/ui/tabs.tsx +82 -0
  96. package/src/components/ui/textarea.tsx +18 -0
  97. package/src/components/ui/toggle-group.tsx +87 -0
  98. package/src/components/ui/toggle.tsx +43 -0
  99. package/src/components/ui/tooltip.tsx +66 -0
  100. package/src/hooks/use-mobile.ts +19 -0
  101. package/src/index.tsx +17 -0
  102. package/src/lib/utils.ts +19 -0
  103. package/src/pages/blog-posts/create.tsx +177 -0
  104. package/src/pages/blog-posts/edit.tsx +197 -0
  105. package/src/pages/blog-posts/index.ts +4 -0
  106. package/src/pages/blog-posts/list.tsx +131 -0
  107. package/src/pages/blog-posts/show.tsx +81 -0
  108. package/src/pages/categories/create.tsx +73 -0
  109. package/src/pages/categories/edit.tsx +73 -0
  110. package/src/pages/categories/index.ts +4 -0
  111. package/src/pages/categories/list.tsx +86 -0
  112. package/src/pages/categories/show.tsx +37 -0
  113. package/src/pages/forgot-password/index.tsx +6 -0
  114. package/src/pages/forgotPassword/index.tsx +5 -0
  115. package/src/pages/login/index.tsx +5 -0
  116. package/src/pages/register/index.tsx +5 -0
  117. package/src/providers/auth.ts +204 -0
  118. package/src/providers/constants.ts +75 -0
  119. package/src/providers/data.ts +232 -0
  120. package/src/vite-env.d.ts +1 -0
  121. package/tsconfig.json +25 -0
  122. package/tsconfig.node.json +21 -0
  123. 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 }