Compare commits
No commits in common. "5f9405d1a07139d6d0c466165d69b1eec2ea07e5" and "76dc9481a5f7a0322328e10f393ffb4816dab469" have entirely different histories.
5f9405d1a0
...
76dc9481a5
@ -374,8 +374,8 @@ const colorClasses = {
|
|||||||
|
|
||||||
export default function TailwindV4ShowcasePage() {
|
export default function TailwindV4ShowcasePage() {
|
||||||
return (
|
return (
|
||||||
<section className="min-h-screen bg-background text-foreground">
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
<div className="container flex flex-col gap-10 pb-10">
|
<div className="container mx-auto flex max-w-7xl flex-col gap-10 px-6 py-10">
|
||||||
<header className="space-y-3">
|
<header className="space-y-3">
|
||||||
<p className="text-sm uppercase tracking-widest text-muted-foreground">Tailwind CSS v4</p>
|
<p className="text-sm uppercase tracking-widest text-muted-foreground">Tailwind CSS v4</p>
|
||||||
<h1 className="text-4xl font-bold tracking-tight">Typography + Colors Showcase</h1>
|
<h1 className="text-4xl font-bold tracking-tight">Typography + Colors Showcase</h1>
|
||||||
@ -603,6 +603,6 @@ export default function TailwindV4ShowcasePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export function PageHeader({
|
|||||||
className,
|
className,
|
||||||
}: PageHeaderProps) {
|
}: PageHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-row items-center justify-between mb-6", className)}>
|
<div className={cn("flex flex-row items-center justify-between", className)}>
|
||||||
{/* Lado izquierdo */}
|
{/* Lado izquierdo */}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
@ -36,7 +36,7 @@ export function PageHeader({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div>
|
||||||
<h1 className="text-xl font-semibold tracking-tight lg:text-2xl h-8 text-foreground sm:truncate sm:tracking-tight">
|
<h1 className="text-xl font-semibold tracking-tight lg:text-2xl h-8 text-foreground sm:truncate sm:tracking-tight">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import { InitialsAvatar } from "@repo/rdx-ui/components";
|
import { Avatar, AvatarFallback, Badge } from "@repo/shadcn-ui/components";
|
||||||
import { Badge } from "@repo/shadcn-ui/components";
|
|
||||||
import { Building2Icon, CopyIcon, UserIcon } from "lucide-react";
|
import { Building2Icon, CopyIcon, UserIcon } from "lucide-react";
|
||||||
|
|
||||||
import type { Proforma } from "../../../../shared";
|
import type { Proforma } from "../../../../shared";
|
||||||
import { ProformaStatusBadge } from "../../components";
|
import { Initials, ProformaStatusBadge } from "../../components";
|
||||||
|
|
||||||
export const ProformaHeader = ({ proforma }: { proforma: Proforma }) => {
|
export const ProformaHeader = ({ proforma }: { proforma: Proforma }) => {
|
||||||
const handleCopyTin = async () => {
|
const handleCopyTin = async () => {
|
||||||
@ -17,7 +16,17 @@ export const ProformaHeader = ({ proforma }: { proforma: Proforma }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<InitialsAvatar name={proforma.recipient.name} />
|
<Avatar className="size-10 border-2 border-background shadow-sm">
|
||||||
|
<AvatarFallback
|
||||||
|
className={
|
||||||
|
proforma.status !== "draft"
|
||||||
|
? "bg-blue-100 text-blue-700"
|
||||||
|
: "bg-muted text-muted-foreground"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Initials name={proforma.recipient.name} />
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@ -103,6 +103,7 @@ export function useProformasGridColumns(
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Button
|
<Button
|
||||||
|
asChild
|
||||||
className="size-6 text-foreground hover:text-primary"
|
className="size-6 text-foreground hover:text-primary"
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -142,7 +143,7 @@ export function useProformasGridColumns(
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
className="text-primary hover:underline font-semibold text-ellipsis"
|
className="text-primary hover:underline font-semibold"
|
||||||
onClick={() => actionHandlers.onPreviewClick?.(proforma)}
|
onClick={() => actionHandlers.onPreviewClick?.(proforma)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@ -157,6 +158,10 @@ export function useProformasGridColumns(
|
|||||||
accessorKey: "series",
|
accessorKey: "series",
|
||||||
header: "Serie",
|
header: "Serie",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "reference",
|
||||||
|
header: "Reference",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "invoiceDate",
|
accessorKey: "invoiceDate",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
|
|||||||
@ -97,7 +97,7 @@ export const ListProformasPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<>
|
||||||
<AppHeader>
|
<AppHeader>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
description={t("pages.proformas.list.description")}
|
description={t("pages.proformas.list.description")}
|
||||||
@ -193,6 +193,6 @@ export const ListProformasPage = () => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
</AppContent>
|
</AppContent>
|
||||||
</section>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -103,7 +103,6 @@ export const ProformaUpdatePage = () => {
|
|||||||
isSubmitting={updateCtrl.isUpdating}
|
isSubmitting={updateCtrl.isUpdating}
|
||||||
onChangeCustomerClick={selectCustomerCtrl.selectCtrl.openDialog}
|
onChangeCustomerClick={selectCustomerCtrl.selectCtrl.openDialog}
|
||||||
//onCreateCustomerClick={selectCustomerCtrl.createCtrl.openDialog}
|
//onCreateCustomerClick={selectCustomerCtrl.createCtrl.openDialog}
|
||||||
onCreateCustomerClick={() => null}
|
|
||||||
onReset={updateCtrl.resetForm}
|
onReset={updateCtrl.resetForm}
|
||||||
onSubmit={updateCtrl.onSubmit}
|
onSubmit={updateCtrl.onSubmit}
|
||||||
selectedCustomer={updateCtrl.selectedCustomer}
|
selectedCustomer={updateCtrl.selectedCustomer}
|
||||||
|
|||||||
@ -3,20 +3,43 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
Item,
|
||||||
|
ItemContent,
|
||||||
|
ItemDescription,
|
||||||
|
ItemGroup,
|
||||||
|
ItemTitle,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { useState } from "react";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
|
||||||
import type { CustomerSelectionOption } from "../../../../../common/features";
|
import type { CustomerSelectionOption } from "../../../../../common/features";
|
||||||
import { useTranslation } from "../../../../../web/i18n";
|
import { useTranslation } from "../../../../../web/i18n";
|
||||||
import type { useSelectCustomerDialogController } from "../../controllers";
|
import type { useSelectCustomerDialogController } from "../../controllers";
|
||||||
|
|
||||||
import { SelectCustomerSelectionList } from "./select-customer-selection-list";
|
import { SelectCustomerEmptyCard } from "./select-customer-empty-card";
|
||||||
|
|
||||||
type SelectCustomerDialogController = ReturnType<typeof useSelectCustomerDialogController>;
|
type SelectCustomerDialogController = ReturnType<typeof useSelectCustomerDialogController>;
|
||||||
|
|
||||||
|
/*export const useSelectCustomerDialogController = () => {
|
||||||
|
isOpen: boolean;
|
||||||
|
closeDialog: () => void;
|
||||||
|
|
||||||
|
search: string;
|
||||||
|
setSearchValue: (value: string) => void;
|
||||||
|
|
||||||
|
customers: CustomerSelectionOption[];
|
||||||
|
isLoading: boolean;
|
||||||
|
isFetching: boolean;
|
||||||
|
|
||||||
|
isErrror: boolean;
|
||||||
|
error: unknown;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
selectCustomer: (customer: CustomerSelectionOption) => void;
|
||||||
|
};*/
|
||||||
|
|
||||||
type SelectCustomerDialogProps = {
|
type SelectCustomerDialogProps = {
|
||||||
ctrl: SelectCustomerDialogController;
|
ctrl: SelectCustomerDialogController;
|
||||||
onCreateNewCustomerClick?: () => void;
|
onCreateNewCustomerClick?: () => void;
|
||||||
@ -26,21 +49,8 @@ export const SelectCustomerDialog = ({
|
|||||||
ctrl,
|
ctrl,
|
||||||
onCreateNewCustomerClick,
|
onCreateNewCustomerClick,
|
||||||
}: SelectCustomerDialogProps) => {
|
}: SelectCustomerDialogProps) => {
|
||||||
const [selectedCustomer, setSelectedCustomer] = useState<CustomerSelectionOption | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleSelect = (customer: CustomerSelectionOption) => {
|
|
||||||
setSelectedCustomer((prev) => (prev?.id === customer.id ? undefined : customer));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirm = () => {
|
|
||||||
if (selectedCustomer) {
|
|
||||||
ctrl.selectCustomer(selectedCustomer);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog onOpenChange={(open) => !open && ctrl.closeDialog()} open={ctrl.isOpen}>
|
<Dialog onOpenChange={(open) => !open && ctrl.closeDialog()} open={ctrl.isOpen}>
|
||||||
<DialogContent className="flex max-h-[85vh] flex-col sm:max-w-3xl">
|
<DialogContent className="flex max-h-[85vh] flex-col sm:max-w-3xl">
|
||||||
@ -70,23 +80,60 @@ export const SelectCustomerDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-hidden">
|
<div className="min-h-0 flex-1 overflow-hidden">
|
||||||
<SelectCustomerSelectionList
|
<CustomerSelectionList
|
||||||
customers={ctrl.customers}
|
customers={ctrl.customers}
|
||||||
isLoading={ctrl.isLoading}
|
isLoading={ctrl.isLoading}
|
||||||
onSelect={handleSelect}
|
onSelect={ctrl.selectCustomer}
|
||||||
selectedCustomer={selectedCustomer}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="gap-2">
|
|
||||||
<Button onClick={ctrl.closeDialog} variant="outline">
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
<Button disabled={ctrl.isLoading || !selectedCustomer} onClick={handleConfirm}>
|
|
||||||
{selectedCustomer ? "Confirmar selección" : "Seleccionar cliente"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type CustomerSelectionListProps = {
|
||||||
|
customers: CustomerSelectionOption[];
|
||||||
|
isLoading: boolean;
|
||||||
|
onSelect: (customer: CustomerSelectionOption) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CustomerSelectionList = ({ customers, isLoading, onSelect }: CustomerSelectionListProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
//const columns = useCustomersGridColumns({});
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2 py-2">
|
||||||
|
<div className="h-14 rounded-md border" />
|
||||||
|
<div className="h-14 rounded-md border" />
|
||||||
|
<div className="h-14 rounded-md border" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customers.length === 0) {
|
||||||
|
return <SelectCustomerEmptyCard />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemGroup>
|
||||||
|
{customers.map((customer) => (
|
||||||
|
<Item
|
||||||
|
asChild
|
||||||
|
className={cn("bg-muted/50 font-medium transition hover:text-primary hover:bg-muted")}
|
||||||
|
key={customer.id}
|
||||||
|
onClick={() => onSelect(customer)}
|
||||||
|
role="listitem"
|
||||||
|
>
|
||||||
|
<a href="#" rel="noopener noreferrer" target="_blank">
|
||||||
|
<ItemContent>
|
||||||
|
<ItemTitle className="line-clamp-1">{customer.name}</ItemTitle>
|
||||||
|
<ItemDescription>{customer.tin}</ItemDescription>
|
||||||
|
</ItemContent>
|
||||||
|
</a>
|
||||||
|
</Item>
|
||||||
|
))}
|
||||||
|
</ItemGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@ -1,77 +0,0 @@
|
|||||||
import { InitialsAvatar } from "@repo/rdx-ui/components";
|
|
||||||
import {
|
|
||||||
Item,
|
|
||||||
ItemContent,
|
|
||||||
ItemDescription,
|
|
||||||
ItemGroup,
|
|
||||||
ItemMedia,
|
|
||||||
ItemTitle,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../../../web/i18n";
|
|
||||||
import type { CustomerSelectionOption } from "../../entities";
|
|
||||||
|
|
||||||
import { SelectCustomerEmptyCard } from "./select-customer-empty-card";
|
|
||||||
|
|
||||||
type SelectCustomerSelectionListProps = {
|
|
||||||
customers: CustomerSelectionOption[];
|
|
||||||
isLoading: boolean;
|
|
||||||
selectedCustomer?: CustomerSelectionOption;
|
|
||||||
onSelect: (customer: CustomerSelectionOption) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SelectCustomerSelectionList = ({
|
|
||||||
customers,
|
|
||||||
isLoading,
|
|
||||||
selectedCustomer,
|
|
||||||
onSelect,
|
|
||||||
}: SelectCustomerSelectionListProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
//const columns = useCustomersGridColumns({});
|
|
||||||
console.log(selectedCustomer);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2 py-2">
|
|
||||||
<div className="h-14 rounded-md border" />
|
|
||||||
<div className="h-14 rounded-md border" />
|
|
||||||
<div className="h-14 rounded-md border" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (customers.length === 0) {
|
|
||||||
return <SelectCustomerEmptyCard />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ItemGroup>
|
|
||||||
{customers.map((customer) => (
|
|
||||||
<Item
|
|
||||||
className={cn(
|
|
||||||
"cursor-pointer",
|
|
||||||
customer.id === selectedCustomer?.id && "border-primary bg-accent"
|
|
||||||
)}
|
|
||||||
key={customer.id}
|
|
||||||
onClick={() => onSelect(customer)}
|
|
||||||
role="listitem"
|
|
||||||
variant={"outline"}
|
|
||||||
>
|
|
||||||
<ItemMedia>
|
|
||||||
<InitialsAvatar
|
|
||||||
name={customer.name}
|
|
||||||
variant={customer.id === selectedCustomer?.id ? "primary" : "muted"}
|
|
||||||
/>
|
|
||||||
</ItemMedia>
|
|
||||||
|
|
||||||
<ItemContent>
|
|
||||||
<ItemTitle className="line-clamp-1">{customer.name}</ItemTitle>
|
|
||||||
<ItemDescription>{customer.tin}</ItemDescription>
|
|
||||||
</ItemContent>
|
|
||||||
</Item>
|
|
||||||
))}
|
|
||||||
</ItemGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,14 +1,24 @@
|
|||||||
import { InitialsAvatar } from "@repo/rdx-ui/components";
|
import { Avatar, AvatarFallback, Badge } from "@repo/shadcn-ui/components";
|
||||||
import { Badge } from "@repo/shadcn-ui/components";
|
|
||||||
import { Building2Icon, CopyIcon, UserIcon } from "lucide-react";
|
import { Building2Icon, CopyIcon, UserIcon } from "lucide-react";
|
||||||
|
|
||||||
import type { Customer } from "../../../../shared";
|
import type { Customer } from "../../../../shared";
|
||||||
|
import { Initials } from "../../components";
|
||||||
|
|
||||||
export const CustomerHeader = ({ customer }: { customer: Customer }) => {
|
export const CustomerHeader = ({ customer }: { customer: Customer }) => {
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<InitialsAvatar name={customer.name} />
|
<Avatar className="size-10 border-2 border-background shadow-sm">
|
||||||
|
<AvatarFallback
|
||||||
|
className={
|
||||||
|
customer.status === "active"
|
||||||
|
? "bg-blue-100 text-blue-700"
|
||||||
|
: "bg-muted text-muted-foreground"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Initials name={customer.name} />
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { safeHTTPUrl } from "@erp/core/client";
|
import { safeHTTPUrl } from "@erp/core/client";
|
||||||
import { DataTableColumnHeader, InitialsAvatar } from "@repo/rdx-ui/components";
|
import { DataTableColumnHeader } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
|
Avatar,
|
||||||
|
AvatarFallback,
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -16,7 +18,7 @@ import * as React from "react";
|
|||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import type { CustomerListRow } from "../../../../shared";
|
import type { CustomerListRow } from "../../../../shared";
|
||||||
import { AddressCell, ContactCell } from "../../components";
|
import { AddressCell, ContactCell, Initials } from "../../components";
|
||||||
|
|
||||||
type GridActionHandlers = {
|
type GridActionHandlers = {
|
||||||
onEditClick?: (customer: CustomerListRow) => void;
|
onEditClick?: (customer: CustomerListRow) => void;
|
||||||
@ -56,7 +58,17 @@ export function useCustomersGridColumns(
|
|||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<InitialsAvatar name={customer.name} variant="primary" />
|
<Avatar className="size-10 border-2 border-background shadow-sm">
|
||||||
|
<AvatarFallback
|
||||||
|
className={
|
||||||
|
customer.status === "active"
|
||||||
|
? "bg-blue-100 text-blue-700"
|
||||||
|
: "bg-muted text-muted-foreground"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Initials name={customer.name} />
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export * from "./address-cell";
|
export * from "./address-cell";
|
||||||
export * from "./contact-cell";
|
export * from "./contact-cell";
|
||||||
|
export * from "./initials";
|
||||||
export * from "./kind-badge";
|
export * from "./kind-badge";
|
||||||
export * from "./soft";
|
export * from "./soft";
|
||||||
|
|||||||
@ -0,0 +1,4 @@
|
|||||||
|
export const Initials = ({ name }: { name: string }) => {
|
||||||
|
const parts = name.trim().split(/\s+/).slice(0, 2);
|
||||||
|
return <> {parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "?"} </>;
|
||||||
|
};
|
||||||
@ -2,6 +2,7 @@ import {
|
|||||||
Field,
|
Field,
|
||||||
FieldDescription,
|
FieldDescription,
|
||||||
FieldError,
|
FieldError,
|
||||||
|
FormControl,
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
@ -89,23 +90,25 @@ export function SelectField<TFormValues extends FieldValues>({
|
|||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
value={field.value ?? undefined}
|
value={field.value ?? undefined}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<FormControl>
|
||||||
aria-invalid={fieldState.invalid}
|
<SelectTrigger
|
||||||
aria-required={required}
|
aria-invalid={fieldState.invalid}
|
||||||
className={cn(
|
aria-required={required}
|
||||||
"bg-muted/50 font-medium",
|
className={cn(
|
||||||
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
"bg-muted/50 font-medium",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||||
"placeholder:text-muted-foreground/50",
|
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||||
inputClassName
|
"placeholder:text-muted-foreground/50",
|
||||||
)}
|
inputClassName
|
||||||
id={triggerId}
|
)}
|
||||||
>
|
id={triggerId}
|
||||||
<SelectValue
|
>
|
||||||
className={"placeholder:font-normal placeholder:italic"}
|
<SelectValue
|
||||||
placeholder={placeholder}
|
className={"placeholder:font-normal placeholder:italic"}
|
||||||
/>
|
placeholder={placeholder}
|
||||||
</SelectTrigger>
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{normalizedItems.map((item) => (
|
{normalizedItems.map((item) => (
|
||||||
<SelectItem key={`key-${item.value}`} value={item.value}>
|
<SelectItem key={`key-${item.value}`} value={item.value}>
|
||||||
|
|||||||
@ -7,7 +7,6 @@ export * from "./error-overlay.tsx";
|
|||||||
export * from "./form/index.ts";
|
export * from "./form/index.ts";
|
||||||
export * from "./full-screen-modal.tsx";
|
export * from "./full-screen-modal.tsx";
|
||||||
export * from "./grid/index.ts";
|
export * from "./grid/index.ts";
|
||||||
export * from "./initials-avatar.tsx";
|
|
||||||
export * from "./layout/index.ts";
|
export * from "./layout/index.ts";
|
||||||
export * from "./loading-overlay/index.ts";
|
export * from "./loading-overlay/index.ts";
|
||||||
export * from "./logo-verifactu.tsx";
|
export * from "./logo-verifactu.tsx";
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
import { Avatar, AvatarFallback } from "@repo/shadcn-ui/components";
|
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
|
||||||
import type * as React from "react";
|
|
||||||
|
|
||||||
type InitialsProps = {
|
|
||||||
name?: string | null;
|
|
||||||
maxParts?: number;
|
|
||||||
fallback?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getInitials = ({ name, maxParts = 2, fallback = "?" }: InitialsProps): string => {
|
|
||||||
if (!name?.trim()) return fallback;
|
|
||||||
|
|
||||||
return (
|
|
||||||
name
|
|
||||||
.trim()
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter(Boolean)
|
|
||||||
.slice(0, maxParts)
|
|
||||||
.map((p) => p[0]?.toUpperCase() ?? "")
|
|
||||||
.join("") || fallback
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Initials = (props: InitialsProps) => {
|
|
||||||
return <>{getInitials(props)}</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const VARIANT_CLASSES: Record<string, string> = {
|
|
||||||
primary: "bg-primary/10 text-primary",
|
|
||||||
muted: "bg-muted text-muted-foreground",
|
|
||||||
};
|
|
||||||
|
|
||||||
type InitialsAvatarProps = React.ComponentProps<typeof Avatar> & {
|
|
||||||
name?: string | null;
|
|
||||||
variant?: "primary" | "muted";
|
|
||||||
maxParts?: number;
|
|
||||||
fallback?: string;
|
|
||||||
fallbackClassName?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const InitialsAvatar = ({
|
|
||||||
name,
|
|
||||||
variant = "muted",
|
|
||||||
maxParts = 2,
|
|
||||||
fallback = "?",
|
|
||||||
className,
|
|
||||||
fallbackClassName,
|
|
||||||
...props
|
|
||||||
}: InitialsAvatarProps) => {
|
|
||||||
return (
|
|
||||||
<Avatar
|
|
||||||
className={cn(
|
|
||||||
"size-10 border-2 shadow-sm",
|
|
||||||
variant === "primary" && "border-primary",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<AvatarFallback className={cn(VARIANT_CLASSES[variant], fallbackClassName)}>
|
|
||||||
<Initials fallback={fallback} maxParts={maxParts} name={name} />
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -5,20 +5,28 @@ import {
|
|||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
|
Separator,
|
||||||
|
SidebarTrigger,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
export const AppBreadcrumb = () => {
|
export const AppBreadcrumb = () => {
|
||||||
return (
|
return (
|
||||||
<Breadcrumb>
|
<header className="app-breadcrumb flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||||
<BreadcrumbList>
|
<div className="flex items-center gap-2 px-6">
|
||||||
<BreadcrumbItem className="hidden md:block">
|
<SidebarTrigger className="-ml-1" />
|
||||||
<BreadcrumbLink href="#">Building Your Application</BreadcrumbLink>
|
<Separator className="mr-2 h-4" orientation="vertical" />
|
||||||
</BreadcrumbItem>
|
<Breadcrumb>
|
||||||
<BreadcrumbSeparator className="hidden md:block" />
|
<BreadcrumbList>
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem className="hidden md:block">
|
||||||
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
|
<BreadcrumbLink href="#">Building Your Application</BreadcrumbLink>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</BreadcrumbList>
|
<BreadcrumbSeparator className="hidden md:block" />
|
||||||
</Breadcrumb>
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,8 +7,8 @@ export const AppHeader = ({
|
|||||||
...props
|
...props
|
||||||
}: PropsWithChildren<{ className?: string }>) => {
|
}: PropsWithChildren<{ className?: string }>) => {
|
||||||
return (
|
return (
|
||||||
<header className={cn("app-header space-y-3", className)} {...props}>
|
<div className={cn("app-header", className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</header>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,24 +2,26 @@ import { SidebarInset, SidebarProvider } from "@repo/shadcn-ui/components";
|
|||||||
import { Outlet } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
|
|
||||||
import { AppSidebar } from "./app-sidebar.tsx";
|
import { AppSidebar } from "./app-sidebar.tsx";
|
||||||
import { AppTopbar } from "./app-topbar.tsx";
|
import { SiteHeader } from "./site-header.tsx";
|
||||||
|
|
||||||
export const AppLayout = () => {
|
export const AppLayout = () => {
|
||||||
return (
|
return (
|
||||||
<SidebarProvider
|
<SidebarProvider
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--sidebar-width": "16rem",
|
"--sidebar-width": "calc(var(--spacing) * 72)",
|
||||||
"--sidebar-width-mobile": "18rem",
|
"--header-height": "calc(var(--spacing) * 12)",
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AppSidebar className="bg-sidebar" />
|
<AppSidebar className="bg-sidebar" variant="inset" />
|
||||||
{/* Aquí está el MAIN */}
|
{/* Aquí está el MAIN */}
|
||||||
<SidebarInset className="app-main bg-muted ">
|
<SidebarInset className="app-main bg-muted ">
|
||||||
<AppTopbar />
|
<SiteHeader />
|
||||||
<div className="flex flex-1 flex-col px-4 py-4 sm:px-6 sm:py-6">
|
<div className="flex flex-1 flex-col">
|
||||||
<Outlet />
|
<div className="@container/main p-(--content-padding) xl:group-data-[theme-content-layout=centered]/layout:container xl:group-data-[theme-content-layout=centered]/layout:mx-auto">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|||||||
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader } from "@repo/shadcn-ui/components";
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarFooter,
|
|
||||||
SidebarHeader,
|
|
||||||
SidebarRail,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
|
||||||
import {
|
import {
|
||||||
CameraIcon,
|
CameraIcon,
|
||||||
CircleIcon,
|
CircleIcon,
|
||||||
@ -146,8 +140,8 @@ const data = {
|
|||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon" variant="sidebar" {...props}>
|
<Sidebar collapsible="icon" {...props}>
|
||||||
<SidebarHeader>
|
<SidebarHeader className="mb-3">
|
||||||
<TeamSwitcher teams={data.teams} />
|
<TeamSwitcher teams={data.teams} />
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
@ -157,7 +151,6 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<NavUser user={data.user} />
|
<NavUser user={data.user} />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
<SidebarRail />
|
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
import {
|
|
||||||
Avatar,
|
|
||||||
AvatarFallback,
|
|
||||||
AvatarImage,
|
|
||||||
Button,
|
|
||||||
Separator,
|
|
||||||
SidebarTrigger,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
|
||||||
|
|
||||||
import { AppBreadcrumb } from "./app-breadcrumb.tsx";
|
|
||||||
import { ProfileDropdown } from "./dropdown-profile.tsx";
|
|
||||||
|
|
||||||
export function AppTopbar() {
|
|
||||||
return (
|
|
||||||
<header className="bg-card sticky top-0 z-50 border-b">
|
|
||||||
<div className="flex items-center justify-between gap-6 px-4 py-2 sm:px-6">
|
|
||||||
<div className="flex items-center gap-4 align-middle">
|
|
||||||
<SidebarTrigger className="[&_svg]:size-5!" />
|
|
||||||
<Separator className="hidden h-8! sm:block" orientation="vertical" />
|
|
||||||
<AppBreadcrumb />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<ProfileDropdown
|
|
||||||
trigger={
|
|
||||||
<Button className="size-9.5" size="icon" variant="ghost">
|
|
||||||
<Avatar className="size-9.5 rounded-md">
|
|
||||||
<AvatarImage src="https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png" />
|
|
||||||
<AvatarFallback>JD</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
263
packages/rdx-ui/src/components/layout/chart-area-interactive.tsx
Normal file
263
packages/rdx-ui/src/components/layout/chart-area-interactive.tsx
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
ChartConfig,
|
||||||
|
ChartContainer,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
ToggleGroup,
|
||||||
|
ToggleGroupItem,
|
||||||
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import { useIsMobile } from "@repo/shadcn-ui/hooks/";
|
||||||
|
|
||||||
|
const chartData = [
|
||||||
|
{ date: "2024-04-01", desktop: 222, mobile: 150 },
|
||||||
|
{ date: "2024-04-02", desktop: 97, mobile: 180 },
|
||||||
|
{ date: "2024-04-03", desktop: 167, mobile: 120 },
|
||||||
|
{ date: "2024-04-04", desktop: 242, mobile: 260 },
|
||||||
|
{ date: "2024-04-05", desktop: 373, mobile: 290 },
|
||||||
|
{ date: "2024-04-06", desktop: 301, mobile: 340 },
|
||||||
|
{ date: "2024-04-07", desktop: 245, mobile: 180 },
|
||||||
|
{ date: "2024-04-08", desktop: 409, mobile: 320 },
|
||||||
|
{ date: "2024-04-09", desktop: 59, mobile: 110 },
|
||||||
|
{ date: "2024-04-10", desktop: 261, mobile: 190 },
|
||||||
|
{ date: "2024-04-11", desktop: 327, mobile: 350 },
|
||||||
|
{ date: "2024-04-12", desktop: 292, mobile: 210 },
|
||||||
|
{ date: "2024-04-13", desktop: 342, mobile: 380 },
|
||||||
|
{ date: "2024-04-14", desktop: 137, mobile: 220 },
|
||||||
|
{ date: "2024-04-15", desktop: 120, mobile: 170 },
|
||||||
|
{ date: "2024-04-16", desktop: 138, mobile: 190 },
|
||||||
|
{ date: "2024-04-17", desktop: 446, mobile: 360 },
|
||||||
|
{ date: "2024-04-18", desktop: 364, mobile: 410 },
|
||||||
|
{ date: "2024-04-19", desktop: 243, mobile: 180 },
|
||||||
|
{ date: "2024-04-20", desktop: 89, mobile: 150 },
|
||||||
|
{ date: "2024-04-21", desktop: 137, mobile: 200 },
|
||||||
|
{ date: "2024-04-22", desktop: 224, mobile: 170 },
|
||||||
|
{ date: "2024-04-23", desktop: 138, mobile: 230 },
|
||||||
|
{ date: "2024-04-24", desktop: 387, mobile: 290 },
|
||||||
|
{ date: "2024-04-25", desktop: 215, mobile: 250 },
|
||||||
|
{ date: "2024-04-26", desktop: 75, mobile: 130 },
|
||||||
|
{ date: "2024-04-27", desktop: 383, mobile: 420 },
|
||||||
|
{ date: "2024-04-28", desktop: 122, mobile: 180 },
|
||||||
|
{ date: "2024-04-29", desktop: 315, mobile: 240 },
|
||||||
|
{ date: "2024-04-30", desktop: 454, mobile: 380 },
|
||||||
|
{ date: "2024-05-01", desktop: 165, mobile: 220 },
|
||||||
|
{ date: "2024-05-02", desktop: 293, mobile: 310 },
|
||||||
|
{ date: "2024-05-03", desktop: 247, mobile: 190 },
|
||||||
|
{ date: "2024-05-04", desktop: 385, mobile: 420 },
|
||||||
|
{ date: "2024-05-05", desktop: 481, mobile: 390 },
|
||||||
|
{ date: "2024-05-06", desktop: 498, mobile: 520 },
|
||||||
|
{ date: "2024-05-07", desktop: 388, mobile: 300 },
|
||||||
|
{ date: "2024-05-08", desktop: 149, mobile: 210 },
|
||||||
|
{ date: "2024-05-09", desktop: 227, mobile: 180 },
|
||||||
|
{ date: "2024-05-10", desktop: 293, mobile: 330 },
|
||||||
|
{ date: "2024-05-11", desktop: 335, mobile: 270 },
|
||||||
|
{ date: "2024-05-12", desktop: 197, mobile: 240 },
|
||||||
|
{ date: "2024-05-13", desktop: 197, mobile: 160 },
|
||||||
|
{ date: "2024-05-14", desktop: 448, mobile: 490 },
|
||||||
|
{ date: "2024-05-15", desktop: 473, mobile: 380 },
|
||||||
|
{ date: "2024-05-16", desktop: 338, mobile: 400 },
|
||||||
|
{ date: "2024-05-17", desktop: 499, mobile: 420 },
|
||||||
|
{ date: "2024-05-18", desktop: 315, mobile: 350 },
|
||||||
|
{ date: "2024-05-19", desktop: 235, mobile: 180 },
|
||||||
|
{ date: "2024-05-20", desktop: 177, mobile: 230 },
|
||||||
|
{ date: "2024-05-21", desktop: 82, mobile: 140 },
|
||||||
|
{ date: "2024-05-22", desktop: 81, mobile: 120 },
|
||||||
|
{ date: "2024-05-23", desktop: 252, mobile: 290 },
|
||||||
|
{ date: "2024-05-24", desktop: 294, mobile: 220 },
|
||||||
|
{ date: "2024-05-25", desktop: 201, mobile: 250 },
|
||||||
|
{ date: "2024-05-26", desktop: 213, mobile: 170 },
|
||||||
|
{ date: "2024-05-27", desktop: 420, mobile: 460 },
|
||||||
|
{ date: "2024-05-28", desktop: 233, mobile: 190 },
|
||||||
|
{ date: "2024-05-29", desktop: 78, mobile: 130 },
|
||||||
|
{ date: "2024-05-30", desktop: 340, mobile: 280 },
|
||||||
|
{ date: "2024-05-31", desktop: 178, mobile: 230 },
|
||||||
|
{ date: "2024-06-01", desktop: 178, mobile: 200 },
|
||||||
|
{ date: "2024-06-02", desktop: 470, mobile: 410 },
|
||||||
|
{ date: "2024-06-03", desktop: 103, mobile: 160 },
|
||||||
|
{ date: "2024-06-04", desktop: 439, mobile: 380 },
|
||||||
|
{ date: "2024-06-05", desktop: 88, mobile: 140 },
|
||||||
|
{ date: "2024-06-06", desktop: 294, mobile: 250 },
|
||||||
|
{ date: "2024-06-07", desktop: 323, mobile: 370 },
|
||||||
|
{ date: "2024-06-08", desktop: 385, mobile: 320 },
|
||||||
|
{ date: "2024-06-09", desktop: 438, mobile: 480 },
|
||||||
|
{ date: "2024-06-10", desktop: 155, mobile: 200 },
|
||||||
|
{ date: "2024-06-11", desktop: 92, mobile: 150 },
|
||||||
|
{ date: "2024-06-12", desktop: 492, mobile: 420 },
|
||||||
|
{ date: "2024-06-13", desktop: 81, mobile: 130 },
|
||||||
|
{ date: "2024-06-14", desktop: 426, mobile: 380 },
|
||||||
|
{ date: "2024-06-15", desktop: 307, mobile: 350 },
|
||||||
|
{ date: "2024-06-16", desktop: 371, mobile: 310 },
|
||||||
|
{ date: "2024-06-17", desktop: 475, mobile: 520 },
|
||||||
|
{ date: "2024-06-18", desktop: 107, mobile: 170 },
|
||||||
|
{ date: "2024-06-19", desktop: 341, mobile: 290 },
|
||||||
|
{ date: "2024-06-20", desktop: 408, mobile: 450 },
|
||||||
|
{ date: "2024-06-21", desktop: 169, mobile: 210 },
|
||||||
|
{ date: "2024-06-22", desktop: 317, mobile: 270 },
|
||||||
|
{ date: "2024-06-23", desktop: 480, mobile: 530 },
|
||||||
|
{ date: "2024-06-24", desktop: 132, mobile: 180 },
|
||||||
|
{ date: "2024-06-25", desktop: 141, mobile: 190 },
|
||||||
|
{ date: "2024-06-26", desktop: 434, mobile: 380 },
|
||||||
|
{ date: "2024-06-27", desktop: 448, mobile: 490 },
|
||||||
|
{ date: "2024-06-28", desktop: 149, mobile: 200 },
|
||||||
|
{ date: "2024-06-29", desktop: 103, mobile: 160 },
|
||||||
|
{ date: "2024-06-30", desktop: 446, mobile: 400 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
visitors: {
|
||||||
|
label: "Visitors",
|
||||||
|
},
|
||||||
|
desktop: {
|
||||||
|
label: "Desktop",
|
||||||
|
color: "hsl(var(--chart-1))",
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
label: "Mobile",
|
||||||
|
color: "hsl(var(--chart-2))",
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
|
export function ChartAreaInteractive() {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const [timeRange, setTimeRange] = React.useState("30d");
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isMobile) {
|
||||||
|
setTimeRange("7d");
|
||||||
|
}
|
||||||
|
}, [isMobile]);
|
||||||
|
|
||||||
|
const filteredData = chartData.filter((item) => {
|
||||||
|
const date = new Date(item.date);
|
||||||
|
const referenceDate = new Date("2024-06-30");
|
||||||
|
let daysToSubtract = 90;
|
||||||
|
if (timeRange === "30d") {
|
||||||
|
daysToSubtract = 30;
|
||||||
|
} else if (timeRange === "7d") {
|
||||||
|
daysToSubtract = 7;
|
||||||
|
}
|
||||||
|
const startDate = new Date(referenceDate);
|
||||||
|
startDate.setDate(startDate.getDate() - daysToSubtract);
|
||||||
|
return date >= startDate;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className='@container/card'>
|
||||||
|
<CardHeader className='relative'>
|
||||||
|
<CardTitle>Total Visitors</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
<span className='@[540px]/card:block hidden'>Total for the last 3 months</span>
|
||||||
|
<span className='@[540px]/card:hidden'>Last 3 months</span>
|
||||||
|
</CardDescription>
|
||||||
|
<div className='absolute right-4 top-4'>
|
||||||
|
<ToggleGroup
|
||||||
|
type='single'
|
||||||
|
value={timeRange}
|
||||||
|
onValueChange={setTimeRange}
|
||||||
|
variant='outline'
|
||||||
|
className='@[767px]/card:flex hidden'
|
||||||
|
>
|
||||||
|
<ToggleGroupItem value='90d' className='h-8 px-2.5'>
|
||||||
|
Last 3 months
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value='30d' className='h-8 px-2.5'>
|
||||||
|
Last 30 days
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value='7d' className='h-8 px-2.5'>
|
||||||
|
Last 7 days
|
||||||
|
</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||||
|
<SelectTrigger className='@[767px]/card:hidden flex w-40' aria-label='Select a value'>
|
||||||
|
<SelectValue placeholder='Last 3 months' />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className='rounded-xl'>
|
||||||
|
<SelectItem value='90d' className='rounded-lg'>
|
||||||
|
Last 3 months
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value='30d' className='rounded-lg'>
|
||||||
|
Last 30 days
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value='7d' className='rounded-lg'>
|
||||||
|
Last 7 days
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='px-2 pt-4 sm:px-6 sm:pt-6'>
|
||||||
|
<ChartContainer config={chartConfig} className='aspect-auto h-[250px] w-full'>
|
||||||
|
<AreaChart data={filteredData}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id='fillDesktop' x1='0' y1='0' x2='0' y2='1'>
|
||||||
|
<stop offset='5%' stopColor='var(--color-desktop)' stopOpacity={1.0} />
|
||||||
|
<stop offset='95%' stopColor='var(--color-desktop)' stopOpacity={0.1} />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id='fillMobile' x1='0' y1='0' x2='0' y2='1'>
|
||||||
|
<stop offset='5%' stopColor='var(--color-mobile)' stopOpacity={0.8} />
|
||||||
|
<stop offset='95%' stopColor='var(--color-mobile)' stopOpacity={0.1} />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<XAxis
|
||||||
|
dataKey='date'
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
tickMargin={8}
|
||||||
|
minTickGap={32}
|
||||||
|
tickFormatter={(value) => {
|
||||||
|
const date = new Date(value);
|
||||||
|
return date.toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
cursor={false}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
labelFormatter={(value) => {
|
||||||
|
return new Date(value).toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
indicator='dot'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey='mobile'
|
||||||
|
type='natural'
|
||||||
|
fill='url(#fillMobile)'
|
||||||
|
stroke='var(--color-mobile)'
|
||||||
|
stackId='a'
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey='desktop'
|
||||||
|
type='natural'
|
||||||
|
fill='url(#fillDesktop)'
|
||||||
|
stroke='var(--color-desktop)'
|
||||||
|
stackId='a'
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
751
packages/rdx-ui/src/components/layout/data-table.tsx
Normal file
751
packages/rdx-ui/src/components/layout/data-table.tsx
Normal file
@ -0,0 +1,751 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
type DragEndEvent,
|
||||||
|
KeyboardSensor,
|
||||||
|
MouseSensor,
|
||||||
|
TouchSensor,
|
||||||
|
type UniqueIdentifier,
|
||||||
|
closestCenter,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
|
||||||
|
import {
|
||||||
|
SortableContext,
|
||||||
|
arrayMove,
|
||||||
|
useSortable,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
import { Badge } from "@repo/shadcn-ui/components/badge";
|
||||||
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
|
import {
|
||||||
|
type ChartConfig,
|
||||||
|
ChartContainer,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
} from "@repo/shadcn-ui/components/chart";
|
||||||
|
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@repo/shadcn-ui/components/dropdown-menu";
|
||||||
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
|
import { Label } from "@repo/shadcn-ui/components/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@repo/shadcn-ui/components/select";
|
||||||
|
import { Separator } from "@repo/shadcn-ui/components/separator";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetFooter,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from "@repo/shadcn-ui/components/sheet";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@repo/shadcn-ui/components/table";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@repo/shadcn-ui/components/tabs";
|
||||||
|
import { useIsMobile } from "@repo/shadcn-ui/hooks";
|
||||||
|
import {
|
||||||
|
type ColumnDef,
|
||||||
|
type ColumnFiltersState,
|
||||||
|
type Row,
|
||||||
|
type SortingState,
|
||||||
|
type VisibilityState,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFacetedRowModel,
|
||||||
|
getFacetedUniqueValues,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import {
|
||||||
|
CheckCircle2Icon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
ChevronsLeftIcon,
|
||||||
|
ChevronsRightIcon,
|
||||||
|
ColumnsIcon,
|
||||||
|
GripVerticalIcon,
|
||||||
|
LoaderIcon,
|
||||||
|
MoreVerticalIcon,
|
||||||
|
PlusIcon,
|
||||||
|
TrendingUpIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const schema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
header: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
target: z.string(),
|
||||||
|
limit: z.string(),
|
||||||
|
reviewer: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a separate component for the drag handle
|
||||||
|
function DragHandle({ id }: { id: number }) {
|
||||||
|
const { attributes, listeners } = useSortable({
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
className="size-7 text-muted-foreground hover:bg-transparent"
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
<GripVerticalIcon className="size-3 text-muted-foreground" />
|
||||||
|
<span className="sr-only">Drag to reorder</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||||
|
{
|
||||||
|
id: "drag",
|
||||||
|
header: () => null,
|
||||||
|
cell: ({ row }) => <DragHandle id={row.original.id} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
aria-label="Select all"
|
||||||
|
checked={
|
||||||
|
table.getIsAllPageRowsSelected() ||
|
||||||
|
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
aria-label="Select row"
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "header",
|
||||||
|
header: "Header",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <TableCellViewer item={row.original} />;
|
||||||
|
},
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "type",
|
||||||
|
header: "Section Type",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="w-32">
|
||||||
|
<Badge className="px-1.5 text-muted-foreground" variant="outline">
|
||||||
|
{row.original.type}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "status",
|
||||||
|
header: "Status",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Badge className="flex gap-1 px-1.5 text-muted-foreground [&_svg]:size-3" variant="outline">
|
||||||
|
{row.original.status === "Done" ? (
|
||||||
|
<CheckCircle2Icon className="text-green-500 dark:text-green-400" />
|
||||||
|
) : (
|
||||||
|
<LoaderIcon />
|
||||||
|
)}
|
||||||
|
{row.original.status}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "target",
|
||||||
|
header: () => <div className="w-full text-right">Target</div>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
|
||||||
|
loading: `Saving ${row.original.header}`,
|
||||||
|
success: "Done",
|
||||||
|
error: "Error",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Label className="sr-only" htmlFor={`${row.original.id}-target`}>
|
||||||
|
Target
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background"
|
||||||
|
defaultValue={row.original.target}
|
||||||
|
id={`${row.original.id}-target`}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "limit",
|
||||||
|
header: () => <div className="w-full text-right">Limit</div>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
|
||||||
|
loading: `Saving ${row.original.header}`,
|
||||||
|
success: "Done",
|
||||||
|
error: "Error",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Label className="sr-only" htmlFor={`${row.original.id}-limit`}>
|
||||||
|
Limit
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background"
|
||||||
|
defaultValue={row.original.limit}
|
||||||
|
id={`${row.original.id}-limit`}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "reviewer",
|
||||||
|
header: "Reviewer",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const isAssigned = row.original.reviewer !== "Assign reviewer";
|
||||||
|
|
||||||
|
if (isAssigned) {
|
||||||
|
return row.original.reviewer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Label className="sr-only" htmlFor={`${row.original.id}-reviewer`}>
|
||||||
|
Reviewer
|
||||||
|
</Label>
|
||||||
|
<Select>
|
||||||
|
<SelectTrigger className="h-8 w-40" id={`${row.original.id}-reviewer`}>
|
||||||
|
<SelectValue placeholder="Assign reviewer" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent align="end">
|
||||||
|
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
|
||||||
|
<SelectItem value="Jamik Tashpulatov">Jamik Tashpulatov</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
cell: () => (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<Button
|
||||||
|
className="flex size-8 text-muted-foreground data-[state=open]:bg-muted"
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
<MoreVerticalIcon />
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-32">
|
||||||
|
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Make a copy</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Favorite</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
|
||||||
|
const { transform, transition, setNodeRef, isDragging } = useSortable({
|
||||||
|
id: row.original.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
className="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
|
||||||
|
data-dragging={isDragging}
|
||||||
|
data-state={row.getIsSelected() && "selected"}
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={{
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition: transition,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTable({ data: initialData }: { data: z.infer<typeof schema>[] }) {
|
||||||
|
const [data, setData] = React.useState(() => initialData);
|
||||||
|
const [rowSelection, setRowSelection] = React.useState({});
|
||||||
|
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
|
||||||
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
||||||
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||||
|
const [pagination, setPagination] = React.useState({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
const sortableId = React.useId();
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(MouseSensor, {}),
|
||||||
|
useSensor(TouchSensor, {}),
|
||||||
|
useSensor(KeyboardSensor, {})
|
||||||
|
);
|
||||||
|
|
||||||
|
const dataIds = React.useMemo<UniqueIdentifier[]>(() => data?.map(({ id }) => id) || [], [data]);
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
columnVisibility,
|
||||||
|
rowSelection,
|
||||||
|
columnFilters,
|
||||||
|
pagination,
|
||||||
|
},
|
||||||
|
getRowId: (row) => row.id.toString(),
|
||||||
|
enableRowSelection: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
onColumnFiltersChange: setColumnFilters,
|
||||||
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getFacetedRowModel: getFacetedRowModel(),
|
||||||
|
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleDragEnd(event: DragEndEvent) {
|
||||||
|
const { active, over } = event;
|
||||||
|
if (active && over && active.id !== over.id) {
|
||||||
|
setData((data) => {
|
||||||
|
const oldIndex = dataIds.indexOf(active.id);
|
||||||
|
const newIndex = dataIds.indexOf(over.id);
|
||||||
|
return arrayMove(data, oldIndex, newIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs className="flex w-full flex-col justify-start gap-6" defaultValue="outline">
|
||||||
|
<div className="flex items-center justify-between px-4 lg:px-6">
|
||||||
|
<Label className="sr-only" htmlFor="view-selector">
|
||||||
|
View
|
||||||
|
</Label>
|
||||||
|
<Select defaultValue="outline">
|
||||||
|
<SelectTrigger className="@4xl/main:hidden flex w-fit" id="view-selector">
|
||||||
|
<SelectValue placeholder="Select a view" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="outline">Outline</SelectItem>
|
||||||
|
<SelectItem value="past-performance">Past Performance</SelectItem>
|
||||||
|
<SelectItem value="key-personnel">Key Personnel</SelectItem>
|
||||||
|
<SelectItem value="focus-documents">Focus Documents</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<TabsList className="@4xl/main:flex hidden">
|
||||||
|
<TabsTrigger value="outline">Outline</TabsTrigger>
|
||||||
|
<TabsTrigger className="gap-1" value="past-performance">
|
||||||
|
Past Performance{" "}
|
||||||
|
<Badge
|
||||||
|
className="flex size-5 items-center justify-center rounded-full bg-muted-foreground/30"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
3
|
||||||
|
</Badge>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger className="gap-1" value="key-personnel">
|
||||||
|
Key Personnel{" "}
|
||||||
|
<Badge
|
||||||
|
className="flex size-5 items-center justify-center rounded-full bg-muted-foreground/30"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
2
|
||||||
|
</Badge>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="focus-documents">Focus Documents</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
<ColumnsIcon />
|
||||||
|
<span className="hidden lg:inline">Customize Columns</span>
|
||||||
|
<span className="lg:hidden">Columns</span>
|
||||||
|
<ChevronDownIcon />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-56">
|
||||||
|
{table
|
||||||
|
.getAllColumns()
|
||||||
|
.filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide())
|
||||||
|
.map((column) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
checked={column.getIsVisible()}
|
||||||
|
className="capitalize"
|
||||||
|
key={column.id}
|
||||||
|
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||||
|
>
|
||||||
|
{column.id}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
<PlusIcon />
|
||||||
|
<span className="hidden lg:inline">Add Section</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TabsContent
|
||||||
|
className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"
|
||||||
|
value="outline"
|
||||||
|
>
|
||||||
|
<div className="overflow-hidden rounded-lg border">
|
||||||
|
<DndContext
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
id={sortableId}
|
||||||
|
modifiers={[restrictToVerticalAxis]}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
sensors={sensors}
|
||||||
|
>
|
||||||
|
<Table>
|
||||||
|
<TableHeader className="sticky top-0 z-10 bg-muted">
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead colSpan={header.colSpan} key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody className="**:data-[slot=table-cell]:first:w-8">
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
<SortableContext items={dataIds} strategy={verticalListSortingStrategy}>
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<DraggableRow key={row.id} row={row} />
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className="h-24 text-center" colSpan={columns.length}>
|
||||||
|
No results.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</DndContext>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between px-4">
|
||||||
|
<div className="hidden flex-1 text-sm text-muted-foreground lg:flex">
|
||||||
|
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
||||||
|
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full items-center gap-8 lg:w-fit">
|
||||||
|
<div className="hidden items-center gap-2 lg:flex">
|
||||||
|
<Label className="text-sm font-medium" htmlFor="rows-per-page">
|
||||||
|
Rows per page
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
table.setPageSize(Number(value));
|
||||||
|
}}
|
||||||
|
value={`${table.getState().pagination.pageSize}`}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-20" id="rows-per-page">
|
||||||
|
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent side="top">
|
||||||
|
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||||
|
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||||
|
{pageSize}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-fit items-center justify-center text-sm font-medium">
|
||||||
|
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto flex items-center gap-2 lg:ml-0">
|
||||||
|
<Button
|
||||||
|
className="hidden size-8 p-0 lg:flex"
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
onClick={() => table.setPageIndex(0)}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to first page</span>
|
||||||
|
<ChevronsLeftIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="size-8"
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to previous page</span>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="size-8"
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to next page</span>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="hidden size-8 lg:flex"
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to last page</span>
|
||||||
|
<ChevronsRightIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent className="flex flex-col px-4 lg:px-6" value="past-performance">
|
||||||
|
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed" />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent className="flex flex-col px-4 lg:px-6" value="key-personnel">
|
||||||
|
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed" />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent className="flex flex-col px-4 lg:px-6" value="focus-documents">
|
||||||
|
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed" />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartData = [
|
||||||
|
{ month: "January", desktop: 186, mobile: 80 },
|
||||||
|
{ month: "February", desktop: 305, mobile: 200 },
|
||||||
|
{ month: "March", desktop: 237, mobile: 120 },
|
||||||
|
{ month: "April", desktop: 73, mobile: 190 },
|
||||||
|
{ month: "May", desktop: 209, mobile: 130 },
|
||||||
|
{ month: "June", desktop: 214, mobile: 140 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
desktop: {
|
||||||
|
label: "Desktop",
|
||||||
|
color: "var(--primary)",
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
label: "Mobile",
|
||||||
|
color: "var(--primary)",
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
|
function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet>
|
||||||
|
<SheetTrigger>
|
||||||
|
<Button className="w-fit px-0 text-left text-foreground" variant="link">
|
||||||
|
{item.header}
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent className="flex flex-col" side="right">
|
||||||
|
<SheetHeader className="gap-1">
|
||||||
|
<SheetTitle>{item.header}</SheetTitle>
|
||||||
|
<SheetDescription>Showing total visitors for the last 6 months</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
<div className="flex flex-1 flex-col gap-4 overflow-y-auto py-4 text-sm">
|
||||||
|
{!isMobile && (
|
||||||
|
<>
|
||||||
|
<ChartContainer config={chartConfig}>
|
||||||
|
<AreaChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={chartData}
|
||||||
|
margin={{
|
||||||
|
left: 0,
|
||||||
|
right: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<XAxis
|
||||||
|
axisLine={false}
|
||||||
|
dataKey="month"
|
||||||
|
hide
|
||||||
|
tickFormatter={(value) => value.slice(0, 3)}
|
||||||
|
tickLine={false}
|
||||||
|
tickMargin={8}
|
||||||
|
/>
|
||||||
|
<ChartTooltip content={<ChartTooltipContent indicator="dot" />} cursor={false} />
|
||||||
|
<Area
|
||||||
|
dataKey="mobile"
|
||||||
|
fill="var(--color-mobile)"
|
||||||
|
fillOpacity={0.6}
|
||||||
|
stackId="a"
|
||||||
|
stroke="var(--color-mobile)"
|
||||||
|
type="natural"
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey="desktop"
|
||||||
|
fill="var(--color-desktop)"
|
||||||
|
fillOpacity={0.4}
|
||||||
|
stackId="a"
|
||||||
|
stroke="var(--color-desktop)"
|
||||||
|
type="natural"
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
<Separator />
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<div className="flex gap-2 font-medium leading-none">
|
||||||
|
Trending up by 5.2% this month <TrendingUpIcon className="size-4" />
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
Showing total visitors for the last 6 months. This is just some random text to
|
||||||
|
test the layout. It spans multiple lines and should wrap around.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<form className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label htmlFor="header">Header</Label>
|
||||||
|
<Input defaultValue={item.header} id="header" />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label htmlFor="type">Type</Label>
|
||||||
|
<Select defaultValue={item.type}>
|
||||||
|
<SelectTrigger className="w-full" id="type">
|
||||||
|
<SelectValue placeholder="Select a type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="Table of Contents">Table of Contents</SelectItem>
|
||||||
|
<SelectItem value="Executive Summary">Executive Summary</SelectItem>
|
||||||
|
<SelectItem value="Technical Approach">Technical Approach</SelectItem>
|
||||||
|
<SelectItem value="Design">Design</SelectItem>
|
||||||
|
<SelectItem value="Capabilities">Capabilities</SelectItem>
|
||||||
|
<SelectItem value="Focus Documents">Focus Documents</SelectItem>
|
||||||
|
<SelectItem value="Narrative">Narrative</SelectItem>
|
||||||
|
<SelectItem value="Cover Page">Cover Page</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label htmlFor="status">Status</Label>
|
||||||
|
<Select defaultValue={item.status}>
|
||||||
|
<SelectTrigger className="w-full" id="status">
|
||||||
|
<SelectValue placeholder="Select a status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="Done">Done</SelectItem>
|
||||||
|
<SelectItem value="In Progress">In Progress</SelectItem>
|
||||||
|
<SelectItem value="Not Started">Not Started</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label htmlFor="target">Target</Label>
|
||||||
|
<Input defaultValue={item.target} id="target" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label htmlFor="limit">Limit</Label>
|
||||||
|
<Input defaultValue={item.limit} id="limit" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label htmlFor="reviewer">Reviewer</Label>
|
||||||
|
<Select defaultValue={item.reviewer}>
|
||||||
|
<SelectTrigger className="w-full" id="reviewer">
|
||||||
|
<SelectValue placeholder="Select a reviewer" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
|
||||||
|
<SelectItem value="Jamik Tashpulatov">Jamik Tashpulatov</SelectItem>
|
||||||
|
<SelectItem value="Emily Whalen">Emily Whalen</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<SheetFooter className="mt-auto flex gap-2 sm:flex-col sm:space-x-0">
|
||||||
|
<Button className="w-full">Submit</Button>
|
||||||
|
<SheetClose>
|
||||||
|
<Button className="w-full" variant="outline">
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</SheetClose>
|
||||||
|
</SheetFooter>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,95 +0,0 @@
|
|||||||
import {
|
|
||||||
Avatar,
|
|
||||||
AvatarFallback,
|
|
||||||
AvatarImage,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
|
||||||
import {
|
|
||||||
CirclePlusIcon,
|
|
||||||
CreditCardIcon,
|
|
||||||
LogOutIcon,
|
|
||||||
SettingsIcon,
|
|
||||||
SquarePenIcon,
|
|
||||||
UserIcon,
|
|
||||||
UsersIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
trigger: ReactNode;
|
|
||||||
defaultOpen?: boolean;
|
|
||||||
align?: "start" | "center" | "end";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProfileDropdown = ({ trigger, defaultOpen, align = "end" }: Props) => {
|
|
||||||
return (
|
|
||||||
<DropdownMenu defaultOpen={defaultOpen}>
|
|
||||||
<DropdownMenuTrigger>{trigger}</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align={align || "end"} className="w-80">
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuLabel className="flex items-center gap-4 px-4 py-2.5 font-normal">
|
|
||||||
<div className="relative">
|
|
||||||
<Avatar className="size-10">
|
|
||||||
<AvatarImage
|
|
||||||
alt="John Doe"
|
|
||||||
src="https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png"
|
|
||||||
/>
|
|
||||||
<AvatarFallback>JD</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<span className="ring-card absolute right-0 bottom-0 block size-2 rounded-full bg-green-600 ring-2" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-1 flex-col items-start">
|
|
||||||
<span className="text-foreground text-lg font-semibold">John Doe</span>
|
|
||||||
<span className="text-muted-foreground text-base">john.doe@example.com</span>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
|
|
||||||
<DropdownMenuItem className="px-4 py-2.5 text-base">
|
|
||||||
<UserIcon className="text-foreground size-5" />
|
|
||||||
<span>My account</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem className="px-4 py-2.5 text-base">
|
|
||||||
<SettingsIcon className="text-foreground size-5" />
|
|
||||||
<span>Settings</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem className="px-4 py-2.5 text-base">
|
|
||||||
<CreditCardIcon className="text-foreground size-5" />
|
|
||||||
<span>Billing</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem className="px-4 py-2.5 text-base">
|
|
||||||
<UsersIcon className="text-foreground size-5" />
|
|
||||||
<span>Manage team</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem className="px-4 py-2.5 text-base">
|
|
||||||
<SquarePenIcon className="text-foreground size-5" />
|
|
||||||
<span>Customization</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem className="px-4 py-2.5 text-base">
|
|
||||||
<CirclePlusIcon className="text-foreground size-5" />
|
|
||||||
<span>Add team account</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
|
|
||||||
<DropdownMenuItem className="px-4 py-2.5 text-base" variant="destructive">
|
|
||||||
<LogOutIcon className="size-5" />
|
|
||||||
<span>Logout</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./app-breadcrumb.tsx";
|
||||||
export * from "./app-content.tsx";
|
export * from "./app-content.tsx";
|
||||||
export * from "./app-header.tsx";
|
export * from "./app-header.tsx";
|
||||||
export * from "./app-layout.tsx";
|
export * from "./app-layout.tsx";
|
||||||
|
|||||||
76
packages/rdx-ui/src/components/layout/nav-documents.tsx
Normal file
76
packages/rdx-ui/src/components/layout/nav-documents.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@repo/shadcn-ui/components/dropdown-menu";
|
||||||
|
import {
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuAction,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
useSidebar,
|
||||||
|
} from "@repo/shadcn-ui/components/sidebar";
|
||||||
|
import { FolderIcon, type LucideIcon, MoreHorizontalIcon, ShareIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export function NavDocuments({
|
||||||
|
items,
|
||||||
|
}: {
|
||||||
|
items: {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
}[];
|
||||||
|
}) {
|
||||||
|
const { isMobile } = useSidebar();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||||
|
<SidebarGroupLabel>Documents</SidebarGroupLabel>
|
||||||
|
<SidebarMenu>
|
||||||
|
{items.map((item) => (
|
||||||
|
<SidebarMenuItem key={item.name}>
|
||||||
|
<SidebarMenuButton>
|
||||||
|
<a href={item.url}>
|
||||||
|
<item.icon />
|
||||||
|
<span>{item.name}</span>
|
||||||
|
</a>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<SidebarMenuAction className="rounded-sm data-[state=open]:bg-accent" showOnHover>
|
||||||
|
<MoreHorizontalIcon />
|
||||||
|
<span className="sr-only">More</span>
|
||||||
|
</SidebarMenuAction>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align={isMobile ? "end" : "start"}
|
||||||
|
className="w-24 rounded-lg"
|
||||||
|
side={isMobile ? "bottom" : "right"}
|
||||||
|
>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<FolderIcon />
|
||||||
|
<span>Open</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<ShareIcon />
|
||||||
|
<span>Share</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
))}
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton className="text-sidebar-foreground/70">
|
||||||
|
<MoreHorizontalIcon className="text-sidebar-foreground/70" />
|
||||||
|
<span>More</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ import {
|
|||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupLabel,
|
SidebarGroupContent,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
@ -11,53 +11,66 @@ import {
|
|||||||
SidebarMenuSubButton,
|
SidebarMenuSubButton,
|
||||||
SidebarMenuSubItem,
|
SidebarMenuSubItem,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { ChevronRightIcon, type LucideIcon } from "lucide-react";
|
import { ChevronRightIcon, type LucideIcon, PlusCircleIcon } from "lucide-react";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
export function NavMain({
|
type NavMainItem = {
|
||||||
items,
|
title: string;
|
||||||
}: {
|
url?: string;
|
||||||
items: {
|
icon?: LucideIcon;
|
||||||
|
isActive?: boolean;
|
||||||
|
items?: {
|
||||||
title: string;
|
title: string;
|
||||||
url?: string;
|
url: string;
|
||||||
icon?: LucideIcon;
|
|
||||||
isActive?: boolean;
|
|
||||||
items?: {
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
}[];
|
|
||||||
}[];
|
}[];
|
||||||
}) {
|
};
|
||||||
|
|
||||||
|
export function NavMain({ items }: { items: NavMainItem[] }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Módulos</SidebarGroupLabel>
|
<SidebarGroupContent className="flex flex-col gap-2">
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{items.map((item) => (
|
<SidebarMenuItem className="flex items-center gap-2">
|
||||||
<Collapsible className="group/collapsible" defaultOpen={item.isActive} key={item.title}>
|
<SidebarMenuButton
|
||||||
<SidebarMenuItem>
|
className="hidden min-w-8 bg-primary text-primary-foreground duration-200 ease-linear hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground"
|
||||||
<CollapsibleTrigger render={<SidebarMenuButton tooltip={item.title} />}>
|
tooltip="Quick Create"
|
||||||
{item.icon && <item.icon />}
|
>
|
||||||
<span>{item.title}</span>
|
<PlusCircleIcon />
|
||||||
<ChevronRightIcon className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
<span>Quick Create</span>
|
||||||
</CollapsibleTrigger>
|
</SidebarMenuButton>
|
||||||
<CollapsibleContent>
|
</SidebarMenuItem>
|
||||||
<SidebarMenuSub>
|
</SidebarMenu>
|
||||||
{item.items?.map((subItem) => (
|
<SidebarMenu>
|
||||||
<SidebarMenuSubItem key={subItem.title}>
|
{items.map((item) => (
|
||||||
<SidebarMenuSubButton
|
<Collapsible asChild className="group/collapsible" defaultOpen={true} key={item.title}>
|
||||||
render={
|
<SidebarMenuItem className="mb-6">
|
||||||
<a href={subItem.url} title={subItem.title}>
|
<CollapsibleTrigger>
|
||||||
|
<SidebarMenuButton tooltip={item.title}>
|
||||||
|
{item.icon && <item.icon />}
|
||||||
|
<span className="font-semibold">{item.title}</span>
|
||||||
|
<ChevronRightIcon className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<SidebarMenuSub>
|
||||||
|
{item.items?.map((subItem) => (
|
||||||
|
<SidebarMenuSubItem key={subItem.title}>
|
||||||
|
<SidebarMenuSubButton>
|
||||||
|
<a href={subItem.url}>
|
||||||
<span>{subItem.title}</span>
|
<span>{subItem.title}</span>
|
||||||
</a>
|
</a>
|
||||||
}
|
</SidebarMenuSubButton>
|
||||||
/>
|
</SidebarMenuSubItem>
|
||||||
</SidebarMenuSubItem>
|
))}
|
||||||
))}
|
</SidebarMenuSub>
|
||||||
</SidebarMenuSub>
|
</CollapsibleContent>
|
||||||
</CollapsibleContent>
|
</SidebarMenuItem>
|
||||||
</SidebarMenuItem>
|
</Collapsible>
|
||||||
</Collapsible>
|
))}
|
||||||
))}
|
</SidebarMenu>
|
||||||
</SidebarMenu>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
82
packages/rdx-ui/src/components/layout/nav-projects.tsx
Normal file
82
packages/rdx-ui/src/components/layout/nav-projects.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@repo/shadcn-ui/components/dropdown-menu";
|
||||||
|
import {
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuAction,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
useSidebar,
|
||||||
|
} from "@repo/shadcn-ui/components/sidebar";
|
||||||
|
import { Folder, Forward, type LucideIcon, MoreHorizontal, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
|
export function NavProjects({
|
||||||
|
projects,
|
||||||
|
}: {
|
||||||
|
projects: {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
}[];
|
||||||
|
}) {
|
||||||
|
const { isMobile } = useSidebar();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||||
|
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
||||||
|
<SidebarMenu>
|
||||||
|
{projects.map((item) => (
|
||||||
|
<SidebarMenuItem key={item.name}>
|
||||||
|
<SidebarMenuButton>
|
||||||
|
<a href={item.url}>
|
||||||
|
<item.icon />
|
||||||
|
<span>{item.name}</span>
|
||||||
|
</a>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<SidebarMenuAction showOnHover>
|
||||||
|
<MoreHorizontal />
|
||||||
|
<span className="sr-only">More</span>
|
||||||
|
</SidebarMenuAction>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align={isMobile ? "end" : "start"}
|
||||||
|
className="w-48 rounded-lg"
|
||||||
|
side={isMobile ? "bottom" : "right"}
|
||||||
|
>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Folder className="text-muted-foreground" />
|
||||||
|
<span>View Project</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Forward className="text-muted-foreground" />
|
||||||
|
<span>Share Project</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Trash2 className="text-muted-foreground" />
|
||||||
|
<span>Delete Project</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
))}
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton className="text-sidebar-foreground/70">
|
||||||
|
<MoreHorizontal className="text-sidebar-foreground/70" />
|
||||||
|
<span>More</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -16,7 +16,13 @@ import {
|
|||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from "@repo/shadcn-ui/components/sidebar";
|
} from "@repo/shadcn-ui/components/sidebar";
|
||||||
import { BadgeCheck, Bell, ChevronsUpDownIcon, CreditCard, LogOut, Sparkles } from "lucide-react";
|
import {
|
||||||
|
BellIcon,
|
||||||
|
CreditCardIcon,
|
||||||
|
LogOutIcon,
|
||||||
|
MoreVerticalIcon,
|
||||||
|
UserCircleIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
export function NavUser({
|
export function NavUser({
|
||||||
user,
|
user,
|
||||||
@ -28,72 +34,63 @@ export function NavUser({
|
|||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
const { isMobile } = useSidebar();
|
const { isMobile } = useSidebar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger>
|
||||||
render={
|
<SidebarMenuButton
|
||||||
<SidebarMenuButton
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
size="lg"
|
||||||
size="lg"
|
>
|
||||||
>
|
<Avatar className="size-8 rounded-lg grayscale">
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
<AvatarImage alt={user.name} src={user.avatar} />
|
||||||
|
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span className="truncate font-medium">{user.name}</span>
|
||||||
|
<span className="truncate text-xs text-muted-foreground">{user.email}</span>
|
||||||
|
</div>
|
||||||
|
<MoreVerticalIcon className="ml-auto size-4" />
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||||
|
side={isMobile ? "bottom" : "right"}
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel className="p-0 font-normal">
|
||||||
|
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
|
<Avatar className="size-8 rounded-lg">
|
||||||
<AvatarImage alt={user.name} src={user.avatar} />
|
<AvatarImage alt={user.name} src={user.avatar} />
|
||||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span className="truncate font-medium">{user.name}</span>
|
<span className="truncate font-medium">{user.name}</span>
|
||||||
<span className="truncate text-xs">{user.email}</span>
|
<span className="truncate text-xs text-muted-foreground">{user.email}</span>
|
||||||
</div>
|
</div>
|
||||||
<ChevronsUpDownIcon className="ml-auto size-4" />
|
</div>
|
||||||
</SidebarMenuButton>
|
</DropdownMenuLabel>
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DropdownMenuContent
|
|
||||||
align="end"
|
|
||||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
|
||||||
side={isMobile ? "bottom" : "right"}
|
|
||||||
sideOffset={4}
|
|
||||||
>
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuLabel className="p-0 font-normal">
|
|
||||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
|
||||||
<AvatarImage alt={user.name} src={user.avatar} />
|
|
||||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
||||||
<span className="truncate font-medium">{user.name}</span>
|
|
||||||
<span className="truncate text-xs">{user.email}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<Sparkles />
|
|
||||||
Upgrade to Pro
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<BadgeCheck />
|
<UserCircleIcon />
|
||||||
Account
|
Account
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<CreditCard />
|
<CreditCardIcon />
|
||||||
Billing
|
Billing
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Bell />
|
<BellIcon />
|
||||||
Notifications
|
Notifications
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<LogOut />
|
<LogOutIcon />
|
||||||
Log out
|
Log out
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
38
packages/rdx-ui/src/components/layout/search-form.tsx
Normal file
38
packages/rdx-ui/src/components/layout/search-form.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Label,
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarInput,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuItem,
|
||||||
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import { BinocularsIcon, SearchIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export function SearchForm({ ...props }: React.ComponentProps<"form">) {
|
||||||
|
return (
|
||||||
|
<form {...props}>
|
||||||
|
<SidebarGroup className="py-0">
|
||||||
|
<SidebarGroupContent className="relative">
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem className="flex items-center gap-2">
|
||||||
|
<Label className="sr-only" htmlFor="search">
|
||||||
|
Searchsss
|
||||||
|
</Label>
|
||||||
|
<SidebarInput className="pl-8 h-9" id="search" placeholder="Search..." />
|
||||||
|
<SearchIcon className="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none " />
|
||||||
|
<Button
|
||||||
|
className="h-9 w-9 shrink-0 group-data-[collapsible=icon]:opacity-0"
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<BinocularsIcon />
|
||||||
|
<span className="sr-only">Advanced search</span>
|
||||||
|
</Button>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
packages/rdx-ui/src/components/layout/section-cards.tsx
Normal file
97
packages/rdx-ui/src/components/layout/section-cards.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { TrendingDownIcon, TrendingUpIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import { Badge } from "@repo/shadcn-ui/components/badge";
|
||||||
|
|
||||||
|
export function SectionCards() {
|
||||||
|
return (
|
||||||
|
<div className='*:data-[slot=card]:shadow-xs @xl/main:grid-cols-2 @5xl/main:grid-cols-4 grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card lg:px-6'>
|
||||||
|
<Card className='@container/card'>
|
||||||
|
<CardHeader className='relative'>
|
||||||
|
<CardDescription>Total Revenue</CardDescription>
|
||||||
|
<CardTitle className='@[250px]/card:text-3xl text-2xl font-semibold tabular-nums'>
|
||||||
|
$1,250.00
|
||||||
|
</CardTitle>
|
||||||
|
<div className='absolute right-4 top-4'>
|
||||||
|
<Badge variant='outline' className='flex gap-1 rounded-lg text-xs'>
|
||||||
|
<TrendingUpIcon className='size-3' />
|
||||||
|
+12.5%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter className='flex-col items-start gap-1 text-sm'>
|
||||||
|
<div className='line-clamp-1 flex gap-2 font-medium'>
|
||||||
|
Trending up this month <TrendingUpIcon className='size-4' />
|
||||||
|
</div>
|
||||||
|
<div className='text-muted-foreground'>Visitors for the last 6 months</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
<Card className='@container/card'>
|
||||||
|
<CardHeader className='relative'>
|
||||||
|
<CardDescription>New Customers</CardDescription>
|
||||||
|
<CardTitle className='@[250px]/card:text-3xl text-2xl font-semibold tabular-nums'>
|
||||||
|
1,234
|
||||||
|
</CardTitle>
|
||||||
|
<div className='absolute right-4 top-4'>
|
||||||
|
<Badge variant='outline' className='flex gap-1 rounded-lg text-xs'>
|
||||||
|
<TrendingDownIcon className='size-3' />
|
||||||
|
-20%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter className='flex-col items-start gap-1 text-sm'>
|
||||||
|
<div className='line-clamp-1 flex gap-2 font-medium'>
|
||||||
|
Down 20% this period <TrendingDownIcon className='size-4' />
|
||||||
|
</div>
|
||||||
|
<div className='text-muted-foreground'>Acquisition needs attention</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
<Card className='@container/card'>
|
||||||
|
<CardHeader className='relative'>
|
||||||
|
<CardDescription>Active Accounts</CardDescription>
|
||||||
|
<CardTitle className='@[250px]/card:text-3xl text-2xl font-semibold tabular-nums'>
|
||||||
|
45,678
|
||||||
|
</CardTitle>
|
||||||
|
<div className='absolute right-4 top-4'>
|
||||||
|
<Badge variant='outline' className='flex gap-1 rounded-lg text-xs'>
|
||||||
|
<TrendingUpIcon className='size-3' />
|
||||||
|
+12.5%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter className='flex-col items-start gap-1 text-sm'>
|
||||||
|
<div className='line-clamp-1 flex gap-2 font-medium'>
|
||||||
|
Strong user retention <TrendingUpIcon className='size-4' />
|
||||||
|
</div>
|
||||||
|
<div className='text-muted-foreground'>Engagement exceed targets</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
<Card className='@container/card'>
|
||||||
|
<CardHeader className='relative'>
|
||||||
|
<CardDescription>Growth Rate</CardDescription>
|
||||||
|
<CardTitle className='@[250px]/card:text-3xl text-2xl font-semibold tabular-nums'>
|
||||||
|
4.5%
|
||||||
|
</CardTitle>
|
||||||
|
<div className='absolute right-4 top-4'>
|
||||||
|
<Badge variant='outline' className='flex gap-1 rounded-lg text-xs'>
|
||||||
|
<TrendingUpIcon className='size-3' />
|
||||||
|
+4.5%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter className='flex-col items-start gap-1 text-sm'>
|
||||||
|
<div className='line-clamp-1 flex gap-2 font-medium'>
|
||||||
|
Steady performance <TrendingUpIcon className='size-4' />
|
||||||
|
</div>
|
||||||
|
<div className='text-muted-foreground'>Meets growth projections</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
237
packages/rdx-ui/src/components/layout/site-header.tsx
Normal file
237
packages/rdx-ui/src/components/layout/site-header.tsx
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import { Button, Separator, SidebarTrigger } from "@repo/shadcn-ui/components";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
export function SiteHeader() {
|
||||||
|
return (
|
||||||
|
<header className="bg-background sticky top-0 z-50 flex h-(--header-height) shrink-0 items-center gap-2 border-b backdrop-blur-md transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height) md:rounded-tl-xl md:rounded-tr-xl">
|
||||||
|
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
||||||
|
<SidebarTrigger className="-ml-1" />
|
||||||
|
<Separator className="mx-2 data-[orientation=vertical]:h-4" orientation="vertical" />
|
||||||
|
<div className="lg:flex-1">
|
||||||
|
<div className="relative hidden max-w-sm flex-1 lg:block">
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="lucide lucide-search text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2"
|
||||||
|
fill="none"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="m21 21-4.34-4.34" />
|
||||||
|
<circle cx="11" cy="11" r="8" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<input
|
||||||
|
className="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input min-w-0 bg-transparent px-3 py-1 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 h-9 w-full cursor-pointer rounded-md border pr-4 pl-10 text-sm shadow-xs"
|
||||||
|
data-slot="input"
|
||||||
|
placeholder="Search..."
|
||||||
|
type="search"
|
||||||
|
/>
|
||||||
|
<div className="absolute top-1/2 right-2 hidden -translate-y-1/2 items-center gap-0.5 rounded-sm bg-zinc-200 p-1 font-mono text-xs font-medium sm:flex dark:bg-neutral-700">
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="lucide lucide-command size-3"
|
||||||
|
fill="none"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
|
||||||
|
</svg>
|
||||||
|
<span>k</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="block lg:hidden">
|
||||||
|
<button
|
||||||
|
className="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 hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 size-9"
|
||||||
|
data-size="icon"
|
||||||
|
data-slot="button"
|
||||||
|
data-variant="ghost"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="lucide lucide-search"
|
||||||
|
fill="none"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="m21 21-4.34-4.34" />
|
||||||
|
<circle cx="11" cy="11" r="8" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex flex-col gap-2 text-center sm:text-left sr-only"
|
||||||
|
data-slot="dialog-header"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
className="text-lg leading-none font-semibold"
|
||||||
|
data-slot="dialog-title"
|
||||||
|
id="radix-_R_rd5ubplbH1_"
|
||||||
|
>
|
||||||
|
Command Palette
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
className="text-muted-foreground text-sm"
|
||||||
|
data-slot="dialog-description"
|
||||||
|
id="radix-_R_rd5ubplbH2_"
|
||||||
|
>
|
||||||
|
Search for a command to run...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto flex items-center gap-2">
|
||||||
|
<Button asChild className="hidden sm:flex" size="sm">
|
||||||
|
<Link
|
||||||
|
className="dark:text-foreground"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
to="https://shadcnuikit.com/"
|
||||||
|
>
|
||||||
|
Get Pro
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-auto flex items-center gap-2">
|
||||||
|
<a
|
||||||
|
className="inline-flex items-center justify-center whitespace-nowrap text-sm 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 underline-offset-4 hover:underline h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5 relative animate-pulse bg-linear-to-r from-violet-600 via-fuchsia-600 to-cyan-600 bg-clip-text font-medium text-transparent hover:bg-transparent"
|
||||||
|
data-size="sm"
|
||||||
|
data-slot="button"
|
||||||
|
data-variant="link"
|
||||||
|
href="https://shadcnuikit.com/pricing"
|
||||||
|
rel="noopener"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Get Pro
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
className="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 hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 size-8 relative"
|
||||||
|
data-size="icon-sm"
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
data-state="closed"
|
||||||
|
data-variant="ghost"
|
||||||
|
id="radix-_R_kd5ubplb_"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="lucide lucide-bell"
|
||||||
|
fill="none"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M10.268 21a2 2 0 0 0 3.464 0" />
|
||||||
|
<path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" />
|
||||||
|
</svg>
|
||||||
|
<span className="bg-destructive absolute end-0.5 top-0.5 block size-1.5 shrink-0 rounded-full" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="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 hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 size-8 relative"
|
||||||
|
data-size="icon-sm"
|
||||||
|
data-slot="button"
|
||||||
|
data-variant="ghost"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="lucide lucide-moon"
|
||||||
|
fill="none"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
||||||
|
</svg>
|
||||||
|
<span className="sr-only">Toggle theme</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
className="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 hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 size-8"
|
||||||
|
data-size="icon-sm"
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
data-state="closed"
|
||||||
|
data-variant="ghost"
|
||||||
|
id="radix-_R_14d5ubplb_"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="lucide lucide-palette"
|
||||||
|
fill="none"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z" />
|
||||||
|
<circle cx="13.5" cy="6.5" fill="currentColor" r=".5" />
|
||||||
|
<circle cx="17.5" cy="10.5" fill="currentColor" r=".5" />
|
||||||
|
<circle cx="6.5" cy="12.5" fill="currentColor" r=".5" />
|
||||||
|
<circle cx="8.5" cy="7.5" fill="currentColor" r=".5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className="bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px mx-2 data-[orientation=vertical]:h-4"
|
||||||
|
data-orientation="vertical"
|
||||||
|
data-slot="separator"
|
||||||
|
role="none"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
className="group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6"
|
||||||
|
data-size="default"
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
data-state="closed"
|
||||||
|
id="radix-_R_1kd5ubplb_"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="shadcn ui kit"
|
||||||
|
className="aspect-square size-full object-cover"
|
||||||
|
data-slot="avatar-image"
|
||||||
|
src="/images/avatars/01.png"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@
|
|||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
@ -37,53 +36,48 @@ export function TeamSwitcher({
|
|||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger>
|
||||||
render={
|
<SidebarMenuButton
|
||||||
<SidebarMenuButton
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
size="lg"
|
||||||
size="lg"
|
>
|
||||||
>
|
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
||||||
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
<activeTeam.logo className="size-4" />
|
||||||
<activeTeam.logo className="size-4" />
|
</div>
|
||||||
</div>
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<span className="truncate font-medium">{activeTeam.name}</span>
|
||||||
<span className="truncate font-medium">{activeTeam.name}</span>
|
<span className="truncate text-xs">{activeTeam.plan}</span>
|
||||||
<span className="truncate text-xs">{activeTeam.plan}</span>
|
</div>
|
||||||
</div>
|
<ChevronsUpDown className="ml-auto" />
|
||||||
<ChevronsUpDown className="ml-auto" />
|
</SidebarMenuButton>
|
||||||
</SidebarMenuButton>
|
</DropdownMenuTrigger>
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="start"
|
align="start"
|
||||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||||
side={isMobile ? "bottom" : "right"}
|
side={isMobile ? "bottom" : "right"}
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
>
|
>
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuLabel className="text-muted-foreground text-xs">Teams</DropdownMenuLabel>
|
||||||
<DropdownMenuLabel className="text-muted-foreground text-xs">Teams</DropdownMenuLabel>
|
{teams.map((team, index) => (
|
||||||
{teams.map((team, index) => (
|
<DropdownMenuItem
|
||||||
<DropdownMenuItem
|
className="gap-2 p-2"
|
||||||
className="gap-2 p-2"
|
key={team.name}
|
||||||
key={team.name}
|
onClick={() => setActiveTeam(team)}
|
||||||
onClick={() => setActiveTeam(team)}
|
>
|
||||||
>
|
<div className="flex size-6 items-center justify-center rounded-md border">
|
||||||
<div className="flex size-6 items-center justify-center rounded-md border">
|
<team.logo className="size-3.5 shrink-0" />
|
||||||
<team.logo className="size-3.5 shrink-0" />
|
|
||||||
</div>
|
|
||||||
{team.name}
|
|
||||||
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem className="gap-2 p-2">
|
|
||||||
<div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
|
|
||||||
<Plus className="size-4" />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground font-medium">Add team</div>
|
{team.name}
|
||||||
|
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
))}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem className="gap-2 p-2">
|
||||||
|
<div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
|
||||||
|
<Plus className="size-4" />
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground font-medium">Add team</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "",
|
"config": "",
|
||||||
"css": "@repo/shadcn-ui/globals.css",
|
"css": "src/styles/globals.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@plugin "tailwindcss-animate";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
@import "shadcn/tailwind.css";
|
@import "shadcn/tailwind.css";
|
||||||
|
|
||||||
@ -13,37 +15,37 @@
|
|||||||
@source "../**/*.{ts,tsx}";
|
@source "../**/*.{ts,tsx}";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: oklch(1 0 0);
|
||||||
--foreground: #0a0a0a;
|
--foreground: oklch(0.145 0 0);
|
||||||
--card: #ffffff;
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: #0a0a0a;
|
--card-foreground: oklch(0.145 0 0);
|
||||||
--popover: #ffffff;
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: #0a0a0a;
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
--primary: #0b7ad0;
|
--primary: oklch(0.205 0 0);
|
||||||
--primary-foreground: #ffffff;
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
--secondary: #e4e8ef;
|
--secondary: oklch(0.97 0 0);
|
||||||
--secondary-foreground: #6a8aa3;
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
--muted: #f5f5f5;
|
--muted: oklch(0.97 0 0);
|
||||||
--muted-foreground: #737373;
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
--accent: #e1e7fd;
|
--accent: oklch(0.97 0 0);
|
||||||
--accent-foreground: #364050;
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
--destructive: #e7000b;
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--border: #e5e5e5;
|
--border: oklch(0.922 0 0);
|
||||||
--input: #c5c5c5;
|
--input: oklch(0.922 0 0);
|
||||||
--ring: #0b7ad0;
|
--ring: oklch(0.708 0 0);
|
||||||
--chart-1: #8ec5ff;
|
--chart-1: oklch(0.87 0 0);
|
||||||
--chart-2: #2b7fff;
|
--chart-2: oklch(0.556 0 0);
|
||||||
--chart-3: #155dfc;
|
--chart-3: oklch(0.439 0 0);
|
||||||
--chart-4: #1447e6;
|
--chart-4: oklch(0.371 0 0);
|
||||||
--chart-5: #193cb8;
|
--chart-5: oklch(0.269 0 0);
|
||||||
--sidebar: #0b7ad0;
|
--sidebar: oklch(0.985 0 0);
|
||||||
--sidebar-foreground: #ffffff;
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
--sidebar-primary: #ffffff;
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
--sidebar-primary-foreground: #0b7ad0;
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-accent: #4a8fe0;
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
--sidebar-accent-foreground: #ffffff;
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
--sidebar-border: #4a8fe0;
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
--sidebar-ring: #ffffff;
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
|
||||||
--font-sans: "Geist Variable", sans-serif;
|
--font-sans: "Geist Variable", sans-serif;
|
||||||
--font-serif: "Geist Variable", serif;
|
--font-serif: "Geist Variable", serif;
|
||||||
@ -51,59 +53,59 @@
|
|||||||
|
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
|
|
||||||
--shadow-2xs: 0 1px 3px 0px rgb(0 0 0 / 0.05);
|
--shadow-2xs: 0 1px 3px 0px oklch(0 0 0 / 0.05);
|
||||||
--shadow-xs: 0 1px 3px 0px rgb(0 0 0 / 0.05);
|
--shadow-xs: 0 1px 3px 0px oklch(0 0 0 / 0.05);
|
||||||
--shadow-sm: 0 1px 3px 0px rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
--shadow-sm: 0 1px 3px 0px oklch(0 0 0 / 0.1), 0 1px 2px -1px oklch(0 0 0 / 0.1);
|
||||||
--shadow: 0 1px 3px 0px rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
--shadow: 0 1px 3px 0px oklch(0 0 0 / 0.1), 0 1px 2px -1px oklch(0 0 0 / 0.1);
|
||||||
--shadow-md: 0 1px 3px 0px rgb(0 0 0 / 0.1), 0 2px 4px -1px rgb(0 0 0 / 0.1);
|
--shadow-md: 0 1px 3px 0px oklch(0 0 0 / 0.1), 0 2px 4px -1px oklch(0 0 0 / 0.1);
|
||||||
--shadow-lg: 0 1px 3px 0px rgb(0 0 0 / 0.1), 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
--shadow-lg: 0 1px 3px 0px oklch(0 0 0 / 0.1), 0 4px 6px -1px oklch(0 0 0 / 0.1);
|
||||||
--shadow-xl: 0 1px 3px 0px rgb(0 0 0 / 0.1), 0 8px 10px -1px rgb(0 0 0 / 0.1);
|
--shadow-xl: 0 1px 3px 0px oklch(0 0 0 / 0.1), 0 8px 10px -1px oklch(0 0 0 / 0.1);
|
||||||
--shadow-2xl: 0 1px 3px 0px rgb(0 0 0 / 0.25);
|
--shadow-2xl: 0 1px 3px 0px oklch(0 0 0 / 0.25);
|
||||||
|
|
||||||
--spacing: 0.24rem;
|
--spacing: 0.24rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: #0a0a0a;
|
--background: oklch(0.145 0 0);
|
||||||
--foreground: #fafafa;
|
--foreground: oklch(0.985 0 0);
|
||||||
--card: #171717;
|
--card: oklch(0.205 0 0);
|
||||||
--card-foreground: #fafafa;
|
--card-foreground: oklch(0.985 0 0);
|
||||||
--popover: #171717;
|
--popover: oklch(0.205 0 0);
|
||||||
--popover-foreground: #fafafa;
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
--primary: #125cff;
|
--primary: oklch(0.922 0 0);
|
||||||
--primary-foreground: #eff6ff;
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
--secondary: #26262b;
|
--secondary: oklch(0.269 0 0);
|
||||||
--secondary-foreground: #fafafa;
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
--muted: #262626;
|
--muted: oklch(0.269 0 0);
|
||||||
--muted-foreground: #a1a1a1;
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
--accent: #374151;
|
--accent: oklch(0.269 0 0);
|
||||||
--accent-foreground: #d1d5db;
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--destructive: #ff6467;
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--border: #a1a1a1;
|
--border: oklch(1 0 0 / 10%);
|
||||||
--input: #d1d5db;
|
--input: oklch(1 0 0 / 15%);
|
||||||
--ring: #003dfa;
|
--ring: oklch(0.556 0 0);
|
||||||
--chart-1: #003dfa;
|
--chart-1: oklch(0.87 0 0);
|
||||||
--chart-2: #00bc7d;
|
--chart-2: oklch(0.556 0 0);
|
||||||
--chart-3: #fe9a00;
|
--chart-3: oklch(0.439 0 0);
|
||||||
--chart-4: #ad46ff;
|
--chart-4: oklch(0.371 0 0);
|
||||||
--chart-5: #ff2056;
|
--chart-5: oklch(0.269 0 0);
|
||||||
--sidebar: #171717;
|
--sidebar: oklch(0.205 0 0);
|
||||||
--sidebar-foreground: #fafafa;
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-primary: #125cff;
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
--sidebar-primary-foreground: #eff6ff;
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-accent: #262626;
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
--sidebar-accent-foreground: #fafafa;
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-border: #ffffff;
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
--sidebar-ring: #003dfa;
|
--sidebar-ring: oklch(0.556 0 0);
|
||||||
|
|
||||||
--shadow-2xs: 1px 1px 6px 0px rgb(0 0 0 / 0.05);
|
--shadow-2xs: 1px 1px 6px 0px oklch(0 0 0 / 0.05);
|
||||||
--shadow-xs: 1px 1px 6px 0px rgb(0 0 0 / 0.05);
|
--shadow-xs: 1px 1px 6px 0px oklch(0 0 0 / 0.05);
|
||||||
--shadow-sm: 1px 1px 6px 0px rgb(0 0 0 / 0.1), 1px 1px 2px -1px rgb(0 0 0 / 0.1);
|
--shadow-sm: 1px 1px 6px 0px oklch(0 0 0 / 0.1), 1px 1px 2px -1px oklch(0 0 0 / 0.1);
|
||||||
--shadow: 1px 1px 6px 0px rgb(0 0 0 / 0.1), 1px 1px 2px -1px rgb(0 0 0 / 0.1);
|
--shadow: 1px 1px 6px 0px oklch(0 0 0 / 0.1), 1px 1px 2px -1px oklch(0 0 0 / 0.1);
|
||||||
--shadow-md: 1px 1px 6px 0px rgb(0 0 0 / 0.1), 1px 2px 4px -1px rgb(0 0 0 / 0.1);
|
--shadow-md: 1px 1px 6px 0px oklch(0 0 0 / 0.1), 1px 2px 4px -1px oklch(0 0 0 / 0.1);
|
||||||
--shadow-lg: 1px 1px 6px 0px rgb(0 0 0 / 0.1), 1px 4px 6px -1px rgb(0 0 0 / 0.1);
|
--shadow-lg: 1px 1px 6px 0px oklch(0 0 0 / 0.1), 1px 4px 6px -1px oklch(0 0 0 / 0.1);
|
||||||
--shadow-xl: 1px 1px 6px 0px rgb(0 0 0 / 0.1), 1px 8px 10px -1px rgb(0 0 0 / 0.1);
|
--shadow-xl: 1px 1px 6px 0px oklch(0 0 0 / 0.1), 1px 8px 10px -1px oklch(0 0 0 / 0.1);
|
||||||
--shadow-2xl: 1px 1px 6px 0px rgb(0 0 0 / 0.25);
|
--shadow-2xl: 1px 1px 6px 0px oklch(0 0 0 / 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
@ -156,6 +158,9 @@
|
|||||||
--shadow-lg: var(--shadow-lg);
|
--shadow-lg: var(--shadow-lg);
|
||||||
--shadow-xl: var(--shadow-xl);
|
--shadow-xl: var(--shadow-xl);
|
||||||
--shadow-2xl: var(--shadow-2xl);
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
|
--radius-2xl: calc(var(--radius) * 1.8);
|
||||||
|
--radius-3xl: calc(var(--radius) * 2.2);
|
||||||
|
--radius-4xl: calc(var(--radius) * 2.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|||||||
@ -977,7 +977,7 @@ importers:
|
|||||||
version: 7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
version: 7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
recharts:
|
recharts:
|
||||||
specifier: ^3.8.1
|
specifier: ^3.8.1
|
||||||
version: 3.8.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react-is@16.13.1)(react@19.2.5)(redux@5.0.1)
|
version: 3.8.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react-is@18.3.1)(react@19.2.5)(redux@5.0.1)
|
||||||
sonner:
|
sonner:
|
||||||
specifier: ^2.0.7
|
specifier: ^2.0.7
|
||||||
version: 2.0.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
version: 2.0.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
@ -1117,10 +1117,10 @@ importers:
|
|||||||
version: 7.72.1(react@19.2.5)
|
version: 7.72.1(react@19.2.5)
|
||||||
react-resizable-panels:
|
react-resizable-panels:
|
||||||
specifier: ^4.9.0
|
specifier: ^4.9.0
|
||||||
version: 4.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
version: 4.9.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
recharts:
|
recharts:
|
||||||
specifier: 3.8.0
|
specifier: 3.8.0
|
||||||
version: 3.8.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react-is@16.13.1)(react@19.2.5)(redux@5.0.1)
|
version: 3.8.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react-is@18.3.1)(react@19.2.5)(redux@5.0.1)
|
||||||
sonner:
|
sonner:
|
||||||
specifier: ^2.0.7
|
specifier: ^2.0.7
|
||||||
version: 2.0.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
version: 2.0.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
@ -6101,6 +6101,9 @@ packages:
|
|||||||
react-is@16.13.1:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
|
react-is@18.3.1:
|
||||||
|
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||||
|
|
||||||
react-qr-code@2.0.18:
|
react-qr-code@2.0.18:
|
||||||
resolution: {integrity: sha512-v1Jqz7urLMhkO6jkgJuBYhnqvXagzceg3qJUWayuCK/c6LTIonpWbwxR1f1APGd4xrW/QcQEovNrAojbUz65Tg==}
|
resolution: {integrity: sha512-v1Jqz7urLMhkO6jkgJuBYhnqvXagzceg3qJUWayuCK/c6LTIonpWbwxR1f1APGd4xrW/QcQEovNrAojbUz65Tg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -6138,8 +6141,8 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
react-resizable-panels@4.10.0:
|
react-resizable-panels@4.9.0:
|
||||||
resolution: {integrity: sha512-frjewRQt7TCv/vCH1pJfjZ7RxAhr5pKuqVQtVgzFq/vherxBFOWyC3xMbryx5Ti2wylViGUFc93Etg4rB3E0UA==}
|
resolution: {integrity: sha512-sEl+hA6y9/kxa0aPlrUC+G1lcShAf/PiIjoeC8kWXxa53RfAVplVCIxEl01Nwa4L2iRa5JXBXq1/mI8ch6qOZQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0.0 || ^19.0.0
|
react: ^18.0.0 || ^19.0.0
|
||||||
react-dom: ^18.0.0 || ^19.0.0
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
@ -11852,6 +11855,8 @@ snapshots:
|
|||||||
|
|
||||||
react-is@16.13.1: {}
|
react-is@16.13.1: {}
|
||||||
|
|
||||||
|
react-is@18.3.1: {}
|
||||||
|
|
||||||
react-qr-code@2.0.18(react@19.2.5):
|
react-qr-code@2.0.18(react@19.2.5):
|
||||||
dependencies:
|
dependencies:
|
||||||
prop-types: 15.8.1
|
prop-types: 15.8.1
|
||||||
@ -11886,7 +11891,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
|
|
||||||
react-resizable-panels@4.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
|
react-resizable-panels@4.9.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.5
|
react: 19.2.5
|
||||||
react-dom: 19.2.5(react@19.2.5)
|
react-dom: 19.2.5(react@19.2.5)
|
||||||
@ -11940,7 +11945,7 @@ snapshots:
|
|||||||
tiny-invariant: 1.3.3
|
tiny-invariant: 1.3.3
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
recharts@3.8.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react-is@16.13.1)(react@19.2.5)(redux@5.0.1):
|
recharts@3.8.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react-is@18.3.1)(react@19.2.5)(redux@5.0.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1))(react@19.2.5)
|
'@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1))(react@19.2.5)
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
@ -11950,7 +11955,7 @@ snapshots:
|
|||||||
immer: 10.2.0
|
immer: 10.2.0
|
||||||
react: 19.2.5
|
react: 19.2.5
|
||||||
react-dom: 19.2.5(react@19.2.5)
|
react-dom: 19.2.5(react@19.2.5)
|
||||||
react-is: 16.13.1
|
react-is: 18.3.1
|
||||||
react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1)
|
react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1)
|
||||||
reselect: 5.1.1
|
reselect: 5.1.1
|
||||||
tiny-invariant: 1.3.3
|
tiny-invariant: 1.3.3
|
||||||
@ -11960,7 +11965,7 @@ snapshots:
|
|||||||
- '@types/react'
|
- '@types/react'
|
||||||
- redux
|
- redux
|
||||||
|
|
||||||
recharts@3.8.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react-is@16.13.1)(react@19.2.5)(redux@5.0.1):
|
recharts@3.8.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react-is@18.3.1)(react@19.2.5)(redux@5.0.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1))(react@19.2.5)
|
'@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1))(react@19.2.5)
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
@ -11970,7 +11975,7 @@ snapshots:
|
|||||||
immer: 10.2.0
|
immer: 10.2.0
|
||||||
react: 19.2.5
|
react: 19.2.5
|
||||||
react-dom: 19.2.5(react@19.2.5)
|
react-dom: 19.2.5(react@19.2.5)
|
||||||
react-is: 16.13.1
|
react-is: 18.3.1
|
||||||
react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1)
|
react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1)
|
||||||
reselect: 5.1.1
|
reselect: 5.1.1
|
||||||
tiny-invariant: 1.3.3
|
tiny-invariant: 1.3.3
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user