Make more UI improvements
This commit is contained in:
parent
cae57e81e3
commit
0cd28d7394
11 changed files with 640 additions and 337 deletions
|
|
@ -48,6 +48,8 @@
|
|||
"@tailwindcss/vite": "^4.0.14",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"ag-grid-community": "^33.2.1",
|
||||
"ag-grid-react": "^33.2.1",
|
||||
"axios": "^1.8.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"react-router-dom": "^7.3.0",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@ importers:
|
|||
'@types/react-router-dom':
|
||||
specifier: ^5.3.3
|
||||
version: 5.3.3
|
||||
ag-grid-community:
|
||||
specifier: ^33.2.1
|
||||
version: 33.2.1
|
||||
ag-grid-react:
|
||||
specifier: ^33.2.1
|
||||
version: 33.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
axios:
|
||||
specifier: ^1.8.4
|
||||
version: 1.8.4
|
||||
|
|
@ -1469,6 +1475,18 @@ packages:
|
|||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
ag-charts-types@11.2.1:
|
||||
resolution: {integrity: sha512-uzN1OUEn5nCFDZ4GTNkYHpg+6hbF+NamIwUOK/aSHBRvJxJU9/sK+K1QkqYpU912mHtpAZ9x0zEddr2sw6pT2Q==}
|
||||
|
||||
ag-grid-community@33.2.1:
|
||||
resolution: {integrity: sha512-eQVRv+x8C3+T2weBux7Y+SN6IMs4lYJjTmNSfm/OX3ANH3GsscvqRiOxoV/R+hQWL7GUXOCLPZHFzZpzSdk0xg==}
|
||||
|
||||
ag-grid-react@33.2.1:
|
||||
resolution: {integrity: sha512-06Jo2fi90Ke/ZXM1kvcWa5MX+9DM3IKVuCgGeTrQn8PzOOccYWBvVPGwLXk1TAQ2b9kQfKwDCU5q5JAJ6+tPUg==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
ajv@6.12.6:
|
||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||
|
||||
|
|
@ -4618,6 +4636,19 @@ snapshots:
|
|||
|
||||
acorn@8.14.0: {}
|
||||
|
||||
ag-charts-types@11.2.1: {}
|
||||
|
||||
ag-grid-community@33.2.1:
|
||||
dependencies:
|
||||
ag-charts-types: 11.2.1
|
||||
|
||||
ag-grid-react@33.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
ag-grid-community: 33.2.1
|
||||
prop-types: 15.8.1
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
|
||||
ajv@6.12.6:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
|
|
|
|||
|
|
@ -12,6 +12,14 @@ import { SessionProvider } from "./contexts/SessionContext";
|
|||
import { OAuthSigninPage } from "./pages/oauth-signin";
|
||||
import { NotFoundPage } from "./pages/NotFoundPage";
|
||||
import { Layout } from "./components/Layout";
|
||||
import { DevisPage } from "./pages/devis";
|
||||
import { FacturesPage } from "./pages/factures";
|
||||
import { PlanningPage } from "./pages/planning";
|
||||
import { ChantiersPage } from "./pages/chantiers";
|
||||
import { AllCommunityModule, ModuleRegistry } from "ag-grid-community";
|
||||
|
||||
// Register all Community features
|
||||
ModuleRegistry.registerModules([AllCommunityModule]);
|
||||
|
||||
export const App = () => {
|
||||
return (
|
||||
|
|
@ -25,15 +33,47 @@ export const App = () => {
|
|||
)}
|
||||
>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<Layout>
|
||||
<ProtectedRoute fallback="/login" />
|
||||
</Layout>
|
||||
}
|
||||
>
|
||||
<Route index element={<TabloPage />} />
|
||||
<Route path="/" element={<ProtectedRoute fallback="/login" />}>
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<Layout>
|
||||
<TabloPage />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="devis"
|
||||
element={
|
||||
<Layout>
|
||||
<DevisPage />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="factures"
|
||||
element={
|
||||
<Layout>
|
||||
<FacturesPage />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="planning"
|
||||
element={
|
||||
<Layout>
|
||||
<PlanningPage />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="chantiers"
|
||||
element={
|
||||
<Layout>
|
||||
<ChantiersPage />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="login-with-oauth" element={<OAuthSigninPage />} />
|
||||
<Route path="landing" element={<LandingPage />} />
|
||||
|
|
|
|||
|
|
@ -1,16 +1,41 @@
|
|||
import { ReactNode } from "react";
|
||||
import { SideNavigation, HamburgerMenu } from "./NavigationBar";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { SideNavigation } from "./NavigationBar";
|
||||
import { Button } from "../ui-library/button";
|
||||
import { Icon } from "../ui-library/icon";
|
||||
import { MenuIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface LayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Layout({ children }: LayoutProps) {
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<HamburgerMenu />
|
||||
<SideNavigation />
|
||||
<main className="flex-1 overflow-auto">{children}</main>
|
||||
<Button
|
||||
variant="plain"
|
||||
isIconOnly
|
||||
className="fixed top-4 left-4 z-50 md:hidden"
|
||||
onPress={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
>
|
||||
<Icon>
|
||||
<MenuIcon className="h-6 w-6" />
|
||||
</Icon>
|
||||
</Button>
|
||||
|
||||
<div
|
||||
className={twMerge(
|
||||
"fixed md:relative transition-all duration-300 z-40",
|
||||
isMobileMenuOpen
|
||||
? "translate-x-0"
|
||||
: "-translate-x-full md:translate-x-0"
|
||||
)}
|
||||
>
|
||||
<SideNavigation />
|
||||
</div>
|
||||
|
||||
<main className="flex-1 overflow-auto p-4 md:p-6">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,45 @@
|
|||
import { twMerge } from "tailwind-merge";
|
||||
// import { useSession } from "../contexts/SessionContext";
|
||||
import {
|
||||
HomeIcon,
|
||||
TableIcon,
|
||||
SettingsIcon,
|
||||
UserIcon,
|
||||
HelpCircleIcon,
|
||||
SendIcon,
|
||||
Menu,
|
||||
Settings2Icon,
|
||||
LogOutIcon,
|
||||
SearchIcon,
|
||||
MenuIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronLeftIcon,
|
||||
Settings,
|
||||
ConstructionIcon,
|
||||
PlusIcon,
|
||||
MinusIcon,
|
||||
} from "lucide-react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import {
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuItemLabel,
|
||||
MenuSeparator,
|
||||
MenuTrigger,
|
||||
} from "../ui-library/menu";
|
||||
import { MenuPopover } from "../ui-library/menu";
|
||||
Separator,
|
||||
Switch,
|
||||
Menu,
|
||||
} from "react-aria-components";
|
||||
import { MenuButton, MenuItemLabel, MenuPopover } from "../ui-library/menu";
|
||||
import { Link } from "../ui-library/link";
|
||||
import { Icon } from "../ui-library/icon";
|
||||
import { Avatar } from "../ui-library/avatar";
|
||||
import { DialogHeader } from "../ui-library/dialog";
|
||||
import { Avatar, AvatarBadge } from "../ui-library/avatar";
|
||||
import { Dialog } from "../ui-library/dialog";
|
||||
import { Button } from "../ui-library/button";
|
||||
import { Modal } from "../ui-library/modal";
|
||||
import { DialogTrigger } from "../ui-library/dialog";
|
||||
import { DialogCloseButton } from "../ui-library/dialog";
|
||||
import { DialogBody } from "../ui-library/dialog";
|
||||
import {
|
||||
DisclosurePanel,
|
||||
DisclosureControl,
|
||||
Disclosure,
|
||||
} from "../ui-library/disclosure";
|
||||
import { LinkProps } from "react-aria-components";
|
||||
import { Popover } from "../ui-library/popover";
|
||||
import {
|
||||
AvailableIcon,
|
||||
AwayIcon,
|
||||
BusyIcon,
|
||||
DoNotDisturbIcon,
|
||||
} from "../ui-library/icons";
|
||||
import { useState, useRef } from "react";
|
||||
import logo from "../assets/icon.jpg";
|
||||
|
||||
type NavLinkItem = {
|
||||
isActive?: boolean;
|
||||
|
|
@ -87,256 +89,270 @@ function NavLink(props: NavLinkProps) {
|
|||
: [
|
||||
"font-medium",
|
||||
"text-foreground/70 [&:not(:hover)>[data-ui=icon]]:text-foreground/35",
|
||||
],
|
||||
rest.className
|
||||
]
|
||||
)}
|
||||
/>
|
||||
>
|
||||
{props.children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarMenuPopover() {
|
||||
return (
|
||||
<MenuPopover placement="top left" className="min-w-64">
|
||||
<Menu>
|
||||
<MenuItem>Clear status</MenuItem>
|
||||
<MenuSeparator />
|
||||
<MenuItem>
|
||||
<Icon>
|
||||
<UserIcon />
|
||||
</Icon>
|
||||
<MenuItemLabel>My profile</MenuItemLabel>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<Icon>
|
||||
<Settings2Icon />
|
||||
</Icon>
|
||||
<MenuItemLabel>Settings</MenuItemLabel>
|
||||
</MenuItem>
|
||||
<MenuSeparator />
|
||||
export function ControlledOpenState({ isCollapsed }: { isCollapsed: boolean }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const ref = useRef(null);
|
||||
|
||||
<MenuItem>
|
||||
<Icon>
|
||||
<LogOutIcon />
|
||||
</Icon>
|
||||
<MenuItemLabel>Sign out</MenuItemLabel>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</MenuPopover>
|
||||
);
|
||||
}
|
||||
|
||||
export function SideNavigation() {
|
||||
return (
|
||||
<div className="group isolate hidden w-64 flex-col overflow-y-auto md:flex">
|
||||
<div className="bg-background sticky top-0 left-0 z-10 flex items-center justify-between gap-x-2.5">
|
||||
<div className="flex flex-1 items-center overflow-hidden px-4 pt-4 pb-2">
|
||||
<MenuTrigger>
|
||||
<MenuButton
|
||||
variant="outline"
|
||||
className="flex-1 gap-x-2.5 overflow-hidden rounded-lg font-semibold sm:px-1.5"
|
||||
>
|
||||
<>
|
||||
<Button
|
||||
aria-label="Settings"
|
||||
onPress={() => setIsOpen(true)}
|
||||
ref={ref}
|
||||
className={twMerge(
|
||||
"flex items-center gap-2",
|
||||
isCollapsed && "justify-center"
|
||||
)}
|
||||
>
|
||||
<Icon>
|
||||
<Settings />
|
||||
</Icon>
|
||||
<span
|
||||
className={twMerge(
|
||||
"transition-all duration-300",
|
||||
isCollapsed ? "opacity-0 w-0" : "opacity-100"
|
||||
)}
|
||||
>
|
||||
Settings
|
||||
</span>
|
||||
</Button>
|
||||
<Popover
|
||||
className="min-w-56 rounded-xl"
|
||||
isOpen={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
triggerRef={ref}
|
||||
>
|
||||
<Dialog aria-label="Settings">
|
||||
<div className="flex flex-col gap-2 p-3">
|
||||
<div className="flex gap-4">
|
||||
<Avatar
|
||||
alt="Acme"
|
||||
className="size-6 [--border-radius:0.25rem]"
|
||||
// fallbackBackground="black"
|
||||
/>
|
||||
<span className="truncate"> Acme, Inc</span>
|
||||
</MenuButton>
|
||||
<MenuPopover placement="bottom left">
|
||||
<Menu>
|
||||
<MenuItem>Item 1</MenuItem>
|
||||
<MenuItem>Item 2</MenuItem>
|
||||
</Menu>
|
||||
</MenuPopover>
|
||||
</MenuTrigger>
|
||||
</div>
|
||||
</div>
|
||||
src="https://i.imgur.com/xIe7Wlb.png"
|
||||
alt="Marissa Whitaker"
|
||||
>
|
||||
<AvatarBadge badge={<AvailableIcon aria-label="Available" />} />
|
||||
</Avatar>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold">Lisa Wilson</span>
|
||||
<span className="sm:leading-4">Admin</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MainNavigation />
|
||||
<Separator />
|
||||
|
||||
<div className="bg-background sticky bottom-0 left-0 flex px-2 py-4">
|
||||
<MenuTrigger>
|
||||
<MenuButton
|
||||
variant="plain"
|
||||
className="flex-1 justify-start overflow-hidden font-normal"
|
||||
<MenuTrigger>
|
||||
<MenuButton
|
||||
variant="plain"
|
||||
className="justify-start gap-3 font-medium"
|
||||
>
|
||||
<AvailableIcon className="size-3" />
|
||||
Available
|
||||
</MenuButton>
|
||||
<MenuPopover placement="end top">
|
||||
<Menu>
|
||||
<MenuItem>
|
||||
<AvailableIcon className="size-3" />
|
||||
<MenuItemLabel>Available</MenuItemLabel>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<BusyIcon className="size-3" />
|
||||
<MenuItemLabel>Busy</MenuItemLabel>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<AwayIcon className="size-3" />
|
||||
<MenuItemLabel>Away</MenuItemLabel>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<DoNotDisturbIcon className="size-3" />
|
||||
<MenuItemLabel>Do not disturb</MenuItemLabel>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</MenuPopover>
|
||||
</MenuTrigger>
|
||||
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-center justify-between px-3",
|
||||
"text-sm font-medium"
|
||||
)}
|
||||
>
|
||||
<span>Notifications</span>
|
||||
<Switch defaultSelected />
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-center justify-between px-3",
|
||||
"text-sm font-medium"
|
||||
)}
|
||||
>
|
||||
<span>Badges</span>
|
||||
<Switch />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const SideNavigation = () => {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"group isolate flex flex-col overflow-y-auto bg-gray-200 dark:bg-gray-900 transition-all duration-300",
|
||||
"fixed md:relative h-screen z-50",
|
||||
isCollapsed ? "w-14" : "w-52",
|
||||
"md:flex",
|
||||
"transform md:transform-none",
|
||||
isCollapsed ? "-translate-x-0" : "-translate-x-full md:translate-x-0"
|
||||
)}
|
||||
>
|
||||
<div className="relative flex flex-col items-center px-3 py-3">
|
||||
<RouterLink
|
||||
to="/"
|
||||
className={twMerge(
|
||||
"flex flex-col items-center gap-2",
|
||||
isCollapsed ? "w-full justify-center" : ""
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={logo}
|
||||
alt="Logo XTablo"
|
||||
className={twMerge(isCollapsed ? "w-8 h-8" : "w-15 h-15")}
|
||||
/>
|
||||
<h1
|
||||
className={twMerge(
|
||||
"text-lg font-bold transition-all duration-300 text-slate-900 dark:text-white whitespace-nowrap",
|
||||
isCollapsed ? "w-0 opacity-0 h-0" : "w-auto opacity-100"
|
||||
)}
|
||||
>
|
||||
<Avatar
|
||||
src="https://i.imgur.com/xIe7Wlb.png"
|
||||
alt="Marissa Whitaker"
|
||||
className="size-8"
|
||||
/>
|
||||
XTablo
|
||||
</h1>
|
||||
</RouterLink>
|
||||
<Button
|
||||
variant="plain"
|
||||
isIconOnly
|
||||
onPress={() => setIsCollapsed(!isCollapsed)}
|
||||
className={twMerge(
|
||||
isCollapsed ? "relative" : "absolute top-2 right-2",
|
||||
"size-5 p-1",
|
||||
"text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100",
|
||||
"transition-all duration-300",
|
||||
"bg-gray-200 dark:bg-gray-900",
|
||||
"rounded-full shadow-md",
|
||||
"opacity-0 group-hover:opacity-100",
|
||||
"hover:scale-110"
|
||||
)}
|
||||
>
|
||||
<Icon>{isCollapsed ? <PlusIcon /> : <MinusIcon />}</Icon>
|
||||
</Button>
|
||||
</div>
|
||||
<MainNavigation isCollapsed={isCollapsed} />
|
||||
|
||||
<span className="text-foreground/70 truncate font-medium">
|
||||
Marissa Whitaker
|
||||
</span>
|
||||
</MenuButton>
|
||||
<AvatarMenuPopover />
|
||||
</MenuTrigger>
|
||||
<div className="bg-gray-200 dark:bg-gray-900 sticky bottom-0 left-0 flex px-2 py-3">
|
||||
<ControlledOpenState isCollapsed={isCollapsed} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HamburgerMenu() {
|
||||
return (
|
||||
<header className="sticky top-0 left-0 flex h-14 items-center px-4 md:hidden">
|
||||
<DialogTrigger>
|
||||
<Button variant="plain" isIconOnly className="text-muted lg:hidden">
|
||||
<Icon aria-label="Open Navigation">
|
||||
<MenuIcon />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Modal drawer size="xs" isDismissable>
|
||||
<Dialog className="h-full">
|
||||
<DialogHeader className="p-0 pt-16">
|
||||
<div className="flex flex-1 gap-x-2 px-2.5 pb-3">
|
||||
<MenuTrigger>
|
||||
<MenuButton
|
||||
variant="outline"
|
||||
className="flex-1 gap-x-2.5 overflow-hidden rounded-lg font-semibold sm:px-1.5"
|
||||
>
|
||||
<Avatar
|
||||
alt="Acme"
|
||||
className="size-6 [--border-radius:0.25rem]"
|
||||
// fallbackBackground="black"
|
||||
/>
|
||||
<span className="truncate"> Acme, Inc</span>
|
||||
</MenuButton>
|
||||
<MenuPopover placement="bottom left">
|
||||
<Menu>
|
||||
<MenuItem>Item 1</MenuItem>
|
||||
<MenuItem>Item 2</MenuItem>
|
||||
</Menu>
|
||||
</MenuPopover>
|
||||
</MenuTrigger>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<DialogCloseButton />
|
||||
|
||||
<DialogBody className="px-0">
|
||||
<MainNavigation />
|
||||
</DialogBody>
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
|
||||
<div className="ml-auto flex items-center gap-4 px-2">
|
||||
<Button isIconOnly variant="plain">
|
||||
<Icon aria-label="Search">
|
||||
<SearchIcon />
|
||||
</Icon>
|
||||
</Button>
|
||||
<MenuTrigger>
|
||||
<MenuButton variant="plain" buttonArrow={null}>
|
||||
<Avatar
|
||||
className="size-8"
|
||||
src="https://i.imgur.com/xIe7Wlb.png"
|
||||
alt="Marissa Whitaker"
|
||||
/>
|
||||
</MenuButton>
|
||||
<AvatarMenuPopover />
|
||||
</MenuTrigger>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export function MainNavigation() {
|
||||
// const location = useLocation();
|
||||
// const { session } = useSession();
|
||||
|
||||
// const isActive = (path: string) => {
|
||||
// return location.pathname === path;
|
||||
// };
|
||||
};
|
||||
|
||||
export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
|
||||
const navItems = [
|
||||
{ path: "/", label: "Accueil", icon: <HomeIcon className="w-5 h-5" /> },
|
||||
{
|
||||
path: "/tablo",
|
||||
label: "Tableaux",
|
||||
icon: <TableIcon className="w-5 h-5" />,
|
||||
path: "/",
|
||||
label: "Tableau de Bord",
|
||||
icon: <TableIcon className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
label: "Paramètres",
|
||||
icon: <SettingsIcon className="w-5 h-5" />,
|
||||
path: "/devis",
|
||||
label: "Devis",
|
||||
icon: <SettingsIcon className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
path: "/factures",
|
||||
label: "Factures",
|
||||
icon: <SettingsIcon className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
path: "/planning",
|
||||
label: "Planning",
|
||||
icon: <SettingsIcon className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
path: "/chantiers",
|
||||
label: "Chantiers",
|
||||
icon: <ConstructionIcon className="w-4 h-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul className="grid gap-y-1 p-4">
|
||||
<ul className="grid gap-y-0.5 px-2 py-3">
|
||||
{navItems.map(({ path, label, icon }) => (
|
||||
<li key={label}>
|
||||
<NavLink to={path}>
|
||||
<Icon>{icon}</Icon>
|
||||
{label}
|
||||
<NavLink>
|
||||
<RouterLink to={path}>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Icon>{icon}</Icon>
|
||||
<span
|
||||
className={twMerge(
|
||||
"text-sm transition-all duration-300",
|
||||
isCollapsed ? "opacity-0 w-0" : "opacity-100"
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<ul className="mt-auto grid gap-y-1 p-4">
|
||||
<ul className="mt-auto grid gap-y-0.5 px-2 py-3">
|
||||
<li>
|
||||
<NavLink to="/">
|
||||
<HelpCircleIcon />
|
||||
Support
|
||||
<NavLink>
|
||||
<RouterLink to="/">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<HelpCircleIcon className="w-4 h-4" />
|
||||
<span
|
||||
className={twMerge(
|
||||
"text-sm transition-all duration-300",
|
||||
isCollapsed ? "opacity-0 w-0" : "opacity-100"
|
||||
)}
|
||||
>
|
||||
Support
|
||||
</span>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink to="/">
|
||||
<SendIcon />
|
||||
Feedback
|
||||
<NavLink>
|
||||
<RouterLink to="/">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<SendIcon className="w-4 h-4" />
|
||||
<span
|
||||
className={twMerge(
|
||||
"text-sm transition-all duration-300",
|
||||
isCollapsed ? "opacity-0 w-0" : "opacity-100"
|
||||
)}
|
||||
>
|
||||
Feedback
|
||||
</span>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <div className="flex flex-col h-screen w-64 bg-white dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700">
|
||||
// <div className="p-4">
|
||||
// <h1 className="text-xl font-bold text-emerald-600">Xtablo</h1>
|
||||
// </div>
|
||||
|
||||
// <nav className="flex-1 px-2 py-4">
|
||||
// <ul className="space-y-1">
|
||||
// {navItems.map((item) => (
|
||||
// <li key={item.path}>
|
||||
// <Link
|
||||
// to={item.path}
|
||||
// className={twMerge(
|
||||
// "flex items-center px-3 py-2 rounded-md text-sm font-medium",
|
||||
// isActive(item.path)
|
||||
// ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
|
||||
// : "text-slate-700 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-700/50"
|
||||
// )}
|
||||
// >
|
||||
// <span className="mr-3">{item.icon}</span>
|
||||
// {item.label}
|
||||
// </Link>
|
||||
// </li>
|
||||
// ))}
|
||||
// </ul>
|
||||
// </nav>
|
||||
|
||||
// {session && (
|
||||
// <div className="p-4 border-t border-slate-200 dark:border-slate-700">
|
||||
// <div className="flex items-center mb-4">
|
||||
// <div className="w-8 h-8 rounded-full bg-emerald-100 dark:bg-emerald-900/30 flex items-center justify-center mr-3">
|
||||
// <UserIcon className="w-5 h-5 text-emerald-600 dark:text-emerald-400" />
|
||||
// </div>
|
||||
// <div>
|
||||
// <p className="text-sm font-medium text-slate-900 dark:text-white">
|
||||
// {session.user?.email}
|
||||
// </p>
|
||||
// <p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
// {session.user?.email?.split("@")[0]}
|
||||
// </p>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
24
ui/src/pages/chantiers.tsx
Normal file
24
ui/src/pages/chantiers.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { useSession } from "../contexts/SessionContext";
|
||||
|
||||
export const ChantiersPage = () => {
|
||||
const { session } = useSession();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Chantiers
|
||||
</h1>
|
||||
</div>
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
Gestion des chantiers
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
106
ui/src/pages/devis.tsx
Normal file
106
ui/src/pages/devis.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { useSession } from "../contexts/SessionContext";
|
||||
import { Button } from "../ui-library/button";
|
||||
import { PlusIcon } from "../ui-library/icons";
|
||||
import {
|
||||
AllCommunityModule,
|
||||
ModuleRegistry,
|
||||
themeAlpine,
|
||||
themeQuartz,
|
||||
} from "ag-grid-community";
|
||||
import { AgGridReact } from "ag-grid-react";
|
||||
import { useState } from "react";
|
||||
|
||||
ModuleRegistry.registerModules([AllCommunityModule]);
|
||||
|
||||
type Devis = {
|
||||
id: number;
|
||||
date: string;
|
||||
client: string;
|
||||
montant: number;
|
||||
status: string;
|
||||
};
|
||||
|
||||
export const DevisPage = () => {
|
||||
const { session } = useSession();
|
||||
const [devisData, setDevisData] = useState<Devis[]>([
|
||||
{
|
||||
id: 1,
|
||||
date: "2024-01-01",
|
||||
client: "John Doe",
|
||||
montant: 1000,
|
||||
status: "En attente",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: "2024-01-01",
|
||||
client: "John Doe",
|
||||
montant: 1000,
|
||||
status: "En attente",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
date: "2024-01-01",
|
||||
client: "John Doe",
|
||||
montant: 1000,
|
||||
status: "En attente",
|
||||
},
|
||||
]);
|
||||
|
||||
const handleNewDevis = () => {
|
||||
const newDevis: Devis = {
|
||||
id: devisData.length + 1,
|
||||
date: new Date().toISOString().split("T")[0],
|
||||
client: "Nouveau Client",
|
||||
montant: 0,
|
||||
status: "En attente",
|
||||
};
|
||||
setDevisData([...devisData, newDevis]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Devis
|
||||
</h1>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="accent"
|
||||
className="px-4"
|
||||
aria-label="Créer un nouveau devis"
|
||||
onPress={handleNewDevis}
|
||||
>
|
||||
<PlusIcon />
|
||||
Nouveau Devis
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div
|
||||
className="ag-theme-alpine dark:ag-theme-alpine-dark"
|
||||
style={{ height: 700, width: "100%" }}
|
||||
>
|
||||
<AgGridReact<Devis>
|
||||
rowData={devisData}
|
||||
gridOptions={{
|
||||
theme: themeQuartz,
|
||||
}}
|
||||
columnDefs={[
|
||||
{ field: "id", headerName: "ID" },
|
||||
{ field: "date", headerName: "Date" },
|
||||
{ field: "client", headerName: "Client" },
|
||||
{ field: "montant", headerName: "Montant" },
|
||||
{ field: "status", headerName: "Status" },
|
||||
]}
|
||||
domLayout="autoHeight"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
24
ui/src/pages/factures.tsx
Normal file
24
ui/src/pages/factures.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { useSession } from "../contexts/SessionContext";
|
||||
|
||||
export const FacturesPage = () => {
|
||||
const { session } = useSession();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Factures
|
||||
</h1>
|
||||
</div>
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
Gestion des factures
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
24
ui/src/pages/planning.tsx
Normal file
24
ui/src/pages/planning.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { useSession } from "../contexts/SessionContext";
|
||||
|
||||
export const PlanningPage = () => {
|
||||
const { session } = useSession();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Planning
|
||||
</h1>
|
||||
</div>
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
Gestion du planning
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
import { Menu } from "react-aria-components";
|
||||
import { MenuTrigger } from "react-aria-components";
|
||||
import { MenuButton } from "../ui-library/menu";
|
||||
import { MenuItem } from "react-aria-components";
|
||||
import { SignOutButton } from "../components/SignOutButton";
|
||||
import { useSession } from "../contexts/SessionContext";
|
||||
|
||||
import { MenuPopover } from "../ui-library/menu";
|
||||
import { ControlledOpenState } from "../components/NavigationBar";
|
||||
export const TabloPage = () => {
|
||||
const { session } = useSession();
|
||||
return (
|
||||
|
|
@ -30,6 +35,7 @@ export const TabloPage = () => {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ControlledOpenState />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from "react";
|
||||
import {
|
||||
Menu as RACMenu,
|
||||
MenuItem as RACMenuItem,
|
||||
|
|
@ -9,15 +10,15 @@ import {
|
|||
MenuSectionProps as RACMenuSectionProps,
|
||||
MenuSection as RACMenuSection,
|
||||
Collection,
|
||||
} from 'react-aria-components';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { Popover, PopoverProps } from './popover';
|
||||
import { Button, ButtonProps } from './button';
|
||||
import { composeTailwindRenderProps } from './utils';
|
||||
import { Small } from './text';
|
||||
import { CheckIcon, ChevronDownIcon, ChevronRightIcon } from './icons';
|
||||
} from "react-aria-components";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Popover, PopoverProps } from "./popover";
|
||||
import { Button, ButtonProps } from "./button";
|
||||
import { composeTailwindRenderProps } from "./utils";
|
||||
import { Small } from "./text";
|
||||
import { CheckIcon, ChevronDownIcon, ChevronRightIcon } from "./icons";
|
||||
|
||||
export { MenuTrigger, SubmenuTrigger } from 'react-aria-components';
|
||||
export { MenuTrigger, SubmenuTrigger } from "react-aria-components";
|
||||
|
||||
type MenuButtonProps = ButtonProps & {
|
||||
buttonArrow?: React.ReactNode;
|
||||
|
|
@ -25,7 +26,7 @@ type MenuButtonProps = ButtonProps & {
|
|||
|
||||
export function MenuButton({
|
||||
buttonArrow = <ChevronDownIcon className="ms-auto" />,
|
||||
variant = 'outline',
|
||||
variant = "outline",
|
||||
children,
|
||||
...props
|
||||
}: MenuButtonProps) {
|
||||
|
|
@ -34,7 +35,7 @@ export function MenuButton({
|
|||
{(renderProps) => {
|
||||
return (
|
||||
<>
|
||||
{typeof children === 'function' ? children(renderProps) : children}
|
||||
{typeof children === "function" ? children(renderProps) : children}
|
||||
{buttonArrow}
|
||||
</>
|
||||
);
|
||||
|
|
@ -43,29 +44,32 @@ export function MenuButton({
|
|||
);
|
||||
}
|
||||
|
||||
export function MenuPopover({ className, ...props }: PopoverProps) {
|
||||
return (
|
||||
<Popover
|
||||
{...props}
|
||||
className={composeTailwindRenderProps(
|
||||
className,
|
||||
twMerge(
|
||||
'max-w-72',
|
||||
'min-w-[max(--spacing(36),var(--trigger-width))]',
|
||||
'has-[[data-ui=content]_[data-ui=icon]]:min-w-[max(--spacing(48),var(--trigger-width))]',
|
||||
'has-[[data-ui=content]_kbd]:min-w-[max(--spacing(11),var(--trigger-width))]',
|
||||
),
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const MenuPopover = React.forwardRef(
|
||||
({ className, ...props }: PopoverProps, ref: React.Ref<HTMLDivElement>) => {
|
||||
return (
|
||||
<Popover
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={composeTailwindRenderProps(
|
||||
className,
|
||||
twMerge(
|
||||
"max-w-72",
|
||||
"min-w-[max(--spacing(36),var(--trigger-width))]",
|
||||
"has-[[data-ui=content]_[data-ui=icon]]:min-w-[max(--spacing(48),var(--trigger-width))]",
|
||||
"has-[[data-ui=content]_kbd]:min-w-[max(--spacing(11),var(--trigger-width))]"
|
||||
)
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type MenuProps<T> = RACMenuProps<T> & {
|
||||
checkIconPlacement?: 'start' | 'end';
|
||||
checkIconPlacement?: "start" | "end";
|
||||
};
|
||||
|
||||
export function Menu<T extends object>({
|
||||
checkIconPlacement = 'end',
|
||||
checkIconPlacement = "end",
|
||||
...props
|
||||
}: MenuProps<T>) {
|
||||
return (
|
||||
|
|
@ -75,57 +79,58 @@ export function Menu<T extends object>({
|
|||
className={composeTailwindRenderProps(
|
||||
props.className,
|
||||
twMerge(
|
||||
'max-h-[inherit] overflow-auto outline-hidden',
|
||||
'flex flex-col',
|
||||
'p-1 has-[header]:pt-0',
|
||||
"max-h-[inherit] overflow-auto outline-hidden",
|
||||
"flex flex-col",
|
||||
"p-1 has-[header]:pt-0",
|
||||
|
||||
// Header, Menu item style when has selectable items
|
||||
'[&_header]:px-2',
|
||||
"[&_header]:px-2",
|
||||
|
||||
checkIconPlacement === 'start' &&
|
||||
'[&:has(:is([role=menuitemradio],[role=menuitemcheckbox]))_:is(header,[role=menuitem])]:ps-7',
|
||||
checkIconPlacement === "start" &&
|
||||
"[&:has(:is([role=menuitemradio],[role=menuitemcheckbox]))_:is(header,[role=menuitem])]:ps-7",
|
||||
|
||||
// Menu item content
|
||||
'**:data-[ui=content]:flex-1',
|
||||
'**:data-[ui=content]:grid',
|
||||
'[&_[data-ui=content]:has([data-ui=label])]:grid-cols-[--spacing(4)_1fr_minmax(--spacing(12),max-content)]',
|
||||
'**:data-[ui=content]:items-center',
|
||||
'**:data-[ui=content]:gap-x-2',
|
||||
"**:data-[ui=content]:flex-1",
|
||||
"**:data-[ui=content]:grid",
|
||||
"[&_[data-ui=content]:has([data-ui=label])]:grid-cols-[--spacing(4)_1fr_minmax(--spacing(12),max-content)]",
|
||||
"**:data-[ui=content]:items-center",
|
||||
"**:data-[ui=content]:gap-x-2",
|
||||
"**:data-[ui=content]:rtl:text-right",
|
||||
|
||||
// Icon
|
||||
'[&_[data-ui=content]:not(:hover)>[data-ui=icon]:not([class*=text-])]:text-muted',
|
||||
'[&_[data-ui=content][data-destructive]>[data-ui=icon]]:text-destructive',
|
||||
'[&_[data-ui=content][data-destructive]:not(:hover)>[data-ui=icon]]:text-destructive/75',
|
||||
'[&_[data-ui=content]>[data-ui=icon]:not([class*=size-])]:size-4',
|
||||
'[&_[data-ui=content]>[data-ui=icon]:first-child]:col-start-1',
|
||||
"[&_[data-ui=content]:not(:hover)>[data-ui=icon]:not([class*=text-])]:text-muted",
|
||||
"[&_[data-ui=content][data-destructive]>[data-ui=icon]]:text-destructive",
|
||||
"[&_[data-ui=content][data-destructive]:not(:hover)>[data-ui=icon]]:text-destructive/75",
|
||||
"[&_[data-ui=content]>[data-ui=icon]:not([class*=size-])]:size-4",
|
||||
"[&_[data-ui=content]>[data-ui=icon]:first-child]:col-start-1",
|
||||
|
||||
// Label
|
||||
'**:data-[ui=label]:col-span-full',
|
||||
'[&:has([data-ui=icon]+[data-ui=label])_[data-ui=label]]:col-start-2',
|
||||
'[&:has([data-ui=kbd])_[data-ui=label]]:-col-end-2',
|
||||
'[&:has([data-ui=icon]+[data-ui=label])_[data-ui=content]:not(:has(>[data-ui=label]))]:ps-6',
|
||||
"**:data-[ui=label]:col-span-full",
|
||||
"[&:has([data-ui=icon]+[data-ui=label])_[data-ui=label]]:col-start-2",
|
||||
"[&:has([data-ui=kbd])_[data-ui=label]]:-col-end-2",
|
||||
"[&:has([data-ui=icon]+[data-ui=label])_[data-ui=content]:not(:has(>[data-ui=label]))]:ps-6",
|
||||
|
||||
// Kbd
|
||||
'**:data-[ui=kbd]:col-span-1',
|
||||
'**:data-[ui=kbd]:row-start-1',
|
||||
'**:data-[ui=kbd]:col-start-3',
|
||||
'**:data-[ui=kbd]:justify-self-end',
|
||||
'**:data-[ui=kbd]:text-xs/6',
|
||||
'[&_:not([data-destructive])>[data-ui=kbd]:not([class*=bg-])]:text-muted/75',
|
||||
'[&_[data-destructive]>[data-ui=kbd]]:text-destructive',
|
||||
"**:data-[ui=kbd]:col-span-1",
|
||||
"**:data-[ui=kbd]:row-start-1",
|
||||
"**:data-[ui=kbd]:col-start-3",
|
||||
"**:data-[ui=kbd]:justify-self-end",
|
||||
"**:data-[ui=kbd]:text-xs/6",
|
||||
"[&_:not([data-destructive])>[data-ui=kbd]:not([class*=bg-])]:text-muted/75",
|
||||
"[&_[data-destructive]>[data-ui=kbd]]:text-destructive",
|
||||
|
||||
// Description
|
||||
'**:data-[ui=description]:col-span-full',
|
||||
'[&:has([data-ui=kbd])_[data-ui=description]]:-col-end-2',
|
||||
'[&:has([data-ui=icon]+[data-ui=label])_[data-ui=description]]:col-start-2',
|
||||
),
|
||||
"**:data-[ui=description]:col-span-full",
|
||||
"[&:has([data-ui=kbd])_[data-ui=description]]:-col-end-2",
|
||||
"[&:has([data-ui=icon]+[data-ui=label])_[data-ui=description]]:col-start-2"
|
||||
)
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SubMenu<T extends object>(
|
||||
props: MenuProps<T> & { 'aria-label': string },
|
||||
props: MenuProps<T> & { "aria-label": string }
|
||||
) {
|
||||
return <Menu {...props} />;
|
||||
}
|
||||
|
|
@ -134,8 +139,8 @@ export function MenuSeparator({ className }: { className?: string }) {
|
|||
return (
|
||||
<Separator
|
||||
className={twMerge(
|
||||
'border-t-border/75 my-1 w-[calc(100%-(--spacing(4)))] self-center border-t',
|
||||
className,
|
||||
"border-t-border/50 my-1 w-[calc(100%-(--spacing(4)))] self-center border-t",
|
||||
className
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
|
@ -148,7 +153,7 @@ type MenuItemProps = RACMenuItemProps & {
|
|||
export function MenuItem({ destructive, ...props }: MenuItemProps) {
|
||||
const textValue =
|
||||
props.textValue ||
|
||||
(typeof props.children === 'string' ? props.children : undefined);
|
||||
(typeof props.children === "string" ? props.children : undefined);
|
||||
|
||||
return (
|
||||
<RACMenuItem
|
||||
|
|
@ -158,16 +163,16 @@ export function MenuItem({ destructive, ...props }: MenuItemProps) {
|
|||
props.className,
|
||||
(className, { isFocused, isDisabled }) => {
|
||||
return twMerge([
|
||||
'group rounded-sm outline-hidden',
|
||||
'flex items-center gap-x-1.5',
|
||||
'px-2 py-2.5 sm:py-1.5',
|
||||
'text-base/6 sm:text-sm/6',
|
||||
isDisabled && 'opacity-50',
|
||||
isFocused && 'bg-zinc-100 dark:bg-zinc-800',
|
||||
destructive && 'text-destructive',
|
||||
"group rounded-sm outline-hidden",
|
||||
"flex items-center gap-x-1.5",
|
||||
"px-2 py-2.5 sm:py-1.5",
|
||||
"text-base/6 sm:text-sm/6",
|
||||
isDisabled && "opacity-50",
|
||||
isFocused && "bg-zinc-100 dark:bg-zinc-800",
|
||||
destructive && "text-destructive",
|
||||
className,
|
||||
]);
|
||||
},
|
||||
}
|
||||
)}
|
||||
>
|
||||
{composeRenderProps(
|
||||
|
|
@ -176,11 +181,11 @@ export function MenuItem({ destructive, ...props }: MenuItemProps) {
|
|||
<>
|
||||
<CheckIcon
|
||||
className={twMerge(
|
||||
'flex h-[1lh] w-4 items-center self-start',
|
||||
selectionMode == 'none'
|
||||
? 'hidden'
|
||||
: 'in-data-[check-icon-placement=end]:hidden',
|
||||
isSelected ? 'visible' : 'invisible',
|
||||
"flex h-[1lh] w-4 items-center self-start",
|
||||
selectionMode == "none"
|
||||
? "hidden"
|
||||
: "in-data-[check-icon-placement=end]:hidden",
|
||||
isSelected ? "visible" : "invisible"
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
|
|
@ -191,18 +196,18 @@ export function MenuItem({ destructive, ...props }: MenuItemProps) {
|
|||
</div>
|
||||
<CheckIcon
|
||||
className={twMerge(
|
||||
'flex h-[1lh] w-4 items-center self-start',
|
||||
selectionMode == 'none'
|
||||
? 'hidden'
|
||||
: 'in-data-[check-icon-placement=start]:hidden',
|
||||
isSelected ? 'visible' : 'invisible',
|
||||
"flex h-[1lh] w-4 items-center self-start",
|
||||
selectionMode == "none"
|
||||
? "hidden"
|
||||
: "in-data-[check-icon-placement=start]:hidden",
|
||||
isSelected ? "visible" : "invisible"
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Submenu indicator */}
|
||||
<ChevronRightIcon className="text-muted hidden size-4 group-data-has-submenu:inline-block" />
|
||||
</>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</RACMenuItem>
|
||||
);
|
||||
|
|
@ -211,12 +216,12 @@ export function MenuItem({ destructive, ...props }: MenuItemProps) {
|
|||
export function MenuItemLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.JSX.IntrinsicElements['span']) {
|
||||
}: React.JSX.IntrinsicElements["span"]) {
|
||||
return (
|
||||
<span
|
||||
slot="label"
|
||||
data-ui="label"
|
||||
className={twMerge('truncate', className)}
|
||||
className={twMerge("truncate", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -225,7 +230,7 @@ export function MenuItemLabel({
|
|||
export function MenuItemDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.JSX.IntrinsicElements['span']) {
|
||||
}: React.JSX.IntrinsicElements["span"]) {
|
||||
return (
|
||||
<Small
|
||||
slot="description"
|
||||
|
|
@ -248,13 +253,13 @@ export function MenuSection<T extends object>({
|
|||
<RACMenuSection
|
||||
{...props}
|
||||
className={twMerge(
|
||||
'not-first:mt-1.5',
|
||||
'not-first:border-t',
|
||||
'not-first:border-t-border/75',
|
||||
className,
|
||||
"not-first:mt-1.5",
|
||||
"not-first:border-t",
|
||||
"not-first:border-t-border/75",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Header className="text-muted bg-background sticky inset-0 z-10 truncate pt-2 text-xs/6">
|
||||
<Header className="text-muted bg-background sticky inset-0 z-10 truncate pt-2 text-xs/6 rtl:text-right">
|
||||
{props.title}
|
||||
</Header>
|
||||
<Collection items={props.items}>{props.children}</Collection>
|
||||
|
|
|
|||
Loading…
Reference in a new issue