12 Commits

Author SHA1 Message Date
Tamani Karim c31ab6423b Add docs link 2026-03-28 19:25:21 +01:00
KarimTamani aa3688a0e6 Improve performance and accessibility; add documentation 2026-03-28 19:22:59 +01:00
KarimTamani 39317143b1 Update Discord link & Security contaact email 2026-02-09 22:11:29 +01:00
Tamani Karim 14db841617 Merge pull request #1 from stackrender/uiux-improvements
V1.0.2
2026-01-01 15:52:47 +01:00
KarimTamani 1d78d05468 V1.0.2 2026-01-01 15:51:17 +01:00
KarimTamani d9e5333f96 Mobile Responsivity 2026-01-01 15:49:48 +01:00
KarimTamani 7c443f0edf Release V1.0.1 2025-09-27 18:19:21 +01:00
KarimTamani 6bb10b86f2 Merging UI/UX Improvements with the main branch 2025-09-26 20:52:12 +01:00
KarimTamani 1348262649 Redesign the app using ShadCn UI + new feature (order tables) 2025-09-26 20:30:57 +01:00
KarimTamani c226adfb27 Edit : make <create_database_modal> default CTA create empty diagram 2025-09-01 15:28:46 +01:00
KarimTamani 0e82e5b965 Test branch protection rules 2025-08-22 17:30:12 +01:00
KarimTamani cd9861bfd8 Add Spaces 2025-08-22 17:27:56 +01:00
170 changed files with 13775 additions and 13148 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
tamanikarim50@gmail.com.
contact@stackrender.io.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
+2 -2
View File
@@ -27,10 +27,10 @@ If you find a bug:
If you have an idea for improvement:
- Open a **feature request** in the issues section.
- Describe the feature and explain why it would be useful.
- You may also discuss ideas with the community on [Discord](https://discord.gg/DsN8RcPR6Y).
- You may also discuss ideas with the community on [Discord](https://discord.gg/4dv26jR4Pj).
## License Agreement
By contributing, you agree that your work will be licensed under the **GNU Affero General Public License v3.0**.
## Questions or Support
If you have questions about the contribution process, open a discussion on GitHub or reach out on our [Discord](https://discord.gg/DsN8RcPR6Y).
If you have questions about the contribution process, open a discussion on GitHub or reach out on our [Discord](https://discord.gg/4dv26jR4Pj).
+4 -4
View File
@@ -6,7 +6,7 @@
<img src="https://img.shields.io/badge/Start%20Building!-gray.svg?logo=data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZENzAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTMgMyBMMiAxMyBoNyBMMTEgMjEgTDIyIDExIGgtNyBMIDEzIDMgeiIvPjwvc3ZnPg==" alt="Start Building!" />
</a>
<a href="https://discord.com/invite/DsN8RcPR6Y">
<a href="https://discord.com/invite/4dv26jR4Pj">
<img src="https://img.shields.io/discord/1352085267535761448?color=5865F2&label=Discord&logo=discord&logoColor=white" alt="Discord community channel" />
</a>
<a href="https://x.com/intent/follow?screen_name=Iam_The_Dev">
@@ -67,8 +67,8 @@ npm run start
6. Export your Database SQL DDL and run it!
## 🤝 Community
- [**Discord**](https://discord.com/invite/DsN8RcPR6Y) Live discussions about upcoming releases and the future of StackRender.
- [**Docs**](https://www.stackrender.io/docs) Explore the StackRender docs to learn more about its features.
- [**Discord**](https://discord.com/invite/4dv26jR4Pj) Live discussions about upcoming releases and the future of StackRender.
- [**GitHub Issues**](https://github.com/stackrender/stackrender/issues) Report bugs and errors to help us improve your experience.
- [**X (Twitter)**](https://x.com/intent/follow?screen_name=Iam_The_Dev) Get the latest StackRender news and updates.
@@ -76,7 +76,7 @@ npm run start
We welcome all contributions, whether small bug fixes or major feature additions.
- Discuss your ideas and contributions in our [Discord](https://discord.com/invite/DsN8RcPR6Y) community.
- Discuss your ideas and contributions in our [Discord](https://discord.com/invite/4dv26jR4Pj) community.
- Follow the [Contributing Guide](./CONTRIBUTING.md) to get started.
- Agree to the [Code of Conduct](./CODE_OF_CONDUCT.md) to ensure a positive and respectful environment.
+1 -1
View File
@@ -11,7 +11,7 @@ We currently provide security updates for the latest stable release of StackRend
## Reporting a Vulnerability
If you discover a security vulnerability in StackRender, please report it to us directly.
- Email: security@stackrender.com
- Email: contact@stackrender.io
- GitHub Security Advisory: Use the "Report a vulnerability" option in the repository's Security tab.
When reporting, please include:
+23
View File
@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks",
"providers": "@/providers"
},
"registries": {}
}
+31 -26
View File
@@ -1,28 +1,33 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>StackRender Studio</title>
<meta key="title" content="StackRender Studio" property="og:title" />
<meta
content="StackRender is an AI-powered tool that helps you design and edit database diagrams effortlessly with a smooth, beautiful UI. Generate clean SQL DDL for PostgreSQL, MySQL, MariaDB, and SQLite in seconds."
property="og:description"
/>
<meta
content="StackRender is an AI-powered tool that helps you design and edit database diagrams effortlessly with a smooth, beautiful UI. Generate clean SQL DDL for PostgreSQL, MySQL, MariaDB, and SQLite in seconds."
name="description"
/>
<meta
key="viewport"
content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
name="viewport"
/>
<link href="/favicon.png" rel="icon" sizes="32x32" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>StackRender Studio</title>
<meta key="title" content="StackRender Studio" property="og:title" />
<meta
content="StackRender is an AI-powered tool that helps you design and edit database diagrams effortlessly with a smooth, beautiful UI. Generate clean SQL DDL for PostgreSQL, MySQL, MariaDB, and SQLite in seconds."
property="og:description" />
<meta
content="StackRender is an AI-powered tool that helps you design and edit database diagrams effortlessly with a smooth, beautiful UI. Generate clean SQL DDL for PostgreSQL, MySQL, MariaDB, and SQLite in seconds."
name="description" />
<meta key="viewport"
content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
name="viewport" />
<link href="/favicon.png" rel="icon" sizes="32x32" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Manrope:wght@200..800&display=swap"
rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+3238 -8139
View File
File diff suppressed because it is too large Load Diff
+34 -11
View File
@@ -1,7 +1,7 @@
{
"name": "vite-template",
"name": "StackRender",
"private": true,
"version": "0.0.0",
"version": "1.0.2",
"type": "module",
"scripts": {
"dev": "vite",
@@ -13,19 +13,39 @@
"@codemirror/lang-sql": "^6.9.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@heroui/react": "^2.7.6",
"@internationalized/date": "^3.8.2",
"@nextui-org/react": "^2.6.11",
"@powersync/drizzle-driver": "^0.4.0",
"@powersync/react": "^1.5.3",
"@powersync/web": "^1.20.0",
"@radix-ui/react-tooltip": "^1.2.3",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-aria/visually-hidden": "3.8.21",
"@react-types/shared": "3.28.0",
"@reecelucas/react-use-hotkeys": "^2.0.0",
"@types/react-resizable": "^3.0.8",
"@tabler/icons-react": "^3.34.1",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.12",
"@uiw/react-codemirror": "^4.23.12",
"@xyflow/react": "^12.6.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"dayjs": "^1.11.13",
"drizzle-orm": "^0.43.1",
@@ -35,23 +55,26 @@
"i18next-browser-languagedetector": "^8.0.5",
"lodash": "^4.17.21",
"lucide-react": "^0.501.0",
"next-themes": "^0.4.6",
"node-sql-parser": "^5.3.9",
"object-hash": "^3.0.0",
"pgsql-ast-parser": "^12.0.1",
"pluralize": "^8.0.0",
"react": "18.3.1",
"react-day-picker": "^9.10.0",
"react-dom": "18.3.1",
"react-i18next": "^15.5.1",
"react-resizable": "^3.0.5",
"react-resizable-panels": "^2.1.9",
"react-router-dom": "6.23.0",
"react-tag-input": "^6.10.6",
"sonner": "^2.0.7",
"sql-formatter": "^15.6.3",
"tailwind-merge": "^3.2.0",
"tailwind-merge": "^3.3.1",
"tailwind-variants": "0.3.0",
"tailwindcss": "3.4.16",
"tailwindcss": "^4.1.13",
"tailwindcss-animate": "^1.0.7",
"use-undo": "^1.1.1",
"uuid": "^11.1.0"
"uuid": "^11.1.0",
"vaul": "^1.1.2"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
@@ -73,8 +96,8 @@
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-unused-imports": "4.1.4",
"postcss": "8.4.38",
"prettier": "3.3.3",
"tw-animate-css": "^1.3.6",
"typescript": "5.6.3",
"vite": "^5.2.0",
"vite-plugin-top-level-await": "^1.5.0",
-6
View File
@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 75 KiB

+3 -9
View File
@@ -1,14 +1,12 @@
import "@/styles/globals.css"
import { TooltipProvider } from "./components/tooltip/tooltip";
import { ReactFlowProvider } from "@xyflow/react";
import useAppRoutes from "./routes/app-route";
import { SyncProvider } from "./providers/sync-provider/sync-provider";
import DatabaseProvider from "./providers/database-provider/database-provider";
import DiagramProvider from "./providers/diagram-provider/diagram-provider";
import { ToastProvider } from "@heroui/react";
import { ModalProvider } from "./providers/modal-provider/modal-provider";
import DatabaseHotkeysProvider from "./providers/database-hotkeys/database-hotkeys-provider";
@@ -18,21 +16,17 @@ function App() {
const appRoutes = useAppRoutes();
return (
<>
<ToastProvider placement="bottom-right" />
<>
<SyncProvider>
<ReactFlowProvider>
<DatabaseProvider>
<DiagramProvider>
<TooltipProvider delayDuration={0}>
<ModalProvider>
<DatabaseHotkeysProvider>
{appRoutes}
</DatabaseHotkeysProvider>
</ModalProvider>
</TooltipProvider>
</DiagramProvider>
</DatabaseProvider>
</ReactFlowProvider>
@@ -1,107 +0,0 @@
import { Autocomplete as HeroUiAutocomplete, AutocompleteItem, AutocompleteSection } from "@heroui/react";
import { Key, useState } from "react";
interface AutocompleteProps {
items?: any[]
label?: string,
defaultSelection?: string | undefined
onSelectionChange?: (item: any) => void,
placeholder?: string
isDisabled?: boolean
selectedItem?: Key,
grouped?: boolean
}
const headingClasses =
"flex w-full py-1.5 px-2 bg-default shadow-md border-1 font-medium rounded-md dark:border-font/5 dark:bg-background-100";
const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSelectionChange, defaultSelection, placeholder, isDisabled, selectedItem, grouped = false }) => {
const onItemChange = (item: Key | null) => {
onSelectionChange && onSelectionChange(item);
}
if (!grouped)
return (
<HeroUiAutocomplete
radius="sm"
isDisabled={isDisabled}
defaultItems={items || []}
size="sm"
onSelectionChange={onItemChange}
defaultSelectedKey={defaultSelection as any}
variant="bordered"
aria-label={placeholder}
placeholder={placeholder}
selectedKey={selectedItem as any}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
clearButton: "text-icon",
selectorButton: "text-icon",
popoverContent: "rounded-md "
}}
inputProps={{
classNames: {
inputWrapper: " border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
},
}}
>
{(item: any) => <AutocompleteItem key={item.id}>{item[label]}</AutocompleteItem>}
</HeroUiAutocomplete>
)
if (grouped) {
return <HeroUiAutocomplete
radius="sm"
isDisabled={isDisabled}
size="sm"
onSelectionChange={onItemChange}
defaultSelectedKey={defaultSelection as any}
variant="bordered"
aria-label={placeholder}
placeholder={placeholder}
className="h-8 w-full focus-visible:ring-0 shadow-none "
selectedKey={selectedItem as any}
classNames={{
clearButton: "text-icon",
selectorButton: "text-icon",
popoverContent: "rounded-md "
}}
inputProps={{
classNames: {
inputWrapper: "border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
},
}}
>
{
items ? Object.keys(items).map((key: string) => (
<AutocompleteSection
key={key.toUpperCase()}
classNames={{
heading: headingClasses,
}}
title={key.toUpperCase()}
>
{
items[key as any].map((item: any) => (
<AutocompleteItem key={item.id}>{item[label]}</AutocompleteItem>
))
}
</AutocompleteSection>
)) : []
}
</HeroUiAutocomplete>
}
}
export default Autocomplete;
@@ -1,37 +0,0 @@
import { DatabaseType } from "@/lib/database"
import { Checkbox, Image } from "@heroui/react";
interface DatabaseCheckboxProps {
database: DatabaseType,
}
const DatabaseCheckbox: React.FC<DatabaseCheckboxProps> = ({ database }) => {
return (
<Checkbox
aria-label={database.name}
value={database.dialect}
className="database-checkbox"
classNames={{
base: "flex min-w-[128px] min-h-[128px] max-w-[128px] max-h-[128px] hover:bg-default rounded-md relative data-[selected=true]:border-1 data-[selected=true]:border-primary data-[selected=true]:bg-primary/5",
wrapper: "absolute top-2 left-2 before:border-divider group-data-[hover=true]:before:bg-default " ,
label : "flex items-center justify-center w-full h-full p-2 "
}}
>
<div className="min-w-full h-full flex items-center justify-center ">
<Image
src={database.logo}
className="w-full rounded-none"
/>
</div>
</Checkbox>
)
}
export default DatabaseCheckbox;
@@ -1,54 +0,0 @@
import { Checkbox, Chip, cn, useCheckbox } from "@heroui/react";
import { useTheme } from "next-themes";
import React from "react";
interface OptionCheckboxProps {
value: string;
label: string;
icon?: React.ReactNode;
logo?: string;
isSelected?: boolean;
}
const OptionCheckbox: React.FC<OptionCheckboxProps> = (props) => {
const { value, icon, logo, label, isSelected } = props;
let variant : any = isSelected ? {
variant: "solid",
color: "default"
} : {
variant: "borderd",
color: "default"
}
return (
<Checkbox
aria-label={value}
value={value}
size="sm"
className="option-checkbox"
classNames={{
wrapper: "hidden",
label: "flex items-center justify-center w-full h-full "
}}
>
<Chip radius="sm" {...variant as any} className={cn("dark:bg-background px-3 h-9 border-1 transition-all duration-300 border-divider text-font/90 ",
!isSelected ? "dark:bg-default" : undefined
)}
avatar={logo ? <img src={logo} height={12} /> : undefined}
startContent={
icon
}
>
<span className="text-xs font-medium ">
{label}
</span>
</Chip>
</Checkbox>
)
}
export default React.memo(OptionCheckbox);
@@ -1,8 +1,14 @@
import React, { useCallback, useState } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip";
import { useTranslation } from "react-i18next";
import { Button } from "@heroui/react";
import { Copy, CopyCheck } from "lucide-react";
import { Tooltip , TooltipContent, TooltipTrigger} from "./ui/tooltip";
import { Button } from "./ui/button";
@@ -17,6 +23,8 @@ const Clipboard: React.FC<ClipboardProps> = ({ text }) => {
const [isCopied, setIsCopied] = useState<boolean>(false);
const copyToClipboard = useCallback(async () => {
if (text)
try {
await navigator.clipboard.writeText(text);
@@ -34,14 +42,14 @@ const Clipboard: React.FC<ClipboardProps> = ({ text }) => {
return (
<Tooltip>
<TooltipTrigger>
<TooltipTrigger asChild>
<span>
<Button
size="sm"
isIconOnly
variant="bordered"
className="text-font/90 border-1 border-divider"
onPressEnd={copyToClipboard}
size="icon"
variant="outline"
className="size-8"
onClick={copyToClipboard}
>
{
!isCopied ? <Copy className="size-4" /> : <CopyCheck className="size-4" />
@@ -1,9 +1,10 @@
import { colorOptions } from "@/lib/colors";
import { Button, Popover, PopoverContent, PopoverTrigger } from "@heroui/react";
import { Slash } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip";
import { useTranslation } from "react-i18next";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { Button } from "./ui/button";
import { IconSlash } from "@tabler/icons-react";
interface ColorPickerProps {
defaultColor?: string;
@@ -15,11 +16,12 @@ interface ColorPickerProps {
const ColorPicker: React.FC<ColorPickerProps> = ({ defaultColor, onChange }) => {
const [color, setColor] = useState<string | undefined>(defaultColor as string);
const [isOpen, setIsOpen] = useState(false);
const { t } = useTranslation();
useEffect(() => {
setColor(defaultColor) ;
} , [defaultColor])
setColor(defaultColor);
}, [defaultColor])
const onSelect = useCallback((color: string | undefined) => {
setIsOpen(false);
@@ -28,6 +30,44 @@ const ColorPicker: React.FC<ColorPickerProps> = ({ defaultColor, onChange }) =>
}, [onChange])
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild >
<Button
size="sm"
className="size-7 cursor-pointer rounded-md border-1 transition-shadow hover:shadow-md border-border rounded-sm bg-secondary hover:bg-secondary"
style={{
backgroundColor: color ? color : undefined
}}
>
</Button>
</PopoverTrigger>
<PopoverContent className="p-2 w-fit">
<div className="grid grid-cols-4 gap-2">
{colorOptions.map(color => (
<div
key={color}
className="size-7 cursor-pointer rounded-sm border-1 transition-shadow hover:shadow-md border-border"
style={{
backgroundColor: color
}}
onClick={() => onSelect(color)}
>
</div>
))}
<div
key={undefined}
className="size-7 cursor-pointer rounded-sm border-2 border-border transition-shadow hover:shadow-md bg-background dark:bg-secondary"
onClick={() => onSelect(undefined)}
>
<IconSlash className="size-8 text-destructive -ml-1 -mt-1" />
</div>
</div>
</PopoverContent>
</Popover>
/*
<Popover placement="bottom" radius="sm" isOpen={isOpen} onOpenChange={(open) => setIsOpen(open)} shadow="sm">
<PopoverTrigger>
<Button size="sm"
@@ -41,42 +81,10 @@ const ColorPicker: React.FC<ColorPickerProps> = ({ defaultColor, onChange }) =>
</Button>
</PopoverTrigger>
<PopoverContent>
<div className="grid grid-cols-4 gap-2">
{colorOptions.map(color => (
<div
key={color}
className="size-8 cursor-pointer rounded-md border-2 transition-shadow hover:shadow-md border-divider"
style={{
backgroundColor: color
}}
onClick={() => onSelect(color)}
>
</div>
))}
<Tooltip>
<TooltipTrigger>
<div
key={undefined}
className="size-8 cursor-pointer rounded-md border-2 border-muted transition-shadow hover:shadow-md dark:border-default-100"
style={{
backgroundColor: "white"
}}
onClick={() => onSelect(undefined)}
>
<Slash className="size-full text-danger-500" />
</div>
</TooltipTrigger>
<TooltipContent>
{t("color_picker.default_color")}
</TooltipContent>
</Tooltip>
</div>
</PopoverContent>
</Popover>
*/
)
}
+116
View File
@@ -0,0 +1,116 @@
"use client"
import * as React from "react"
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
interface ComboboxProps {
items: any[]
label?: string;
placeholder?: string;
selectedItem?: string;
onSelectionChange?: (id: string) => void;
className?: string;
isDisabled?: boolean
}
export const Combobox = (props: ComboboxProps) => {
const { items, label = "label", placeholder, onSelectionChange, selectedItem, className, isDisabled } = props;
const [query, setQuery] = React.useState<string>("");
const [open, setOpen] = React.useState(false)
const [value, setValue] = React.useState<string | undefined>("")
React.useEffect(() => {
setValue(selectedItem);
}, [selectedItem]);
React.useEffect(() => {
onSelectionChange && onSelectionChange(value as string);
}, [value])
React.useEffect(() => {
if (open)
setQuery("");
}, [open])
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild disabled={isDisabled}>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn("justify-between", className)}
>
<label className="truncate">
{
value
? items.find((item) => item.id === value)?.[label]
: (placeholder ? placeholder : "Select items...")}
</label>
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command
filter={(value, search) => {
return 1
}}
>
<CommandInput placeholder={placeholder ? placeholder : "Select items..."}
onValueChange={setQuery}
/>
<CommandList>
<CommandEmpty>No type found.</CommandEmpty>
<CommandGroup>
{items.filter((item: any) => (item[label] as string).toLocaleLowerCase().includes(query.toLocaleLowerCase())).map((item) => (
<CommandItem
key={item.id}
value={item.id}
onSelect={(currentValue) => {
setValue(currentValue);
setOpen(false)
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
value === item.id ? "opacity-100" : "opacity-0"
)}
/>
{item[label]}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
+105
View File
@@ -0,0 +1,105 @@
import React from 'react'
import {
IconArrowRightDashed,
IconChevronRight,
IconDeviceLaptop,
IconMoon,
IconSun,
} from '@tabler/icons-react'
import { useSearch } from '@/providers/search-provider/search-provider'
import { useTheme } from '@/providers/theme-provider/theme-provider'
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from '@/components/ui/command'
import { useSidebarData } from '@/components/layout/app-sidebar/data/sidebar-data'
import { ScrollArea } from './ui/scroll-area'
import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
export function CommandMenu() {
const navigate = useNavigate()
const { setTheme } = useTheme()
const { open, setOpen } = useSearch()
const runCommand = React.useCallback(
(command: () => unknown) => {
setOpen(false)
command()
},
[setOpen]
)
const { t } = useTranslation() ;
const sidebarData = useSidebarData();
return (
<CommandDialog modal open={open} onOpenChange={setOpen}>
<CommandInput placeholder={t("navbar.command")} />
<CommandList>
<ScrollArea type='hover' className='h-72 pr-1'>
<CommandEmpty>No results found.</CommandEmpty>
{sidebarData.navGroups.map((group: any) => (
<CommandGroup key={group.title} heading={group.title}>
{group.items.map((navItem: any, i: number) => {
if (navItem.url)
return (
<CommandItem
key={`${navItem.url}-${i}`}
value={navItem.title}
onSelect={() => {
runCommand(() => navigate(navItem.url))
}}
>
<div className='mr-2 flex h-4 w-4 items-center justify-center'>
<IconArrowRightDashed className='text-muted-foreground/80 size-2' />
</div>
{navItem.title}
</CommandItem>
)
return navItem.items?.map((subItem: any, i: number) => (
<CommandItem
key={`${navItem.title}-${subItem.url}-${i}`}
value={`${navItem.title}-${subItem.url}`}
onSelect={() => {
runCommand(() => navigate(subItem.url))
}}
>
<div className='mr-2 flex h-4 w-4 items-center justify-center'>
<IconArrowRightDashed className='text-muted-foreground/80 size-2' />
</div>
{navItem.title} <IconChevronRight /> {subItem.title}
</CommandItem>
))
})}
</CommandGroup>
))}
<CommandSeparator />
<CommandGroup heading='Theme'>
<CommandItem onSelect={() => runCommand(() => setTheme('light'))}>
<IconSun /> <span>{t("menu.light")}</span>
</CommandItem>
<CommandItem onSelect={() => runCommand(() => setTheme('dark'))}>
<IconMoon className='scale-90' />
<span>{t("menu.dark")}</span>
</CommandItem>
<CommandItem onSelect={() => runCommand(() => setTheme('system'))}>
<IconDeviceLaptop />
<span>{t("menu.system")}</span>
</CommandItem>
</CommandGroup>
</ScrollArea>
</CommandList>
</CommandDialog>
)
}
+61
View File
@@ -0,0 +1,61 @@
import * as React from "react"
import { ChevronDownIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import { Label } from "@/components/ui/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
interface DatePickerProps {
value?: Date | undefined;
onValueChange?: (value?: Date) => void
}
const DatePicker: React.FC<DatePickerProps> = ({value, onValueChange}) => {
const [open, setOpen] = React.useState(false)
const [date, setDate] = React.useState<Date | undefined>(value)
React.useEffect(() => {
onValueChange && onValueChange(date)
}, [date])
return (
<div className="flex flex-col gap-3">
<Label htmlFor="date" className="px-1">
Date of birth
</Label>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
id="date"
className="w-full justify-between font-normal"
>
{date ? date.toLocaleDateString() : "Select date"}
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
<Calendar
mode="single"
selected={date}
captionLayout="dropdown"
onSelect={(date) => {
setDate(date)
setOpen(false)
}}
/>
</PopoverContent>
</Popover>
</div>
)
}
export { DatePicker }
@@ -1,28 +1,17 @@
import { Image } from "@heroui/react";
import { useTheme } from "next-themes";
interface EmptyListProps {
title?: string;
description?: string;
}
const EmptyList: React.FC<EmptyListProps> = ({ title, description }) => {
return (
<div className="flex flex-col items-center gap-1 h-full pt-[86px] main-linear-background rounded-lg">
{
<Image
<img
width={64}
src={"/stackrender.png"}
/>
}
{
@@ -32,7 +21,6 @@ const EmptyList: React.FC<EmptyListProps> = ({ title, description }) => {
</h1>
}
{
description &&
<p className="text-sm text-font/70">
{description}
+95
View File
@@ -0,0 +1,95 @@
import React, { useCallback } from 'react'
import { cn } from '@/lib/utils'
import { Separator } from '@/components/ui/separator'
import { SidebarTrigger } from '@/components/ui/sidebar'
import Menu from './layout/menu/menu'
import { Search } from './search'
import { Button } from './ui/button'
//import { ProfileDropdown } from './profile-dropdown'
import { useModal } from '@/providers/modal-provider/modal-provider'
import { Modals } from '@/providers/modal-provider/modal-contxet'
import { useTranslation } from 'react-i18next'
import { ThemeSwitch } from './theme-switch'
import RenameDb from '@/features/database/components/rename-db'
import LanguagesDropdown from './languages-dropdown'
import { useIsMobile } from '@/hooks/use-mobile'
interface HeaderProps extends React.HTMLAttributes<HTMLElement> {
fixed?: boolean
ref?: React.Ref<HTMLElement>
}
export const Header = ({
className,
fixed,
children,
...props
}: HeaderProps) => {
const [offset, setOffset] = React.useState(0)
React.useEffect(() => {
const onScroll = () => {
setOffset(document.body.scrollTop || document.documentElement.scrollTop)
}
// Add scroll listener to the body
document.addEventListener('scroll', onScroll, { passive: true })
// Clean up the event listener on unmount
return () => document.removeEventListener('scroll', onScroll)
}, []);
const { t } = useTranslation();
const isMobile = useIsMobile();
return (
<header
className={cn(
'bg-background flex-col md:flex items-center gap-3 p-2 sm:gap-4 ',
fixed && 'header-fixed peer/header fixed z-50 w-[inherit] rounded-md ',
offset > 10 && fixed ? 'shadow-sm' : 'shadow-none',
className
)}
{...props}
>
<div className='grid grid-cols-3 items-center w-full '>
<div className="justify-self-start flex h-8 gap-3 items-center ">
<SidebarTrigger variant='outline' className='scale-125 sm:scale-100' />
<Separator orientation='vertical' className='h-6' />
{!isMobile && <Menu />}
</div>
<div className=" justify-self-center ">
<Search className='hidden lg:flex' placeholder={t("navbar.search")} />
{
isMobile &&
<div className='w-full'>
<RenameDb />
</div>
}
</div>
<div className="justify-self-end flex gap-2">
{
!isMobile &&
<div className='w-full'>
<RenameDb />
</div>
}
<ThemeSwitch />
<LanguagesDropdown />
</div>
</div>
{isMobile && <div className='flex justify-center'>
<Menu />
</div>
}
</header>
)
}
Header.displayName = 'Header'
+90
View File
@@ -0,0 +1,90 @@
// input-tags.tsx
import * as React from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
type InputTagsProps = Omit<React.ComponentProps<"input">, "value" | "onChange"> & {
value: string[];
onChange: React.Dispatch<React.SetStateAction<string[]>>;
};
const InputTags = React.forwardRef<HTMLInputElement, InputTagsProps>(
({ className, value, onChange, ...props }, ref) => {
const [pendingDataPoint, setPendingDataPoint] = React.useState("");
React.useEffect(() => {
if (pendingDataPoint.includes(",")) {
const newDataPoints = new Set([
...value,
...pendingDataPoint.split(",").map((chunk) => chunk.trim()),
]);
onChange(Array.from(newDataPoints));
setPendingDataPoint("");
}
}, [pendingDataPoint, onChange, value]);
const addPendingDataPoint = () => {
if (pendingDataPoint) {
const newDataPoints = new Set([...value, pendingDataPoint]);
onChange(Array.from(newDataPoints));
setPendingDataPoint("");
}
};
return (
<div
className={cn(
// caveat: :has() variant requires tailwind v3.4 or above: https://tailwindcss.com/blog/tailwindcss-v3-4#new-has-variant
"has-[:focus-visible]:outline-none has-[:focus-visible]:ring-offset-2 dark:has-[:focus-visible]:ring-neutral-300 min-h-10 flex w-full flex-wrap gap-2 rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 dark:border-border dark:bg-background ",
className
)}
>
{value.map((item) => (
<Badge key={item} variant="secondary">
{item}
<Button
variant="ghost"
size="icon"
className="ml-2 h-3 w-3"
onClick={() => {
onChange(value.filter((i) => i !== item));
}}
>
<XIcon className="w-3" />
</Button>
</Badge>
))}
<input
className="flex-1 outline-none "
value={pendingDataPoint}
onChange={(e) => setPendingDataPoint(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === ",") {
e.preventDefault();
addPendingDataPoint();
} else if (
e.key === "Backspace" &&
pendingDataPoint.length === 0 &&
value.length > 0
) {
e.preventDefault();
onChange(value.slice(0, -1));
}
}}
{...props}
ref={ref}
/>
</div>
);
}
);
InputTags.displayName = "InputTags";
export { InputTags };
+84
View File
@@ -0,0 +1,84 @@
import { languages } from "@/i18";
import { Language } from "@/i18/types";
import { Check } from "lucide-react";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Command,
CommandEmpty,
CommandInput,
CommandItem,
CommandList,
CommandShortcut,
} from "@/components/ui/command"
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { Button } from "./ui/button";
import { IconLanguage } from "@tabler/icons-react";
const LanguagesDropDonw: React.FC = ({ }) => {
const { t, i18n } = useTranslation();
const [currentLanguage, setCurrentLanguage] = useState<Language>(() => languages.find((language: Language) => language.code == i18n.language) as Language);
const changeLanguage = useCallback((language: Language) => {
i18n.changeLanguage(language.code);
}, [i18n])
useEffect(() => {
setCurrentLanguage(languages.find((language: Language) => language.code == i18n.language) as Language)
}, [i18n.language])
return (
<Popover>
<PopoverTrigger asChild>
<Button
size={"icon"}
variant={"outline"}
>
<IconLanguage className="size-5"/>
</Button>
</PopoverTrigger>
<PopoverContent className="w-60 p-1 rounded-md">
<Command className="rounded-lg border shadow-md ">
<CommandInput
aria-label={t("navbar.search")}
placeholder={`${t("navbar.search")}`}
/>
<CommandList>
<CommandEmpty>No language found.</CommandEmpty>
{
languages.map((language: Language) => (
<CommandItem className="flex justify-between cursor-pointer" key={language.code} onSelect={() => changeLanguage(language)}>
<span>{language.name} <span className="text-muted-foreground">({language.nativeName})</span></span>
<CommandShortcut>
{
currentLanguage.code == language.code && <Check className="text-icon size-4" />
}
</CommandShortcut>
</CommandItem>
))
}
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
export default React.memo(LanguagesDropDonw);
@@ -0,0 +1,60 @@
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarRail,
} from '@/components/ui/sidebar'
import { NavGroup } from '../nav-group'
import { useSidebarData } from './data/sidebar-data'
import { Separator } from '@/components/ui/separator';
import { useDiagramOps } from '@/providers/diagram-provider/diagram-provider';
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const sidebarData = useSidebarData();
const { openController } = useDiagramOps();
return (
<Sidebar collapsible='icon' variant='floating' {...props} className='bg-card border-r' >
<SidebarHeader>
<div className='flex items-center'>
<img
className='w-8 h-8 p-[6px] rounded-md '
src='/stackrender.png'
/>
<h3 className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground truncate font-semibold text-sm '>
StackRender
</h3>
</div>
</SidebarHeader>
<SidebarContent>
{sidebarData.navGroups.map((props: any, index: number) => (
<div key={props.title}>
<NavGroup key={props.title} {...props} onClick={() => openController(true)} />
{
index != sidebarData.navGroups.length - 1 &&
<div className='px-2'>
<Separator />
</div>
}
</div>
))}
</SidebarContent>
<SidebarFooter className='p-0'>
{sidebarData.footerNavGroups.map((props: any, index: number) => (
<div key={props.title}>
<NavGroup key={props.title} {...props} />
{
index != sidebarData.navGroups.length - 1 &&
<div className='px-2'>
<Separator />
</div>
}
</div>
))}
</SidebarFooter>
<SidebarRail />
</Sidebar>
)
}
@@ -0,0 +1,66 @@
import {
IconBrandDiscord,
IconBrandGithub,
IconBrandX,
IconSparkles,
IconTableAlias,
IconVectorSpline,
} from '@tabler/icons-react'
import type { SidebarData } from '../../types';
import { useTranslation } from 'react-i18next';
import { useMemo } from 'react';
import { BookIcon } from 'lucide-react';
export const useSidebarData = () => {
const { t } = useTranslation();
const sidebarData: SidebarData = useMemo(() => ({
navGroups: [
{
title: t("sidebar.database"),
items: [
{
title: t("sidebar.tables"),
url: '/database/tables',
icon: IconTableAlias,
},
{
title: t("sidebar.relationships"),
url: '/database/relationships',
icon: IconVectorSpline,
},
],
},
],
footerNavGroups:[ {
title: '',
items: [
{
title: "Discord",
url: "https://discord.gg/4dv26jR4Pj",
icon: IconBrandDiscord,
newTab : true
},
{
title: "Github",
url: "https://github.com/stackrender/stackrender",
icon: IconBrandGithub,
newTab : true
},
{
title : t("sidebar.documentation") ,
url : "https://www.stackrender.io/docs" ,
icon : BookIcon,
newTab : true
}
],
}]
}), [t])
return sidebarData;
}
+22
View File
@@ -0,0 +1,22 @@
import { Spinner } from "../ui/shadcn-io/spinner";
interface LoadingProps {
text?: string;
}
export const Loading: React.FC<LoadingProps> = ({ text }) => {
return (
<div className="fixed bg-black/50 backdrop-opacity-disabled w-screen h-screen fixed flex items-center justify-center z-[50] left-0 top-0 text-white flex-col gap-2">
<Spinner size={48} variant="ellipsis" className="text-primary" />
<span className="font-medium">
Loading ...
</span>
</div>
)
}
@@ -1,29 +1,29 @@
import { useTheme } from "next-themes";
import DropdownMenu, { MenuDropdownProps } from "./menu-dropdown";
import React, { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useModal } from "@/providers/modal-provider/modal-provider";
import { useTranslation } from "react-i18next"
import { MenuItem } from "../../types";
import { useMemo } from "react";
import { Modals } from "@/providers/modal-provider/modal-contxet";
import { useDatabaseHistory } from "@/providers/database-history/database-history-provider";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { TableType } from "@/lib/schemas/table-schema";
import { useModal } from "@/providers/modal-provider/modal-provider";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { TableType } from "@/lib/schemas/table-schema";
import { CardinalityStyle } from "@/lib/database";
import { useTheme } from "@/providers/theme-provider/theme-provider";
const Menu: React.FC = ({ }) => {
const { setTheme, resolvedTheme } = useTheme()
export const useMenuData = () => {
const { t } = useTranslation();
const { setTheme, theme } = useTheme()
const { open } = useModal();
const { canRedo, canUndo, undo, redo } = useDatabaseHistory();
const { deleteMultiTables } = useDatabaseOperations();
const { database } = useDatabase();
const { openController, showController, cardinalityStyle, changeCardinalityStyle } = useDiagramOps();
const menu: MenuDropdownProps[] = useMemo(() => [
const menuData: MenuItem[] = useMemo(() => [
{
id: "menu.file",
title: t("menu.file"),
@@ -53,13 +53,15 @@ const Menu: React.FC = ({ }) => {
id: "menu.export_sql",
title: t("menu.export_sql"),
clickHandler: () => {
open(Modals.EXPORT_SQL)
}
},
//{ title: t("menu.export_orm_models"), divide: true },
{
id: "menu.delete_project",
title: t("menu.delete_project"), theme: "danger", clickHandler: () => {
title: t("menu.delete_project"), theme: "destructive", clickHandler: () => {
open(Modals.DELETE_DATABASE)
}
},
@@ -93,7 +95,7 @@ const Menu: React.FC = ({ }) => {
{
id: "menu.symbolic",
selected: cardinalityStyle == CardinalityStyle.SYMBOLIC,
title: t("menu.symbolic"), clickHandler: () => {
title: t("menu.symbolic"), clickHandler: () => {
changeCardinalityStyle(CardinalityStyle.SYMBOLIC)
}
},
@@ -108,6 +110,8 @@ const Menu: React.FC = ({ }) => {
id: "menu.hidden",
selected: cardinalityStyle == CardinalityStyle.HIDDEN,
title: t("menu.hidden"), clickHandler: () => {
changeCardinalityStyle(CardinalityStyle.HIDDEN)
}
},
@@ -120,23 +124,21 @@ const Menu: React.FC = ({ }) => {
openController(!showController)
}
},
{
id: "menu.theme",
title: t("menu.theme"),
children: [
{
id: "light",
title: t("menu.light"),
clickHandler: () => setTheme("light"),
selected: resolvedTheme == "light",
selected: theme != "dark",
},
{
id: "dark",
title: t("menu.dark"),
clickHandler: () => setTheme("dark"),
selected: resolvedTheme == "dark"
selected: theme == "dark"
},
],
},
@@ -146,23 +148,25 @@ const Menu: React.FC = ({ }) => {
id: "menu.help",
title: t("menu.help"),
children: [
{ id: "menu.join_discord", title: t("menu.join_discord") ,
clickHandler : () => {
window.open("https://discord.gg/DsN8RcPR6Y", "_blank")
{
id: "menu.documentation", title: t("sidebar.documentation"),
clickHandler: () => {
window.open("https://www.stackrender.io/docs", "_blank")
}
},
},
{
id: "menu.join_discord", title: t("menu.join_discord"),
clickHandler: () => {
window.open("https://discord.gg/4dv26jR4Pj", "_blank")
}
},
],
},
], [t, canRedo, canUndo, undo, redo, deleteMultiTables, database, showController, openController ,changeCardinalityStyle , cardinalityStyle, resolvedTheme]);
return <div className="gap-1 flex">
{
menu.map((menuItem, index) => (
<DropdownMenu {...menuItem} key={menuItem.id} />
))
}
</div>
}
], [t, canRedo, canUndo, undo, redo , deleteMultiTables, database, showController, openController, changeCardinalityStyle, cardinalityStyle, theme]);
export default React.memo(Menu);
return menuData ;
}
+103
View File
@@ -0,0 +1,103 @@
import type React from "react";
import {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarShortcut,
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
} from "@/components/ui/menubar"
import { useMenuData } from "./data/menu-data";
import { MenuItem } from "../types";
import { Check } from "lucide-react"
const Menu: React.FC = ({ }) => {
const menuData = useMenuData();
return (
<Menubar className="bg-card">
{
menuData.map((menuItem: MenuItem) => (
<MenubarMenu key={menuItem.id}>
<MenubarTrigger
onClick={menuItem.clickHandler}
>
{menuItem.title}
</MenubarTrigger>
{
menuItem.children &&
<MenubarContent>
{
menuItem.children.map((subMenuItem: MenuItem) => (
<div key={subMenuItem.id}>
{
!subMenuItem.children ?
<>
<MenubarItem
variant={subMenuItem.theme}
disabled={subMenuItem.isDisabled}
onClick={subMenuItem.clickHandler}
>
{subMenuItem.title}
{
subMenuItem.shortcut &&
<MenubarShortcut>{subMenuItem.shortcut}</MenubarShortcut>
}
</MenubarItem>
{
subMenuItem.divide &&
<MenubarSeparator />
}
</>
:
<>
<MenubarSub>
<MenubarSubTrigger>{subMenuItem.title}</MenubarSubTrigger>
<MenubarSubContent>
{
subMenuItem.children.map((child: MenuItem) => (
<MenubarItem
disabled={child.isDisabled}
onClick={child.clickHandler}
key={child.id}
className="flex items-center justify-between"
>
{
child.title
}
{
child.selected &&
<Check className="h-4 w-4" />
}
</MenubarItem>
))
}
</MenubarSubContent>
</MenubarSub>
{
subMenuItem.divide &&
<MenubarSeparator />
}
</>
}
</div>
))
}
</MenubarContent>
}
</MenubarMenu>
))
}
</Menubar>
)
}
export default Menu;
+183
View File
@@ -0,0 +1,183 @@
import { type ReactNode } from 'react'
import { Link, useLocation } from "react-router-dom";
import { ChevronRight } from 'lucide-react'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
useSidebar,
} from '@/components/ui/sidebar'
import { Badge } from '../ui/badge'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '../ui/dropdown-menu'
import { NavCollapsible, NavItem, NavLink, NavGroup as NavGroupType } from './types.ts'
export function NavGroup({ title, items , onClick }: NavGroupType) {
const { state, isMobile } = useSidebar();
const location = useLocation();
const href = location.pathname
return (
<SidebarGroup>
<SidebarGroupLabel>{title}</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => {
const key = `${item.title}-${item.url}`
if (!item.items)
return <SidebarMenuLink key={key} item={item} href={href} target={item.newTab ? "_blank" : undefined} onClick={onClick}/>
if (state === 'collapsed' && !isMobile)
return (
<SidebarMenuCollapsedDropdown key={key} item={item} href={href} />
)
return <SidebarMenuCollapsible key={key} item={item} href={href} />
})}
</SidebarMenu>
</SidebarGroup>
)
}
const NavBadge = ({ children }: { children: ReactNode }) => (
<Badge className='rounded-full px-1 py-0 text-xs'>{children}</Badge>
)
const SidebarMenuLink = ({ item, href , target , onClick}: { item: NavLink; href: string; target?: string , onClick? : ()=> void }) => {
const { setOpenMobile } = useSidebar()
return (
<SidebarMenuItem>
<SidebarMenuButton
asChild
isActive={checkIsActive(href, item)}
tooltip={item.title}
onClick={onClick}
>
<Link to={item.url} onClick={() => setOpenMobile(false)} target={target}>
{item.icon && <item.icon />}
<span>{item.title}</span>
{item.badge && <NavBadge>{item.badge}</NavBadge>}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
}
const SidebarMenuCollapsible = ({
item,
href,
}: {
item: NavCollapsible
href: string
}) => {
const { setOpenMobile } = useSidebar()
return (
<Collapsible
asChild
defaultOpen={checkIsActive(href, item, true)}
className='group/collapsible'
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
{item.icon && <item.icon />}
<span>{item.title}</span>
{item.badge && <NavBadge>{item.badge}</NavBadge>}
<ChevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent className='CollapsibleContent'>
<SidebarMenuSub>
{item.items.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton
asChild
isActive={checkIsActive(href, subItem)}
>
<Link to={subItem.url} onClick={() => setOpenMobile(false)}>
{subItem.icon && <subItem.icon />}
<span>{subItem.title}</span>
{subItem.badge && <NavBadge>{subItem.badge}</NavBadge>}
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
)
}
const SidebarMenuCollapsedDropdown = ({
item,
href,
}: {
item: NavCollapsible
href: string
}) => {
return (
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
isActive={checkIsActive(href, item)}
>
{item.icon && <item.icon />}
<span>{item.title}</span>
{item.badge && <NavBadge>{item.badge}</NavBadge>}
<ChevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent side='right' align='start' sideOffset={4}>
<DropdownMenuLabel>
{item.title} {item.badge ? `(${item.badge})` : ''}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{item.items.map((sub) => (
<DropdownMenuItem key={`${sub.title}-${sub.url}`} asChild>
<Link
to={sub.url}
className={`${checkIsActive(href, sub) ? 'bg-secondary' : ''}`}
>
{sub.icon && <sub.icon />}
<span className='max-w-52 text-wrap'>{sub.title}</span>
{sub.badge && (
<span className='ml-auto text-xs'>{sub.badge}</span>
)}
</Link>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
)
}
function checkIsActive(href: string, item: NavItem, mainNav = false) {
return (
href === item.url || // /endpint?search=param
href.split('?')[0] === item.url || // endpoint
!!item?.items?.filter((i) => i.url === href).length || // if child nav is active
(mainNav &&
href.split('/')[1] !== '' &&
href.split('/')[1] === item?.url?.split('/')[1])
)
}
+51
View File
@@ -0,0 +1,51 @@
interface BaseNavItem {
title: string
badge?: string
icon?: React.ElementType
}
type NavLink = BaseNavItem & {
url: string
items?: never;
newTab?: boolean
}
type NavCollapsible = BaseNavItem & {
items: (BaseNavItem & { url: string })[]
url?: never
}
type NavItem = NavCollapsible | NavLink
interface NavGroup {
title: string
items: NavItem[];
onClick?: () => void;
}
interface SidebarData {
footerNavGroups: NavGroup[]
navGroups: NavGroup[]
}
interface MenuItem {
id: string;
title?: string;
children?: MenuItem[];
theme?: "default" | "destructive",
isDisabled?: boolean,
divide?: boolean,
shortcut?: string,
isOpen?: boolean,
selected?: boolean;
clickHandler?: () => void
}
export type { SidebarData, NavGroup, NavItem, NavCollapsible, NavLink, MenuItem }
-91
View File
@@ -1,91 +0,0 @@
import { Dropdown, DropdownMenu as HeroDropdownMenu, DropdownItem, DropdownTrigger, Button, Popover, PopoverTrigger, PopoverContent, cn } from "@heroui/react";
import { useCallback, useMemo } from "react";
import SubmenuDropdown from "./submenu-dropdown";
import { Check } from "lucide-react";
export interface MenuDropdownProps {
id: string;
title?: string;
children?: MenuDropdownProps[];
theme?: "default" | "danger",
isDisabled?: boolean,
divide?: boolean,
shortcut?: string,
isOpen?: boolean,
selected?: boolean;
clickHandler?: () => void
}
const DropdownMenu: React.FC<MenuDropdownProps> = ({ title, children, clickHandler, selected = false }) => {
const disabledChilds: string[] = useMemo(() => {
return children ? children?.filter((child: MenuDropdownProps) => child.isDisabled).map((child: MenuDropdownProps) => child.id as string) : []
}, [children]);
return <Dropdown radius="sm" shadow="sm" showArrow>
{
title &&
<DropdownTrigger onPressEnd={clickHandler}>
<Button size="sm" variant="light" className="min-w-[42px]" aria-label={title} >
<span className=" text-left flex justify-between text-small font-semibold text-font/90">
{title}
</span>
</Button>
</DropdownTrigger>
}
<HeroDropdownMenu
disabledKeys={disabledChilds}
>
{
children ? children?.map((child: MenuDropdownProps) => (
<DropdownItem
key={child.id as string}
color={child.theme}
shortcut={child.shortcut}
classNames={{
shortcut: "dark:border-font/10",
base: child.children ? "p-0" : "p-2"
}}
textValue={child.title}
endContent={
child.selected ? <Check className="text-icon size-4" /> : undefined
}
className={child.theme == "danger" ? "text-danger" : "text-font/90"}
onPressEnd={child.clickHandler}
>
{
!child.children ?
<div >
{child.title}
{
child.divide &&
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-divider "></div>
}
</div>
:
<div >
<SubmenuDropdown {...child} />
{
child.divide &&
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-divider"></div>
}
</div>
}
</DropdownItem>
)) : []
}
</HeroDropdownMenu>
</Dropdown>
}
export default DropdownMenu;
-40
View File
@@ -1,40 +0,0 @@
import { Button, Popover, PopoverContent, PopoverTrigger, useDisclosure } from "@heroui/react";
import { MenuDropdownProps } from "./menu-dropdown";
import { Check, ChevronRight } from "lucide-react";
import DropdownMenu from "./menu-dropdown";
const SubmenuDropdown: React.FC<MenuDropdownProps> = ({ id , title, children }) => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<div onMouseEnter={onOpen} onMouseLeave={onClose} className="p-2">
<Popover placement="right" isOpen={isOpen} radius="sm" shadow="sm" >
<PopoverTrigger >
<Button className="w-full h-6 bg-transparent p-0 text-font/90 " size="sm" value={"light"}>
<span className="w-full text-left flex justify-between text-small font-normal">
{title}
<ChevronRight className="size-4 text-icon" />
</span>
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 border-0 shadow-none block w-[200px] ">
<div className="h-8">
<DropdownMenu
id = { id }
children={children}
/>
</div>
</PopoverContent>
</Popover>
</div>
)
};
export default SubmenuDropdown;
+206
View File
@@ -0,0 +1,206 @@
import React, { ReactNode, useState } from "react";
import { useTranslation } from "react-i18next";
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
import { Button } from "./ui/button";
import { Spinner } from "./ui/shadcn-io/spinner";
export interface ModalProps {
isOpen?: boolean,
onOpenChange?: (open: boolean) => void,
className?: string,
backdrop?: "blur" | "transparent" | "opaque",
title: string,
children: ReactNode,
actionName?: string,
actionHandler?: () => void,
isDisabled?: boolean,
description?: string,
variant?: "default" | "danger",
closable?: boolean,
footer?: boolean
}
const Modal: React.FC<ModalProps> = ({
isOpen,
onOpenChange,
className,
title,
description,
children,
actionName = "Action",
actionHandler,
isDisabled,
closable = true,
footer = true,
variant = "default"
}) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const { t } = useTranslation();
const handleAction = async () => {
setIsLoading(true)
try {
actionHandler && await actionHandler();
} catch (error) {
setIsLoading(false);
} finally {
setIsLoading(false);
}
}
return (
<Dialog open={isOpen} onOpenChange={ closable ? onOpenChange : () => {}}>
<DialogContent className={className} showCloseButton={closable}>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
{
description &&
<DialogDescription>
{description}
</DialogDescription>
}
</DialogHeader>
{children}
{
footer &&
<DialogFooter >
<div className="flex justify-between w-full flex-col gap-4 sm:flex-row">
{
variant == "default" &&
<>
{
closable &&
<DialogClose asChild >
<Button variant="outline">{t("modals.close")}</Button>
</DialogClose>
}
{
!closable && <div></div>
}
{
actionHandler &&
<Button type="submit" onClick={handleAction} disabled={isDisabled || isLoading} >
{actionName}
{isLoading && <Spinner />}
</Button>
}
</>
}
{
variant == "danger" &&
<>
{
closable &&
<DialogClose asChild >
<Button variant="outline">{t("modals.close")}</Button>
</DialogClose>
}
{
actionHandler &&
<Button type="submit" variant="destructive" onClick={handleAction} disabled={isDisabled || isLoading} >
{actionName}
</Button>
}
</>
}
</div>
</DialogFooter>
}
</DialogContent>
</Dialog>
/*
<HeroUiModal
ref={targetRef}
isOpen={isOpen}
onOpenChange={onOpenChange && closable ? onOpenChange : undefined}
className={className}
backdrop={backdrop}
radius="sm"
classNames={{
base: "dark:bg-background-100",
closeButton: cn("dark:bg-background-100 dark:hover:bg-background rounded-md", !closable ? "hidden" : "")
}}
>
<ModalContent>
{(onClose) => (
<>
<ModalHeader {...moveProps} className=" flex flex-col gap-1" >
{title}
{
header &&
<p className="text-sm text-font/70 block">
{header}
</p>
}
</ModalHeader>
<ModalBody>
{children}
</ModalBody>
{footer &&
<ModalFooter>
<div className="flex w-full justify-between">
{
variant == "default" &&
<>
{
closable &&
<Button color="danger" variant="light" onPress={onClose} size="sm">
{t("modals.close")}
</Button>
}
{
!closable && <div></div>
}
{
actionHandler &&
<Button color="primary" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
{actionName}
</Button>
}
</>
}
{
variant == "danger" &&
<>
{
closable &&
<Button color="default" variant="light" onPress={onClose} size="sm">
{t("modals.close")}
</Button>
}
{
!closable && <div></div>
}
{
actionHandler &&
<Button color="danger" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
{actionName}
</Button>
}
</>
}
</div>
</ModalFooter>
}
</>
)}
</ModalContent>
</HeroUiModal>
*/
)
}
export default Modal;
-22
View File
@@ -1,22 +0,0 @@
import { Spinner } from "@heroui/react";
interface LoadingProps {
text?: string;
}
export const Loading: React.FC<LoadingProps> = ({ text }) => {
return (
<div className="fixed bg-overlay/50 backdrop-opacity-disabled w-screen h-screen fixed flex items-center justify-center z-[99] left-0 top-0 text-white flex-col gap-2">
<Spinner size="lg" />
<span className="font-medium">
Loading ...
</span>
</div>
)
}
-140
View File
@@ -1,140 +0,0 @@
import React, { ReactNode, useState } from "react";
import {
Modal as HeroUiModal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Button,
useDraggable,
cn,
} from "@heroui/react";
import { useTranslation } from "react-i18next";
export interface ModalProps {
isOpen?: boolean,
onOpenChange?: (open: boolean) => void,
className?: string,
backdrop?: "blur" | "transparent" | "opaque",
title: string,
children: ReactNode,
actionName?: string,
actionHandler?: () => void,
isDisabled?: boolean,
header?: string,
variant?: "default" | "danger",
closable?: boolean
}
const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop = "opaque", title, children, actionName = "Action", header, actionHandler, isDisabled, variant = "default", closable = true }) => {
const targetRef = React.useRef(null);
const { moveProps } = useDraggable({ targetRef, canOverflow: true, isDisabled: !isOpen });
const [isLoading, setIsLoading] = useState<boolean>(false);
const { t } = useTranslation();
const handleAction = async (onClose: () => void) => {
setIsLoading(true)
try {
actionHandler && await actionHandler();
} catch (error) {
setIsLoading(false);
} finally {
setIsLoading(false);
}
}
return (
<HeroUiModal
ref={targetRef}
isOpen={isOpen}
onOpenChange={onOpenChange && closable ? onOpenChange : undefined}
className={className}
backdrop={backdrop}
radius="sm"
classNames={{
base: "dark:bg-background-100",
closeButton: cn("dark:bg-background-100 dark:hover:bg-background rounded-md", !closable ? "hidden" : "")
}}
>
<ModalContent>
{(onClose) => (
<>
<ModalHeader {...moveProps} className=" flex flex-col gap-1" >
{title}
{
header &&
<p className="text-sm text-font/70 block">
{header}
</p>
}
</ModalHeader>
<ModalBody>
{children}
</ModalBody>
<ModalFooter>
<div className="flex w-full justify-between">
{
variant == "default" &&
<>
{
closable &&
<Button color="danger" variant="light" onPress={onClose} size="sm">
{t("modals.close")}
</Button>
}
{
!closable && <div></div>
}
{
actionHandler &&
<Button color="primary" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
{actionName}
</Button>
}
</>
}
{
variant == "danger" &&
<>
{
closable &&
<Button color="default" variant="light" onPress={onClose} size="sm">
{t("modals.close")}
</Button>
}
{
!closable && <div></div>
}
{
actionHandler &&
<Button color="danger" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
{actionName}
</Button>
}
</>
}
</div>
</ModalFooter>
</>
)}
</ModalContent>
</HeroUiModal>
)
}
export default Modal;
File diff suppressed because it is too large Load Diff
@@ -1,108 +0,0 @@
import { languages } from "@/i18";
import { Language } from "@/i18/types";
import { Button, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger } from "@heroui/react";
import { Check, Languages, Search } from "lucide-react";
import React, { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
const LanguagesDropDonw: React.FC = ({ }) => {
const { t, i18n } = useTranslation();
const [currentLanguage, setCurrentLanguage] = useState<Language>(() => languages.find((language: Language) => language.code == i18n.language) as Language);
const [languageList, setLanguageList] = useState<Language[]>(languages);
const searchRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const changeLanguage = useCallback((language: Language) => {
i18n.changeLanguage(language.code);
}, [i18n])
useEffect(() => {
setCurrentLanguage(languages.find((language: Language) => language.code == i18n.language) as Language)
}, [i18n.language])
const searchLanguage = () => {
if (!searchRef.current?.value || searchRef.current?.value.trim().length == 0) {
setLanguageList(languages);
}else
setLanguageList(languages.filter((language: Language) =>
language.code.toLowerCase().includes(searchRef.current?.value.toLowerCase() as string) ||
language.name.toLowerCase().includes(searchRef.current?.value.toLowerCase() as string) ||
language.nativeName.toLowerCase().includes(searchRef.current?.value.toLowerCase() as string)
))
console.log(searchRef.current?.value);
}
const openChange = useCallback((isOpen : boolean) => {
if (isOpen)
setLanguageList(languages)
} , [])
return (
<Popover showArrow placement="bottom" radius="sm" shadow="sm" onOpenChange={openChange}>
<PopoverTrigger>
<Button
variant="bordered"
radius="sm"
startContent={
<Languages className="size-4 text-font/90 dark:text-white" />
}
className="p-0 h-9 min-w-[142px] border-1 border-divider dark:border-font/10 text-xs bg-transparent hover:bg-default text-font/90 font-semibold "
>
<span>
{currentLanguage.nativeName} <span className="text-font/70">({currentLanguage.name})</span>
</span>
</Button>
</PopoverTrigger>
<PopoverContent className="p-2">
<Input
variant="flat"
size="sm"
aria-label={t("navbar.search")}
placeholder={`${t("navbar.search")}`}
ref={searchRef}
onKeyUp={searchLanguage}
startContent={
<Search className="size-4 text-icon" />
}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary group-data-[focus=true]:border-primary group-data-[focus=true]:bg-default",
}}
/>
<hr className="my-2 w-full border-divider" />
<Listbox aria-label="Languages"
>
{
languageList.map((language: Language) => (
<ListboxItem
key={language.code}
textValue={language.code}
endContent={
currentLanguage.code == language.code && <Check className="text-icon size-4" />
}
onPressEnd={() => changeLanguage(language)}
className="text-font/90"
>
{language.nativeName} <span className="text-font/70"> ({language.name})</span>
</ListboxItem>
))
}
</Listbox>
</PopoverContent>
</Popover>
)
}
export default React.memo(LanguagesDropDonw);
-44
View File
@@ -1,44 +0,0 @@
import { useDatabase } from "@/providers/database-provider/database-provider";
import Menu from "../menu/menu";
import RenameDatabase from "./rename-database";
import LanguagesDropdown from "./languages-dropdown";
const Navbar: React.FC = ({ }) => {
const { database } = useDatabase();
return (
<nav className="h-12 fixed z-50 bg-background w-full flex items-center p-4 border-b border-divider ">
<img
src={`/stackrender.png`}
width={22}
alt="logo"
/>
<h3 className="font-semibold ml-2 text-slate-900 text-sm dark:text-white">StackRender</h3>
<div className="ml-4 w-full">
<div className="flex w-full justify-between items-center gap-2">
<Menu />
<div className=" absolute left-[50%] -translate-x-[50%]">
{database && <RenameDatabase database={database} />}
</div>
<div className="flex gap-2 items-center">
<a className="gh-button text-font font-medium bg-background text-xs p-4 px-2 pr-2.5 flex justif-center items-center"
target="_blank"
href="https://github.com/stackrender/stackrender">
<span className="gh-button__icon"></span>
</a>
<LanguagesDropdown />
</div>
</div>
</div>
</nav>
)
}
export default Navbar;
+33
View File
@@ -0,0 +1,33 @@
import { IconSearch } from '@tabler/icons-react'
import { cn } from '@/lib/utils'
import { useSearch } from '@/providers/search-provider/search-provider'
import { Button } from './ui/button'
interface Props {
className?: string
type?: React.HTMLInputTypeAttribute
placeholder?: string
}
export function Search({ className = '', placeholder = 'Search' }: Props) {
const { setOpen } = useSearch()
return (
<Button
variant='outline'
className={cn(
'bg-muted/25 text-muted-foreground hover:bg-muted/50 relative h-8 w-full flex-1 justify-start rounded-md text-sm font-normal shadow-none sm:pr-12 md:w-40 md:flex-none lg:w-56 xl:w-64',
className
)}
onClick={() => setOpen(true)}
>
<IconSearch
aria-hidden='true'
className='absolute top-1/2 left-1.5 -translate-y-1/2'
/>
<span className='ml-3'>{placeholder}</span>
<kbd className='bg-muted pointer-events-none absolute top-[0.3rem] right-[0.3rem] hidden h-5 items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium opacity-100 select-none sm:flex'>
<span className='text-xs'></span>K
</kbd>
</Button>
)
}
-65
View File
@@ -1,65 +0,0 @@
import { Link } from "react-router-dom";
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip";
import { Divider } from "@heroui/react";
import React from "react";
export interface SidebarItemProps {
type?: "item" | "divider"
title?: string;
icon?: React.ReactNode;
href?: string;
isActive?: boolean,
newTab?: boolean
}
const SidebarItem: React.FC<SidebarItemProps> = ({ title, icon, href, type = "item", isActive, newTab }) => {
if (type == "item")
return (
<Tooltip delayDuration={0} >
{
!newTab &&
<TooltipTrigger asChild>
<Link
to={href as string}
target=""
className="p-2 rounded-md hover:bg-default-100 dark:hover:bg-background-50 sidebar-item " data-active={isActive}>
{icon}
</Link>
</TooltipTrigger>
}
{
newTab &&
<TooltipTrigger asChild>
<a
href={href as string}
target="_blank"
className="p-2 rounded-md hover:bg-default-100 dark:hover:bg-background-50 sidebar-item " data-active={isActive}>
{icon}
</a>
</TooltipTrigger>
}
<TooltipContent
side="left"
align="center"
>
{title}
</TooltipContent>
</Tooltip >
)
else {
return <Divider className="bg-default-200 dark:bg-divider" />
}
}
export default SidebarItem
-84
View File
@@ -1,84 +0,0 @@
import { Github, TableProperties, Workflow } from "lucide-react";
import SidebarItem, { SidebarItemProps } from "./sidebar-item";
import { useMemo } from "react";
import { useLocation } from "react-router-dom";
import { useTranslation } from 'react-i18next';
import { Discord } from "../icon/discord";
import { X } from "../icon/x";
interface SidebarProps {
children?: React.ReactNode
}
const sidebarItemClass: string = "text-font size-4 data-[active=true]:text-primary-900";
const Sidebar: React.FC<SidebarProps> = ({ }) => {
const { t } = useTranslation();
const location = useLocation();
const sidebarItems: SidebarItemProps[] = useMemo(() => [
{
title: t("sidebar.tables"),
icon: <TableProperties className={sidebarItemClass}></TableProperties>,
href: "/database/tables",
isActive: location.pathname.endsWith("/database/tables")
},
{
title: t("sidebar.relationships"),
icon: <Workflow className={sidebarItemClass}></Workflow>,
href: "/database/relationships",
isActive: location.pathname.endsWith("/database/relationships")
},
], [location , t])
const bottomSidebarItems: SidebarItemProps[] = useMemo(() => [
{
title: "X",
icon: <X className={sidebarItemClass}></X>,
href: "https://x.com/Iam_The_Dev",
newTab : true ,
},
{
title: "Discord",
icon: <Discord className={sidebarItemClass}></Discord>,
href: "https://discord.gg/DsN8RcPR6Y",
newTab : true ,
},
{
title: "Github",
icon: <Github className={sidebarItemClass}></Github>,
href: "https://github.com/stackrender/stackrender",
newTab : true ,
},
], []);
return (
<aside className="h-full z-[20] flex flex-col items-between py-2 justify-between sticky top-0 duration-500 w-12 bg-sidebar pt-[56px] dark:bg-background">
<div className="flex flex-col items-center gap-2">
{
sidebarItems.map((item: SidebarItemProps, index: number) => (
<SidebarItem
key={`top-${index}`}
{...item}
/>
))
}
</div>
<div className="flex flex-col items-center gap-2 ">
{
bottomSidebarItems.map((item: SidebarItemProps, index: number) => (
<SidebarItem
key={`bottom-${index}`}
{...item}
/>
))
}
</div>
</aside>
)
}
export default Sidebar;
-68
View File
@@ -1,68 +0,0 @@
import React, { useEffect, useState } from "react"
import { useTranslation } from "react-i18next";
import { WithContext as ReactTagInput, Tag } from 'react-tag-input';
interface TagInputProps {
defaultItems?: string[];
onItemsChange? : (items: string[]) => void
}
const KeyCodes = {
comma: 188,
enter: 13,
};
const delimiters = [KeyCodes.comma, KeyCodes.enter];
const TagInput: React.FC<TagInputProps> = ({ defaultItems = [], onItemsChange }) => {
const { t } = useTranslation();
const [tags, setTags] = useState<Tag[]>(defaultItems.map((item: string) => ({
id: item.toLowerCase(),
text: item,
className: ""
}) as Tag));
const handleDelete = (i: number) => {
setTags(tags.filter((_, index) => index !== i));
};
const handleAddition = (tag: Tag) => {
setTags([...tags, tag]);
};
const handleDrag = (tag: Tag, currPos: number, newPos: number) => {
const newTags = tags.slice();
newTags.splice(currPos, 1);
newTags.splice(newPos, 0, tag);
setTags(newTags);
};
useEffect(() => onItemsChange && onItemsChange(tags.map((tag: Tag) => tag.text)), [tags])
return (
<div>
<ReactTagInput
tags={tags}
classNames={{
tag: "font-normal inline-block m-0.5 border-1 border-default dark:border-divider rounded-full px-2 py-1 flex-row ",
remove: " bg-default-900 dark:bg-default-500 rounded-full text-xs text-center ml-1 min-w-[15px] max-w-[15px] min-h-[15px] max-h-[15px] transition-colors duration-300 hover:bg-black",
tagInputField: "relative w-full inline-flex flex-row items-center bg-default-100 border-1 border-divider hover:border-primary focus-within:border-primary h-8 min-h-8 px-2 rounded-small transition-background !duration-150 transition-colors outline-none dark:bg-default placeholder:text-foreground-500 mt-2" ,
tags : "max-h-[256px] overflow-auto "
}}
handleDelete={handleDelete}
autoFocus={false}
handleAddition={handleAddition}
handleDrag={handleDrag}
delimiters={delimiters}
placeholder={t("db_controller.field_settings.type_enter")}
/>
</div>
);
}
export default React.memo(TagInput)
+60
View File
@@ -0,0 +1,60 @@
import { useEffect } from 'react'
import { IconCheck, IconMoon, IconSun } from '@tabler/icons-react'
import { cn } from '@/lib/utils'
import { useTheme } from '@/providers/theme-provider/theme-provider'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useTranslation } from 'react-i18next'
export function ThemeSwitch() {
const { theme, setTheme } = useTheme() ;
const { t } = useTranslation() ;
/* Update theme-color meta tag
* when theme is updated */
useEffect(() => {
const themeColor = theme === 'dark' ? '#020817' : '#fff'
const metaThemeColor = document.querySelector("meta[name='theme-color']")
if (metaThemeColor) metaThemeColor.setAttribute('content', themeColor)
}, [theme])
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button variant='ghost' size='icon' className='scale-95 rounded-full'>
<IconSun className='size-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90' />
<IconMoon className='absolute size-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0' />
<span className='sr-only'>Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => setTheme('light')}>
{t("menu.light")}{' '}
<IconCheck
size={14}
className={cn('ml-auto', theme !== 'light' && 'hidden')}
/>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
{t("menu.dark")}
<IconCheck
size={14}
className={cn('ml-auto', theme !== 'dark' && 'hidden')}
/>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
{t("menu.system")}
<IconCheck
size={14}
className={cn('ml-auto', theme !== 'system' && 'hidden')}
/>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
-46
View File
@@ -1,46 +0,0 @@
import { cn } from "@heroui/react"
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip"
interface ToggleProps {
active?: boolean,
children: React.ReactNode,
onToggle?: (value: boolean) => void,
className?: string ,
label? : string
}
const ToggleButton: React.FC<ToggleProps> = ({ active = false, children, onToggle, className , label }) => {
const toggle = () => {
onToggle && onToggle(!active)
}
return (
<Tooltip>
<TooltipTrigger asChild>
<button className={
cn("p-1 px-2 transition-all hover:bg-default hover:text-font/90 rounded duration-200 ",
className ? className : "",
active ?
"text-font/90 bg-default dark:text-font/90 white dark:bg-default/40" :
"text-icon"
)
}
onClick={toggle}
>
<span className="text-sm">
{children}
</span>
</button>
</TooltipTrigger>
<TooltipContent>
{label}
</TooltipContent>
</Tooltip>
)
}
export default ToggleButton
-30
View File
@@ -1,30 +0,0 @@
import React from 'react';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { cn } from '@heroui/react';
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 shadow-lg overflow-hidden rounded-md bg-foreground px-3 py-2 text-xs text-default animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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',
className
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+90
View File
@@ -0,0 +1,90 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
)
}
type AccordionTriggerProps = React.ComponentProps<typeof AccordionPrimitive.Trigger> & {
position?: "left" | "right" ;
leftContent? : React.ReactNode
}
function AccordionTrigger({
className,
children,
position = "left",
leftContent ,
...props
}: AccordionTriggerProps) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-center gap-2 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{
leftContent
}
{
position === "left" &&
<>
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
{children}
</>
}
{
position == "right" &&
<>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</>
}
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
+66
View File
@@ -0,0 +1,66 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className
)}
{...props}
/>
)
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription }
+51
View File
@@ -0,0 +1,51 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }
+46
View File
@@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }
+59
View File
@@ -0,0 +1,59 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Button = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
asChild?: boolean
}
>(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref} // ✅ now Button can accept refs
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
})
Button.displayName = "Button"
export { Button, buttonVariants }
+211
View File
@@ -0,0 +1,211 @@
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months
),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_next
),
month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption
),
dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root
),
dropdown: cn(
"absolute bg-popover inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday
),
week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn(
"select-none w-(--cell-size)",
defaultClassNames.week_number_header
),
week_number: cn(
"text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number
),
day: cn(
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
defaultClassNames.day
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }
+92
View File
@@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
+31
View File
@@ -0,0 +1,31 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
+184
View File
@@ -0,0 +1,184 @@
"use client"
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...props}
/>
)
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn("overflow-hidden p-0", className)}
showCloseButton={showCloseButton}
>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
)
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props}
/>
)
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
)
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}
+142
View File
@@ -0,0 +1,142 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
))
DialogOverlay.displayName = "DialogOverlay"
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}
+133
View File
@@ -0,0 +1,133 @@
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
)
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn(
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
className
)}
{...props}
/>
)
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}
+258
View File
@@ -0,0 +1,258 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName = "DropdownMenuSubContent"
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}
+26
View File
@@ -0,0 +1,26 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<
HTMLInputElement,
React.ComponentProps<"input">
>(({ className, type, ...props }, ref) => {
return (
<input
ref={ref} // ✅ allow refs
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
})
Input.displayName = "Input"
export { Input }
+26
View File
@@ -0,0 +1,26 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-foreground/75",
className
)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
+274
View File
@@ -0,0 +1,274 @@
import * as React from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Menubar({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
return (
<MenubarPrimitive.Root
data-slot="menubar"
className={cn(
"bg-background flex h-9 items-center gap-1 rounded-md p-1 ",
className
)}
{...props}
/>
)
}
function MenubarMenu({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />
}
function MenubarGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />
}
function MenubarPortal({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />
}
function MenubarRadioGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return (
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
)
}
function MenubarTrigger({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
return (
<MenubarPrimitive.Trigger
data-slot="menubar-trigger"
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
className
)}
{...props}
/>
)
}
function MenubarContent({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
return (
<MenubarPortal>
<MenubarPrimitive.Content
data-slot="menubar-content"
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in 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 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</MenubarPortal>
)
}
function MenubarItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<MenubarPrimitive.Item
data-slot="menubar-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function MenubarCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
return (
<MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
)
}
function MenubarRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
return (
<MenubarPrimitive.RadioItem
data-slot="menubar-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
)
}
function MenubarLabel({
className,
inset,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean
}) {
return (
<MenubarPrimitive.Label
data-slot="menubar-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function MenubarSeparator({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
return (
<MenubarPrimitive.Separator
data-slot="menubar-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function MenubarShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="menubar-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function MenubarSub({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
}
function MenubarSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<MenubarPrimitive.SubTrigger
data-slot="menubar-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
)
}
function MenubarSubContent({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
return (
<MenubarPrimitive.SubContent
data-slot="menubar-sub-content"
className={cn(
"bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
Menubar,
MenubarPortal,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarGroup,
MenubarSeparator,
MenubarLabel,
MenubarItem,
MenubarShortcut,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
}
+46
View File
@@ -0,0 +1,46 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground 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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
+43
View File
@@ -0,0 +1,43 @@
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
)
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
}
export { RadioGroup, RadioGroupItem }
+56
View File
@@ -0,0 +1,56 @@
"use client"
import * as React from "react"
import { GripVerticalIcon } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
}
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
+56
View File
@@ -0,0 +1,56 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }
+186
View File
@@ -0,0 +1,186 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon, X } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild >
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground 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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}
+26
View File
@@ -0,0 +1,26 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}
export { Separator }
@@ -0,0 +1,272 @@
import {
LoaderCircleIcon,
LoaderIcon,
LoaderPinwheelIcon,
type LucideProps,
} from 'lucide-react';
import { cn } from '@/lib/utils';
type SpinnerVariantProps = Omit<SpinnerProps, 'variant'>;
const Default = ({ className, ...props }: SpinnerVariantProps) => (
<LoaderIcon className={cn('animate-spin', className)} {...props} />
);
const Circle = ({ className, ...props }: SpinnerVariantProps) => (
<LoaderCircleIcon className={cn('animate-spin', className)} {...props} />
);
const Pinwheel = ({ className, ...props }: SpinnerVariantProps) => (
<LoaderPinwheelIcon className={cn('animate-spin', className)} {...props} />
);
const CircleFilled = ({
className,
size = 24,
...props
}: SpinnerVariantProps) => (
<div className="relative" style={{ width: size, height: size }}>
<div className="absolute inset-0 rotate-180">
<LoaderCircleIcon
className={cn('animate-spin', className, 'text-foreground opacity-20')}
size={size}
{...props}
/>
</div>
<LoaderCircleIcon
className={cn('relative animate-spin', className)}
size={size}
{...props}
/>
</div>
);
const Ellipsis = ({ size = 24, ...props }: SpinnerVariantProps) => {
return (
<svg
height={size}
viewBox="0 0 24 24"
width={size}
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>Loading...</title>
<circle cx="4" cy="12" fill="currentColor" r="2">
<animate
attributeName="cy"
begin="0;ellipsis3.end+0.25s"
calcMode="spline"
dur="0.6s"
id="ellipsis1"
keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/>
</circle>
<circle cx="12" cy="12" fill="currentColor" r="2">
<animate
attributeName="cy"
begin="ellipsis1.begin+0.1s"
calcMode="spline"
dur="0.6s"
keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/>
</circle>
<circle cx="20" cy="12" fill="currentColor" r="2">
<animate
attributeName="cy"
begin="ellipsis1.begin+0.2s"
calcMode="spline"
dur="0.6s"
id="ellipsis3"
keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/>
</circle>
</svg>
);
};
const Ring = ({ size = 24, ...props }: SpinnerVariantProps) => (
<svg
height={size}
stroke="currentColor"
viewBox="0 0 44 44"
width={size}
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>Loading...</title>
<g fill="none" fillRule="evenodd" strokeWidth="2">
<circle cx="22" cy="22" r="1">
<animate
attributeName="r"
begin="0s"
calcMode="spline"
dur="1.8s"
keySplines="0.165, 0.84, 0.44, 1"
keyTimes="0; 1"
repeatCount="indefinite"
values="1; 20"
/>
<animate
attributeName="stroke-opacity"
begin="0s"
calcMode="spline"
dur="1.8s"
keySplines="0.3, 0.61, 0.355, 1"
keyTimes="0; 1"
repeatCount="indefinite"
values="1; 0"
/>
</circle>
<circle cx="22" cy="22" r="1">
<animate
attributeName="r"
begin="-0.9s"
calcMode="spline"
dur="1.8s"
keySplines="0.165, 0.84, 0.44, 1"
keyTimes="0; 1"
repeatCount="indefinite"
values="1; 20"
/>
<animate
attributeName="stroke-opacity"
begin="-0.9s"
calcMode="spline"
dur="1.8s"
keySplines="0.3, 0.61, 0.355, 1"
keyTimes="0; 1"
repeatCount="indefinite"
values="1; 0"
/>
</circle>
</g>
</svg>
);
const Bars = ({ size = 24, ...props }: SpinnerVariantProps) => (
<svg
height={size}
viewBox="0 0 24 24"
width={size}
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>Loading...</title>
<style>{`
.spinner-bar {
animation: spinner-bars-animation .8s linear infinite;
animation-delay: -.8s;
}
.spinner-bars-2 {
animation-delay: -.65s;
}
.spinner-bars-3 {
animation-delay: -0.5s;
}
@keyframes spinner-bars-animation {
0% {
y: 1px;
height: 22px;
}
93.75% {
y: 5px;
height: 14px;
opacity: 0.2;
}
}
`}</style>
<rect
className="spinner-bar"
fill="currentColor"
height="22"
width="6"
x="1"
y="1"
/>
<rect
className="spinner-bar spinner-bars-2"
fill="currentColor"
height="22"
width="6"
x="9"
y="1"
/>
<rect
className="spinner-bar spinner-bars-3"
fill="currentColor"
height="22"
width="6"
x="17"
y="1"
/>
</svg>
);
const Infinite = ({ size = 24, ...props }: SpinnerVariantProps) => (
<svg
height={size}
preserveAspectRatio="xMidYMid"
viewBox="0 0 100 100"
width={size}
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>Loading...</title>
<path
d="M24.3 30C11.4 30 5 43.3 5 50s6.4 20 19.3 20c19.3 0 32.1-40 51.4-40 C88.6 30 95 43.3 95 50s-6.4 20-19.3 20C56.4 70 43.6 30 24.3 30z"
fill="none"
stroke="currentColor"
strokeDasharray="205.271142578125 51.317785644531256"
strokeLinecap="round"
strokeWidth="10"
style={{
transform: 'scale(0.8)',
transformOrigin: '50px 50px',
}}
>
<animate
attributeName="stroke-dashoffset"
dur="2s"
keyTimes="0;1"
repeatCount="indefinite"
values="0;256.58892822265625"
/>
</path>
</svg>
);
export type SpinnerProps = LucideProps & {
variant?:
| 'default'
| 'circle'
| 'pinwheel'
| 'circle-filled'
| 'ellipsis'
| 'ring'
| 'bars'
| 'infinite';
};
export const Spinner = ({ variant, ...props }: SpinnerProps) => {
switch (variant) {
case 'circle':
return <Circle {...props} />;
case 'pinwheel':
return <Pinwheel {...props} />;
case 'circle-filled':
return <CircleFilled {...props} />;
case 'ellipsis':
return <Ellipsis {...props} />;
case 'ring':
return <Ring {...props} />;
case 'bars':
return <Bars {...props} />;
case 'infinite':
return <Infinite {...props} />;
default:
return <Default {...props} />;
}
};
+139
View File
@@ -0,0 +1,139 @@
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
+740
View File
@@ -0,0 +1,740 @@
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, VariantProps } from "class-variance-authority"
import { PanelLeftIcon } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContextProps = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}) {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
_setOpen(openState)
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
)
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
/*
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
*/
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
return (
<SidebarContext.Provider value={contextValue}>
<div
data-slot="sidebar-wrapper"
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className
)}
{...props}
>
{children}
</div>
</SidebarContext.Provider>
)
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
}: React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
return (
<div
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className
)}
{...props}
>
{children}
</div>
)
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
}
return (
<div
className="group peer text-sidebar-foreground hidden md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
data-slot="sidebar"
>
{/* This is what handles the sidebar gap on desktop */}
<div
data-slot="sidebar-gap"
className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
)}
/>
<div
data-slot="sidebar-container"
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...props}
>
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
>
{children}
</div>
</div>
</div>
)
}
function SidebarTrigger({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar()
return (
<Button
data-sidebar="trigger"
data-slot="sidebar-trigger"
variant="ghost"
size="icon"
className={cn("size-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}
>
<PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar()
return (
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
)
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return (
<main
data-slot="sidebar-inset"
className={cn(
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className
)}
{...props}
/>
)
}
function SidebarInput({
className,
...props
}: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn("bg-background h-8 w-full shadow-none", className)}
{...props}
/>
)
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-header"
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-footer"
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
}
function SidebarSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props}
/>
)
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props}
/>
)
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group"
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
}
function SidebarGroupLabel({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div"
return (
<Comp
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
{...props}
/>
)
}
function SidebarGroupAction({
className,
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="sidebar-group-action"
data-sidebar="group-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
)
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
)
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
)
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>
>(
(
{
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
},
ref
) => {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
<Comp
ref={ref}
data-slot="sidebar-menu-button"
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
)
if (!tooltip) {
return button
}
if (typeof tooltip === "string") {
tooltip = { children: tooltip }
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
)
}
)
SidebarMenuButton.displayName = "SidebarMenuButton"
function SidebarMenuAction({
className,
asChild = false,
showOnHover = false,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className
)}
{...props}
/>
)
}
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
showIcon?: boolean
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
return (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
)
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
)
}
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
}
>(({ asChild = false, size = "md", isActive = false, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
})
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}
+13
View File
@@ -0,0 +1,13 @@
import { cn } from "@/lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
)
}
export { Skeleton }
+25
View File
@@ -0,0 +1,25 @@
import { useTheme } from "@/providers/theme-provider/theme-provider"
import { Toaster as Sonner, ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }
+29
View File
@@ -0,0 +1,29 @@
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }
+114
View File
@@ -0,0 +1,114 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
+23
View File
@@ -0,0 +1,23 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
ref={ref}
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }
+73
View File
@@ -0,0 +1,73 @@
"use client"
import * as React from "react"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import { type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
})
function ToggleGroup({
className,
variant,
size,
children,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
className={cn(
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
className
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
)
}
function ToggleGroupItem({
className,
children,
variant,
size,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
}
export { ToggleGroup, ToggleGroupItem }
+45
View File
@@ -0,0 +1,45 @@
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Toggle({
className,
variant,
size,
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Toggle, toggleVariants }
+59
View File
@@ -0,0 +1,59 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className={cn( "bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" , className) } />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
+19
View File
@@ -0,0 +1,19 @@
/**
* List of available font names (visit the url `/settings/appearance`).
* This array is used to generate dynamic font classes (e.g., `font-inter`, `font-manrope`).
*
* 📝 How to Add a New Font (Tailwind v4+):
* 1. Add the font name here.
* 2. Update the `<link>` tag in 'index.html' to include the new font from Google Fonts (or any other source).
* 3. Add the new font family to 'index.css' using the `@theme inline` and `font-family` CSS variable.
*
* Example:
* fonts.ts → Add 'roboto' to this array.
* index.html → Add Google Fonts link for Roboto.
* index.css → Add the new font in the CSS, e.g.:
* @theme inline {
* // ... other font families
* --font-roboto: 'Roboto', var(--font-sans);
* }
*/
export const fonts = ['inter', 'manrope', 'system'] as const
+42
View File
@@ -0,0 +1,42 @@
import { Outlet } from "react-router-dom";
import { SidebarProvider } from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";
import { AppSidebar } from "@/components/layout/app-sidebar/app-sidebar";
import { Header } from "@/components/header";
interface Props {
children?: React.ReactNode
}
const Dashboard: React.FC<Props> = ({ children }) => {
return (
<SidebarProvider defaultOpen={false}>
<AppSidebar />
<div
id='content'
className={cn(
'ml-auto w-full max-w-full',
'peer-data-[state=collapsed]:w-[calc(100%-var(--sidebar-width-icon)-1rem)]',
'peer-data-[state=expanded]:w-[calc(100%-var(--sidebar-width))]',
'sm:transition-[width] sm:duration-200 sm:ease-linear',
'flex h-svh flex-col',
'group-data-[scroll-locked=1]/body:h-full',
'has-[main.fixed-main]:group-data-[scroll-locked=1]/body:h-svh'
)}
>
<Header className="bg-card border-b pl-3"/>
{
children ? children : <Outlet />
}
</div>
</SidebarProvider>
)
}
export default Dashboard;
@@ -1,5 +1,6 @@
import { CardinalityStyle } from "@/lib/database";
import { cn } from "@heroui/react";
import { cn } from "@/lib/utils";
import React from "react";
export interface CardinalityMarkerProps {
@@ -16,15 +17,12 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
const id = `${cardinality}_${direction}${selected ? "_selected" : ""}`;
const renderMarker = () => {
if (cardinality == "many") {
if (direction == "start")
return (<path d="M 0 50 L 100 50 M 100 50 L 0 0 M 100 50 L 0 100 " />)
else if (direction == "end")
return (<path d="M 100 50 L 0 50 M 0 50 L 100 0 M 0 50 L 100 100" />)
}
if (cardinality == "one") {
if (direction == "start") {
return (<path d="M 0 50 L 100 50 M 50 50 M 50 50 M 75 25 L 75 75" />)
@@ -33,8 +31,6 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
return (<path d="M 100 50 L 0 50 M 50 50 M 50 50 M 25 25 L 25 75" />)
}
}
}
@@ -52,7 +48,7 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
>
<svg
fill="transparent"
className={selected ? "stroke-primary" : "stroke-default-600 dark:stroke-default-400"}
className={selected ? "stroke-ring dark:!stroke-primary-foreground" : "stroke-ring/60 dark:!stroke-muted-foreground/60"}
strokeWidth="4"
width="24"
height="24"
@@ -81,8 +77,9 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
r="8"
strokeWidth="1"
className={
cn("dark:fill-default fill-default",
selected ? " stroke-primary fill-background dark:fill-primary-900" : " stroke-default-600 dark:stroke-default-400"
cn(" fill-background",
selected ? " stroke-ring fill-background dark:!stroke-primary-foreground" :
" stroke-ring/60 dark:!stroke-muted-foreground "
)
}
/>
@@ -93,8 +90,8 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
dominantBaseline="middle"
fontSize="8"
className={
cn("fill-font/90 dark:fill-font/90 font-semibold" ,
selected ? "fill-primary dark:fill-primary" : "" ,
cn("fill-ring/60 font-semibold dark:!fill-muted-foreground" ,
selected ? "fill-ring dark:!fill-primary-foreground" : "" ,
)
}
>
@@ -1,10 +1,15 @@
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/tooltip/tooltip";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { useDatabaseHistory } from "@/providers/database-history/database-history-provider";
import { Button, cn, Divider, Navbar } from "@heroui/react";
import { useOnViewportChange, useReactFlow } from "@xyflow/react";
import { LayoutGrid, Redo, Scan, Undo, ZoomIn, ZoomOut } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
interface DbControlButtons {
@@ -52,39 +57,30 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
})
}, []);
return (
<Navbar className="flex rounded-md border-1 border-default-200 bg-background dark:bg-transparent bg-default dark:border-divider " isBlurred
classNames={{
wrapper: "h-14 p-2 gap-1",
}}
<div className="flex !rounded-md p-2 border-1 !overflow-hidden !bg-background/20 dark:!bg-card/20 text-muted-foreground shadow-md backdrop-blur-sm"
>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
size="md"
isIconOnly
variant="light"
onPressEnd={undo}
radius="sm"
size={"icon"}
onClick={undo}
disabled={!canUndo}
variant={"ghost"}
>
<Undo className={cn(
"size-4 dark:text-white",
!canUndo ? "text-font/30 dark:text-font/30" : ""
"size-4"
)} />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t("control_buttons.undo")}
<span className="ml-2 text-default-400">
<span className="ml-2">
Cntl + Z
</span>
</TooltipContent>
@@ -94,12 +90,11 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
<TooltipTrigger asChild>
<span>
<Button
size="md"
isIconOnly
variant="light"
radius="sm"
onPressEnd={adjustPositions}>
<LayoutGrid className="size-4 dark:text-white" />
size={"icon"}
onClick={adjustPositions}
variant={"ghost"}
>
<LayoutGrid className="size-4" />
</Button>
</span>
</TooltipTrigger>
@@ -108,18 +103,17 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
</TooltipContent>
</Tooltip>
<Divider orientation="vertical" className="bg-font/10" />
<Separator orientation="vertical" />
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
size="md"
isIconOnly
variant="light"
radius="sm"
onPressEnd={onZoomOut}>
<ZoomOut className="size-4 dark:text-white" />
size="icon"
onClick={onZoomOut}
variant={"ghost"}
>
<ZoomOut className="size-4" />
</Button>
</span>
</TooltipTrigger>
@@ -129,11 +123,10 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
</Tooltip>
<Button
size="md"
isIconOnly
variant="light"
onPressEnd={resetZoom}
className="w-[60px] p-2 hover:bg-primary-foreground dark:text-white"
size="icon"
variant={"ghost"}
onClick={resetZoom}
className="w-[60px] p-2 "
>
{zoom}
</Button>
@@ -141,12 +134,11 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
<TooltipTrigger asChild>
<span>
<Button
size="md"
isIconOnly
variant="light"
radius="sm"
onPressEnd={onZoomIn}>
<ZoomIn className="size-4 dark:text-white" />
size="icon"
onClick={onZoomIn}
variant={"ghost"}
>
<ZoomIn className="size-4" />
</Button>
</span>
</TooltipTrigger>
@@ -155,25 +147,23 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
{t("control_buttons.zoom_in")}
</TooltipContent>
</Tooltip>
<Divider orientation="vertical" className="bg-font/10" />
<Separator orientation="vertical" />
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
size="md"
isIconOnly
variant="light"
radius="sm"
onPressEnd={onFitView}
size="icon"
onClick={onFitView}
variant={"ghost"}
>
<Scan className="size-4 dark:text-white" />
<Scan className="size-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t("control_buttons.show_all")}
<span className="ml-2 text-default-400">
<span className="ml-2">
Cntl + A
</span>
</TooltipContent>
@@ -182,30 +172,26 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
<TooltipTrigger asChild>
<span>
<Button
size="md"
isIconOnly
variant="light"
radius="sm"
onPressEnd={redo}
size="icon"
variant={"ghost"}
onClick={redo}
disabled={!canRedo}
>
<Redo className={cn(
"size-4 dark:text-white",
!canRedo ? "text-font/30 dark:text-font/30" : ""
)} />
<Redo className={cn("size-4")} />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t("control_buttons.redo")}
<span className="ml-2 text-default-400">
<span className="ml-2 ">
Ctnl + Shift + Z
</span>
</TooltipContent>
</Tooltip>
</Navbar>
</div>
)
}
export default DatabaseControlButtons;
export default DatabaseControlButtons;
@@ -1,6 +1,6 @@
// Importing necessary types and hooks from React Flow (XYFlow)
import {
addEdge, Background, ColorMode, Connection, Controls, EdgeChange,
Background, ColorMode, Connection, Controls, EdgeChange,
EdgeRemoveChange, MiniMap, NodeChange, NodePositionChange,
NodeRemoveChange, OnEdgesChange, OnNodesChange,
ReactFlow, useEdgesState, useNodesState, useReactFlow
@@ -10,51 +10,59 @@ import {
import { useCallback, useEffect, useMemo } from "react";
// Custom components and styles
import Table from "./table/table";
import Table from "./table";
import '@xyflow/react/dist/style.css';
import Relationship from "./table/relationship";
import DBController from "./db-controller/db-controller";
import Relationship from "./relationship";
import { TableInsertType } from "@/lib/schemas/table-schema";
// Custom context providers and hooks
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useTableToNode } from "@/hooks/use-table-to-node";
import { useRelationshipToEdge } from "@/hooks/use-relationship-to-edge";
import { Cardinality, RelationshipInsertType } from "@/lib/schemas/relationship-schema";
import { RelationshipInsertType } from "@/lib/schemas/relationship-schema";
// Utils and constants
import { v4 } from "uuid";
import { TARGET_PREFIX } from "./table/field";
import CardinalityMarker from "@/components/cardinality-marker/cardinality-marker";
import { TARGET_PREFIX } from "./field";
import CardinalityMarker from "@/features/database/components/cardinality-marker";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
import { addToast, Button } from "@heroui/react";
import { AlertTriangle } from "lucide-react";
import { adjustTablesPositions, isTablesOverlapping } from "@/utils/tables";
import DatabaseControlButtons from "./database-control-buttons";
import { AlertTriangle, Menu } from "lucide-react";
import { adjustTablesPositions } from "@/utils/tables";
//import DatabaseControlButtons from "./database-control-buttons";
import { FieldType } from "@/lib/schemas/field-schema";
import useHighlightedEdges from "@/hooks/use-highlighted-edges";
import { useTranslation } from "react-i18next";
import useOverlappingTables from "@/hooks/use-overlapping-tables";
import { useTheme } from "next-themes";
import { getRelationshipSourceAndTarget } from "@/utils/relationship";
import { useModal } from "@/providers/modal-provider/modal-provider";
import { Modals } from "@/providers/modal-provider/modal-contxet";
import { Loading } from "@/components/modal/loading-modal";
import { usePowerSync } from "@powersync/react";
import { Loading } from "@/components/layout/loading-modal";
import { CardinalityStyle } from "@/lib/database";
import { useTheme } from "@/providers/theme-provider/theme-provider";
import { toast } from "sonner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import DatabaseControlButtons from "./database-control-buttons";
import { Modals } from "@/providers/modal-provider/modal-contxet";
import { useModal } from "@/providers/modal-provider/modal-provider";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
const DatabasePage: React.FC = () => {
const DatabaseDiagram: React.FC = () => {
const { open } = useModal();
const { t } = useTranslation();
const { resolvedTheme } = useTheme();
const { theme: resolvedTheme } = useTheme();
// Extract database state and operations
const { database, getField, databases, isSwitchingDatabase, isLoading, currentDatabaseId } = useDatabase();
const { database, getField, isSwitchingDatabase, isLoading, databases } = useDatabase();
const { updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship } = useDatabaseOperations();
@@ -63,7 +71,7 @@ const DatabasePage: React.FC = () => {
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
// Diagram-related state (e.g. connection in progress)
const { setIsConnectionInProgress, cardinalityStyle } = useDiagramOps();
const { setIsConnectionInProgress, cardinalityStyle, showController, openController } = useDiagramOps();
// Destructure tables and relationships from database
const { tables, relationships } = database || { tables: [], relationships: [] };
@@ -75,17 +83,18 @@ const DatabasePage: React.FC = () => {
const nodeTypes = useMemo(() => ({ table: Table }), []);
const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []);
useEffect(() => {
if (isLoading)
return;
// no database selected , open (open database) Modal
if (!database && databases?.length > 0) {
open(Modals.OPEN_DATABASE, {
closable: false
})
}
else if (databases.length == 0) {
else if (databases.length == 0) {
// there is no databases in the first place to select from ,
// we have to create a new one .
open(Modals.CREATE_DATABASE, {
@@ -99,9 +108,17 @@ const DatabasePage: React.FC = () => {
}, [database?.id, isLoading])
useEffect(() => {
setNodes([]);
setEdges([]);
}, [currentDatabaseId])
if (isSwitchingDatabase) {
setNodes([]);
setEdges([]);
} else {
fitView({
duration: 500
});
}
}, [isSwitchingDatabase, database?.id])
// Called when a connection is made between fields
const onConnect = useCallback(async (connection: Connection) => {
@@ -122,16 +139,19 @@ const DatabasePage: React.FC = () => {
targetTableId,
sourceFieldId,
targetFieldId,
} as RelationshipInsertType);
} else {
// Show error toast if invalid relationship
addToast({
title: t("db_controller.invalid_relationship.title"),
toast(t("db_controller.invalid_relationship.title"), {
description: t("db_controller.invalid_relationship.description"),
color: "danger",
variant: "solid"
});
classNames: {
description: "!text-destructive",
title: "!text-destructive"
},
})
}
setIsConnectionInProgress(false);
@@ -147,28 +167,30 @@ const DatabasePage: React.FC = () => {
const nodeRemoveChanges: NodeRemoveChange[] = changes.filter((change: NodeChange) => change.type == "remove");
// Save new positions to the database
if (nodePositionChanges.length > 0)
updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({
id: change.id,
posX: change.position?.x,
posY: change.position?.y
} as TableInsertType)));
// Delete tables if removed
if (nodeRemoveChanges.length > 0) {
if (nodeRemoveChanges.length > 0 && !isSwitchingDatabase) {
deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id));
}
return onNodesChange(changes);
}, [onNodesChange, database]);
}, [onNodesChange, database, isSwitchingDatabase]);
// Called when edges (relationships) change
const handleEdgeChanges: OnEdgesChange<any> = useCallback((changes: EdgeChange<any>[]) => {
const edgeRemoveChanges: EdgeRemoveChange[] = changes.filter((change: EdgeChange) => change.type == "remove") as EdgeRemoveChange[];
// Delete relationships from database
if (edgeRemoveChanges.length > 0) {
if (edgeRemoveChanges.length > 0 && !isSwitchingDatabase) {
deleteMultiRelationships(edgeRemoveChanges.map((change: EdgeRemoveChange) => change.id));
}
return onEdgesChange(changes as EdgeChange<never>[]);
}, [onEdgesChange]);
}, [onEdgesChange, isSwitchingDatabase]);
// When user starts connecting fields
const onConnectStart = useCallback(() => {
@@ -199,18 +221,15 @@ const DatabasePage: React.FC = () => {
useHighlightedEdges(nodes, relationships, edges);
const { isOverlapping, puls } = useOverlappingTables(tables);
const isMobile = useIsMobile();
return (
<div className="w-full h-screen flex relative overflow-hidden">
<div className="w-full h-full flex relative overflow-hidden">
{
(isSwitchingDatabase || isLoading) && <Loading />
}
<div className="flex max-w-full">
<DBController />
</div>
{
{
<div className="relative w-full h-full ">
<ReactFlow
colorMode={resolvedTheme as ColorMode}
@@ -236,33 +255,35 @@ const DatabasePage: React.FC = () => {
>
<Controls
position="bottom-center"
position={isMobile ? "top-center" : "bottom-center"}
showFitView={false}
showZoom={false}
showInteractive={false}
className="shadow-none">
className="!shadow-none !border-none">
<DatabaseControlButtons
adjustPositions={adjustPositions}
/>
</Controls >
<MiniMap
nodeStrokeWidth={4}
className="dark:bg-content1 bg-background border-1 border-default-200 dark:border-divider rounded-md "
maskStrokeColor={resolvedTheme == "dark" ? "#272c35" : "#e9edf1"}
maskColor={resolvedTheme == "dark" ? "#20252c99" : "#f2f4f733"}
className="!bg-background border-1 rounded-lg overflow-hidden "
maskStrokeColor={resolvedTheme == "dark" ? "#FFFFFF1A" : "#e2e8f0"}
maskColor={resolvedTheme == "dark" ? "#21262d77" : "#62748e05"}
maskStrokeWidth={1}
nodeClassName={"fill-default-300 dark:fill-font/30"}
nodeClassName={"!fill-muted-foreground/20 "}
style={{
width: 164,
height: 128
}}
/>
<Background className="dark:bg-background-100 " />
<Background color="#62748e" className="dark:!bg-background " />
</ReactFlow>
<div
className="absolute left-[24px] bottom-[24px] "
className={cn("absolute left-[24px] ", {
"bottom-[24px]": !isMobile,
"top-[24px]": isMobile
})}
>
{
isOverlapping &&
@@ -270,22 +291,30 @@ const DatabasePage: React.FC = () => {
<TooltipTrigger asChild>
<span>
<Button
variant="shadow"
size="sm"
isIconOnly
color="danger"
className="size-8 p-1 "
onPressEnd={puls}>
variant="destructive"
size="icon"
className="shadow-lg size-8"
onClick={puls}>
<AlertTriangle className="size-4 text-white" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
<TooltipContent className="bg-destructive fill-destructive ">
{t("table.overlapping_tables")}
</TooltipContent>
</Tooltip>
}
</div>
{
isMobile &&
<Button size={"icon"} className="absolute bottom-[24px] left-[24px]"
onClick={() => {
openController(!showController)
}}
>
<Menu className="size-5" />
</Button>
}
</div>
}
@@ -314,7 +343,7 @@ const DatabasePage: React.FC = () => {
}
export default DatabasePage;
export default DatabaseDiagram;
@@ -1,11 +1,12 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { CircularDependencyError } from "@/utils/render/render-uttils";
import { addToast, Alert, Button, Listbox, ListboxItem, toast } from "@heroui/react";
import { CircularDependencyError } from "@/utils/render/render-uttils";
import { AlertTriangle, Trash } from "lucide-react";
import React, { useCallback, useEffect, useMemo } from "react";
import React, { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
interface CircularDependencyAlertProps {
@@ -53,41 +54,38 @@ const CircularDependencyAlert: React.FC<CircularDependencyAlertProps> = ({ error
return (
<div className="flex flex-col gap-2 w-full h-full items-center pt-12 ">
<AlertTriangle
className="size-12 text-danger"
className="size-12 text-destructive"
/>
<h3 className="text-danger font-semibold">
<h3 className="text-destructive font-semibold">
{t("db_controller.circular_dependency.title")}
</h3>
<p className="text-sm text-font/70 text-center w-[80%] max-w-[360px]">
{t("db_controller.circular_dependency.description")} , <span className="font-semibold text-font/90"> {t("db_controller.circular_dependency.suggestion")} </span>
<p className="text-sm text-muted-foreground text-center w-[80%] max-w-[360px]">
{t("db_controller.circular_dependency.description")} , <span className="font-medium text-foreground"> {t("db_controller.circular_dependency.suggestion")} </span>
</p>
<Listbox aria-label="Relationships" className="w-[80%] max-w-[360px]" onAction={focus}
<ul aria-label="Relationships" className="w-[80%] max-w-[360px]"
>
{
circularRelationships.map((relationship: RelationshipType) => (
<ListboxItem
variant={"faded"}
className="data-[hover=true]:bg-default-500"
<li
onClick={() => focus(relationship.id)}
key={relationship.id}>
<div className="flex items-center justify-between tex-font/90">
<span>
<div className="hover:bg-secondary flex items-center justify-between h-10 mb-2 px-3 rounded-md cursor-pointer">
<span className="text-sm">
{relationship.sourceTable.name} -&gt; {relationship.targetTable.name}
</span>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
isIconOnly
variant="light"
color={"danger"}
size="sm"
onPress={() => removeRelationship(relationship.id)}
variant={"outline"}
size="icon"
onClick={() => removeRelationship(relationship.id)}
className="shadow-sm size-7"
>
<Trash className="text-danger size-4" />
<Trash className="text-destructive size-4" />
</Button>
</span>
</TooltipTrigger>
@@ -96,10 +94,10 @@ const CircularDependencyAlert: React.FC<CircularDependencyAlertProps> = ({ error
</TooltipContent>
</Tooltip>
</div>
</ListboxItem>
</li>
))
}
</Listbox>
</ul>
</div>
)
@@ -0,0 +1,148 @@
import { Ref, useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDatabase } from "@/providers/database-provider/database-provider";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { useModal } from "@/providers/modal-provider/modal-provider";
import { Modals } from "@/providers/modal-provider/modal-contxet";
import { getDefaultRelationshipName } from "@/utils/relationship";
import EmptyList from "@/components/empty-list";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { IconFilter2Up, IconPlus } from "@tabler/icons-react";
import { Accordion } from "@/components/ui/accordion";
import RelationshipAccordionItem from "./relationship-accordion-item/relationship-accordion-item";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
const RelationshipController: React.FC = ({ }) => {
const { open } = useModal();
const { database } = useDatabase();
const { relationships: allRelationships } = database || { relationships: [] };
const [relationships, setRelationships] = useState<RelationshipType[]>(allRelationships);
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const { t } = useTranslation();
const { focusedRelationshipId , setFocusedRelationshipId } = useDiagram();
useEffect(() => searchRelationships(), [allRelationships]);
useEffect(() => {
if (focusedRelationshipId) {
const accordionItem = document.getElementById(focusedRelationshipId)
if (accordionItem)
accordionItem?.scrollIntoView({
behavior: 'smooth', block: "nearest"
})
}
}, [focusedRelationshipId]);
const onOpen = useCallback(() => {
open(Modals.CREATE_RELATIONSHIP, {
onRlationshipCreated: (id: string) => setFocusedRelationshipId(id)
})
}, []);
const searchRelationships = useCallback(() => {
const keyword: string | undefined = nameRef.current?.value;
if (keyword !== undefined) {
setRelationships(() => {
return allRelationships.filter((relationship: RelationshipType) => {
if (relationship.name)
return relationship.name.toLowerCase().trim().includes(keyword.toLocaleLowerCase().trim());
else
return getDefaultRelationshipName(relationship).trim().toLocaleLowerCase().includes(
keyword.trim().toLocaleLowerCase()
)
})
})
}
}, [nameRef, allRelationships]);
const collapseAll = useCallback(() => {
setFocusedRelationshipId(undefined);
}, []);
return (
<div className="w-full h-full flex flex-col min-h-0">
<div className="flex items-center gap-2 p-3">
<Tooltip >
<TooltipTrigger asChild>
<Button
variant={"ghost"}
size={"icon"}
className="rounded-md h-8 w-8"
onClick={collapseAll}
>
<IconFilter2Up className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
{t("db_controller.collapse")}
</TooltipContent>
</Tooltip>
<Input
className="h-8 bg-secondary dark:bg-background"
placeholder={t("db_controller.filter")}
ref={nameRef}
type="text"
onKeyUp={searchRelationships}
/>
<Tooltip >
<TooltipTrigger asChild>
<Button
variant={"default"}
size={"icon"}
className="rounded-md h-8 w-8"
onClick={onOpen}
>
<IconPlus className="size-4 " />
</Button>
</TooltipTrigger>
<TooltipContent>
{t("db_controller.add_relationship")}
</TooltipContent>
</Tooltip>
</div>
{
allRelationships.length > 0 &&
<ScrollArea className="flex-1 px-3 overflow-hidden">
<Accordion
type="single"
collapsible
className="w-full "
value={focusedRelationshipId}
onValueChange={setFocusedRelationshipId}
>
{relationships.map((relationship: RelationshipType) => (
<>
<RelationshipAccordionItem relationship={relationship} key={relationship.id} />
<Separator className="my-1" />
</>
))}
</Accordion>
</ScrollArea>
}
{
(allRelationships.length == 0) &&
<div className="px-3 h-full">
<EmptyList
title={t("db_controller.empty_list.no_relationships")}
description={t("db_controller.empty_list.no_relationships_description")}
/>
</div>
}
</div>
)
}
export default RelationshipController
@@ -0,0 +1,211 @@
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { ForeignKeyActions } from "@/lib/field";
import { Cardinality, RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
interface RelationshipAccordionContentProps {
relationship: RelationshipType
}
const RelationshipAccordionContent: React.FC<RelationshipAccordionContentProps> = ({ relationship }) => {
const [cardinality, setCardinality] = useState<string>(relationship.cardinality);
const [onDeleteAction, setOnDeleteAction] = useState<string>(relationship.onDelete || ForeignKeyActions.NO_ACTION);
const [onUpdateAction, setOnUpdateAction] = useState<string>(relationship.onUpdate || ForeignKeyActions.NO_ACTION);
const { editRelationship, deleteRelationship } = useDatabaseOperations();
const { t } = useTranslation();
const changeCardinality = (selection: string) => {
setCardinality(selection);
editRelationship({
id: relationship.id,
cardinality: selection as Cardinality,
} as RelationshipInsertType);
}
const changeOnDelete = (selection: string) => {
editRelationship({
id: relationship.id,
onDelete: selection as ForeignKeyActions,
} as RelationshipInsertType);
setOnDeleteAction(selection);
}
const changeOnUpdate = (selection: string) => {
editRelationship({
id: relationship.id,
onUpdate: selection as ForeignKeyActions,
} as RelationshipInsertType);
setOnUpdateAction(selection);
}
const removeRelationship = () => {
deleteRelationship(relationship.id);
}
useEffect(() => {
setCardinality(relationship.cardinality);
}, [relationship.cardinality])
useEffect(() => {
if (!relationship.onDelete)
setOnDeleteAction(ForeignKeyActions.NO_ACTION)
else
setOnDeleteAction(relationship.onDelete as ForeignKeyActions)
}, [relationship.onDelete]);
useEffect(() => {
if (!relationship.onUpdate)
setOnUpdateAction(ForeignKeyActions.NO_ACTION)
else
setOnUpdateAction(relationship.onUpdate as ForeignKeyActions)
}, [relationship.onUpdate]);
if (!relationship.sourceTable || !relationship.targetTable)
return;
return (
<div className="w-full p-2 space-y-4">
<div className="flex">
<div className="w-full space-y-1">
<Label >
{t("db_controller.source_table")}
</Label>
<Tooltip>
<TooltipTrigger asChild>
<span className="truncate text-sm text-muted-foreground">
{relationship.sourceTable?.name}({relationship.sourceField?.name})
</span>
</TooltipTrigger>
<TooltipContent>
{relationship.sourceTable?.name}({relationship.sourceField?.name})
</TooltipContent>
</Tooltip>
</div>
<div className="w-full space-y-1">
<Label >
{t("db_controller.referenced_table")}
</Label>
<Tooltip>
<TooltipTrigger asChild>
<span className="truncate text-sm text-muted-foreground">
{relationship.targetTable?.name}({relationship.targetField?.name})
</span>
</TooltipTrigger>
<TooltipContent>
{relationship.targetTable?.name}({relationship.targetField?.name})
</TooltipContent>
</Tooltip>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="cardinality">
{t('db_controller.cardinality.name')}
</Label>
<Select
aria-label="cardinality"
value={cardinality as any}
onValueChange={changeCardinality as any}
>
<SelectTrigger id="cardinality" className="w-full flex ">
<SelectValue placeholder={t('db_controller.cardinality.name')} />
</SelectTrigger>
<SelectContent>
<SelectItem value={Cardinality.one_to_one}>{t("db_controller.cardinality.one_to_one")}</SelectItem>
<SelectItem value={Cardinality.one_to_many}>{t("db_controller.cardinality.one_to_many")}</SelectItem>
<SelectItem value={Cardinality.many_to_one}>{t("db_controller.cardinality.many_to_one")}</SelectItem>
<SelectItem value={Cardinality.many_to_many}>{t("db_controller.cardinality.many_to_many")}</SelectItem>
</SelectContent>
</Select>
</div>
{
cardinality != Cardinality.many_to_many &&
<div className="space-y-4">
<Label>
{t('db_controller.foreign_key_actions.title')}
</Label>
<div className="flex gap-2">
<div className="w-full space-y-2">
<Label >
{t('db_controller.foreign_key_actions.on_delete')}
</Label>
<Select
aria-label="On delete actions"
value={onDeleteAction as any}
onValueChange={changeOnDelete as any}
>
<SelectTrigger className="w-full flex ">
<SelectValue placeholder={t('db_controller.foreign_key_actions.on_delete')} />
</SelectTrigger>
<SelectContent>
{
Object.values(ForeignKeyActions).map((value: string) => (
<SelectItem
key={value}
value={value}>{t(`db_controller.foreign_key_actions.actions.${value}`)}</SelectItem>
))
}
</SelectContent>
</Select>
</div>
<div className="w-full space-y-2">
<div className="w-full space-y-2">
<Label >
{t('db_controller.foreign_key_actions.on_update')}
</Label>
<Select
aria-label="On update actions"
value={onUpdateAction}
onValueChange={changeOnUpdate}
>
<SelectTrigger className="w-full flex ">
<SelectValue placeholder={t('db_controller.foreign_key_actions.on_update')} />
</SelectTrigger>
<SelectContent>
{
Object.values(ForeignKeyActions).map((value: string) => (
<SelectItem
key={value}
value={value}>{t(`db_controller.foreign_key_actions.actions.${value}`)}</SelectItem>
))
}
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
}
<div className="flex justify-center">
<Button
variant="destructive"
className="w-full"
onClick={removeRelationship}
>
<Trash2 className="mr-1 size-3.5 text-danger" />
{t("db_controller.delete")}
</Button>
</div>
</div>
)
}
export default RelationshipAccordionContent;
@@ -0,0 +1,30 @@
import {
AccordionContent,
AccordionItem,
} from "@/components/ui/accordion"
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import RelationshipAccordionTrigger from "./relationship-accordion-trigger";
import RelationshipAccordionContent from "./relationship-accordion-content";
interface RelationshipAccordionItemProps {
relationship: RelationshipType
}
const RelationshipAccordionItem: React.FC<RelationshipAccordionItemProps> = ({ relationship }) => {
return (
<AccordionItem value={relationship.id} className="border-none" id={relationship.id}>
<RelationshipAccordionTrigger relationship={relationship} />
<AccordionContent className="flex flex-col gap-4 text-balance">
<RelationshipAccordionContent
relationship={relationship}
/>
</AccordionContent>
</AccordionItem>
)
}
export default RelationshipAccordionItem;
@@ -0,0 +1,138 @@
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import hash from "object-hash";
import { getDefaultRelationshipName } from "@/utils/relationship";
import { AccordionTrigger } from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import { IconCheck, IconDotsVertical, IconFocus2, IconPencil, IconTrash } from "@tabler/icons-react";
import { Input } from "@/components/ui/input";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
interface RelationshipAccordionTriggerProps {
isOpen?: boolean;
relationship: RelationshipType
}
const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps> = ({ isOpen, relationship }) => {
const defaultName: string = useMemo(() => {
return getDefaultRelationshipName(relationship);
}, [relationship])
const [editMode, setEditMode] = useState<boolean>(false);
const { editRelationship, deleteRelationship } = useDatabaseOperations();
const { t } = useTranslation();
const [name, setName] = useState<string>(relationship.name ? relationship.name : defaultName);
const { focusOnRelationship } = useDiagramOps();
const editRelationshipName = () => {
if (relationship.name || name.trim().toLocaleLowerCase() != defaultName)
editRelationship({
id: relationship.id,
name
} as RelationshipInsertType);
setEditMode(false);
}
const onDeleteRelationship = () => {
deleteRelationship(relationship.id);
}
useEffect(() => {
setName(relationship.name ? relationship.name : defaultName);
}, [relationship.name]);
return (
<AccordionTrigger className="group h-12 py-3">
{
!editMode &&
<>
<div className="w-full flex">
<label
className="py-2 !truncate text-sm cursor-pointer max-w-80"
>
{relationship.name ? relationship.name : defaultName}
</label>
</div>
<div className="hidden shrink-0 flex-row group-hover:flex gap-2">
<Button variant="outline" size="icon" className="size-8 shrink-0 " onClick={(event: any) => {
event.stopPropagation();
setEditMode(true)
}}>
<IconPencil className="size-4 text-muted-foreground " />
</Button>
<Button variant="outline" size="icon" className="size-8 shrink-0 " onClick={(event: any) => {
event.stopPropagation();
focusOnRelationship(relationship.id, true)
}}>
<IconFocus2 className="size-4 text-muted-foreground " />
</Button>
</div>
</>
}
{
editMode && <>
<Input
placeholder={"Relationship name"}
value={name}
onChange={(event: any) => setName(event.target.value)}
onBlur={editRelationshipName}
autoFocus
type="text"
onClick={(event) => event.stopPropagation()}
className="h-8"
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault();
editRelationshipName();
e.target.blur();
}
}}
/>
<Button variant="ghost" size="icon" className="size-8 shrink-0 rounded-sm" onClick={editRelationshipName}>
<IconCheck className="size-4 text-muted-foreground " />
</Button>
</>
}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="size-8 shrink-0 rounded-sm" >
<IconDotsVertical className="size-4 text-muted-foreground " />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[164px]">
<DropdownMenuLabel>
{t("db_controller.actions")}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onClick={onDeleteRelationship}>
{t("db_controller.delete")}
<DropdownMenuShortcut>
<IconTrash className="size-4 text-destructive" />
</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</AccordionTrigger>
)
}
export default React.memo(RelationshipAccordionTrigger, (previousState, newState) => {
return hash(previousState) == hash(newState);
});
@@ -1,21 +1,21 @@
import { useRenderSql } from "@/hooks/user-render-sql";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useDatabase } from "@/providers/database-provider/database-provider";
import React, { useEffect, useMemo } from "react";
import CodeMirror, { EditorView } from '@uiw/react-codemirror';
import CodeMirror from '@uiw/react-codemirror';
import { sql } from '@codemirror/lang-sql';
import { oneDark } from '@codemirror/theme-one-dark';
import { useTheme } from "next-themes";
import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
import { DatabaseType } from "@/lib/schemas/database-schema";
import CircularDependencyAlert from "./circular-dependecy-alert";
import { addToast } from "@heroui/react";
import { useTranslation } from "react-i18next";
import Clipboard from "@/components/clipboard/clipboard";
import Clipboard from "@/components/clipboard";
import { TableType } from "@/lib/schemas/table-schema";
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import { Parser } from "node-sql-parser";
import { useTheme } from "@/providers/theme-provider/theme-provider";
import { toast } from "sonner";
interface SqlPreviewProps {
@@ -23,14 +23,14 @@ interface SqlPreviewProps {
}
const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
const { database: currentDatabase } = useDatabase();
const database = useMemo(() => {
if ( !tableFilterIds )
return currentDatabase ;
if (!tableFilterIds)
return currentDatabase;
return {
...currentDatabase,
tables: currentDatabase?.tables.filter((table: TableType) => tableFilterIds?.includes(table.id)),
@@ -41,16 +41,17 @@ const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
}, [currentDatabase, tableFilterIds])
const { sql: sqlCode, circularDependency } = useRenderSql(database as DatabaseType);
const { resolvedTheme } = useTheme();
const { theme } = useTheme();
const { t } = useTranslation();
useEffect(() => {
if (circularDependency)
addToast({
title: t("db_controller.circular_dependency.title"),
toast(t("db_controller.circular_dependency.title"), {
description: t("db_controller.circular_dependency.description"),
color: "danger",
variant: "solid"
classNames: {
description: "!text-destructive",
title: "!text-destructive"
},
});
}, [circularDependency]);
@@ -60,19 +61,19 @@ const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
else
return (
<div className="flex w-full h-full relative ">
<div className="absolute right-[12px] top-[4px] z-[1] bg-background ">
<div className="absolute right-[12px] top-[4px] z-[1] ">
<Clipboard
text={sqlCode}
/>
</div>
<CodeMirror
defaultValue={sqlCode}
value={sqlCode}
className="flex flex-1 w-full "
extensions={[sql()]}
readOnly
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
theme={theme != "dark" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
/>
@@ -0,0 +1,227 @@
import { Code } from "lucide-react"
import { useTranslation } from "react-i18next";
import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { TableInsertType, TableType } from "@/lib/schemas/table-schema";
import { v4 } from "uuid";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { useReactFlow } from "@xyflow/react";
import SqlPreview from "../sql-preview";
import EmptyList from "@/components/empty-list";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { IconListDetails, IconPlus } from "@tabler/icons-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
const PADDING_X = 40;
const PADDING_Y = 80;
import {
Accordion,
} from "@/components/ui/accordion"
import TableAccordionItem from "./table-accordion-item/table-accordion-item";
import { ScrollArea } from "@/components/ui/scroll-area";
import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { getTableNextSequence } from "@/utils/tables";
import { Separator } from "@/components/ui/separator";
const TablesController: React.FC = ({ }) => {
const { database } = useDatabase();
const { createTable, getInteger, orderTables } = useDatabaseOperations();
const { getViewport } = useReactFlow();
const { tables: allTables } = database || { tables: [] };
const [tables, setTables] = useState<TableType[]>(allTables);
const { t } = useTranslation();
const [showSqlPreview, setShowSqlPreview] = useState<boolean>(false);
const { focusedTableId , setFocusedTableId} = useDiagram();
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const sensors = useSensors(
useSensor(PointerSensor)
);
function handleDragEnd(event: any) {
const { active, over } = event;
if (active.id !== over?.id) {
setTables((items) => {
const oldIndex = items.findIndex((item: TableType) => item.id == active.id);
const newIndex = items.findIndex((item: TableType) => item.id == over.id);
const tables = arrayMove(items, oldIndex, newIndex);
orderTables(tables)
return tables;
});
}
}
useEffect(() => searchTables(), [allTables]);
const addNewTable = useCallback(async () => {
const newTableId: string = v4();
const viewport = getViewport();
const { x, y, zoom } = viewport;
const posX = -x / zoom + (PADDING_X / zoom);
const posY = -y / zoom + (PADDING_Y / zoom);
await createTable({
id: newTableId,
name: `table_${tables.length + 1}`,
posX,
posY,
sequence: getTableNextSequence(tables),
fields: [{
id: v4(),
name: "id",
isPrimary: true,
typeId: getInteger()?.id,
autoIncrement: true,
}]
} as TableInsertType);
setFocusedTableId(newTableId);
}, [database, tables, getViewport, getInteger]);
useEffect(() => {
if (focusedTableId) {
const accordionItem = document.getElementById(focusedTableId)
if (accordionItem)
accordionItem?.scrollIntoView({
behavior: 'smooth', block: "nearest"
})
}
}, [focusedTableId]);
const searchTables = useCallback(() => {
const keyword = nameRef.current?.value;
if (keyword !== undefined)
setTables(() => allTables.filter((table: TableType) => table.name.toLowerCase().trim().includes(keyword?.toLowerCase().trim())))
else
setTables(allTables)
}, [nameRef, allTables]);
const toggleSqlPreview = useCallback(() => {
setShowSqlPreview(preview => !preview);
}, []);
const tableFilterIds = useMemo(() => {
return tables.map((table: TableType) => table.id);
}, [tables]);
return (
<div className="w-full h-full flex flex-col min-h-0">
<div className="flex items-center gap-2 p-3">
<Tooltip >
<TooltipTrigger asChild>
<Button
variant={"ghost"}
size={"icon"}
className="rounded-md h-8 w-8"
onClick={toggleSqlPreview}
>
{showSqlPreview ? (
<IconListDetails className="size-4" stroke={1} />
) : (
<Code className="size-4 text-muted-foreground" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
{t("db_controller.show_code")}
</TooltipContent>
</Tooltip>
<Input
className="h-8 bg-secondary dark:bg-background"
placeholder={t("db_controller.filter")}
ref={nameRef}
type="text"
onKeyUp={searchTables}
/>
<Tooltip >
<TooltipTrigger asChild>
<Button
variant={"default"}
size={"icon"}
className="rounded-md h-8 w-8"
onClick={addNewTable}
>
<IconPlus className="size-4 " />
</Button>
</TooltipTrigger>
<TooltipContent>
{t("db_controller.add_table")}
</TooltipContent>
</Tooltip>
</div>
{
allTables.length > 0 && !showSqlPreview &&
<ScrollArea className="px-3 h-full w-full overflow-hidden">
<Accordion
type="single"
collapsible
className="w-full"
value={focusedTableId}
onValueChange={setFocusedTableId}
>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={tables}
strategy={verticalListSortingStrategy}
>
{tables.map((table: TableType) => (
<>
<TableAccordionItem table={table} key={table.id} />
<Separator className="my-1" />
</>
))}
</SortableContext>
</DndContext>
</Accordion>
</ScrollArea>
}
{
allTables.length > 0 && showSqlPreview &&
<div className=" flex-1 overflow-auto -ml-3">
<SqlPreview
tableFilterIds={tableFilterIds}
/>
</div>
}
{
(allTables.length == 0) &&
<div className="px-3 h-full">
<EmptyList
title={t("db_controller.empty_list.no_tables")}
description={t("db_controller.empty_list.no_tables_description")}
/>
</div>
}
</div>
)
}
export default TablesController
@@ -0,0 +1,413 @@
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema"
import React, { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { ModifierValidation } from "./field-setting";
import { DataTypes, TimeDefaultValues } from "@/lib/field";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import dayjs from 'dayjs';
import { now } from "@internationalized/date";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { MultiSelect } from "@/components/multi-select";
import { DatePicker } from "@/components/date-picker";
interface DefaultValueType {
number?: boolean;
string?: boolean;
boolean?: boolean;
time?: boolean;
select?: boolean;
multiSelect?: boolean
}
interface FieldDefaultValueProps {
field: FieldType;
}
const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
const { editField } = useDatabaseOperations();
const { t } = useTranslation();
const [defaultValueValidation, setDefaultValueValidation] = useState<ModifierValidation>({
isValid: true,
});
const defaultValueRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const [values, setValues] = useState<string[]>([]);
const [timeSelection, setTimeSelection] = useState<string>(() => {
if (field.defaultValue == TimeDefaultValues.NOW)
return TimeDefaultValues.NOW;
if (!field.defaultValue)
return TimeDefaultValues.NO_VALUE
if (field.type.name == "time" && field.defaultValue)
return TimeDefaultValues.CUSTOM
const date = new Date(field.defaultValue);
if (!isNaN(date.getTime())) {
return TimeDefaultValues.CUSTOM;
}
return TimeDefaultValues.NO_VALUE;
});
const [defaultDateTime, setDefaultDateTime] = useState<any>(() => {
try {
if (field.type.name == "time" ) {
return field.defaultValue ;
}
if (field.defaultValue) {
const date = new Date(field.defaultValue);
return date;
}
return undefined;
} catch (error) {
return undefined;
}
});
const defaultValueType: DefaultValueType = useMemo(() => {
return {
number: field.type?.type == DataTypes.INTEGER || field.type?.type == DataTypes.NUMERIC,
string: field.type?.type == DataTypes.TEXT || field.type?.name == "year",
boolean: field.type?.type == DataTypes.BOOLEAN,
time: field.type?.type == DataTypes.TIME && field.type?.name != "year",
select: field.type?.type == DataTypes.ENUM && field.type?.name != "set",
multiSelect: field.type?.type == DataTypes.ENUM && field.type?.name == "set"
}
}, [field]);
const [selectedValues, setSelectedValues] = useState<string[] | string>(() => {
if (defaultValueType.select) {
if (!field.defaultValue)
return "none";
else
return field.defaultValue
}
else {
if (!field.defaultValue)
return []
if (defaultValueType.multiSelect)
return field.defaultValue.split(",")
return [field.defaultValue as string];
}
});
useEffect(() => {
if (field.values && field.values.length > 0) {
try {
const jsonValues: string[] = JSON.parse(field.values);
setValues(jsonValues);
} catch (error) {
setValues([]);
}
}
}, [field.values]);
const defaultValueChange = useCallback((event: any) => {
const value = event.target.value;
if (value && value.trim().length > 0) {
if (field.type?.type == DataTypes.INTEGER) {
const isValid: boolean = Number.isInteger(Number(value));
setDefaultValueValidation({
isValid,
errorMessage: t("db_controller.field_settings.errors.integer_default_value")
})
}
} else {
setDefaultValueValidation({
isValid: true,
errorMessage: undefined
});
}
}, [field, defaultValueRef]);
const saveDefaultValue = useCallback((booleanValue?: boolean) => {
if (defaultValueValidation.isValid) {
let value: string | undefined;
if (field.type.type != DataTypes.BOOLEAN)
value = defaultValueRef.current?.value;
else {
value = String(booleanValue);
}
editField({
id: field.id,
defaultValue: value ? String(value) : null,
} as FieldInsertType);
}
}, [defaultValueRef, field, defaultValueValidation]);
const changeTimeDefaultValue = (selection: any) => {
let value: string | undefined;
if (!selection)
return;
if (selection == TimeDefaultValues.NOW)
value = TimeDefaultValues.NOW;
if (selection == TimeDefaultValues.CUSTOM) {
const currentDateTime = now(Intl.DateTimeFormat().resolvedOptions().timeZone)
const date: Date = currentDateTime?.toDate();
setDefaultDateTime(date);
if (field.type.name == "date")
value = dayjs(date).format("YYYY-MM-DD")
else if (field.type.name == "datetime" || field.type.name == "timestamp")
value = dayjs(date).format("YYYY-MM-DD HH:mm:ss")
else if (field.type.name == "time")
value = dayjs(date).format("HH:mm:ss")
}
editField(({
id: field.id,
defaultValue: value ? String(value) : null
}) as FieldInsertType)
setTimeSelection(selection);
}
const saveDefaultDateTime = useCallback((date: Date | undefined) => {
let value: string | undefined;
if (field.type.name != "time" && date) {
setDefaultDateTime(date);
if (field.type.name == "date")
value = dayjs(date).format("YYYY-MM-DD")
else if (field.type.name == "datetime" || field.type.name == "timestamp" || field.type.name == "timestamptz")
value = dayjs(date).format("YYYY-MM-DD HH:mm:ss")
}
else if ( field.type.name == "time") {
value = defaultDateTime ;
}
editField(({
id: field.id,
defaultValue: value ? String(value) : null
}) as FieldInsertType);
}, [field, defaultDateTime]);
const enumValueChange = useCallback((selection: any) => {
if (defaultValueType.select) {
setSelectedValues(selection);
editField({
id: field.id,
defaultValue: selection == "none" ? null : selection
} as FieldInsertType);
return;
}
let values: string | null = null;
const arraySelection = Array.from(selection);
if (arraySelection.length > 0) {
values = arraySelection.join(',');
} else {
values = null;
}
editField({
id: field.id,
defaultValue: values
} as FieldInsertType);
setSelectedValues(selection);
}, [field, defaultValueType])
return (
<>
{
!defaultValueType.boolean &&
<Label htmlFor="default_value">
{t("db_controller.field_settings.default_value")}
</Label>
}
{
(defaultValueType.number || defaultValueType.string) &&
<>
<Input
type={defaultValueType.number ? "number" : "text"}
id="default_value"
ref={defaultValueRef}
aria-invalid={!defaultValueValidation.isValid}
onChange={defaultValueChange}
defaultValue={field.defaultValue as string}
onBlur={saveDefaultValue as any}
placeholder={t("db_controller.field_settings.value")}
/>
{
!defaultValueValidation.isValid &&
<p className="text-destructive text-xs">
{defaultValueValidation.errorMessage}
</p>
}
</>
}
{
defaultValueType.boolean &&
<div className="flex w-full justify-between">
<Label htmlFor="default_value">
{t("db_controller.field_settings.default_value")}
</Label>
<Switch
id="default_value"
onCheckedChange={saveDefaultValue as any}
defaultChecked={field.defaultValue == "true"} />
</div>
}
{
(defaultValueType.select) &&
<Select
aria-label="value"
value={selectedValues as any}
onValueChange={enumValueChange as any}
>
<SelectTrigger id="charset" className="w-full flex ">
<SelectValue placeholder={t('db_controller.field_settings.pick_value')} />
</SelectTrigger>
<SelectContent>
<SelectItem value={"none"} >
No Default value
</SelectItem>
{
values.map((value: string) => (
<SelectItem
key={value}
value={value}
>
{value}
</SelectItem>
))
}
</SelectContent>
</Select>
}
{
(defaultValueType.multiSelect) &&
<MultiSelect
options={
values.map((value: string) => ({ value, label: value })) as any
}
onValueChange={enumValueChange}
defaultValue={selectedValues as any}
placeholder="Select fields..."
variant={"secondary"}
hideSelectAll
/>
}
{
defaultValueType.time &&
<>
<Select
aria-label="Time"
value={timeSelection}
onValueChange={changeTimeDefaultValue}
>
<SelectTrigger id="time" className="w-full flex">
<SelectValue placeholder={t('db_controller.field_settings.pick_value')} />
</SelectTrigger>
<SelectContent>
<SelectItem value={TimeDefaultValues.NO_VALUE} >
{t("db_controller.field_settings.time_default_value.no_value")}
</SelectItem>
<SelectItem value={TimeDefaultValues.CUSTOM}>{t("db_controller.field_settings.time_default_value.custom")}</SelectItem>
{
(field.type.name == "datetime" || field.type.name?.includes("timestamp")) ?
<SelectItem value={TimeDefaultValues.NOW}>{t("db_controller.field_settings.time_default_value.now")}</SelectItem> : null
}
</SelectContent>
</Select>
{
timeSelection == TimeDefaultValues.CUSTOM && field.type.name != "time" &&
<DatePicker
value={defaultDateTime}
onValueChange={saveDefaultDateTime}
/>
}
{
timeSelection == TimeDefaultValues.CUSTOM && field.type.name == "time" &&
<>
<Label htmlFor="time">
Time
</Label>
<Input
id="time"
type="time"
step={1}
defaultValue={defaultDateTime}
onChange={(event) => setDefaultDateTime( event.target.value)}
onBlur={saveDefaultDateTime as any}
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
/>
</>
}
</>
}
</>
)
}
export default React.memo(fieldDefautlValue);
@@ -0,0 +1,176 @@
import { useSortable } from "@dnd-kit/sortable";
import { Settings2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { CSS } from "@dnd-kit/utilities";
import { FieldType } from "@/lib/schemas/field-schema";
import { Key, useEffect, useState } from "react";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import FieldSetting from "./field-setting";
import { DataType } from "@/lib/schemas/data-type-schema";
import { IconGripVertical, IconKey, IconKeyframe } from "@tabler/icons-react";
import { Input } from "@/components/ui/input";
import { Combobox } from "@/components/combobox";
import { Toggle } from "@/components/ui/toggle";
import { TooltipTrigger, Tooltip, TooltipContent } from "@/components/ui/tooltip";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
interface Props {
field: FieldType
}
const FieldItem: React.FC<Props> = ({ field }) => {
const [fieldName, setFieldName] = useState<string>(field.name);
const { data_types } = useDatabaseOperations();
const { editField } = useDatabaseOperations();
const [selectedType, setSelectedType] = useState<string | undefined>(field.typeId as string | undefined);
const { t } = useTranslation();
const { attributes, listeners, setNodeRef, transform } = useSortable({ id: field.id });
const style = {
transform: CSS.Transform.toString(transform),
};
useEffect(() => {
setFieldName(field.name);
}, [field.name]);
useEffect(() => {
setSelectedType(field.typeId as string | undefined);
}, [field.typeId])
const saveFieldName = () => {
editField({
id: field.id,
name: fieldName
} as FieldType);
}
const updateFieldType = (key: Key | null) => {
const dataType: DataType | undefined = data_types.find((dataTypes: DataType) => dataTypes.id == key);
if (!dataType)
return;
if (dataType.id == field.typeId)
return;
if (key != null) {
editField({
id: field.id,
typeId: key
} as FieldType);
setSelectedType(key as string | undefined);
}
}
const toggleNullable = (nullable: boolean) => {
editField({
id: field.id,
nullable: !nullable
} as FieldType);
}
const togglePrimaryKey = (primaryKey: boolean) => {
editField({
id: field.id,
isPrimary: primaryKey
} as FieldType);
}
return (
<div className="flex w-full gap-1 items-center " style={style} ref={setNodeRef} {...attributes}>
<div {...listeners}>
<IconGripVertical className="size-4 text-muted-foreground hover:text-foreground cursor-move shrink-0" />
</div>
<div className="flex gap-2 w-full">
<Input
aria-label={t("db_controller.name")}
placeholder={t("db_controller.name")}
value={fieldName}
onChange={(event: any) => setFieldName(event.target.value)}
onBlur={saveFieldName}
className=" flex flex-1 !bg-transparent"
onKeyDown={(e : any) => {
if (e.key === "Enter") {
e.preventDefault();
saveFieldName() ;
e.target.blur() ;
}
}}
/>
<Combobox
items={data_types}
label="name"
placeholder={t("db_controller.type")}
selectedItem={selectedType}
onSelectionChange={updateFieldType}
className="flex flex-1 !bg-transparent !font-normal min-w-0"
/>
</div>
<div className="flex gap-2 ml-2 ">
<Tooltip>
<TooltipTrigger asChild>
<span>
<Toggle size={"sm"}
className="size-9 text-muted-foreground "
pressed={!field.nullable as boolean}
onPressedChange={toggleNullable}
>
<IconKeyframe className="size-4" />
</Toggle>
</span>
</TooltipTrigger>
<TooltipContent>
{t("db_controller.required")} {field.nullable ? "?" : ""}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Toggle size={"sm"}
className="size-9 text-muted-foreground "
pressed={field.isPrimary as boolean}
onPressedChange={togglePrimaryKey}
>
<IconKey className="size-4" />
</Toggle>
</span>
</TooltipTrigger>
<TooltipContent>
{t("db_controller.primary_key")} {!field.isPrimary ? "?" : ""}
</TooltipContent>
</Tooltip>
<Popover >
<PopoverTrigger asChild>
<Button
size="icon"
variant={"ghost"}
className="size-9 dark:bg-card dark:border-none text-muted-foreground hover:text-foreground"
>
<Settings2 className="size-4 " />
</Button>
</PopoverTrigger>
<PopoverContent side="right">
<FieldSetting field={field} />
</PopoverContent>
</Popover>
</div>
</div>
)
}
export default FieldItem;
@@ -1,16 +1,15 @@
import { Button } from "@heroui/react";
import { Plus } from "lucide-react";
import FieldItem from "./field-item";
import { useTranslation } from "react-i18next";
import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { FieldType } from "@/lib/schemas/field-schema";
import { TableType } from "@/lib/schemas/table-schema";
import React, { useCallback, useEffect, useState } from "react";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { v4 } from "uuid";
import { getNextSequence } from "@/utils/field";
import { FieldType } from "@/lib/schemas/field-schema";
import React, { useEffect, useState } from "react";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import hash from "object-hash" ;
interface Props {
@@ -20,10 +19,9 @@ interface Props {
const FieldList: React.FC<Props> = ({ tableFields , tableId}) => {
const { t } = useTranslation();
const [fields, setFields] = useState<FieldType[]>(tableFields);
const { createField, orderTableFields , getInteger } = useDatabaseOperations();
const { orderTableFields } = useDatabaseOperations();
useEffect(() => {
@@ -52,16 +50,7 @@ const FieldList: React.FC<Props> = ({ tableFields , tableId}) => {
}
}
const addField = () => {
createField({
id: v4(),
name: `field_${fields.length + 1}`,
tableId: tableId,
sequence: getNextSequence(fields) ,
nullable: true,
typeId: getInteger()?.id
})
}
return (
<div className="w-full space-y-2 no-select">
<DndContext
@@ -83,18 +72,7 @@ const FieldList: React.FC<Props> = ({ tableFields , tableId}) => {
</SortableContext>
</div>
</DndContext>
<Button
variant="flat"
radius="sm"
startContent={
<Plus className="size-4 text-font/90 dark:text-white" />
}
className="h-8 p-2 text-xs bg-transparent hover:bg-default text-font/90 font-semibold"
onPressEnd={addField}
>
{t("db_controller.add_field")}
</Button>
</div >
)
}
@@ -1,14 +1,24 @@
import TagInput from "@/components/tag-input/tag-input";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
import { DatabaseDialect } from "@/lib/database";
import { DataTypes, Modifiers, MySQLCharset, MySQLCollation, PostgreSQLCharset, PostgreSQLCollation, SQLiteCharset, SQLiteCollation } from "@/lib/field";
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { Button, Checkbox, Input, Select, SelectItem, SharedSelection, Switch, Textarea, Tooltip as HeroUITooltip } from "@heroui/react";
import { CircleHelp, Trash2, TriangleAlert } from "lucide-react";
import React, { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import FieldDefaultValue from "./field-default-value";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { IconInfoCircle, IconTrash } from "@tabler/icons-react";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { InputTags } from "@/components/input-tags";
import { Textarea } from "@/components/ui/textarea";
@@ -20,6 +30,7 @@ interface FieldSettingProps {
export interface ModifierValidation {
isValid: boolean;
errorMessage?: string;
}
const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
@@ -28,10 +39,10 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
const { deleteField, editField } = useDatabaseOperations();
const { t } = useTranslation();
const [charset, setCharset] = useState(new Set([field.charset]));
const [collation, setCollation] = useState(new Set([field.collate]));
const [charset, setCharset] = useState<string | undefined>(field.charset || "none");
const [collation, setCollation] = useState<string | undefined>(field.collate || "none");
const [note, setNote] = useState<string | undefined>(field.note as string | undefined);
const [note, setNote] = useState<string >(field.note ? field.note : "");
const maxLengthRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const scaleRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
@@ -41,8 +52,6 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
isValid: true,
});
const [precisionValidation, setPrecisionValidation] = useState<ModifierValidation>({
isValid: true,
});
@@ -53,6 +62,9 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
});
const [jsonValues, setJsonValues] = useState<any[]>(field.values ? JSON.parse(field.values) : []);
const [mountScale, setMountScale] = useState<boolean>(false);
const collations = useMemo(() => {
if (!field.type)
return undefined;
@@ -83,13 +95,14 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
const updateFieldNote = useCallback(() => {
editField({
id: field.id,
note: note,
} as FieldInsertType)
}, [field , note]) ;
}, [field, note]);
const toggleUnqiue = useCallback((value: boolean) => {
const toggleUnique = useCallback((value: boolean) => {
editField({
id: field.id,
unique: value
@@ -119,31 +132,24 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
}, [field]);
const changeCharset = useCallback((keys: SharedSelection) => {
if (keys.anchorKey != field.charset) {
editField({
id: field.id,
charset: keys.anchorKey,
} as FieldInsertType);
}
setCharset(keys as any);
const changeCharset = useCallback((charset: string) => {
setCharset(charset);
editField({
id: field.id,
charset: charset == "none" ? null : charset,
} as FieldInsertType);
}, [field]);
const changeCollation = useCallback((collation: string) => {
const changeCollation = useCallback((keys: SharedSelection) => {
setCollation(collation);
editField({
id: field.id,
collate: collation == "none" ? null : collation,
if (keys.anchorKey != field.collate) {
} as FieldInsertType);
editField({
id: field.id,
collate: keys.anchorKey,
} as FieldInsertType);
}
setCollation(keys as any);
}, [field])
@@ -177,8 +183,9 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
const updateValues = useCallback((values: string[]) => {
const jsonValues = JSON.stringify(values);
setJsonValues(values)
const jsonValues = JSON.stringify(values);
if (jsonValues != field.values)
editField({
id: field.id,
@@ -188,13 +195,15 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
}, [field])
const showNumericModifiers: boolean = modifiers.includes(Modifiers.AUTO_INCREMENT) || modifiers.includes(Modifiers.UNSIGNED) || modifiers.includes(Modifiers.ZEROFILL)
const showNumericModifiers: boolean = modifiers.includes(Modifiers.UNSIGNED) || modifiers.includes(Modifiers.ZEROFILL)
const showDecimalModifiers: boolean = modifiers.includes(Modifiers.PRECISION) || modifiers.includes(Modifiers.SCALE);
const showTextModifiers: boolean = modifiers.includes(Modifiers.COLLATE) || modifiers.includes(Modifiers.CHARSET);
const maxLengthChange = useCallback((value: any) => {
const maxLengthChange = useCallback((input: any) => {
const value = input.target.value;
if (value && value.trim().length > 0) {
const isValid = Number.isInteger(Number(value)) && value > 0;
@@ -210,14 +219,8 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
isValid: true,
errorMessage: undefined
});
}, [maxLengthRef, field]);
const validateScaleAndPrecision = useCallback(() => {
const scale: number | null = scaleRef.current && Number(scaleRef.current?.value);
const precision: number | null = precisionRef.current && Number(precisionRef.current?.value);
@@ -261,232 +264,222 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
useEffect(() => {
if (precisionRef.current) {
setMountScale(true);
}
}, []);
return (
<div className="w-full flex flex-col gap-2 p-2 min-w-[260px] max-w-[260px]">
<h3 className="font-semibold text-sm text-font/90">
<div className="w-full flex flex-col gap-2 max-w-[260px]">
<h3 className="font-medium text-sm">
{t("db_controller.field_settings.title")}
</h3>
<hr className="border-divider" />
<Separator />
{
!modifiers.includes(Modifiers.NO_UNIQUE) &&
<div className="flex w-full justify-between">
<span className="text-xs text-font/70 font-medium dark:text-font/90">
<div className="flex items-center justify-between ">
<Label htmlFor="unique">
{t("db_controller.field_settings.unique")}
</span>
<Checkbox
defaultSelected={field.unique as boolean}
size="md"
classNames={{
wrapper: "before:border-divider group-data-[hover=true]:before:bg-default",
}}
onValueChange={toggleUnqiue} />
</Label>
<Switch id="unique" onCheckedChange={toggleUnique} defaultChecked={field.unique as boolean} />
</div>
}
{
modifiers.includes(Modifiers.AUTO_INCREMENT) &&
<div className="flex w-full justify-between">
<Label htmlFor="autoincrement">
{t("db_controller.field_settings.autoIncrement")}
</Label>
<Switch id="autoincrement" onCheckedChange={toggleAutoIncrement} defaultChecked={field.autoIncrement as boolean} />
</div>
}
{
showNumericModifiers && <>
<h3 className="font-semibold text-sm text-font/90">
<h3 className="font-medium text-sm">
{t("db_controller.field_settings.numeric_setting")}
</h3>
<hr className="border-divider" />
{
modifiers.includes(Modifiers.AUTO_INCREMENT) &&
<div className="flex w-full justify-between">
<span className="text-xs text-font/70 font-medium dark:text-font/90">
{t("db_controller.field_settings.autoIncrement")}
</span>
<Checkbox defaultSelected={field.autoIncrement as boolean}
classNames={{
wrapper: "before:border-divider group-data-[hover=true]:before:bg-default",
}}
size="md" onValueChange={toggleAutoIncrement} />
</div>
}
<Separator />
{
modifiers.includes(Modifiers.UNSIGNED) &&
<div className="flex w-full justify-between">
<span className="text-xs text-font/70 font-medium dark:text-font/90">
<Label htmlFor="unsigned">
{t("db_controller.field_settings.unsigned")}
</span>
<Checkbox
classNames={{
wrapper: "before:border-divider group-data-[hover=true]:before:bg-default",
}}
defaultSelected={field.unsigned as boolean} size="md" onValueChange={toggleUnsigned} />
</Label>
<Switch id="unsigned" onCheckedChange={toggleUnsigned} defaultChecked={field.unsigned as boolean} />
</div>
}
{
modifiers.includes(Modifiers.ZEROFILL) &&
<div className="flex w-full justify-between">
<span className="text-xs text-font/70 font-medium dark:text-font/90">
<Label htmlFor="zerofill">
{t("db_controller.field_settings.zeroFill")}
</span>
<Checkbox
classNames={{
wrapper: "before:border-divider group-data-[hover=true]:before:bg-default",
}}
defaultSelected={field.zeroFill as boolean} size="md" onValueChange={toggleZeroFill} />
</Label>
<Switch id="zerofill" onCheckedChange={toggleZeroFill} defaultChecked={field.zeroFill as boolean} />
</div>
}
<Separator/>
</>
}
{
showDecimalModifiers && <>
<h3 className="font-semibold text-sm text-font/90">
<h3 className="font-medium text-sm ">
{t("db_controller.field_settings.decimal_setting")}
</h3>
<hr className="border-divider" />
<Separator />
<div className="flex gap-2">
{
modifiers.includes(Modifiers.PRECISION) &&
<div className="flex flex-col gap-2 w-full">
<Tooltip>
<TooltipTrigger>
<label className="flex items-center text-font/70 justify-between text-xs font-medium dark:text-font/90">
<TooltipTrigger asChild>
<Label className="flex items-center justify-between font-medium" htmlFor="precision">
<span>
{t("db_controller.field_settings.precision")}
</span>
<CircleHelp className="size-3.5" />
</label>
<IconInfoCircle className="size-3.5" />
</Label>
</TooltipTrigger>
<TooltipContent>
{t("db_controller.field_settings.precision_def")}
</TooltipContent>
</Tooltip>
<Input
id="precision"
type="number"
size="sm"
ref={precisionRef}
defaultValue={String(field.precision)}
onBlur={saveScaleAndPrecision}
onValueChange={validateScaleAndPrecision}
isInvalid={!precisionValidation.isValid}
endContent={
!precisionValidation.isValid && <>
<HeroUITooltip showArrow={true} content={precisionValidation.errorMessage} radius="sm" color="danger">
<TriangleAlert className="size-4 text-danger cursor-default" />
</HeroUITooltip>
</>
}
radius="sm"
variant="faded"
onChange={validateScaleAndPrecision}
aria-invalid={!precisionValidation.isValid}
placeholder={t("db_controller.field_settings.precision")}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary group-data-[focus=true]:border-primary ",
}}
/>
</div>
}
{
modifiers.includes(Modifiers.SCALE) &&
<div className="flex flex-col gap-2 w-full">
<Tooltip>
<TooltipTrigger>
<label className="flex items-center justify-between text-xs font-medium text-icon dark:text-font/90">
<TooltipTrigger asChild>
<Label className="flex items-center justify-between font-medium" htmlFor="scale">
<span>
{t("db_controller.field_settings.scale")}
</span>
<CircleHelp className="size-3.5" />
</label>
<IconInfoCircle className="size-3.5" />
</Label>
</TooltipTrigger>
<TooltipContent>
{t("db_controller.field_settings.scale_def")}
</TooltipContent>
</Tooltip>
<Input
type="number"
size="sm"
ref={scaleRef}
defaultValue={String(field.scale)}
onBlur={saveScaleAndPrecision}
isDisabled={!precisionRef.current?.value || !precisionValidation.isValid}
onValueChange={validateScaleAndPrecision}
isInvalid={!scaleValidation.isValid}
endContent={
!scaleValidation.isValid && <>
<HeroUITooltip showArrow={true} content={scaleValidation.errorMessage} radius="sm" color="danger">
<TriangleAlert className="size-4 text-danger cursor-default" />
</HeroUITooltip>
</>
}
radius="sm"
variant="faded"
placeholder={t("db_controller.field_settings.scale")}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary group-data-[focus=true]:border-primary ",
}}
{
mountScale &&
<Input
id="scale"
type="number"
ref={scaleRef}
defaultValue={String(field.scale)}
onBlur={saveScaleAndPrecision}
disabled={!precisionRef.current?.value || !precisionValidation.isValid}
onChange={validateScaleAndPrecision}
aria-invalid={!scaleValidation.isValid}
placeholder={t("db_controller.field_settings.scale")}
/>
}
/>
</div>
}
</div>
{
!precisionValidation.isValid &&
<p className="text-destructive text-xs">
{precisionValidation.errorMessage}
</p>
}
{
!scaleValidation.isValid &&
<p className="text-destructive text-xs">
{scaleValidation.errorMessage}
</p>
}
</>
}
{
showTextModifiers &&
<>
<h3 className="font-semibold text-sm text-font/90">
<h3 className="font-medium text-sm ">
{t("db_controller.field_settings.text_setting")}
</h3>
<hr className="border-divider" />
<Separator />
{
modifiers.includes(Modifiers.CHARSET) && charsets && <>
<label className="text-xs font-medium text-icon dark:text-font/90">
(modifiers.includes(Modifiers.CHARSET) && charsets) && <>
<Label htmlFor="charset">
{t("db_controller.field_settings.charset")}
</label>
</Label>
<Select
size="sm"
variant="bordered"
aria-label="charset"
placeholder={t("db_controller.field_settings.charset")}
selectedKeys={charset as any}
onSelectionChange={changeCharset}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
trigger: "border-divider group-hover:border-primary data-[focus=true]:border-primary data-[open=true]:border-primary",
selectorIcon: "text-icon",
popoverContent: "rounded-md "
}}
value={charset as any}
onValueChange={changeCharset as any}
>
{
Object.values(charsets).map((charset: string) => (<SelectItem key={charset}>{charset}</SelectItem>))
}
<SelectTrigger id="charset" className="w-full flex ">
<SelectValue placeholder={t("db_controller.field_settings.charset")} />
</SelectTrigger>
<SelectContent>
<SelectItem value={"none"} >
No charset
</SelectItem>
{
Object.values(charsets).map((charset: string) => (
<SelectItem
key={charset}
value={charset}
>
{charset}
</SelectItem>
))
}
</SelectContent>
</Select>
</>
}
{
modifiers.includes(Modifiers.COLLATE) && collations && <>
<label className="text-xs font-medium text-icon dark:text-font/90">
(modifiers.includes(Modifiers.COLLATE) && collations) && <>
<Label htmlFor="collation">
{t("db_controller.field_settings.collation")}
</label>
</Label>
<Select
size="sm"
variant="bordered"
aria-label="collation"
placeholder={t("db_controller.field_settings.collation")}
selectedKeys={collation as any}
onSelectionChange={changeCollation}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
trigger: "border-divider group-hover:border-primary data-[focus=true]:border-primary data-[open=true]:border-primary",
selectorIcon: "text-icon",
popoverContent: "rounded-md "
value={collation as any}
onValueChange={changeCollation as any}
}}
>
{
Object.values(collations).map((collation: string) => (<SelectItem key={collation}>{collation}</SelectItem>))
}
<SelectTrigger id="collation" className="w-full">
<SelectValue placeholder={t("db_controller.field_settings.collation")} />
</SelectTrigger>
<SelectContent>
<SelectItem value={"none"} >
No collation
</SelectItem>
{
Object.values(collations).map((collation: string) => (<SelectItem
key={collation}
value={collation}>{collation}</SelectItem>))
}
</SelectContent>
</Select>
</>
}
@@ -495,99 +488,87 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
}
{
modifiers.includes(Modifiers.LENGTH) && <>
<label className="text-xs font-medium text-font/70 dark:text-font/90">
<Label htmlFor="length">
{
field.type?.type == DataTypes.INTEGER ?
t("db_controller.field_settings.integer_width")
:
t("db_controller.field_settings.max_length")
}
</label>
</Label>
<Input
type="number"
size="sm"
id="length"
ref={maxLengthRef}
defaultValue={String(field.maxLength)}
onBlur={saveMaxLength}
radius="sm"
isInvalid={!maxLengthValidation.isValid}
onValueChange={maxLengthChange}
endContent={
!maxLengthValidation.isValid && <>
<HeroUITooltip showArrow={true} content={maxLengthValidation.errorMessage} radius="sm" color="danger">
<TriangleAlert className="size-4 text-danger cursor-default" />
</HeroUITooltip>
</>
}
variant="faded"
aria-invalid={!maxLengthValidation.isValid}
onChange={maxLengthChange}
placeholder={
field.type?.type == DataTypes.INTEGER ?
t("db_controller.field_settings.width")
:
t("db_controller.field_settings.max_length")
}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary group-data-[focus=true]:border-primary ",
}}
/>
{
!maxLengthValidation.isValid &&
<p className="text-destructive text-xs">
{maxLengthValidation.errorMessage}
</p>
}
</>
}
{
modifiers.includes(Modifiers.VALUES) && <>
<h3 className="font-semibold text-sm text-font/90">
<Label htmlFor="values">
{field.type.name != null && (field.type.name?.[0].toUpperCase() + field.type.name?.slice(1))} {t("db_controller.field_settings.values")}
</h3>
<hr className="border-divider" />
<TagInput
onItemsChange={updateValues}
defaultItems={field.values ? JSON.parse(field.values) : []}
</Label>
<Separator />
<InputTags
value={jsonValues}
onChange={updateValues as any}
placeholder="Enter values, comma separated..."
/>
</>
}
{
!modifiers.includes(Modifiers.NO_DEFAULT) &&
<FieldDefaultValue field={field} />
<>
<Separator />
<FieldDefaultValue field={field} />
</>
}
<label className="text-xs font-medium text-font/70 dark:text-font/90">
<Label htmlFor="collation">
{t("db_controller.field_settings.note")}
</label>
</Label>
<Textarea
variant="bordered"
className="w-full"
label={t("db_controller.field_settings.field_note")}
placeholder={t("db_controller.field_settings.field_note")}
value={note}
disableAutosize
disableAnimation
onValueChange={setNote}
onChange={(event: any) => setNote(event.target.value)}
onBlur={updateFieldNote}
classNames={{
base: "max-w-xs",
input: "resize-y min-h-[60px] max-h-[180px]",
inputWrapper: "bg-default border-divider dark:bg-background-100 group-hover:border-primary group-data-[focus=true]:border-primary",
label: "text-font/90 group-data-[focus=true]:text-font/70 group-data-[filled-within=true]:text-font/70 "
}} />
<hr className="border-divider" />
className="resize-none min-h-[86px] focus-visible:ring-0 bg-secondary dark:bg-background"
/>
<Separator />
<Button
className="bg-default dark:bg-danger dark:border-none dark:text-white"
radius="sm"
variant="faded"
color="danger"
variant={"destructive"}
size="sm"
onPressEnd={removeField}>
<span className="font-medium text-sm ">
{t("db_controller.field_settings.delete_field")}
</span>
<Trash2 className="mr-1 size-3.5 text-danger dark:text-white" />
onClick={removeField}>
{t("db_controller.field_settings.delete_field")}
<IconTrash className="size-4" />
</Button>
</div>
)
}
export default React.memo(FieldSetting);
export default React.memo(FieldSetting);
@@ -0,0 +1,147 @@
import { MultiSelect } from "@/components/multi-select";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { FieldType } from "@/lib/schemas/field-schema";
import { FieldIndexType } from "@/lib/schemas/field_index-schema";
import { IndexInsertType, IndexType } from "@/lib/schemas/index-schema";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { areArraysEqual } from "@/utils/utils";
import { IconTrash } from "@tabler/icons-react";
import { Settings2 } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
interface Props {
index: IndexType;
fields: FieldType[]
}
const IndexItem: React.FC<Props> = ({ index, fields }) => {
const { t } = useTranslation();
const { editIndex, deleteIndex, editFieldIndices } = useDatabaseOperations();
const [isUnique, setIsUnique] = useState<boolean>(index.unique as boolean);
const [fieldIndices, setFieldIndices] = useState<string[]>([]);
useEffect(() => {
setFieldIndices(
index.fieldIndices.map((fieldIndex: FieldIndexType) => fieldIndex.fieldId)
)
}, [index.fieldIndices]);
useEffect(() => {
setIsUnique(index.unique as boolean);
}, [index.unique])
const toggleUnique = (unique: boolean) => {
setIsUnique(unique);
editIndex({
id: index.id,
unique
} as IndexInsertType);
}
const editIndexName = (event: any) => {
editIndex({
id: index.id,
name: event.target.value
} as IndexInsertType);
}
const removeIndex = () => {
deleteIndex(index.id)
}
const updateFieldIndices = useCallback((fieldIds: string[]) => {
setFieldIndices(fieldIds);
if (!areArraysEqual(fieldIds, index.fieldIndices.map((fieldIndex: FieldIndexType) => fieldIndex.fieldId)))
editFieldIndices(index.id, fieldIds);
}, [index.fieldIndices])
return (
<div className="flex gap-2 w-full items-center justify-stretch">
<div className="flex w-full flex-1 ">
<MultiSelect
options={
fields.map((field: FieldType) => ({ value: field.id, label: field.name })) as any
}
onValueChange={updateFieldIndices}
defaultValue={fieldIndices}
placeholder={t("db_controller.select_fields")}
variant={"secondary"}
hideSelectAll
className="dark:bg-background"
/>
</div>
<div className="shrink-0 ">
<Popover >
<PopoverTrigger asChild>
<Button
size="icon"
variant={"ghost"}
className="dark:bg-card dark:border-none size-9 text-muted-foreground hover:text-foreground"
>
<Settings2 className="size-4 " />
</Button>
</PopoverTrigger>
<PopoverContent side="right" className="max-w-[210px]">
<div className="w-full flex flex-col gap-2 ">
<h3 className="font-medium text-sm ">
{t("db_controller.index_setting")}
</h3>
<Separator />
<div className="flex items-center justify-between ">
<Label htmlFor="unique">
{t("db_controller.unique")}
</Label>
<Switch id="unique" onCheckedChange={toggleUnique} checked={isUnique} />
</div>
<Label htmlFor="name">
{t("db_controller.name")}
</Label>
<Input
id="name"
aria-label={t("db_controller.index_name")}
placeholder={t("db_controller.index_name")}
onBlur={editIndexName}
autoFocus
defaultValue={index.name}
/>
<Separator />
<Button
variant="destructive"
size="sm"
onClick={removeIndex}>
<span >
{t("db_controller.delete_index")}
</span>
<IconTrash className="size-4" />
</Button>
</div>
</PopoverContent>
</Popover>
</div>
</div>
)
}
export default IndexItem;
@@ -1,26 +1,24 @@
import { Button, } from "@heroui/react";
import IndexItem from "./index-item";
import { Plus } from "lucide-react";
import { useTranslation } from "react-i18next";
import { IndexInsertType, IndexType } from "@/lib/schemas/index-schema";
import hash from "object-hash";
import React from "react";
import { FieldType } from "@/lib/schemas/field-schema";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { v4 } from "uuid";
import { IconPlus } from "@tabler/icons-react";
interface IndexesListProps {
indices: IndexType[],
fields: FieldType[],
tableId: string
}
const IndexesList: React.FC<IndexesListProps> = ({ indices, fields, tableId }) => {
const { t } = useTranslation();
const { createIndex } = useDatabaseOperations();
const addIndex = (event: any) => {
@@ -28,26 +26,22 @@ const IndexesList: React.FC<IndexesListProps> = ({ indices, fields, tableId }) =
createIndex({
id: v4(),
name: `index_${indices.length + 1}`,
unique: true,
unique: false,
tableId: tableId
} as IndexInsertType);
}
return (
<div className="w-full flex-col space-y-2">
<div className="w-full flex-col space-y-2 ">
{
indices.map((index: IndexType) => <IndexItem index={index} fields={fields} key={index.id}/>)
indices.map((index: IndexType) => <IndexItem index={index} fields={fields} key={index.id} />)
}
{
indices.length == 0 && <p className="text-muted-foreground py-1">
No Indices found <span className="text-foreground hover:underline cursor-pointer" onClick={addIndex}>
create new one <IconPlus className="size-3.5 shrink-0 inline"/>
</span>
</p>
}
<Button
variant="flat"
radius="sm"
startContent={
<Plus className="size-4 text-font/90" />
}
className="h-8 p-2 text-xs bg-transparent hover:bg-default text-font/90 font-semibold"
onPressEnd={addIndex}
>
{t("db_controller.add_index")}
</Button>
</div>
)
@@ -0,0 +1,171 @@
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import FieldList from "./field/field-list";
import IndexesList from "./index/indexes-list";
import { TableType } from "@/lib/schemas/table-schema";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { getNextSequence } from "@/utils/field";
import { v4 } from "uuid";
import { IndexInsertType } from "@/lib/schemas/index-schema";
import { FieldInsertType } from "@/lib/schemas/field-schema";
import { AccordionContent, Accordion, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { IconBolt , IconFolder, IconMessageCircle, } from "@tabler/icons-react";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import ColorPicker from "@/components/color-picker";
export interface TableAccordionBodyProps {
table: TableType,
keys?: string[];
}
const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys }) => {
const [selectedItems, setSelectedItems] = useState<string[]>(["fields"]);
const [note, setNote] = useState<string>(table.note ? table.note : "");
const { t } = useTranslation();
const { editTable, createField, createIndex, getInteger } = useDatabaseOperations();
const onColorChange = useCallback((color: string | undefined) => {
editTable({ id: table.id, color: color ? color : null } as TableType);
}, [table]);
const addField = (event: any) => {
event.stopPropagation && event.stopPropagation();
createField({
id: v4(),
name: `field_${table.fields.length + 1}`,
tableId: table.id,
sequence: getNextSequence(table.fields),
nullable: true,
typeId: getInteger()?.id
} as FieldInsertType);
}
const addIndex = (event: any) => {
event.stopPropagation && event.stopPropagation();
createIndex({
id: v4(),
name: `index_${table.indices.length + 1}`,
unique: false,
tableId: table.id
} as IndexInsertType);
}
const saveNote = () => {
editTable({
id: table.id,
note
} as TableType)
}
useEffect(() => {
setNote(table.note ? table.note : "");
}, [table.note]);
useEffect(() => {
setSelectedItems([...selectedItems, "fields"])
}, [table.fields.length])
useEffect(() => {
if (table.indices.length > 0)
setSelectedItems([...selectedItems, "indexes"])
}, [table.indices.length])
return (
<AccordionContent className=" p-0 ">
<Accordion
type="multiple"
className="w-full"
value={selectedItems}
onValueChange={setSelectedItems as any}
>
<AccordionItem value="fields" className="border-none">
<AccordionTrigger className="py-1 rounded-none text-muted-foreground pr-1.5" position="right" >
<div className="w-full flex gap-1 items-center">
<IconFolder className="size-4" />
{t("db_controller.fields")}
</div>
</AccordionTrigger>
<AccordionContent className="pb-0 ">
<FieldList tableFields={table.fields} tableId={table.id} />
</AccordionContent>
</AccordionItem>
<AccordionItem value="indexes" className="border-none">
<AccordionTrigger className=" py-1 rounded-none text-muted-foreground pr-1.5" position="right">
<div className="w-full flex gap-1 items-center">
<IconBolt className="size-4" />
{t("db_controller.indexes")}
</div>
</AccordionTrigger>
<AccordionContent className="pb-2">
<IndexesList indices={table.indices} fields={table.fields} tableId={table.id} />
</AccordionContent>
</AccordionItem>
<AccordionItem value="note" className="border-none">
<AccordionTrigger className=" py-1 rounded-none text-muted-foreground pr-1.5" position="right">
<div className="w-full flex gap-1 items-center">
<IconMessageCircle className="size-4 " />
{t("db_controller.note")}
</div>
</AccordionTrigger>
<AccordionContent className="pb-0">
<Textarea
placeholder={t("db_controller.table_note")}
value={note}
onChange={(event : any) => setNote(event.target.value) }
onBlur={saveNote}
className="resize-none min-h-[86px] focus-visible:ring-0 bg-secondary dark:bg-background"
/>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="flex pt-2 justify-between pr-1.5 items-center ">
<div className="h-full flex items-center">
<ColorPicker
defaultColor={table.color as string}
onChange={onColorChange}
/>
</div>
<div className="flex gap-2 ">
<Button
variant="outline"
onClick={addIndex}
size="sm"
className="h-9"
>
{t("db_controller.add_index")}
</Button>
<Button
onClick={addField}
variant="outline"
size="sm"
className="h-9"
>
{t("db_controller.add_field")}
</Button>
</div>
</div>
</AccordionContent>
)
}
export default React.memo(TableAccordionContent);
@@ -0,0 +1,50 @@
import { TableType } from "@/lib/schemas/table-schema";
import {
AccordionContent,
AccordionItem,
} from "@/components/ui/accordion";
import TableAccordionTrigger from "./table-accordion-trigger";
import TableAccordionContent from "./table-accordion-content";
import React, { forwardRef } from "react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
interface TableAccordionItemProps {
table: TableType;
}
const TableAccordionItem = forwardRef<HTMLDivElement, TableAccordionItemProps>(
({ table }, ref) => {
const { attributes, setNodeRef, transform, transition } = useSortable({ id: table.id });
if (transform?.scaleY)
transform.scaleY = 1
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<AccordionItem value={table.id} className=" border-none" id={table.id} ref={ref}>
<div
ref={setNodeRef}
style={style}
{...attributes}
className="w-full "
>
<TableAccordionTrigger table={table} />
<AccordionContent className="flex flex-col gap-4 text-balance">
<TableAccordionContent table={table} />
</AccordionContent>
</div>
</AccordionItem>
);
}
);
TableAccordionItem.displayName = "TableAccordionItem";
export default React.memo(TableAccordionItem);
@@ -0,0 +1,221 @@
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { TableInsertType, TableType } from "@/lib/schemas/table-schema";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { v4 } from "uuid";
import { getNextSequence } from "@/utils/field";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import hash from "object-hash";
import { cloneTable } from "@/utils/tables";
import { IndexInsertType } from "@/lib/schemas/index-schema";
import { FieldInsertType } from "@/lib/schemas/field-schema";
import { AccordionTrigger } from "@/components/ui/accordion";
import { IconBolt, IconCheck, IconCopy, IconDotsVertical, IconFocus2, IconFolder, IconGripVertical, IconPencil, IconTrash } from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { DropdownMenuTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuItem, DropdownMenuShortcut } from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useSortable } from "@dnd-kit/sortable";
export interface TableAccordionHeaderProps {
table: TableType,
isOpen?: boolean,
}
const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOpen }) => {
const { editTable, deleteTable, createField, createTable, createIndex, getInteger } = useDatabaseOperations();
const [tableName, setTableName] = useState<string>(table.name);
const { t } = useTranslation();
const [editMode, setEditMode] = useState<boolean>(false);
const { focusOnTable } = useDiagramOps();
useEffect(() => {
setTableName(table.name);
}, [table.name])
const saveTableName = useCallback(async () => {
await editTable({ id: table.id, name: tableName } as TableInsertType);
setEditMode(false);
}, [tableName])
const onDeleteTable = useCallback(async (event: any) => {
event.stopPropagation();
deleteTable(table.id)
}, [table]);
const addField = useCallback((event: any) => {
event.stopPropagation();
createField({
id: v4(),
name: `field_${table.fields.length + 1}`,
tableId: table.id,
sequence: getNextSequence(table.fields),
nullable: true,
typeId: getInteger()?.id
} as FieldInsertType)
}, [table, getInteger])
const addIndex = useCallback((event: any) => {
event.stopPropagation();
createIndex({
id: v4(),
name: `index_${table.indices.length + 1}`,
unique: false,
tableId: table.id
} as IndexInsertType);
}, [table])
const duplicate = useCallback(async (event : any) => {
event.stopPropagation()
const clonedTable: TableType = cloneTable(table);
await createTable(clonedTable)
focusOnTable(clonedTable.id);
}, [table]);
const { listeners } = useSortable({ id: table.id });
return (
<AccordionTrigger className="group h-12 py-3 "
leftContent={
<div className="h-full bg-secondary w-1 rounded-lg shrink-0 bg-primary" style={{
backgroundColor: table.color as string
}}>
</div>
}
>
<div {...listeners}>
<IconGripVertical className="size-4 text-muted-foreground hover:text-foreground cursor-move shrink-0" />
</div>
{
!editMode &&
<>
<div className="w-full flex items-center ">
<Tooltip>
<TooltipTrigger asChild>
<label
className=" py-2 truncate text-sm cursor-pointer max-w-60"
onDoubleClick={() => setEditMode(true)}
onClick={((event: any) => event.stopPropagation())}
>
{tableName}
</label>
</TooltipTrigger>
<TooltipContent>
{t("table.double_click")}
</TooltipContent>
</Tooltip>
</div>
<div className="hidden !shrink-0 flex-row group-hover:flex gap-2 ">
<Button variant="outline" size="icon" className="size-8 shrink-0" onClick={(event: any) => {
event.stopPropagation();
setEditMode(true)
}}>
<IconPencil className="size-4 text-muted-foreground " />
</Button>
<Button variant="outline" size="icon" className="size-8 shrink-0 " onClick={(event: any) => {
event.stopPropagation();
focusOnTable(table.id, true)
}}>
<IconFocus2 className="size-4 text-muted-foreground " />
</Button>
</div>
</>
}
{
editMode && <>
<Input
placeholder={"Table name"}
value={tableName}
onChange={(event: any) => setTableName(event.target.value)}
onBlur={saveTableName}
autoFocus
type="text"
onClick={(event) => event.stopPropagation()}
className="h-8"
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault();
saveTableName();
e.target.blur();
}
}}
/>
<Button variant="ghost" size="icon" className="size-8 shrink-0 rounded-sm" onClick={saveTableName} >
<IconCheck className="size-4 text-muted-foreground " />
</Button>
</>
}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="size-8 shrink-0 flex rounded-sm" >
<IconDotsVertical className="size-4 text-muted-foreground " />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[164px]">
<DropdownMenuLabel>
{t("db_controller.table_actions")}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={addField}>
{t("db_controller.add_field")}
<DropdownMenuShortcut>
<IconFolder className="size-4" />
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem onClick={addIndex}>
{t("db_controller.add_index")}
<DropdownMenuShortcut>
<IconBolt className="size-4" />
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem onClick={duplicate}>
{t("db_controller.duplicate")}
<DropdownMenuShortcut>
<IconCopy className="size-4 " />
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem variant="destructive" onClick={onDeleteTable}>
{t("db_controller.delete_table")}
<DropdownMenuShortcut>
<IconTrash className="size-4 text-destructive" />
</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</AccordionTrigger>
)
}
export default React.memo(TableAccordionHeader, (prevState, newState) => {
return hash(prevState) == hash(newState);
});

Some files were not shown because too many files have changed in this diff Show More